diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index 61d6f4bca91..a7def23c98e 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -74,7 +74,6 @@ Canvas itself handles: activate(clientID: number, attributeID?: number): void; rotate(rotation: Rotation, remember?: boolean): void; focus(clientID: number, padding?: number): void; - fitCanvas(): void; fit(): void; grid(stepX: number, stepY: number): void; @@ -84,6 +83,10 @@ Canvas itself handles: merge(mergeData: MergeData): void; select(objectState: any): void; + fitCanvas(): void; + dragCanvas(enable: boolean): void; + zoomCanvas(enable: boolean): void; + cancel(): void; } ``` @@ -118,6 +121,10 @@ Standard JS events are used. - canvas.groupped => {states: ObjectState[]} - canvas.merged => {states: ObjectState[]} - canvas.canceled + - canvas.dragstart + - canvas.dragstop + - canvas.zoomstart + - canvas.zoomstop ``` ### WEB @@ -138,64 +145,26 @@ Standard JS events are used. }); ``` -### TypeScript -- Add to ```tsconfig.json```: -```json - "compilerOptions": { - "paths": { - "cvat-canvas.node": ["3rdparty/cvat-canvas.node"] - } - } -``` - -- ```3rdparty``` directory contains both ```cvat-canvas.node.js``` and ```cvat-canvas.node.d.ts```. -- Add alias to ```webpack.config.js```: -```js -module.exports = { - resolve: { - alias: { - 'cvat-canvas.node': path.resolve(__dirname, '3rdparty/cvat-canvas.node.js'), - } - } -} -``` - -Than you can use it in TypeScript: -```ts - import * as CANVAS from 'cvat-canvas.node'; - // Create an instance of a canvas - const canvas = new CANVAS.Canvas(); - - // Put canvas to a html container - htmlContainer.appendChild(canvas.html()); - - // Next you can use its API methods. For example: - canvas.rotate(CANVAS.Rotation.CLOCKWISE90); - canvas.draw({ - enabled: true, - shapeType: 'rectangle', - crosshair: true, - }); -``` - ## States ![](images/states.svg) ## API Reaction -| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | -|-------------|------|----------|-----------|---------|---------|---------| -| html() | + | + | + | + | + | + | -| setup() | + | + | + | + | + | - | -| activate() | + | - | - | - | - | - | -| rotate() | + | + | + | + | + | + | -| focus() | + | + | + | + | + | + | -| fit() | + | + | + | + | + | + | -| fitCanvas() | + | + | + | + | + | + | -| grid() | + | + | + | + | + | + | -| draw() | + | - | - | - | - | - | -| split() | + | - | + | - | - | - | -| group | + | + | - | - | - | - | -| merge() | + | - | - | - | + | - | -| cancel() | - | + | + | + | + | + | +| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM | +|--------------|------|----------|-----------|---------|---------|---------|------|------| +| html() | + | + | + | + | + | + | + | + | +| setup() | + | + | + | + | + | - | + | + | +| activate() | + | - | - | - | - | - | - | - | +| rotate() | + | + | + | + | + | + | + | + | +| focus() | + | + | + | + | + | + | + | + | +| fit() | + | + | + | + | + | + | + | + | +| grid() | + | + | + | + | + | + | + | + | +| draw() | + | - | - | - | - | - | - | - | +| split() | + | - | + | - | - | - | - | - | +| group() | + | + | - | - | - | - | - | - | +| merge() | + | - | - | - | + | - | - | - | +| fitCanvas() | + | + | + | + | + | + | + | + | +| dragCanvas() | + | - | - | - | - | - | + | - | +| zoomCanvas() | + | - | - | - | - | - | - | + | +| cancel() | - | + | + | + | + | + | + | + | diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 7cf72018b66..529067ce2e8 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -3,7 +3,7 @@ } .cvat_canvas_shape { - fill-opacity: 0.05; + fill-opacity: 0.03; stroke-opacity: 1; } @@ -68,6 +68,12 @@ polyline.cvat_canvas_shape_merging { stroke: black; } +.cvat_canvas_zoom_selection { + stroke: #096dd9; + fill-opacity: 0; + stroke-dasharray: 4; +} + .cvat_canvas_shape_occluded { stroke-dasharray: 5; } @@ -78,9 +84,9 @@ polyline.cvat_canvas_shape_merging { } #cvat_canvas_wrapper { - width: 98%; - height: 98%; - margin: 10px; + width: calc(100% - 10px); + height: calc(100% - 10px); + margin: 5px; border-radius: 5px; background-color: white; overflow: hidden; @@ -103,6 +109,7 @@ polyline.cvat_canvas_shape_merging { } #cvat_canvas_text_content { + text-rendering: optimizeSpeed; position: absolute; z-index: 3; pointer-events: none; @@ -135,6 +142,7 @@ polyline.cvat_canvas_shape_merging { } #cvat_canvas_content { + filter: contrast(120%) saturate(150%); position: absolute; z-index: 2; outline: 10px solid black; diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index 1f9e04465d6..3c6dfec3421 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -44,6 +44,10 @@ interface Canvas { merge(mergeData: MergeData): void; select(objectState: any): void; + fitCanvas(): void; + dragCanvas(enable: boolean): void; + zoomCanvas(enable: boolean): void; + cancel(): void; } @@ -73,7 +77,15 @@ class CanvasImpl implements Canvas { ); } - public activate(clientID: number, attributeID: number = null): void { + public dragCanvas(enable: boolean): void { + this.model.dragCanvas(enable); + } + + public zoomCanvas(enable: boolean): void { + this.model.zoomCanvas(enable); + } + + public activate(clientID: number, attributeID: number | null = null): void { this.model.activate(clientID, attributeID); } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index d4959af81f2..af08193894e 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -33,8 +33,8 @@ export interface FocusData { } export interface ActiveElement { - clientID: number; - attributeID: number; + clientID: number | null; + attributeID: number | null; } export interface DrawData { @@ -74,21 +74,26 @@ export enum Rotation { } export enum UpdateReasons { - IMAGE = 'image', - OBJECTS = 'objects', - ZOOM = 'zoom', - FIT = 'fit', - FIT_CANVAS = 'fit_canvas', - MOVE = 'move', - GRID = 'grid', - FOCUS = 'focus', - ACTIVATE = 'activate', + IMAGE_CHANGED = 'image_changed', + IMAGE_ZOOMED = 'image_zoomed', + IMAGE_FITTED = 'image_fitted', + IMAGE_MOVED = 'image_moved', + GRID_UPDATED = 'grid_updated', + + OBJECTS_UPDATED = 'objects_updated', + SHAPE_ACTIVATED = 'shape_activated', + SHAPE_FOCUSED = 'shape_focused', + + FITTED_CANVAS = 'fitted_canvas', + DRAW = 'draw', MERGE = 'merge', SPLIT = 'split', GROUP = 'group', SELECT = 'select', CANCEL = 'cancel', + DRAG_CANVAS = 'drag_canvas', + ZOOM_CANVAS = 'ZOOM_CANVAS', } export enum Mode { @@ -100,6 +105,8 @@ export enum Mode { MERGE = 'merge', SPLIT = 'split', GROUP = 'group', + DRAG_CANVAS = 'drag_canvas', + ZOOM_CANVAS = 'zoom_canvas', } export interface CanvasModel { @@ -120,11 +127,10 @@ export interface CanvasModel { move(topOffset: number, leftOffset: number): void; setup(frameData: any, objectStates: any[]): void; - activate(clientID: number, attributeID: number): void; + activate(clientID: number, attributeID: number | null): void; rotate(rotation: Rotation, remember: boolean): void; focus(clientID: number, padding: number): void; fit(): void; - fitCanvas(width: number, height: number): void; grid(stepX: number, stepY: number): void; draw(drawData: DrawData): void; @@ -133,6 +139,10 @@ export interface CanvasModel { merge(mergeData: MergeData): void; select(objectState: any): void; + fitCanvas(width: number, height: number): void; + dragCanvas(enable: boolean): void; + zoomCanvas(enable: boolean): void; + cancel(): void; } @@ -193,8 +203,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { top: 0, drawData: { enabled: false, - shapeType: null, - numberOfPoints: null, initialState: null, }, mergeData: { @@ -207,7 +215,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { enabled: false, }, selected: null, - mode: null, + mode: Mode.IDLE, }; } @@ -232,13 +240,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { * (oldScale / this.data.scale - 1)) * this.data.scale; } - this.notify(UpdateReasons.ZOOM); + this.notify(UpdateReasons.IMAGE_ZOOMED); } public move(topOffset: number, leftOffset: number): void { this.data.top += topOffset; this.data.left += leftOffset; - this.notify(UpdateReasons.MOVE); + this.notify(UpdateReasons.IMAGE_MOVED); } public fitCanvas(width: number, height: number): void { @@ -250,15 +258,41 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.canvasSize.width / FrameZoom.MIN, )); - this.notify(UpdateReasons.FIT_CANVAS); - this.notify(UpdateReasons.OBJECTS); + this.notify(UpdateReasons.FITTED_CANVAS); + this.notify(UpdateReasons.OBJECTS_UPDATED); + } + + public dragCanvas(enable: boolean): void { + if (enable && this.data.mode !== Mode.IDLE) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + + if (!enable && this.data.mode !== Mode.DRAG_CANVAS) { + throw Error(`Canvas is not in the drag mode. Action: ${this.data.mode}`); + } + + this.data.mode = enable ? Mode.DRAG_CANVAS : Mode.IDLE; + this.notify(UpdateReasons.DRAG_CANVAS); + } + + public zoomCanvas(enable: boolean): void { + if (enable && this.data.mode !== Mode.IDLE) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + + if (!enable && this.data.mode !== Mode.ZOOM_CANVAS) { + throw Error(`Canvas is not in the zoom mode. Action: ${this.data.mode}`); + } + + this.data.mode = enable ? Mode.ZOOM_CANVAS : Mode.IDLE; + this.notify(UpdateReasons.ZOOM_CANVAS); } public setup(frameData: any, objectStates: any[]): void { frameData.data( (): void => { this.data.image = ''; - this.notify(UpdateReasons.IMAGE); + this.notify(UpdateReasons.IMAGE_CHANGED); }, ).then((data: string): void => { this.data.imageSize = { @@ -271,15 +305,15 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } this.data.image = data; - this.notify(UpdateReasons.IMAGE); + this.notify(UpdateReasons.IMAGE_CHANGED); this.data.objects = objectStates; - this.notify(UpdateReasons.OBJECTS); + this.notify(UpdateReasons.OBJECTS_UPDATED); }).catch((exception: any): void => { throw exception; }); } - public activate(clientID: number, attributeID: number): void { + public activate(clientID: number, attributeID: number | null): void { if (this.data.mode !== Mode.IDLE) { // Exception or just return? throw Error(`Canvas is busy. Action: ${this.data.mode}`); @@ -290,7 +324,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { attributeID, }; - this.notify(UpdateReasons.ACTIVATE); + this.notify(UpdateReasons.SHAPE_ACTIVATED); } public rotate(rotation: Rotation, remember: boolean = false): void { @@ -311,7 +345,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { padding, }; - this.notify(UpdateReasons.FOCUS); + this.notify(UpdateReasons.SHAPE_FOCUSED); } public fit(): void { @@ -338,7 +372,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2); this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2); - this.notify(UpdateReasons.FIT); + this.notify(UpdateReasons.IMAGE_FITTED); } public grid(stepX: number, stepY: number): void { @@ -347,7 +381,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { width: stepX, }; - this.notify(UpdateReasons.GRID); + this.notify(UpdateReasons.GRID_UPDATED); } public draw(drawData: DrawData): void { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 5af68f30a9e..f14606cea91 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -16,6 +16,7 @@ import { EditHandler, EditHandlerImpl } from './editHandler'; import { MergeHandler, MergeHandlerImpl } from './mergeHandler'; import { SplitHandler, SplitHandlerImpl } from './splitHandler'; import { GroupHandler, GroupHandlerImpl } from './groupHandler'; +import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler'; import consts from './consts'; import { translateToSVG, @@ -29,7 +30,6 @@ import { CanvasModel, Geometry, UpdateReasons, - FocusData, FrameZoom, ActiveElement, DrawData, @@ -86,10 +86,11 @@ export class CanvasViewImpl implements CanvasView, Listener { private mergeHandler: MergeHandler; private splitHandler: SplitHandler; private groupHandler: GroupHandler; + private zoomHandler: ZoomHandler; private activeElement: { state: any; attributeID: number; - }; + } | null; private set mode(value: Mode) { this.controller.mode = value; @@ -206,7 +207,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.mode = Mode.IDLE; } - private onGroupDone(objects: any[]): void { + private onGroupDone(objects?: any[]): void { if (objects) { const event: CustomEvent = new CustomEvent('canvas.groupped', { bubbles: false, @@ -252,12 +253,141 @@ export class CanvasViewImpl implements CanvasView, Listener { } } + private onFocusRegion(x: number, y: number, width: number, height: number): void { + // First of all, compute and apply scale + let scale = null; + + if ((this.geometry.angle / 90) % 2) { + // 90, 270, .. + scale = Math.min(Math.max(Math.min( + this.geometry.canvas.width / height, + this.geometry.canvas.height / width, + ), FrameZoom.MIN), FrameZoom.MAX); + } else { + scale = Math.min(Math.max(Math.min( + this.geometry.canvas.width / width, + this.geometry.canvas.height / height, + ), FrameZoom.MIN), FrameZoom.MAX); + } + + this.geometry = { ...this.geometry, scale }; + this.transformCanvas(); + + const [canvasX, canvasY] = translateFromSVG(this.content, [ + x + width / 2, + y + height / 2, + ]); + + const [cx, cy] = [ + this.canvas.clientWidth / 2 + this.canvas.offsetLeft, + this.canvas.clientHeight / 2 + this.canvas.offsetTop, + ]; + + const dragged = { + ...this.geometry, + top: this.geometry.top + cy - canvasY, + left: this.geometry.left + cx - canvasX, + scale, + }; + + this.controller.geometry = dragged; + this.geometry = dragged; + this.moveCanvas(); + } + + private moveCanvas(): void { + for (const obj of [this.background, this.grid, this.loadingAnimation]) { + obj.style.top = `${this.geometry.top}px`; + obj.style.left = `${this.geometry.left}px`; + } + + for (const obj of [this.content, this.text]) { + obj.style.top = `${this.geometry.top - this.geometry.offset}px`; + obj.style.left = `${this.geometry.left - this.geometry.offset}px`; + } + + // Transform handlers + this.drawHandler.transform(this.geometry); + this.editHandler.transform(this.geometry); + this.zoomHandler.transform(this.geometry); + } + + private transformCanvas(): void { + // Transform canvas + for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) { + obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`; + } + + // Transform grid + this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / (this.geometry.scale)}px`); + + // Transform all shape points + for (const element of window.document.getElementsByClassName('svg_select_points')) { + element.setAttribute( + 'stroke-width', + `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`, + ); + element.setAttribute( + 'r', + `${consts.BASE_POINT_SIZE / this.geometry.scale}`, + ); + } + + for (const element of + window.document.getElementsByClassName('cvat_canvas_selected_point')) { + const previousWidth = element.getAttribute('stroke-width') as string; + element.setAttribute( + 'stroke-width', + `${+previousWidth * 2}`, + ); + } + + // Transform all drawn shapes + for (const key in this.svgShapes) { + if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) { + const object = this.svgShapes[key]; + if (object.attr('stroke-width')) { + object.attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + }); + } + } + } + + // Transform all text + for (const key in this.svgShapes) { + if (Object.prototype.hasOwnProperty.call(this.svgShapes, key) + && Object.prototype.hasOwnProperty.call(this.svgTexts, key)) { + this.updateTextPosition( + this.svgTexts[key], + this.svgShapes[key], + ); + } + } + + // Transform handlers + this.drawHandler.transform(this.geometry); + this.editHandler.transform(this.geometry); + } + + private resizeCanvas(): void { + for (const obj of [this.background, this.grid, this.loadingAnimation]) { + obj.style.width = `${this.geometry.image.width}px`; + obj.style.height = `${this.geometry.image.height}px`; + } + + for (const obj of [this.content, this.text]) { + obj.style.width = `${this.geometry.image.width + this.geometry.offset * 2}px`; + obj.style.height = `${this.geometry.image.height + this.geometry.offset * 2}px`; + } + } + private selectize(value: boolean, shape: SVG.Element): void { const self = this; function dblClickHandler(e: MouseEvent): void { const pointID = Array.prototype.indexOf - .call((e.target as HTMLElement).parentElement.children, e.target); + .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target); if (self.activeElement) { if (e.ctrlKey) { @@ -324,6 +454,7 @@ export class CanvasViewImpl implements CanvasView, Listener { public constructor(model: CanvasModel & Master, controller: CanvasController) { this.controller = controller; + this.geometry = controller.geometry; this.svgShapes = {}; this.svgTexts = {}; this.activeElement = null; @@ -426,7 +557,11 @@ export class CanvasViewImpl implements CanvasView, Listener { this.onFindObject.bind(this), this.adoptedContent, ); - + this.zoomHandler = new ZoomHandlerImpl( + this.onFocusRegion.bind(this), + this.adoptedContent, + this.geometry, + ); // Setup event handlers this.content.addEventListener('dblclick', (e: MouseEvent): void => { @@ -436,10 +571,12 @@ export class CanvasViewImpl implements CanvasView, Listener { }); this.content.addEventListener('mousedown', (event): void => { - if ((event.which === 1 && this.mode === Mode.IDLE) || (event.which === 2)) { - self.controller.enableDrag(event.clientX, event.clientY); - - event.preventDefault(); + if ([1, 2].includes(event.which)) { + if ([Mode.DRAG_CANVAS, Mode.IDLE].includes(this.mode)) { + self.controller.enableDrag(event.clientX, event.clientY); + } else if (this.mode === Mode.ZOOM_CANVAS && event.which === 2) { + self.controller.enableDrag(event.clientX, event.clientY); + } } }); @@ -452,6 +589,10 @@ export class CanvasViewImpl implements CanvasView, Listener { this.content.addEventListener('wheel', (event): void => { const point = translateToSVG(self.background, [event.clientX, event.clientY]); self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1); + this.canvas.dispatchEvent(new CustomEvent('canvas.zoom', { + bubbles: false, + cancelable: true, + })); event.preventDefault(); }); @@ -480,140 +621,6 @@ export class CanvasViewImpl implements CanvasView, Listener { } public notify(model: CanvasModel & Master, reason: UpdateReasons): void { - function transform(): void { - // Transform canvas - for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) { - obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`; - } - - // Transform grid - this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / (this.geometry.scale)}px`); - - // Transform all shape points - for (const element of window.document.getElementsByClassName('svg_select_points')) { - element.setAttribute( - 'stroke-width', - `${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`, - ); - element.setAttribute( - 'r', - `${consts.BASE_POINT_SIZE / this.geometry.scale}`, - ); - } - - for (const element of - window.document.getElementsByClassName('cvat_canvas_selected_point')) { - element.setAttribute( - 'stroke-width', - `${+element.getAttribute('stroke-width') * 2}`, - ); - } - - // Transform all drawn shapes - for (const key in this.svgShapes) { - if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) { - const object = this.svgShapes[key]; - if (object.attr('stroke-width')) { - object.attr({ - 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, - }); - } - } - } - - // Transform all text - for (const key in this.svgShapes) { - if (Object.prototype.hasOwnProperty.call(this.svgShapes, key) - && Object.prototype.hasOwnProperty.call(this.svgTexts, key)) { - this.updateTextPosition( - this.svgTexts[key], - this.svgShapes[key], - ); - } - } - - // Transform handlers - this.drawHandler.transform(this.geometry); - this.editHandler.transform(this.geometry); - } - - function resize(): void { - for (const obj of [this.background, this.grid, this.loadingAnimation]) { - obj.style.width = `${this.geometry.image.width}px`; - obj.style.height = `${this.geometry.image.height}px`; - } - - for (const obj of [this.content, this.text]) { - obj.style.width = `${this.geometry.image.width + this.geometry.offset * 2}px`; - obj.style.height = `${this.geometry.image.height + this.geometry.offset * 2}px`; - } - } - - function move(): void { - for (const obj of [this.background, this.grid, this.loadingAnimation]) { - obj.style.top = `${this.geometry.top}px`; - obj.style.left = `${this.geometry.left}px`; - } - - for (const obj of [this.content, this.text]) { - obj.style.top = `${this.geometry.top - this.geometry.offset}px`; - obj.style.left = `${this.geometry.left - this.geometry.offset}px`; - } - - // Transform handlers - this.drawHandler.transform(this.geometry); - this.editHandler.transform(this.geometry); - } - - function computeFocus(focusData: FocusData): void { - // This computation cann't be done in the model because of lack of data - const object = this.svgShapes[focusData.clientID]; - if (!object) { - return; - } - - // First of all, compute and apply scale - - let scale = null; - const bbox: SVG.BBox = object.bbox(); - if ((this.geometry.angle / 90) % 2) { - // 90, 270, .. - scale = Math.min(Math.max(Math.min( - this.geometry.canvas.width / bbox.height, - this.geometry.canvas.height / bbox.width, - ), FrameZoom.MIN), FrameZoom.MAX); - } else { - scale = Math.min(Math.max(Math.min( - this.geometry.canvas.width / bbox.width, - this.geometry.canvas.height / bbox.height, - ), FrameZoom.MIN), FrameZoom.MAX); - } - - this.geometry = { ...this.geometry, scale }; - transform.call(this); - - const [x, y] = translateFromSVG(this.content, [ - bbox.x + bbox.width / 2, - bbox.y + bbox.height / 2, - ]); - - const [cx, cy] = [ - this.canvas.clientWidth / 2 + this.canvas.offsetLeft, - this.canvas.clientHeight / 2 + this.canvas.offsetTop, - ]; - - const dragged = { - ...this.geometry, - top: this.geometry.top + cy - y, - left: this.geometry.left + cx - x, - scale, - }; - - this.controller.geometry = dragged; - this.geometry = dragged; - move.call(this); - } - function setupObjects(objects: any[]): void { const ctm = this.content.getScreenCTM() .inverse().multiply(this.background.getScreenCTM()); @@ -640,36 +647,83 @@ export class CanvasViewImpl implements CanvasView, Listener { } this.geometry = this.controller.geometry; - if (reason === UpdateReasons.IMAGE) { + if (reason === UpdateReasons.IMAGE_CHANGED) { if (!model.image.length) { this.loadingAnimation.classList.remove('cvat_canvas_hidden'); } else { this.loadingAnimation.classList.add('cvat_canvas_hidden'); this.background.style.backgroundImage = `url("${model.image}")`; - move.call(this); - resize.call(this); - transform.call(this); + this.moveCanvas(); + this.resizeCanvas(); + this.transformCanvas(); + } + } else if (reason === UpdateReasons.FITTED_CANVAS) { + this.moveCanvas(); + this.resizeCanvas(); + } else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) { + this.moveCanvas(); + this.transformCanvas(); + } else if (reason === UpdateReasons.IMAGE_MOVED) { + this.moveCanvas(); + } else if (reason === UpdateReasons.OBJECTS_UPDATED) { + if (this.mode === Mode.GROUP) { + this.groupHandler.resetSelectedObjects(); } - } else if (reason === UpdateReasons.FIT_CANVAS) { - move.call(this); - resize.call(this); - } else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) { - move.call(this); - transform.call(this); - } else if (reason === UpdateReasons.MOVE) { - move.call(this); - } else if (reason === UpdateReasons.OBJECTS) { setupObjects.call(this, this.controller.objects); + if (this.mode === Mode.MERGE) { + this.mergeHandler.repeatSelection(); + } const event: CustomEvent = new CustomEvent('canvas.setup'); this.canvas.dispatchEvent(event); - } else if (reason === UpdateReasons.GRID) { + } else if (reason === UpdateReasons.GRID_UPDATED) { const size: Size = this.geometry.grid; this.gridPattern.setAttribute('width', `${size.width}`); this.gridPattern.setAttribute('height', `${size.height}`); - } else if (reason === UpdateReasons.FOCUS) { - computeFocus.call(this, this.controller.focusData); - } else if (reason === UpdateReasons.ACTIVATE) { + } else if (reason === UpdateReasons.SHAPE_FOCUSED) { + const { + padding, + clientID, + } = this.controller.focusData; + const object = this.svgShapes[clientID]; + if (object) { + const bbox: SVG.BBox = object.bbox(); + this.onFocusRegion(bbox.x - padding, bbox.y - padding, + bbox.width + padding, bbox.height + padding); + } + } else if (reason === UpdateReasons.SHAPE_ACTIVATED) { this.activate(this.controller.activeElement); + } else if (reason === UpdateReasons.DRAG_CANVAS) { + this.deactivate(); + if (this.mode === Mode.DRAG_CANVAS) { + this.canvas.dispatchEvent(new CustomEvent('canvas.dragstart', { + bubbles: false, + cancelable: true, + })); + this.canvas.style.cursor = 'move'; + } else { + this.canvas.dispatchEvent(new CustomEvent('canvas.dragstop', { + bubbles: false, + cancelable: true, + })); + this.canvas.style.cursor = ''; + } + } else if (reason === UpdateReasons.ZOOM_CANVAS) { + this.deactivate(); + if (this.mode === Mode.ZOOM_CANVAS) { + this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstart', { + bubbles: false, + cancelable: true, + })); + this.canvas.style.cursor = 'zoom-in'; + this.zoomHandler.zoom(); + } else { + this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstop', { + bubbles: false, + cancelable: true, + })); + this.canvas.style.cursor = ''; + this.zoomHandler.cancel(); + } } else if (reason === UpdateReasons.DRAW) { const data: DrawData = this.controller.drawData; if (data.enabled) { @@ -717,7 +771,20 @@ export class CanvasViewImpl implements CanvasView, Listener { this.groupHandler.cancel(); } else if (this.mode === Mode.EDIT) { this.editHandler.cancel(); + } else if (this.mode === Mode.DRAG_CANVAS) { + this.canvas.dispatchEvent(new CustomEvent('canvas.dragstop', { + bubbles: false, + cancelable: true, + })); + } else if (this.mode === Mode.ZOOM_CANVAS) { + this.zoomHandler.cancel(); + this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstop', { + bubbles: false, + cancelable: true, + })); } + this.mode = Mode.IDLE; + this.canvas.style.cursor = ''; } } @@ -871,7 +938,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.selectize(true, shape); } - let shapeSizeElement: ShapeSizeElement = null; + let shapeSizeElement: ShapeSizeElement | null = null; let resized = false; (shape as any).resize().on('resizestart', (): void => { this.mode = Mode.RESIZE; @@ -980,7 +1047,7 @@ export class CanvasViewImpl implements CanvasView, Listener { id: `cvat_canvas_shape_${state.clientID}`, fill: state.color, 'shape-rendering': 'geometricprecision', - stroke: darker(state.color, 50), + stroke: darker(state.color, 20), 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, zOrder: state.zOrder, }).move(xtl, ytl) @@ -1000,7 +1067,7 @@ export class CanvasViewImpl implements CanvasView, Listener { id: `cvat_canvas_shape_${state.clientID}`, fill: state.color, 'shape-rendering': 'geometricprecision', - stroke: darker(state.color, 50), + stroke: darker(state.color, 20), 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, zOrder: state.zOrder, }).addClass('cvat_canvas_shape'); @@ -1019,7 +1086,7 @@ export class CanvasViewImpl implements CanvasView, Listener { id: `cvat_canvas_shape_${state.clientID}`, fill: state.color, 'shape-rendering': 'geometricprecision', - stroke: darker(state.color, 50), + stroke: darker(state.color, 20), 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, zOrder: state.zOrder, }).addClass('cvat_canvas_shape'); diff --git a/cvat-canvas/src/typescript/consts.ts b/cvat-canvas/src/typescript/consts.ts index 69a926d15ea..7d0b65d3c60 100644 --- a/cvat-canvas/src/typescript/consts.ts +++ b/cvat-canvas/src/typescript/consts.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MIT */ -const BASE_STROKE_WIDTH = 2; +const BASE_STROKE_WIDTH = 1.75; const BASE_GRID_WIDTH = 1; const BASE_POINT_SIZE = 5; const TEXT_MARGIN = 10; diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 0c27618b1bb..7e8e7690122 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -26,6 +26,7 @@ import { export interface DrawHandler { draw(drawData: DrawData, geometry: Geometry): void; + transform(geometry: Geometry): void; cancel(): void; } diff --git a/cvat-canvas/src/typescript/groupHandler.ts b/cvat-canvas/src/typescript/groupHandler.ts index 9b6f1ec38b3..b16a6a1c420 100644 --- a/cvat-canvas/src/typescript/groupHandler.ts +++ b/cvat-canvas/src/typescript/groupHandler.ts @@ -10,16 +10,17 @@ export interface GroupHandler { group(groupData: GroupData): void; select(state: any): void; cancel(): void; + resetSelectedObjects(): void; } export class GroupHandlerImpl implements GroupHandler { // callback is used to notify about grouping end - private onGroupDone: (objects: any[]) => void; + private onGroupDone: (objects?: any[]) => void; private getStates: () => any[]; private onFindObject: (event: MouseEvent) => void; - private onSelectStart: (event: MouseEvent) => void; - private onSelectUpdate: (event: MouseEvent) => void; - private onSelectStop: (event: MouseEvent) => void; + private bindedOnSelectStart: (event: MouseEvent) => void; + private bindedOnSelectUpdate: (event: MouseEvent) => void; + private bindedOnSelectStop: (event: MouseEvent) => void; private selectionRect: SVG.Rect; private startSelectionPoint: { x: number; @@ -27,7 +28,7 @@ export class GroupHandlerImpl implements GroupHandler { }; private canvas: SVG.Container; private initialized: boolean; - private states: any[]; + private statesToBeGroupped: any[]; private highlightedShapes: Record; private getSelectionBox(event: MouseEvent): { @@ -53,19 +54,72 @@ export class GroupHandlerImpl implements GroupHandler { }; } + private onSelectStart(event: MouseEvent): void { + if (!this.selectionRect) { + const point = translateToSVG( + this.canvas.node as any as SVGSVGElement, + [event.clientX, event.clientY], + ); + this.startSelectionPoint = { + x: point[0], + y: point[1], + }; + + this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping'); + this.selectionRect.attr({ ...this.startSelectionPoint }); + } + } + + private onSelectUpdate(event: MouseEvent): void { + // called on mousemove + if (this.selectionRect) { + const box = this.getSelectionBox(event); + + this.selectionRect.attr({ + x: box.xtl, + y: box.ytl, + width: box.xbr - box.xtl, + height: box.ybr - box.ytl, + }); + } + } + + private onSelectStop(event: MouseEvent): void { + // called on mouseup, mouseleave + if (this.selectionRect) { + this.selectionRect.remove(); + this.selectionRect = null; + + const box = this.getSelectionBox(event); + const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members; + for (const shape of shapes) { + // TODO: Doesn't work properly for groups + const bbox = shape.bbox(); + const clientID = shape.attr('clientID'); + if (bbox.x > box.xtl && bbox.y > box.ytl + && bbox.x + bbox.width < box.xbr + && bbox.y + bbox.height < box.ybr + && !(clientID in this.highlightedShapes)) { + const objectState = this.getStates() + .filter((state: any): boolean => state.clientID === clientID)[0]; + + if (objectState) { + this.statesToBeGroupped.push(objectState); + this.highlightedShapes[clientID] = shape; + (shape as any).addClass('cvat_canvas_shape_grouping'); + } + } + } + } + } + private release(): void { this.canvas.node.removeEventListener('click', this.onFindObject); - this.canvas.node.removeEventListener('mousedown', this.onSelectStart); - this.canvas.node.removeEventListener('mousemove', this.onSelectUpdate); - this.canvas.node.removeEventListener('mouseup', this.onSelectStop); - this.canvas.node.removeEventListener('mouseleave', this.onSelectStop); + this.canvas.node.removeEventListener('mousedown', this.bindedOnSelectStart); + this.canvas.node.removeEventListener('mousemove', this.bindedOnSelectUpdate); + this.canvas.node.removeEventListener('mouseup', this.bindedOnSelectStop); - for (const state of this.states) { - const shape = this.highlightedShapes[state.clientID]; - shape.removeClass('cvat_canvas_shape_grouping'); - } - this.states = []; - this.highlightedShapes = {}; + this.resetSelectedObjects(); this.initialized = false; this.selectionRect = null; this.startSelectionPoint = { @@ -76,29 +130,28 @@ export class GroupHandlerImpl implements GroupHandler { private initGrouping(): void { this.canvas.node.addEventListener('click', this.onFindObject); - this.canvas.node.addEventListener('mousedown', this.onSelectStart); - this.canvas.node.addEventListener('mousemove', this.onSelectUpdate); - this.canvas.node.addEventListener('mouseup', this.onSelectStop); - this.canvas.node.addEventListener('mouseleave', this.onSelectStop); + this.canvas.node.addEventListener('mousedown', this.bindedOnSelectStart); + this.canvas.node.addEventListener('mousemove', this.bindedOnSelectUpdate); + this.canvas.node.addEventListener('mouseup', this.bindedOnSelectStop); this.initialized = true; } private closeGrouping(): void { if (this.initialized) { - const { states } = this; + const { statesToBeGroupped } = this; this.release(); - if (states.length) { - this.onGroupDone(states); + if (statesToBeGroupped.length) { + this.onGroupDone(statesToBeGroupped); } else { - this.onGroupDone(null); + this.onGroupDone(); } } } public constructor( - onGroupDone: (objects: any[]) => void, + onGroupDone: (objects?: any[]) => void, getStates: () => any[], onFindObject: (event: MouseEvent) => void, canvas: SVG.Container, @@ -107,69 +160,18 @@ export class GroupHandlerImpl implements GroupHandler { this.getStates = getStates; this.onFindObject = onFindObject; this.canvas = canvas; - this.states = []; + this.statesToBeGroupped = []; this.highlightedShapes = {}; this.selectionRect = null; + this.initialized = false; this.startSelectionPoint = { x: null, y: null, }; - this.onSelectStart = function (event: MouseEvent): void { - if (!this.selectionRect) { - const point = translateToSVG(this.canvas.node, [event.clientX, event.clientY]); - this.startSelectionPoint = { - x: point[0], - y: point[1], - }; - - this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping'); - this.selectionRect.attr({ ...this.startSelectionPoint }); - } - }.bind(this); - - this.onSelectUpdate = function (event: MouseEvent): void { - // called on mousemove - if (this.selectionRect) { - const box = this.getSelectionBox(event); - - this.selectionRect.attr({ - x: box.xtl, - y: box.ytl, - width: box.xbr - box.xtl, - height: box.ybr - box.ytl, - }); - } - }.bind(this); - - this.onSelectStop = function (event: MouseEvent): void { - // called on mouseup, mouseleave - if (this.selectionRect) { - this.selectionRect.remove(); - this.selectionRect = null; - - const box = this.getSelectionBox(event); - const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members; - for (const shape of shapes) { - // TODO: Doesn't work properly for groups - const bbox = shape.bbox(); - const clientID = shape.attr('clientID'); - if (bbox.x > box.xtl && bbox.y > box.ytl - && bbox.x + bbox.width < box.xbr - && bbox.y + bbox.height < box.ybr - && !(clientID in this.highlightedShapes)) { - const objectState = this.getStates() - .filter((state: any): boolean => state.clientID === clientID)[0]; - - if (objectState) { - this.states.push(objectState); - this.highlightedShapes[clientID] = shape; - (shape as any).addClass('cvat_canvas_shape_grouping'); - } - } - } - } - }.bind(this); + this.bindedOnSelectStart = this.onSelectStart.bind(this); + this.bindedOnSelectUpdate = this.onSelectUpdate.bind(this); + this.bindedOnSelectStop = this.onSelectStop.bind(this); } /* eslint-disable-next-line */ @@ -182,11 +184,11 @@ export class GroupHandlerImpl implements GroupHandler { } public select(objectState: any): void { - const stateIndexes = this.states.map((state): number => state.clientID); + const stateIndexes = this.statesToBeGroupped.map((state): number => state.clientID); const includes = stateIndexes.indexOf(objectState.clientID); if (includes !== -1) { const shape = this.highlightedShapes[objectState.clientID]; - this.states.splice(includes, 1); + this.statesToBeGroupped.splice(includes, 1); if (shape) { delete this.highlightedShapes[objectState.clientID]; shape.removeClass('cvat_canvas_shape_grouping'); @@ -194,15 +196,24 @@ export class GroupHandlerImpl implements GroupHandler { } else { const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); if (shape) { - this.states.push(objectState); + this.statesToBeGroupped.push(objectState); this.highlightedShapes[objectState.clientID] = shape; shape.addClass('cvat_canvas_shape_grouping'); } } } + public resetSelectedObjects(): void { + for (const state of this.statesToBeGroupped) { + const shape = this.highlightedShapes[state.clientID]; + shape.removeClass('cvat_canvas_shape_grouping'); + } + this.statesToBeGroupped = []; + this.highlightedShapes = {}; + } + public cancel(): void { this.release(); - this.onGroupDone(null); + this.onGroupDone(); } } diff --git a/cvat-canvas/src/typescript/mergeHandler.ts b/cvat-canvas/src/typescript/mergeHandler.ts index a19961d8764..6e96fb43070 100644 --- a/cvat-canvas/src/typescript/mergeHandler.ts +++ b/cvat-canvas/src/typescript/mergeHandler.ts @@ -5,6 +5,7 @@ export interface MergeHandler { merge(mergeData: MergeData): void; select(state: any): void; cancel(): void; + repeatSelection(): void; } @@ -14,7 +15,7 @@ export class MergeHandlerImpl implements MergeHandler { private onFindObject: (event: MouseEvent) => void; private canvas: SVG.Container; private initialized: boolean; - private states: any[]; // are being merged + private statesToBeMerged: any[]; // are being merged private highlightedShapes: Record; private constraints: { labelID: number; @@ -22,7 +23,7 @@ export class MergeHandlerImpl implements MergeHandler { }; private addConstraints(): void { - const shape = this.states[0]; + const shape = this.statesToBeMerged[0]; this.constraints = { labelID: shape.label.id, shapeType: shape.shapeType, @@ -41,11 +42,11 @@ export class MergeHandlerImpl implements MergeHandler { private release(): void { this.removeConstraints(); this.canvas.node.removeEventListener('click', this.onFindObject); - for (const state of this.states) { + for (const state of this.statesToBeMerged) { const shape = this.highlightedShapes[state.clientID]; shape.removeClass('cvat_canvas_shape_merging'); } - this.states = []; + this.statesToBeMerged = []; this.highlightedShapes = {}; this.initialized = false; } @@ -57,11 +58,11 @@ export class MergeHandlerImpl implements MergeHandler { private closeMerging(): void { if (this.initialized) { - const { states } = this; + const { statesToBeMerged } = this; this.release(); - if (states.length > 1) { - this.onMergeDone(states); + if (statesToBeMerged.length > 1) { + this.onMergeDone(statesToBeMerged); } else { this.onMergeDone(null); // here is a cycle @@ -79,7 +80,7 @@ export class MergeHandlerImpl implements MergeHandler { this.onMergeDone = onMergeDone; this.onFindObject = onFindObject; this.canvas = canvas; - this.states = []; + this.statesToBeMerged = []; this.highlightedShapes = {}; this.constraints = null; this.initialized = false; @@ -94,35 +95,45 @@ export class MergeHandlerImpl implements MergeHandler { } public select(objectState: any): void { - const stateIndexes = this.states.map((state): number => state.clientID); - const stateFrames = this.states.map((state): number => state.frame); + const stateIndexes = this.statesToBeMerged.map((state): number => state.clientID); + const stateFrames = this.statesToBeMerged.map((state): number => state.frame); const includes = stateIndexes.indexOf(objectState.clientID); if (includes !== -1) { const shape = this.highlightedShapes[objectState.clientID]; - this.states.splice(includes, 1); + this.statesToBeMerged.splice(includes, 1); if (shape) { delete this.highlightedShapes[objectState.clientID]; shape.removeClass('cvat_canvas_shape_merging'); } - if (!this.states.length) { + if (!this.statesToBeMerged.length) { this.removeConstraints(); } } else { const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); if (shape && this.checkConstraints(objectState) && !stateFrames.includes(objectState.frame)) { - this.states.push(objectState); + this.statesToBeMerged.push(objectState); this.highlightedShapes[objectState.clientID] = shape; shape.addClass('cvat_canvas_shape_merging'); - if (this.states.length === 1) { + if (this.statesToBeMerged.length === 1) { this.addConstraints(); } } } } + public repeatSelection(): void { + for (const objectState of this.statesToBeMerged) { + const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); + if (shape) { + this.highlightedShapes[objectState.clientID] = shape; + shape.addClass('cvat_canvas_shape_merging'); + } + } + } + public cancel(): void { this.release(); this.onMergeDone(null); diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 37cecd2b6e3..226dc95bde9 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -26,11 +26,11 @@ export interface BBox { y: number; } -// Translate point array from the client coordinate system -// to a coordinate system of a canvas +// Translate point array from the canvas coordinate system +// to the coordinate system of a client export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] { const output = []; - const transformationMatrix = svg.getScreenCTM(); + const transformationMatrix = svg.getScreenCTM() as DOMMatrix; let pt = svg.createSVGPoint(); for (let i = 0; i < points.length - 1; i += 2) { pt.x = points[i]; @@ -42,11 +42,11 @@ export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] return output; } -// Translate point array from a coordinate system of a canvas -// to the client coordinate system +// Translate point array from the coordinate system of a client +// to the canvas coordinate system export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] { const output = []; - const transformationMatrix = svg.getScreenCTM().inverse(); + const transformationMatrix = (svg.getScreenCTM() as DOMMatrix).inverse(); let pt = svg.createSVGPoint(); for (let i = 0; i < points.length; i += 2) { pt.x = points[i]; diff --git a/cvat-canvas/src/typescript/zoomHandler.ts b/cvat-canvas/src/typescript/zoomHandler.ts new file mode 100644 index 00000000000..bc0a6597496 --- /dev/null +++ b/cvat-canvas/src/typescript/zoomHandler.ts @@ -0,0 +1,141 @@ +import * as SVG from 'svg.js'; +import consts from './consts'; + +import { + translateToSVG, +} from './shared'; + +import { + Geometry, +} from './canvasModel'; + + +export interface ZoomHandler { + zoom(): void; + cancel(): void; + transform(geometry: Geometry): void; +} + +export class ZoomHandlerImpl implements ZoomHandler { + private onZoomRegion: (x: number, y: number, width: number, height: number) => void; + private bindedOnSelectStart: (event: MouseEvent) => void; + private bindedOnSelectUpdate: (event: MouseEvent) => void; + private bindedOnSelectStop: (event: MouseEvent) => void; + private geometry: Geometry; + private canvas: SVG.Container; + private selectionRect: SVG.Rect | null; + private startSelectionPoint: { + x: number; + y: number; + }; + + private onSelectStart(event: MouseEvent): void { + if (!this.selectionRect && event.which === 1) { + const point = translateToSVG( + (this.canvas.node as any as SVGSVGElement), + [event.clientX, event.clientY], + ); + this.startSelectionPoint = { + x: point[0], + y: point[1], + }; + + this.selectionRect = this.canvas.rect().addClass('cvat_canvas_zoom_selection'); + this.selectionRect.attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + ...this.startSelectionPoint, + }); + } + } + + private getSelectionBox(event: MouseEvent): { + x: number; + y: number; + width: number; + height: number; + } { + const point = translateToSVG( + (this.canvas.node as any as SVGSVGElement), + [event.clientX, event.clientY], + ); + const stopSelectionPoint = { + x: point[0], + y: point[1], + }; + + const xtl = Math.min(this.startSelectionPoint.x, stopSelectionPoint.x); + const ytl = Math.min(this.startSelectionPoint.y, stopSelectionPoint.y); + const xbr = Math.max(this.startSelectionPoint.x, stopSelectionPoint.x); + const ybr = Math.max(this.startSelectionPoint.y, stopSelectionPoint.y); + + return { + x: xtl, + y: ytl, + width: xbr - xtl, + height: ybr - ytl, + }; + } + + private onSelectUpdate(event: MouseEvent): void { + if (this.selectionRect) { + this.selectionRect.attr({ + ...this.getSelectionBox(event), + }); + } + } + + private onSelectStop(event: MouseEvent): void { + if (this.selectionRect) { + const box = this.getSelectionBox(event); + this.selectionRect.remove(); + this.selectionRect = null; + this.startSelectionPoint = { + x: 0, + y: 0, + }; + const threshold = 5; + if (box.width > threshold && box.height > threshold) { + this.onZoomRegion(box.x, box.y, box.width, box.height); + } + } + } + + public constructor( + onZoomRegion: (x: number, y: number, width: number, height: number) => void, + canvas: SVG.Container, + geometry: Geometry, + ) { + this.onZoomRegion = onZoomRegion; + this.canvas = canvas; + this.geometry = geometry; + this.selectionRect = null; + this.startSelectionPoint = { + x: 0, + y: 0, + }; + this.bindedOnSelectStart = this.onSelectStart.bind(this); + this.bindedOnSelectUpdate = this.onSelectUpdate.bind(this); + this.bindedOnSelectStop = this.onSelectStop.bind(this); + } + + public zoom(): void { + this.canvas.node.addEventListener('mousedown', this.bindedOnSelectStart); + this.canvas.node.addEventListener('mousemove', this.bindedOnSelectUpdate); + this.canvas.node.addEventListener('mouseup', this.bindedOnSelectStop); + } + + public cancel(): void { + this.canvas.node.removeEventListener('mousedown', this.bindedOnSelectStart); + this.canvas.node.removeEventListener('mousemove', this.bindedOnSelectUpdate); + this.canvas.node.removeEventListener('mouseup ', this.bindedOnSelectStop); + } + + public transform(geometry: Geometry): void { + this.geometry = geometry; + if (this.selectionRect) { + this.selectionRect.style({ + 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, + }); + } + } +} diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index a95b16e7ba5..873d2070f6b 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -36,24 +36,11 @@ ObjectType, } = require('./enums'); const ObjectState = require('./object-state'); - const colors = [ - '#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA', - '#8359A3', '#FBE870', '#C5E17A', '#03BB85', '#FFDF00', '#8B8680', '#0A6B0D', - '#8FD8D8', '#A36F40', '#F653A6', '#CA3435', '#FFCBA4', '#FF99CC', '#FA9D5A', - '#FFAE42', '#A78B00', '#788193', '#514E49', '#1164B4', '#F4FA9F', '#FED8B1', - '#C32148', '#01796F', '#E90067', '#FF91A4', '#404E5A', '#6CDAE7', '#FFC1CC', - '#006A93', '#867200', '#E2B631', '#6EEB6E', '#FFC800', '#CC99BA', '#FF007C', - '#BC6CAC', '#DCCCD7', '#EBE1C2', '#A6AAAE', '#B99685', '#0086A7', '#5E4330', - '#C8A2C8', '#708EB3', '#BC8777', '#B2592D', '#497E48', '#6A2963', '#E6335F', - '#00755E', '#B5A895', '#0048ba', '#EED9C4', '#C88A65', '#FF6E4A', '#87421F', - '#B2BEB5', '#926F5B', '#00B9FB', '#6456B7', '#DB5079', '#C62D42', '#FA9C44', - '#DA8A67', '#FD7C6E', '#93CCEA', '#FCF686', '#503E32', '#FF5470', '#9DE093', - '#FF7A00', '#4F69C6', '#A50B5E', '#F0E68C', '#FDFF00', '#F091A9', '#FFFF66', - '#6F9940', '#FC74FD', '#652DC1', '#D6AEDD', '#EE34D2', '#BB3385', '#6B3FA0', - '#33CC99', '#FFDB00', '#87FF2A', '#6EEB6E', '#FFC800', '#CC99BA', '#7A89B8', - '#006A93', '#867200', '#E2B631', '#D9D6CF', - ]; + '#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66', + '#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33', + '#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37', + '#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380']; function shapeFactory(shapeData, clientID, injection) { const { type } = shapeData; @@ -416,7 +403,7 @@ } const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b); - if (frame <= +keyframes[0] || frame > keyframes[keyframes.length - 1]) { + if (frame <= +keyframes[0]) { return; } diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index ba4508dffda..0f1a605202f 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -278,6 +278,7 @@ group: this.group, color: this.color, visibility: this.visibility, + frame, }; } @@ -491,6 +492,7 @@ lock: this.lock, color: this.color, visibility: this.visibility, + frame, }; this.cache[frame] = interpolation; @@ -716,9 +718,21 @@ // Add/update keyframe if (positionUpdated || (updated.keyframe && data.keyframe)) { - // Remove all cache after this keyframe because it have just become outdated - for (const cacheFrame in this.cache) { - if (+cacheFrame > frame) { + // Remove affected cached frames + const { + leftFrame, + rightFrame, + } = this.neighborsFrames(frame); + for (const cacheFrame of Object.keys(this.cache)) { + if (leftFrame === null && +cacheFrame < frame) { + delete this.cache[cacheFrame]; + } else if (+cacheFrame < frame && +cacheFrame > leftFrame) { + delete this.cache[cacheFrame]; + } + + if (rightFrame === null && +cacheFrame > frame) { + delete this.cache[cacheFrame]; + } else if (+cacheFrame > frame && +cacheFrame < rightFrame) { delete this.cache[cacheFrame]; } } @@ -860,6 +874,7 @@ attributes: { ...this.attributes }, label: this.label, group: this.group, + frame, }; } diff --git a/cvat-core/src/annotations-saver.js b/cvat-core/src/annotations-saver.js index 80c1bccf985..b4f9e0e7306 100644 --- a/cvat-core/src/annotations-saver.js +++ b/cvat-core/src/annotations-saver.js @@ -200,80 +200,67 @@ }; } - try { - const exported = this.collection.export(); - const { flush } = this.collection; - if (flush) { - onUpdate('New objects are being saved..'); - const indexes = this._receiveIndexes(exported); - const savedData = await this._put({ ...exported, version: this.version }); - this.version = savedData.version; - this.collection.flush = false; - - onUpdate('Saved objects are being updated in the client'); - this._updateCreatedObjects(savedData, indexes); - - onUpdate('Initial state is being updated'); - - this._resetState(); - for (const type of Object.keys(this.initialObjects)) { - for (const object of savedData[type]) { - this.initialObjects[type][object.id] = object; - } + const exported = this.collection.export(); + const { flush } = this.collection; + if (flush) { + onUpdate('Created objects are being saved on the server'); + const indexes = this._receiveIndexes(exported); + const savedData = await this._put({ ...exported, version: this.version }); + this.version = savedData.version; + this.collection.flush = false; + + this._updateCreatedObjects(savedData, indexes); + + this._resetState(); + for (const type of Object.keys(this.initialObjects)) { + for (const object of savedData[type]) { + this.initialObjects[type][object.id] = object; } - } else { - const { - created, - updated, - deleted, - } = this._split(exported); - - onUpdate('New objects are being saved..'); - const indexes = this._receiveIndexes(created); - const createdData = await this._create({ ...created, version: this.version }); - this.version = createdData.version; - - onUpdate('Saved objects are being updated in the client'); - this._updateCreatedObjects(createdData, indexes); - - onUpdate('Initial state is being updated'); - for (const type of Object.keys(this.initialObjects)) { - for (const object of createdData[type]) { - this.initialObjects[type][object.id] = object; - } + } + } else { + const { + created, + updated, + deleted, + } = this._split(exported); + + onUpdate('Created objects are being saved on the server'); + const indexes = this._receiveIndexes(created); + const createdData = await this._create({ ...created, version: this.version }); + this.version = createdData.version; + + this._updateCreatedObjects(createdData, indexes); + + for (const type of Object.keys(this.initialObjects)) { + for (const object of createdData[type]) { + this.initialObjects[type][object.id] = object; } + } - onUpdate('Changed objects are being saved..'); - this._receiveIndexes(updated); - const updatedData = await this._update({ ...updated, version: this.version }); - this.version = updatedData.version; + onUpdate('Updated objects are being saved on the server'); + this._receiveIndexes(updated); + const updatedData = await this._update({ ...updated, version: this.version }); + this.version = updatedData.version; - onUpdate('Initial state is being updated'); - for (const type of Object.keys(this.initialObjects)) { - for (const object of updatedData[type]) { - this.initialObjects[type][object.id] = object; - } + for (const type of Object.keys(this.initialObjects)) { + for (const object of updatedData[type]) { + this.initialObjects[type][object.id] = object; } + } - onUpdate('Changed objects are being saved..'); - this._receiveIndexes(deleted); - const deletedData = await this._delete({ ...deleted, version: this.version }); - this._version = deletedData.version; + onUpdate('Deleted objects are being deleted from the server'); + this._receiveIndexes(deleted); + const deletedData = await this._delete({ ...deleted, version: this.version }); + this._version = deletedData.version; - onUpdate('Initial state is being updated'); - for (const type of Object.keys(this.initialObjects)) { - for (const object of deletedData[type]) { - delete this.initialObjects[type][object.id]; - } + for (const type of Object.keys(this.initialObjects)) { + for (const object of deletedData[type]) { + delete this.initialObjects[type][object.id]; } } - - this.hash = this._getHash(); - onUpdate('Saving is done'); - } catch (error) { - onUpdate(`Can not save annotations: ${error.message}`); - throw error; } + + this.hash = this._getHash(); } hasUnsavedChanges() { diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 55ff05b35d1..f02e073cd62 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -26,9 +26,9 @@ return result; }, - async save() { + async save(onUpdate) { const result = await PluginRegistry - .apiWrapper.call(this, prototype.annotations.save); + .apiWrapper.call(this, prototype.annotations.save, onUpdate); return result; }, diff --git a/cvat-ui/.eslintrc.js b/cvat-ui/.eslintrc.js index f40cac1879f..279f581cca0 100644 --- a/cvat-ui/.eslintrc.js +++ b/cvat-ui/.eslintrc.js @@ -39,9 +39,9 @@ module.exports = { }, 'settings': { 'import/resolver': { - 'node': { - 'extensions': ['.tsx', '.ts', '.jsx', '.js', '.json'], - }, + 'typescript': { + 'directory': './tsconfig.json' + } }, }, }; diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 41ef8cc917d..d7edd37f5a0 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1002,6 +1002,12 @@ "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3849,6 +3855,19 @@ } } }, + "eslint-import-resolver-typescript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.0.0.tgz", + "integrity": "sha512-bT5Frpl8UWoHBtY25vKUOMoVIMlJQOMefHLyQ4Tz3MQpIZ2N6yYKEEIHMo38bszBNUuMBW6M3+5JNYxeiGFH4w==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "is-glob": "^4.0.1", + "resolve": "^1.12.0", + "tiny-glob": "^0.2.6", + "tsconfig-paths": "^3.9.0" + } + }, "eslint-module-utils": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", @@ -5408,6 +5427,12 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, + "globalyzer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.4.tgz", + "integrity": "sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==", + "dev": true + }, "globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", @@ -5429,6 +5454,12 @@ } } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "globule": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", @@ -11116,6 +11147,16 @@ "setimmediate": "^1.0.4" } }, + "tiny-glob": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.6.tgz", + "integrity": "sha512-A7ewMqPu1B5PWwC3m7KVgAu96Ch5LA0w4SnEN/LbDREj/gAD0nPWboRbn8YoP9ISZXqeNAlMvKSKoEuhcfK3Pw==", + "dev": true, + "requires": { + "globalyzer": "^0.1.0", + "globrex": "^0.1.1" + } + }, "tiny-invariant": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", @@ -11234,6 +11275,40 @@ "glob": "^7.1.2" } }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + } + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "tsconfig-paths": "^3.4.0" + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index eb3ca983d9e..8d6f75f58ce 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -20,6 +20,7 @@ "babel-plugin-import": "^1.12.2", "css-loader": "^3.2.0", "eslint-config-airbnb-typescript": "^4.0.1", + "eslint-import-resolver-typescript": "^2.0.0", "eslint-plugin-import": "^2.18.2", "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-react": "^7.17.0", @@ -33,6 +34,7 @@ "react-svg-loader": "^3.0.3", "sass-loader": "^8.0.0", "style-loader": "^1.0.0", + "tsconfig-paths-webpack-plugin": "^3.2.0", "typescript": "^3.7.3", "webpack": "^4.41.2", "webpack-cli": "^3.3.8", diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 904fa2a89ec..11603ac51c3 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -3,11 +3,14 @@ import { ThunkAction } from 'redux-thunk'; import { CombinedState, + ActiveControl, + ShapeType, + ObjectType, Task, -} from '../reducers/interfaces'; +} from 'reducers/interfaces'; -import getCore from '../core'; -import { getCVATStore } from '../store'; +import getCore from 'cvat-core'; +import { getCVATStore } from 'cvat-store'; const cvat = getCore(); @@ -18,8 +21,24 @@ export enum AnnotationActionTypes { CHANGE_FRAME = 'CHANGE_FRAME', CHANGE_FRAME_SUCCESS = 'CHANGE_FRAME_SUCCESS', CHANGE_FRAME_FAILED = 'CHANGE_FRAME_FAILED', + SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS', + SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS', + SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED', + SAVE_ANNOTATIONS_UPDATED_STATUS = 'SAVE_ANNOTATIONS_UPDATED_STATUS', SWITCH_PLAY = 'SWITCH_PLAY', CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY', + DRAG_CANVAS = 'DRAG_CANVAS', + ZOOM_CANVAS = 'ZOOM_CANVAS', + DRAW_SHAPE = 'DRAW_SHAPE', + SHAPE_DRAWN = 'SHAPE_DRAWN', + MERGE_OBJECTS = 'MERGE_OBJECTS', + OBJECTS_MERGED = 'OBJECTS_MERGED', + GROUP_OBJECTS = 'GROUP_OBJECTS', + OBJECTS_GROUPPED = 'OBJECTS_GROUPPED', + SPLIT_TRACK = 'SPLIT_TRACK', + TRACK_SPLITTED = 'TRACK_SPLITTED', + RESET_CANVAS = 'RESET_CANVAS', + ANNOTATIONS_UPDATED = 'ANNOTATIONS_UPDATED', } export function switchPlay(playing: boolean): AnyAction { @@ -76,6 +95,31 @@ ThunkAction, {}, {}, AnyAction> { }; } +export function dragCanvas(enabled: boolean): AnyAction { + return { + type: AnnotationActionTypes.DRAG_CANVAS, + payload: { + enabled, + }, + }; +} + +export function zoomCanvas(enabled: boolean): AnyAction { + return { + type: AnnotationActionTypes.ZOOM_CANVAS, + payload: { + enabled, + }, + }; +} + +export function resetCanvas(): AnyAction { + return { + type: AnnotationActionTypes.RESET_CANVAS, + payload: {}, + }; +} + export function confirmCanvasReady(): AnyAction { return { type: AnnotationActionTypes.CONFIRM_CANVAS_READY, @@ -130,3 +174,121 @@ ThunkAction, {}, {}, AnyAction> { } }; } + +export function saveAnnotationsAsync(sessionInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + dispatch({ + type: AnnotationActionTypes.SAVE_ANNOTATIONS, + payload: {}, + }); + + try { + await sessionInstance.annotations.save((status: string) => { + dispatch({ + type: AnnotationActionTypes.SAVE_ANNOTATIONS_UPDATED_STATUS, + payload: { + status, + }, + }); + }); + + dispatch({ + type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS, + payload: {}, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function drawShape( + shapeType: ShapeType, + labelID: number, + objectType: ObjectType, + points?: number, +): AnyAction { + let activeControl = ActiveControl.DRAW_RECTANGLE; + if (shapeType === ShapeType.POLYGON) { + activeControl = ActiveControl.DRAW_POLYGON; + } else if (shapeType === ShapeType.POLYLINE) { + activeControl = ActiveControl.DRAW_POLYLINE; + } else if (shapeType === ShapeType.POINTS) { + activeControl = ActiveControl.DRAW_POINTS; + } + + return { + type: AnnotationActionTypes.DRAW_SHAPE, + payload: { + shapeType, + labelID, + objectType, + points, + activeControl, + }, + }; +} + +export function shapeDrawn(): AnyAction { + return { + type: AnnotationActionTypes.SHAPE_DRAWN, + payload: {}, + }; +} + +export function mergeObjects(): AnyAction { + return { + type: AnnotationActionTypes.MERGE_OBJECTS, + payload: {}, + }; +} + +export function objectsMerged(): AnyAction { + return { + type: AnnotationActionTypes.OBJECTS_MERGED, + payload: {}, + }; +} + +export function groupObjects(): AnyAction { + return { + type: AnnotationActionTypes.GROUP_OBJECTS, + payload: {}, + }; +} + +export function objectsGroupped(): AnyAction { + return { + type: AnnotationActionTypes.OBJECTS_GROUPPED, + payload: {}, + }; +} + +export function splitTrack(): AnyAction { + return { + type: AnnotationActionTypes.SPLIT_TRACK, + payload: {}, + }; +} + +export function trackSplitted(): AnyAction { + return { + type: AnnotationActionTypes.TRACK_SPLITTED, + payload: {}, + }; +} + +export function annotationsUpdated(annotations: any[]): AnyAction { + return { + type: AnnotationActionTypes.ANNOTATIONS_UPDATED, + payload: { + annotations, + }, + }; +} diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index a8b9748f03b..d225f2d5982 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -1,7 +1,7 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import getCore from '../core'; +import getCore from 'cvat-core'; const cvat = getCore(); diff --git a/cvat-ui/src/actions/formats-actions.ts b/cvat-ui/src/actions/formats-actions.ts index a3e74685a9d..d34510c87d6 100644 --- a/cvat-ui/src/actions/formats-actions.ts +++ b/cvat-ui/src/actions/formats-actions.ts @@ -1,7 +1,7 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import getCore from '../core'; +import getCore from 'cvat-core'; const cvat = getCore(); diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index e8b60da5609..f120d11d923 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -1,8 +1,8 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import getCore from '../core'; -import { getCVATStore } from '../store'; +import getCore from 'cvat-core'; +import { getCVATStore } from 'cvat-store'; import { Model, ModelFiles, diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 0e27013fa4f..26e9faaac72 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -1,7 +1,7 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import { SupportedPlugins } from '../reducers/interfaces'; -import PluginChecker from '../utils/plugin-checker'; +import { SupportedPlugins } from 'reducers/interfaces'; +import PluginChecker from 'utils/plugin-checker'; export enum PluginsActionTypes { CHECK_PLUGINS = 'CHECK_PLUGINS', diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 6371f70f1fc..33802c780f8 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -1,7 +1,7 @@ import { AnyAction } from 'redux'; import { GridColor, -} from '../reducers/interfaces'; +} from 'reducers/interfaces'; export enum SettingsActionTypes { SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL', diff --git a/cvat-ui/src/actions/share-actions.ts b/cvat-ui/src/actions/share-actions.ts index 47fbcb90145..978f4fa8655 100644 --- a/cvat-ui/src/actions/share-actions.ts +++ b/cvat-ui/src/actions/share-actions.ts @@ -1,8 +1,8 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import { ShareFileInfo } from '../reducers/interfaces'; -import getCore from '../core'; +import { ShareFileInfo } from 'reducers/interfaces'; +import getCore from 'cvat-core'; const core = getCore(); diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 015c12ff336..fa9c3a5851f 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -1,10 +1,9 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import { TasksQuery } from '../reducers/interfaces'; +import { TasksQuery } from 'reducers/interfaces'; +import getCore from 'cvat-core'; import { getInferenceStatusAsync } from './models-actions'; -import getCore from '../core'; - const cvat = getCore(); export enum TasksActionTypes { diff --git a/cvat-ui/src/actions/users-actions.ts b/cvat-ui/src/actions/users-actions.ts index 3d75197a5c2..d69ca0246d1 100644 --- a/cvat-ui/src/actions/users-actions.ts +++ b/cvat-ui/src/actions/users-actions.ts @@ -1,7 +1,7 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import getCore from '../core'; +import getCore from 'cvat-core'; const core = getCore(); diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index bf6155c8dc5..11e32c1349c 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -7,8 +7,8 @@ import { Result, } from 'antd'; -import AnnotationTopBarContainer from '../../containers/annotation-page/top-bar/top-bar'; -import StandardWorkspaceContainer from '../../containers/annotation-page/standard-workspace/standard-workspace'; +import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; +import StandardWorkspaceContainer from 'containers/annotation-page/standard-workspace/standard-workspace'; interface Props { jobInstance: any | null | undefined; 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 abd5dbc2bb2..947e264becd 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 @@ -6,20 +6,40 @@ import { import { GridColor, -} from '../../../reducers/interfaces'; + ObjectType, +} from 'reducers/interfaces'; -import { Canvas } from '../../../canvas'; +import { + Canvas, +} from 'cvat-canvas'; + +import getCore from 'cvat-core'; + +const cvat = getCore(); + +const MAX_DISTANCE_TO_OPEN_SHAPE = 50; interface Props { canvasInstance: Canvas; jobInstance: any; annotations: any[]; frameData: any; + frame: number; grid: boolean; gridSize: number; gridColor: GridColor; gridOpacity: number; + activeLabelID: number; + activeObjectType: ObjectType; onSetupCanvas: () => void; + onDragCanvas: (enabled: boolean) => void; + onZoomCanvas: (enabled: boolean) => void; + onShapeDrawn: () => void; + onObjectsMerged: () => void; + onObjectsGroupped: () => void; + onTrackSplitted: () => void; + onResetCanvas: () => void; + onAnnotationsUpdated: (annotations: any[]) => void; } export default class CanvasWrapperComponent extends React.PureComponent { @@ -75,6 +95,118 @@ export default class CanvasWrapperComponent extends React.PureComponent { this.updateCanvas(); } + private async onShapeDrawn(event: any): Promise { + const { + jobInstance, + activeLabelID, + activeObjectType, + frame, + onShapeDrawn, + onAnnotationsUpdated, + } = this.props; + + onShapeDrawn(); + + const { state } = event.detail; + if (!state.objectType) { + state.objectType = activeObjectType; + } + + if (!state.label) { + [state.label] = jobInstance.task.labels + .filter((label: any) => label.id === activeLabelID); + } + + if (!state.occluded) { + state.occluded = false; + } + + state.frame = frame; + const objectState = new cvat.classes.ObjectState(state); + await jobInstance.annotations.put([objectState]); + + const annotations = await jobInstance.annotations.get(frame); + onAnnotationsUpdated(annotations); + } + + private async onShapeEdited(event: any): Promise { + const { + jobInstance, + frame, + onAnnotationsUpdated, + } = this.props; + + const { + state, + points, + } = event.detail; + state.points = points; + state.save(); + + const annotations = await jobInstance.annotations.get(frame); + onAnnotationsUpdated(annotations); + } + + private async onObjectsMerged(event: any): Promise { + const { + jobInstance, + frame, + onAnnotationsUpdated, + onObjectsMerged, + } = this.props; + + onObjectsMerged(); + + const { states } = event.detail; + await jobInstance.annotations.merge(states); + const annotations = await jobInstance.annotations.get(frame); + onAnnotationsUpdated(annotations); + } + + private async onObjectsGroupped(event: any): Promise { + const { + jobInstance, + frame, + onAnnotationsUpdated, + onObjectsGroupped, + } = this.props; + + onObjectsGroupped(); + + const { states } = event.detail; + await jobInstance.annotations.group(states); + const annotations = await jobInstance.annotations.get(frame); + onAnnotationsUpdated(annotations); + } + + private async onTrackSplitted(event: any): Promise { + const { + jobInstance, + frame, + onAnnotationsUpdated, + onTrackSplitted, + } = this.props; + + onTrackSplitted(); + + const { state } = event.detail; + await jobInstance.annotations.split(state, frame); + const annotations = await jobInstance.annotations.get(frame); + onAnnotationsUpdated(annotations); + } + + private updateCanvas(): void { + const { + annotations, + frameData, + canvasInstance, + } = this.props; + + if (frameData !== null) { + canvasInstance.setup(frameData, annotations); + } + } + private initialSetup(): void { const { grid, @@ -84,6 +216,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance, jobInstance, onSetupCanvas, + onDragCanvas, + onZoomCanvas, + onResetCanvas, } = this.props; // Size @@ -112,18 +247,65 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.fit(); }, { once: true }); - } - private updateCanvas(): void { - const { - annotations, - frameData, - canvasInstance, - } = this.props; + canvasInstance.html().addEventListener('canvas.canceled', () => { + onResetCanvas(); + }); - if (frameData !== null) { - canvasInstance.setup(frameData, annotations); - } + canvasInstance.html().addEventListener('canvas.dragstart', () => { + onDragCanvas(true); + }); + + canvasInstance.html().addEventListener('canvas.dragstop', () => { + onDragCanvas(false); + }); + + canvasInstance.html().addEventListener('canvas.zoomstart', () => { + onZoomCanvas(true); + }); + + canvasInstance.html().addEventListener('canvas.zoomstop', () => { + onZoomCanvas(false); + }); + + canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise => { + const result = await jobInstance.annotations.select( + event.detail.states, + event.detail.x, + event.detail.y, + ); + + if (result && result.state) { + if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { + if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) { + return; + } + } + + canvasInstance.activate(result.state.clientID); + } + }); + + canvasInstance.html().addEventListener('canvas.find', async (e: any) => { + const result = await jobInstance.annotations + .select(e.detail.states, e.detail.x, e.detail.y); + + if (result && result.state) { + if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') { + if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) { + return; + } + } + + canvasInstance.select(result.state); + } + }); + + canvasInstance.html().addEventListener('canvas.edited', this.onShapeEdited.bind(this)); + canvasInstance.html().addEventListener('canvas.drawn', this.onShapeDrawn.bind(this)); + canvasInstance.html().addEventListener('canvas.merged', this.onObjectsMerged.bind(this)); + canvasInstance.html().addEventListener('canvas.groupped', this.onObjectsGroupped.bind(this)); + canvasInstance.html().addEventListener('canvas.splitted', this.onTrackSplitted.bind(this)); } public render(): JSX.Element { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar.tsx deleted file mode 100644 index 2abb67addb9..00000000000 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React from 'react'; - -import { - Icon, - Layout, - Tooltip, - Popover, -} from 'antd'; - -import { - CursorIcon, - MoveIcon, - RotateIcon, - FitIcon, - ZoomIcon, - RectangleIcon, - PolygonIcon, - PointIcon, - PolylineIcon, - TagIcon, - MergeIcon, - GroupIcon, - SplitIcon, -} from '../../../icons'; - -import { - Canvas, - Rotation, -} from '../../../canvas'; - -interface Props { - canvasInstance: Canvas; - rotateAll: boolean; -} - -export default function ControlsSideBarComponent(props: Props): JSX.Element { - const { - rotateAll, - canvasInstance, - } = props; - - return ( - - - - - - - - - - - canvasInstance - .rotate(Rotation.ANTICLOCKWISE90, rotateAll)} - component={RotateIcon} - /> - canvasInstance - .rotate(Rotation.CLOCKWISE90, rotateAll)} - component={RotateIcon} - /> - - )} - trigger='hover' - > - - - - - -
- - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
- ); -} 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 new file mode 100644 index 00000000000..ddbb6db407e --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import { + Icon, + Layout, + Tooltip, +} from 'antd'; + +import { + ActiveControl, +} from 'reducers/interfaces'; + +import { + TagIcon, +} from 'icons'; + +import { + Canvas, +} from 'cvat-canvas'; + +import CursorControl from './cursor-control'; +import MoveControl from './move-control'; +import RotateControl from './rotate-control'; +import FitControl from './fit-control'; +import ResizeControl from './resize-control'; +import DrawRectangleControl from './draw-rectangle-control'; +import DrawPolygonControl from './draw-polygon-control'; +import DrawPolylineControl from './draw-polyline-control'; +import DrawPointsControl from './draw-points-control'; +import MergeControl from './merge-control'; +import GroupControl from './group-control'; +import SplitControl from './split-control'; + +interface Props { + canvasInstance: Canvas; + rotateAll: boolean; + activeControl: ActiveControl; + + onMergeStart(): void; + onGroupStart(): void; + onSplitStart(): void; +} + +export default function ControlsSideBarComponent(props: Props): JSX.Element { + return ( + + + + + +
+ + + + +
+ + + + + + + + + + +
+ + + + +
+ ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx new file mode 100644 index 00000000000..f8c53e1f563 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { + Icon, + Tooltip, +} from 'antd'; + +import { + CursorIcon, +} from 'icons'; + +import { + ActiveControl, +} from 'reducers/interfaces'; + +import { + Canvas, +} from 'cvat-canvas'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function CursorControl(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + } = props; + + return ( + + { + if (activeControl !== ActiveControl.CURSOR) { + canvasInstance.cancel(); + } + }} + /> + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx new file mode 100644 index 00000000000..0c6ba7e0b9e --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + Popover, + Icon, +} from 'antd'; + +import { Canvas } from 'cvat-canvas'; +import { PointIcon } from 'icons'; +import { + ShapeType, + ActiveControl, +} from 'reducers/interfaces'; + +import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function DrawRectangleControl(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + } = props; + + const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POINTS + ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = activeControl === ActiveControl.DRAW_POINTS + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; + + return ( + + )} + > + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx new file mode 100644 index 00000000000..ca9cc605de7 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + Popover, + Icon, +} from 'antd'; + +import { Canvas } from 'cvat-canvas'; +import { PolygonIcon } from 'icons'; +import { + ShapeType, + ActiveControl, +} from 'reducers/interfaces'; + +import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function DrawRectangleControl(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + } = props; + + const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POLYGON + ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = activeControl === ActiveControl.DRAW_POLYGON + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; + + return ( + + )} + > + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx new file mode 100644 index 00000000000..56b3a51cb66 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + Popover, + Icon, +} from 'antd'; + +import { Canvas } from 'cvat-canvas'; +import { PolylineIcon } from 'icons'; +import { + ShapeType, + ActiveControl, +} from 'reducers/interfaces'; + +import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function DrawRectangleControl(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + } = props; + + const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POLYLINE + ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = activeControl === ActiveControl.DRAW_POLYLINE + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; + + return ( + + )} + > + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx new file mode 100644 index 00000000000..c762c593d2c --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { + Popover, + Icon, +} from 'antd'; + +import { Canvas } from 'cvat-canvas'; +import { RectangleIcon } from 'icons'; +import { + ShapeType, + ActiveControl, +} from 'reducers/interfaces'; + +import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function DrawRectangleControl(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + } = props; + + const dynamcPopoverPros = activeControl === ActiveControl.DRAW_RECTANGLE + ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = activeControl === ActiveControl.DRAW_RECTANGLE + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; + + return ( + + )} + > + + + ); +} 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 new file mode 100644 index 00000000000..3af282a35ce --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -0,0 +1,186 @@ +import React from 'react'; + +import { + Row, + Col, + Select, + Button, + InputNumber, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +import { + ShapeType, + ObjectType, + StringObject, +} from 'reducers/interfaces'; + +import { + Canvas, +} from 'cvat-canvas'; + +interface Props { + canvasInstance: Canvas; + shapeType: ShapeType; + labels: StringObject; + + onDrawStart( + shapeType: ShapeType, + labelID: number, + objectType: ObjectType, + points?: number, + ): void; +} + +interface State { + numberOfPoints?: number; + selectedLabeID: number; +} + +function defineMinimumPoints(shapeType: ShapeType): number { + if (shapeType === ShapeType.POLYGON) { + return 3; + } + if (shapeType === ShapeType.POLYLINE) { + return 2; + } + if (shapeType === ShapeType.POINTS) { + return 1; + } + return 0; +} + +export default class DrawShapePopoverComponent extends React.PureComponent { + constructor(props: Props) { + super(props); + const defaultLabelID = +Object.keys(props.labels)[0]; + this.state = { + selectedLabeID: defaultLabelID, + }; + } + + private onChangePoints = (value: number | undefined): void => { + this.setState({ + numberOfPoints: value, + }); + }; + + private onChangeLabel = (value: string): void => { + this.setState({ + selectedLabeID: +value, + }); + }; + + private onDrawTrackStart = (): void => { + this.onDrawStart(ObjectType.TRACK); + }; + + private onDrawShapeStart = (): void => { + this.onDrawStart(ObjectType.SHAPE); + }; + + private onDrawStart = (objectType: ObjectType): void => { + const { + numberOfPoints, + selectedLabeID, + } = this.state; + + const { + shapeType, + onDrawStart, + canvasInstance, + } = this.props; + + canvasInstance.cancel(); + canvasInstance.draw({ + enabled: true, + numberOfPoints, + shapeType, + crosshair: shapeType === ShapeType.RECTANGLE, + }); + + onDrawStart(shapeType, selectedLabeID, + objectType, numberOfPoints); + }; + + public render(): JSX.Element { + const { + selectedLabeID, + } = this.state; + + const { + shapeType, + labels, + } = this.props; + + const minimumPoints = defineMinimumPoints(shapeType); + + return ( +
+ + + {`Draw new ${shapeType}`} + + + + + Label + + + + + + + + { + shapeType !== ShapeType.RECTANGLE && ( + + + Number of points: + + + + + + ) + } + + + + + + + + +
+ ); + } +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx new file mode 100644 index 00000000000..dc35d4d578a --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { + Icon, + Tooltip, +} from 'antd'; + +import { + FitIcon, +} from 'icons'; + +import { + Canvas, +} from 'cvat-canvas'; + +interface Props { + canvasInstance: Canvas; +} + +export default function FitControl(props: Props): JSX.Element { + const { + canvasInstance, + } = props; + + return ( + + canvasInstance.fit()} /> + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx new file mode 100644 index 00000000000..5d35c483afd --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { + Tooltip, + Icon, +} from 'antd'; + +import { + GroupIcon, +} from 'icons'; + +import { Canvas } from 'cvat-canvas'; +import { ActiveControl } from 'reducers/interfaces'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; + + onGroupStart(): void; +} + +export default function GroupControl(props: Props): JSX.Element { + const { + activeControl, + canvasInstance, + onGroupStart, + } = props; + + const dynamicIconProps = activeControl === ActiveControl.GROUP + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.group({ enabled: false }); + }, + } : { + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.group({ enabled: true }); + onGroupStart(); + }, + }; + + return ( + + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx new file mode 100644 index 00000000000..5a2f8d53e42 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { + Tooltip, + Icon, +} from 'antd'; + +import { + MergeIcon, +} from 'icons'; + +import { Canvas } from 'cvat-canvas'; +import { ActiveControl } from 'reducers/interfaces'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; + + onMergeStart(): void; +} + +export default function MergeControl(props: Props): JSX.Element { + const { + activeControl, + canvasInstance, + onMergeStart, + } = props; + + const dynamicIconProps = activeControl === ActiveControl.MERGE + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.merge({ enabled: false }); + }, + } : { + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.merge({ enabled: true }); + onMergeStart(); + }, + }; + + return ( + + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx new file mode 100644 index 00000000000..82fafb5f58c --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { + Icon, + Tooltip, +} from 'antd'; + +import { + MoveIcon, +} from 'icons'; + +import { + ActiveControl, +} from 'reducers/interfaces'; + +import { + Canvas, +} from 'cvat-canvas'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function MoveControl(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + } = props; + + return ( + + { + if (activeControl === ActiveControl.DRAG_CANVAS) { + canvasInstance.dragCanvas(false); + } else { + canvasInstance.cancel(); + canvasInstance.dragCanvas(true); + } + }} + /> + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx new file mode 100644 index 00000000000..6922195c095 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { + Icon, + Tooltip, +} from 'antd'; + +import { + ZoomIcon, +} from 'icons'; + +import { + ActiveControl, +} from 'reducers/interfaces'; + +import { + Canvas, +} from 'cvat-canvas'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; +} + +export default function ResizeControl(props: Props): JSX.Element { + const { + activeControl, + canvasInstance, + } = props; + + return ( + + { + if (activeControl === ActiveControl.ZOOM_CANVAS) { + canvasInstance.zoomCanvas(false); + } else { + canvasInstance.cancel(); + canvasInstance.zoomCanvas(true); + } + }} + /> + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx new file mode 100644 index 00000000000..10cfcb765e9 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { + Icon, + Tooltip, + Popover, +} from 'antd'; + +import { + RotateIcon, +} from 'icons'; + +import { + Rotation, + Canvas, +} from 'cvat-canvas'; + +interface Props { + canvasInstance: Canvas; + rotateAll: boolean; +} + +export default function RotateControl(props: Props): JSX.Element { + const { + rotateAll, + canvasInstance, + } = props; + + return ( + + + canvasInstance + .rotate(Rotation.ANTICLOCKWISE90, rotateAll)} + component={RotateIcon} + /> + + + canvasInstance + .rotate(Rotation.CLOCKWISE90, rotateAll)} + component={RotateIcon} + /> + + + )} + trigger='hover' + > + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx new file mode 100644 index 00000000000..8f41dc51fb1 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { + Tooltip, + Icon, +} from 'antd'; + +import { + SplitIcon, +} from 'icons'; + +import { Canvas } from 'cvat-canvas'; +import { ActiveControl } from 'reducers/interfaces'; + +interface Props { + canvasInstance: Canvas; + activeControl: ActiveControl; + + onSplitStart(): void; +} + +export default function SplitControl(props: Props): JSX.Element { + const { + activeControl, + canvasInstance, + onSplitStart, + } = props; + + const dynamicIconProps = activeControl === ActiveControl.SPLIT + ? { + className: 'cvat-annotation-page-active-control', + onClick: (): void => { + canvasInstance.split({ enabled: false }); + }, + } : { + onClick: (): void => { + canvasInstance.cancel(); + canvasInstance.split({ enabled: true }); + onSplitStart(); + }, + }; + + return ( + + + + ); +} 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 09b490b7597..8f41e67a6ae 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 @@ -5,10 +5,10 @@ import { Layout, } from 'antd'; -import { Canvas } from '../../../canvas'; +import { Canvas } from 'cvat-canvas'; -import CanvasWrapperContainer from '../../../containers/annotation-page/standard-workspace/canvas-wrapper'; -import ControlsSideBarContainer from '../../../containers/annotation-page/standard-workspace/controls-side-bar'; +import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper'; +import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import ObjectSideBarComponent from './objects-side-bar/objects-side-bar'; interface Props { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index 3814e9178dc..a7ba264bb02 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -39,13 +39,17 @@ } } +.cvat-annotation-page-active-control { + background: $header-color; + transform: scale(0.75); +} + .cvat-annotation-page-controls-rotate-left, .cvat-annotation-page-controls-rotate-right { transform: scale(0.65); border-radius: 5px; &:hover { - background: $header-color; transform: scale(0.75); } &:active { @@ -62,4 +66,37 @@ .cvat-annotation-page-controls-rotate-right > svg { transform: scaleX(-1); +} + +.cvat-draw-shape-popover > +.ant-popover-content > +.ant-popover-inner > div > +.ant-popover-inner-content { + padding: 0px; +} + +.cvat-draw-shape-popover-points-selector { + width: 100%; +} + +.cvat-draw-shape-popover-content { + padding: 10px; + border-radius: 5px; + background: $background-color-2; + width: 250px; + > div { + margin-top: 5px; + } + > div:nth-child(3) > div > div { + width: 100%; + } + div:last-child > div > button { + width: 100%; + &:nth-child(1) { + border-radius: 3px 0px 0px 3px; + } + &:nth-child(2) { + border-radius: 0px 3px 3px 0px; + } + } } \ No newline at end of file diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index deb40196044..12ed5d90168 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -53,6 +53,12 @@ } } +.cvat-annotation-disabled-header-button { + @extend .cvat-annotation-header-button; + opacity: 0.5; + pointer-events: none; +} + .cvat-annotation-header-player-group > div { height: 54px; line-height: 0px; diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index 2b948517ab9..3d1f6c2d5b6 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -9,6 +9,8 @@ import { Input, Tooltip, Select, + Modal, + Timeline, } from 'antd'; import { SliderValue } from 'antd/lib/slider'; @@ -36,9 +38,34 @@ interface Props { frame: number; frameStep: number; playing: boolean; + saving: boolean; + savingStatuses: string[]; canvasIsReady: boolean; onChangeFrame(frame: number, playing: boolean): void; onSwitchPlay(playing: boolean): void; + onSaveAnnotation(sessionInstance: any): void; +} + +function SavingOverlay(saving: boolean, statuses: string[]): JSX.Element { + return ( + + + { + statuses.slice(0, -1) + .map(( + status: string, + id: number, + // eslint-disable-next-line react/no-array-index-key + ) => {status}) + } + + + ); } export default function AnnotationTopBarComponent(props: Props): JSX.Element { @@ -47,9 +74,12 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { frame, frameStep, playing, + saving, + savingStatuses, canvasIsReady, onChangeFrame, onSwitchPlay, + onSaveAnnotation, } = props; if (playing && canvasIsReady) { @@ -62,6 +92,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { } } + const savingOverlay = SavingOverlay(saving, savingStatuses); + return ( @@ -70,9 +102,17 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { Menu -
- - Save +
+ => { + onSaveAnnotation(jobInstance); + }} + /> + + { saving ? 'Saving...' : 'Save' } + + { savingOverlay }
diff --git a/cvat-ui/src/components/create-model-page/create-model-content.tsx b/cvat-ui/src/components/create-model-page/create-model-content.tsx index a2ada3765b8..947122a9ccc 100644 --- a/cvat-ui/src/components/create-model-page/create-model-content.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-content.tsx @@ -13,13 +13,14 @@ import { import Text from 'antd/lib/typography/Text'; +import ConnectedFileManager, { + FileManagerContainer, +} from 'containers/file-manager/file-manager'; +import { ModelFiles } from 'reducers/interfaces'; + import CreateModelForm, { CreateModelForm as WrappedCreateModelForm, } from './create-model-form'; -import ConnectedFileManager, { - FileManagerContainer, -} from '../../containers/file-manager/file-manager'; -import { ModelFiles } from '../../reducers/interfaces'; interface Props { createModel(name: string, files: ModelFiles, global: boolean): void; diff --git a/cvat-ui/src/components/create-model-page/create-model-page.tsx b/cvat-ui/src/components/create-model-page/create-model-page.tsx index ab5014b29db..7ac33219477 100644 --- a/cvat-ui/src/components/create-model-page/create-model-page.tsx +++ b/cvat-ui/src/components/create-model-page/create-model-page.tsx @@ -7,9 +7,8 @@ import { } from 'antd'; import Text from 'antd/lib/typography/Text'; - +import { ModelFiles } from 'reducers/interfaces'; import CreateModelContent from './create-model-content'; -import { ModelFiles } from '../../reducers/interfaces'; interface Props { createModel(name: string, files: ModelFiles, global: boolean): void; diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index 1ea5a9eb3ca..2c654661b75 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -12,7 +12,7 @@ import { import Form, { FormComponentProps } from 'antd/lib/form/Form'; import Text from 'antd/lib/typography/Text'; -import patterns from '../../utils/validation-patterns'; +import patterns from 'utils/validation-patterns'; export interface AdvancedConfiguration { bugTracker?: string; diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index c4543d5cb10..26006d0711a 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -11,10 +11,10 @@ import { import Text from 'antd/lib/typography/Text'; +import FileManagerContainer from 'containers/file-manager/file-manager'; import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form'; import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form'; import LabelsEditor from '../labels-editor/labels-editor'; -import FileManagerContainer from '../../containers/file-manager/file-manager'; import { Files } from '../file-manager/file-manager'; export interface CreateTaskData { diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index e8eabc3d7c1..1c908c3d487 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -13,18 +13,18 @@ import { notification, } from 'antd'; -import SettingsPageComponent from './settings-page/settings-page'; -import TasksPageContainer from '../containers/tasks-page/tasks-page'; -import CreateTaskPageContainer from '../containers/create-task-page/create-task-page'; -import TaskPageContainer from '../containers/task-page/task-page'; -import ModelsPageContainer from '../containers/models-page/models-page'; -import CreateModelPageContainer from '../containers/create-model-page/create-model-page'; -import AnnotationPageContainer from '../containers/annotation-page/annotation-page'; -import LoginPageContainer from '../containers/login-page/login-page'; -import RegisterPageContainer from '../containers/register-page/register-page'; -import HeaderContainer from '../containers/header/header'; +import SettingsPageComponent from 'components/settings-page/settings-page'; +import TasksPageContainer from 'containers/tasks-page/tasks-page'; +import CreateTaskPageContainer from 'containers/create-task-page/create-task-page'; +import TaskPageContainer from 'containers/task-page/task-page'; +import ModelsPageContainer from 'containers/models-page/models-page'; +import CreateModelPageContainer from 'containers/create-model-page/create-model-page'; +import AnnotationPageContainer from 'containers/annotation-page/annotation-page'; +import LoginPageContainer from 'containers/login-page/login-page'; +import RegisterPageContainer from 'containers/register-page/register-page'; +import HeaderContainer from 'containers/header/header'; -import { NotificationsState } from '../reducers/interfaces'; +import { NotificationsState } from 'reducers/interfaces'; type CVATAppProps = { loadFormats: () => void; @@ -155,13 +155,15 @@ export default class CVATApplication extends React.PureComponent { const { users } = notifications.errors; const { share } = notifications.errors; const { models } = notifications.errors; + const { annotation } = notifications.errors; const shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register || !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading || !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching || !!users.fetching || !!share.fetching || !!models.creating || !!models.starting || !!models.fetching || !!models.deleting || !!models.inferenceStatusFetching - || !!models.metaFetching; + || !!models.metaFetching || !!annotation.frameFetching || !!annotation.saving + || !!annotation.jobFetching; if (auth.authorized) { showError(auth.authorized.message, auth.authorized.reason); @@ -226,6 +228,15 @@ export default class CVATApplication extends React.PureComponent { models.inferenceStatusFetching.reason, ); } + if (annotation.jobFetching) { + showError(annotation.jobFetching.message, annotation.jobFetching.reason); + } + if (annotation.frameFetching) { + showError(annotation.frameFetching.message, annotation.frameFetching.reason); + } + if (annotation.saving) { + showError(annotation.saving.message, annotation.saving.reason); + } if (shown) { resetErrors(); diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 29dc680f33f..427ca21be7a 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -14,11 +14,11 @@ import { import Text from 'antd/lib/typography/Text'; -import getCore from '../../core'; +import getCore from 'cvat-core'; import { CVATLogo, AccountIcon, -} from '../../icons'; +} from 'icons'; const core = getCore(); const serverHost = core.config.backendAPI.slice(0, -7); diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index b23f84f659f..c71ce87c497 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -13,6 +13,7 @@ import { import Form, { FormComponentProps } from 'antd/lib/form/Form'; import Text from 'antd/lib/typography/Text'; +import patterns from 'utils/validation-patterns'; import { equalArrayHead, @@ -20,7 +21,7 @@ import { Label, Attribute, } from './common'; -import patterns from '../../utils/validation-patterns'; + export enum AttributeType { SELECT = 'SELECT', diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx index 2fec246ed19..3589e1456d8 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -14,11 +14,10 @@ import { notification, } from 'antd'; -import { Model } from '../../reducers/interfaces'; - -interface StringObject { - [index: string]: string; -} +import { + Model, + StringObject, +} from 'reducers/interfaces'; interface Props { modelsFetching: boolean; diff --git a/cvat-ui/src/components/models-page/built-model-item.tsx b/cvat-ui/src/components/models-page/built-model-item.tsx index 5b996955c59..f00dc5e1ca5 100644 --- a/cvat-ui/src/components/models-page/built-model-item.tsx +++ b/cvat-ui/src/components/models-page/built-model-item.tsx @@ -9,7 +9,7 @@ import { import Text from 'antd/lib/typography/Text'; -import { Model } from '../../reducers/interfaces'; +import { Model } from 'reducers/interfaces'; interface Props { model: Model; diff --git a/cvat-ui/src/components/models-page/built-models-list.tsx b/cvat-ui/src/components/models-page/built-models-list.tsx index 1201e231871..b4f5fc61200 100644 --- a/cvat-ui/src/components/models-page/built-models-list.tsx +++ b/cvat-ui/src/components/models-page/built-models-list.tsx @@ -7,8 +7,8 @@ import { import Text from 'antd/lib/typography/Text'; +import { Model } from 'reducers/interfaces'; import BuiltModelItemComponent from './built-model-item'; -import { Model } from '../../reducers/interfaces'; interface Props { models: Model[]; diff --git a/cvat-ui/src/components/models-page/empty-list.tsx b/cvat-ui/src/components/models-page/empty-list.tsx index 0a6fdd4dfd7..f6d2c61f992 100644 --- a/cvat-ui/src/components/models-page/empty-list.tsx +++ b/cvat-ui/src/components/models-page/empty-list.tsx @@ -10,7 +10,7 @@ import { import { EmptyTasksIcon as EmptyModelsIcon, -} from '../../icons'; +} from 'icons'; export default function EmptyListComponent(): JSX.Element { return ( diff --git a/cvat-ui/src/components/models-page/uploaded-model-item.tsx b/cvat-ui/src/components/models-page/uploaded-model-item.tsx index a7fb28d8aac..4a42de1da04 100644 --- a/cvat-ui/src/components/models-page/uploaded-model-item.tsx +++ b/cvat-ui/src/components/models-page/uploaded-model-item.tsx @@ -13,8 +13,8 @@ import { import Text from 'antd/lib/typography/Text'; import moment from 'moment'; -import { MenuIcon } from '../../icons'; -import { Model } from '../../reducers/interfaces'; +import { MenuIcon } from 'icons'; +import { Model } from 'reducers/interfaces'; interface Props { model: Model; diff --git a/cvat-ui/src/components/models-page/uploaded-models-list.tsx b/cvat-ui/src/components/models-page/uploaded-models-list.tsx index 0c67cf8aff1..dfdcebc0b1b 100644 --- a/cvat-ui/src/components/models-page/uploaded-models-list.tsx +++ b/cvat-ui/src/components/models-page/uploaded-models-list.tsx @@ -7,8 +7,9 @@ import { import Text from 'antd/lib/typography/Text'; +import { Model } from 'reducers/interfaces'; import UploadedModelItem from './uploaded-model-item'; -import { Model } from '../../reducers/interfaces'; + interface Props { registeredUsers: any[]; diff --git a/cvat-ui/src/components/register-page/register-form.tsx b/cvat-ui/src/components/register-page/register-form.tsx index 8683b3ce670..e2332f64122 100644 --- a/cvat-ui/src/components/register-page/register-form.tsx +++ b/cvat-ui/src/components/register-page/register-form.tsx @@ -7,7 +7,7 @@ import { Form, } from 'antd'; -import patterns from '../../utils/validation-patterns'; +import patterns from 'utils/validation-patterns'; export interface RegisterData { username: string; diff --git a/cvat-ui/src/components/settings-page/player-settings.tsx b/cvat-ui/src/components/settings-page/player-settings.tsx index 8a4c3af5783..cf9d553b2ed 100644 --- a/cvat-ui/src/components/settings-page/player-settings.tsx +++ b/cvat-ui/src/components/settings-page/player-settings.tsx @@ -16,12 +16,12 @@ import { CheckboxChangeEvent } from 'antd/lib/checkbox'; import { PlaycontrolBackJumpIcon, PlaycontrolForwardJumpIcon, -} from '../../icons'; +} from 'icons'; import { FrameSpeed, GridColor, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; interface Props { frameStep: number; diff --git a/cvat-ui/src/components/settings-page/settings-page.tsx b/cvat-ui/src/components/settings-page/settings-page.tsx index 702ba39ed76..9be48c87acb 100644 --- a/cvat-ui/src/components/settings-page/settings-page.tsx +++ b/cvat-ui/src/components/settings-page/settings-page.tsx @@ -13,8 +13,8 @@ import Text from 'antd/lib/typography/Text'; import { RouteComponentProps } from 'react-router'; import { withRouter } from 'react-router-dom'; -import WorkspaceSettingsContainer from '../../containers/settings-page/workspace-settings'; -import PlayerSettingsContainer from '../../containers/settings-page/player-settings'; +import WorkspaceSettingsContainer from 'containers/settings-page/workspace-settings'; +import PlayerSettingsContainer from 'containers/settings-page/player-settings'; function SettingsPage(props: RouteComponentProps): JSX.Element { return ( diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index fc8ea41eb3e..1d551f6adeb 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -15,11 +15,11 @@ import Title from 'antd/lib/typography/Title'; import moment from 'moment'; +import getCore from 'cvat-core'; +import patterns from 'utils/validation-patterns'; +import { getReposData, syncRepos } from 'utils/git-utils'; import UserSelector from './user-selector'; import LabelsEditorComponent from '../labels-editor/labels-editor'; -import getCore from '../../core'; -import patterns from '../../utils/validation-patterns'; -import { getReposData, syncRepos } from '../../utils/git-utils'; const core = getCore(); diff --git a/cvat-ui/src/components/task-page/job-list.tsx b/cvat-ui/src/components/task-page/job-list.tsx index 589e0b78baf..fb29cca56d7 100644 --- a/cvat-ui/src/components/task-page/job-list.tsx +++ b/cvat-ui/src/components/task-page/job-list.tsx @@ -14,9 +14,8 @@ import Text from 'antd/lib/typography/Text'; import moment from 'moment'; import copy from 'copy-to-clipboard'; +import getCore from 'cvat-core'; import UserSelector from './user-selector'; -import getCore from '../../core'; - const core = getCore(); diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index ec252e9c182..8238be5a792 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -10,11 +10,11 @@ import { Result, } from 'antd'; +import DetailsContainer from 'containers/task-page/details'; +import JobListContainer from 'containers/task-page/job-list'; +import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog'; +import { Task } from 'reducers/interfaces'; import TopBarComponent from './top-bar'; -import DetailsContainer from '../../containers/task-page/details'; -import JobListContainer from '../../containers/task-page/job-list'; -import ModelRunnerModalContainer from '../../containers/model-runner-dialog/model-runner-dialog'; -import { Task } from '../../reducers/interfaces'; interface TaskPageComponentProps { task: Task | null | undefined; diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index ebeb59d791c..1edb8085e03 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -10,8 +10,8 @@ import { import Text from 'antd/lib/typography/Text'; -import ActionsMenuContainer from '../../containers/actions-menu/actions-menu'; -import { MenuIcon } from '../../icons'; +import ActionsMenuContainer from 'containers/actions-menu/actions-menu'; +import { MenuIcon } from 'icons'; interface DetailsComponentProps { taskInstance: any; diff --git a/cvat-ui/src/components/tasks-page/empty-list.tsx b/cvat-ui/src/components/tasks-page/empty-list.tsx index afc08f23f2f..6f358bf13a8 100644 --- a/cvat-ui/src/components/tasks-page/empty-list.tsx +++ b/cvat-ui/src/components/tasks-page/empty-list.tsx @@ -8,7 +8,7 @@ import { Icon, } from 'antd'; -import { EmptyTasksIcon } from '../../icons'; +import { EmptyTasksIcon } from 'icons'; export default function EmptyListComponent(): JSX.Element { return ( diff --git a/cvat-ui/src/components/tasks-page/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index 09799c6444e..cedd0b5ce74 100644 --- a/cvat-ui/src/components/tasks-page/task-item.tsx +++ b/cvat-ui/src/components/tasks-page/task-item.tsx @@ -14,9 +14,9 @@ import { import moment from 'moment'; -import ActionsMenuContainer from '../../containers/actions-menu/actions-menu'; -import { ActiveInference } from '../../reducers/interfaces'; -import { MenuIcon } from '../../icons'; +import ActionsMenuContainer from 'containers/actions-menu/actions-menu'; +import { ActiveInference } from 'reducers/interfaces'; +import { MenuIcon } from 'icons'; export interface TaskItemProps { taskInstance: any; diff --git a/cvat-ui/src/components/tasks-page/task-list.tsx b/cvat-ui/src/components/tasks-page/task-list.tsx index 936276eb440..bef5acf602d 100644 --- a/cvat-ui/src/components/tasks-page/task-list.tsx +++ b/cvat-ui/src/components/tasks-page/task-list.tsx @@ -6,8 +6,8 @@ import { Pagination, } from 'antd'; -import ModelRunnerModalContainer from '../../containers/model-runner-dialog/model-runner-dialog'; -import TaskItem from '../../containers/tasks-page/task-item'; +import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog'; +import TaskItem from 'containers/tasks-page/task-item'; export interface ContentListProps { onSwitchPage(page: number): void; diff --git a/cvat-ui/src/components/tasks-page/tasks-page.tsx b/cvat-ui/src/components/tasks-page/tasks-page.tsx index 51232923bc4..a9ee5ee6b33 100644 --- a/cvat-ui/src/components/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/components/tasks-page/tasks-page.tsx @@ -13,12 +13,13 @@ import Text from 'antd/lib/typography/Text'; import { TasksQuery, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; +import FeedbackComponent from 'components/feedback/feedback'; +import TaskListContainer from 'containers/tasks-page/tasks-list'; import TopBar from './top-bar'; -import FeedbackComponent from '../feedback/feedback'; import EmptyListComponent from './empty-list'; -import TaskListContainer from '../../containers/tasks-page/tasks-list'; + interface TasksPageProps { tasksFetching: boolean; diff --git a/cvat-ui/src/containers/actions-menu/actions-menu.tsx b/cvat-ui/src/containers/actions-menu/actions-menu.tsx index dc8692c6145..8914c83b6dc 100644 --- a/cvat-ui/src/containers/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/containers/actions-menu/actions-menu.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; -import ActionsMenuComponent from '../../components/actions-menu/actions-menu'; +import ActionsMenuComponent from 'components/actions-menu/actions-menu'; import { CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; -import { showRunModelDialog } from '../../actions/models-actions'; +import { showRunModelDialog } from 'actions/models-actions'; import { dumpAnnotationsAsync, loadAnnotationsAsync, exportDatasetAsync, deleteTaskAsync, -} from '../../actions/tasks-actions'; +} from 'actions/tasks-actions'; interface OwnProps { taskInstance: any; diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index b6343c40832..c84c021a33c 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -3,12 +3,12 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { RouteComponentProps } from 'react-router'; -import AnnotationPageComponent from '../../components/annotation-page/annotation-page'; -import { getJobAsync } from '../../actions/annotation-actions'; +import AnnotationPageComponent from 'components/annotation-page/annotation-page'; +import { getJobAsync } from 'actions/annotation-actions'; import { CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; type OwnProps = RouteComponentProps<{ tid: string; 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 36251baf32b..b4a62e517af 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 @@ -1,31 +1,51 @@ import React from 'react'; import { connect } from 'react-redux'; -import CanvasWrapperComponent from '../../../components/annotation-page/standard-workspace/canvas-wrapper'; +import CanvasWrapperComponent from 'components/annotation-page/standard-workspace/canvas-wrapper'; import { confirmCanvasReady, -} from '../../../actions/annotation-actions'; + dragCanvas, + zoomCanvas, + resetCanvas, + shapeDrawn, + objectsMerged, + objectsGroupped, + trackSplitted, + annotationsUpdated, +} from 'actions/annotation-actions'; import { GridColor, + ObjectType, CombinedState, -} from '../../../reducers/interfaces'; +} from 'reducers/interfaces'; -import { Canvas } from '../../../canvas'; +import { Canvas } from 'cvat-canvas'; interface StateToProps { canvasInstance: Canvas; jobInstance: any; annotations: any[]; frameData: any; + frame: number; grid: boolean; gridSize: number; gridColor: GridColor; gridOpacity: number; + activeLabelID: number; + activeObjectType: ObjectType; } interface DispatchToProps { onSetupCanvas(): void; + onDragCanvas: (enabled: boolean) => void; + onZoomCanvas: (enabled: boolean) => void; + onResetCanvas: () => void; + onShapeDrawn: () => void; + onObjectsMerged: () => void; + onObjectsGroupped: () => void; + onTrackSplitted: () => void; + onAnnotationsUpdated: (annotations: any[]) => void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -33,9 +53,16 @@ function mapStateToProps(state: CombinedState): StateToProps { canvasInstance, jobInstance, frameData, + frame, annotations, + drawing, } = state.annotation; + const { + activeLabelID, + activeObjectType, + } = drawing; + const { grid, gridSize, @@ -47,11 +74,14 @@ function mapStateToProps(state: CombinedState): StateToProps { canvasInstance, jobInstance, frameData, + frame, annotations, grid, gridSize, gridColor, gridOpacity, + activeLabelID, + activeObjectType, }; } @@ -60,6 +90,30 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSetupCanvas(): void { dispatch(confirmCanvasReady()); }, + onDragCanvas(enabled: boolean): void { + dispatch(dragCanvas(enabled)); + }, + onZoomCanvas(enabled: boolean): void { + dispatch(zoomCanvas(enabled)); + }, + onResetCanvas(): void { + dispatch(resetCanvas()); + }, + onShapeDrawn(): void { + dispatch(shapeDrawn()); + }, + onObjectsMerged(): void { + dispatch(objectsMerged()); + }, + onObjectsGroupped(): void { + dispatch(objectsGroupped()); + }, + onTrackSplitted(): void { + dispatch(trackSplitted()); + }, + onAnnotationsUpdated(annotations: any[]): void { + dispatch(annotationsUpdated(annotations)); + }, }; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar.tsx deleted file mode 100644 index bc8bf1d9a73..00000000000 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; - -import { Canvas } from '../../../canvas'; - -import ControlsSideBarComponent from '../../../components/annotation-page/standard-workspace/controls-side-bar'; -import { CombinedState } from '../../../reducers/interfaces'; - - -interface StateToProps { - canvasInstance: Canvas; - rotateAll: boolean; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const { - annotation, - settings, - } = state; - - return { - rotateAll: settings.player.rotateAll, - canvasInstance: annotation.canvasInstance, - }; -} - -function StandardWorkspaceContainer(props: StateToProps): JSX.Element { - return ( - - ); -} - -export default connect( - mapStateToProps, -)(StandardWorkspaceContainer); 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 new file mode 100644 index 00000000000..c7d9189831a --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { Canvas } from 'cvat-canvas'; + +import { + mergeObjects, + groupObjects, + splitTrack, +} from 'actions/annotation-actions'; +import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; +import { + ActiveControl, + CombinedState, + StringObject, +} from 'reducers/interfaces'; + +interface StateToProps { + canvasInstance: Canvas; + rotateAll: boolean; + activeControl: ActiveControl; + labels: StringObject; +} + +interface DispatchToProps { + onMergeStart(): void; + onGroupStart(): void; + onSplitStart(): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation, + settings, + } = state; + + const { + canvasInstance, + activeControl, + } = annotation; + + const labels = annotation.jobInstance.task.labels + .reduce((acc: StringObject, label: any): StringObject => { + acc[label.id as number] = label.name; + return acc; + }, {}); + + return { + rotateAll: settings.player.rotateAll, + canvasInstance, + activeControl, + labels, + }; +} + +function dispatchToProps(dispatch: any): DispatchToProps { + return { + onMergeStart(): void { + dispatch(mergeObjects()); + }, + onGroupStart(): void { + dispatch(groupObjects()); + }, + onSplitStart(): void { + dispatch(splitTrack()); + }, + }; +} + +function StandardWorkspaceContainer(props: StateToProps & DispatchToProps): JSX.Element { + return ( + + ); +} + +export default connect( + mapStateToProps, + dispatchToProps, +)(StandardWorkspaceContainer); diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx new file mode 100644 index 00000000000..49297946894 --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { + CombinedState, + ShapeType, + ObjectType, + StringObject, +} from 'reducers/interfaces'; + +import { + drawShape, +} from 'actions/annotation-actions'; +import { Canvas } from 'cvat-canvas'; +import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; + +interface OwnProps { + shapeType: ShapeType; +} + +interface DispatchToProps { + onDrawStart( + shapeType: ShapeType, + labelID: number, + objectType: ObjectType, + points?: number, + ): void; +} + +interface StateToProps { + canvasInstance: Canvas; + shapeType: ShapeType; + labels: StringObject; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onDrawStart( + shapeType: ShapeType, + labelID: number, + objectType: ObjectType, + points?: number, + ): void { + dispatch(drawShape(shapeType, labelID, objectType, points)); + }, + }; +} + +function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { + const { + annotation, + } = state; + + const { + canvasInstance, + } = annotation; + + const labels = annotation.jobInstance.task.labels + .reduce((acc: StringObject, label: any): StringObject => { + acc[label.id as number] = label.name; + return acc; + }, {}); + + return { + ...own, + canvasInstance, + labels, + }; +} + +function DrawShapePopoverContainer(props: DispatchToProps & StateToProps): JSX.Element { + return ( + + ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(DrawShapePopoverContainer); diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/standard-workspace.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/standard-workspace.tsx index a9835c5b3b6..d98234ae7c9 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/standard-workspace.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/standard-workspace.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Canvas } from '../../../canvas'; +import { Canvas } from 'cvat-canvas'; -import StandardWorkspaceComponent from '../../../components/annotation-page/standard-workspace/standard-workspace'; -import { CombinedState } from '../../../reducers/interfaces'; +import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace'; +import { CombinedState } from 'reducers/interfaces'; interface StateToProps { 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 a1f8bc88064..652dac9a402 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 @@ -4,10 +4,11 @@ import { connect } from 'react-redux'; import { changeFrameAsync, switchPlay as switchPlayAction, -} from '../../../actions/annotation-actions'; + saveAnnotationsAsync, +} from 'actions/annotation-actions'; -import AnnotationTopBarComponent from '../../../components/annotation-page/top-bar/top-bar'; -import { CombinedState } from '../../../reducers/interfaces'; +import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; +import { CombinedState } from 'reducers/interfaces'; interface StateToProps { jobInstance: any; @@ -15,11 +16,14 @@ interface StateToProps { frameStep: number; playing: boolean; canvasIsReady: boolean; + saving: boolean; + savingStatuses: string[]; } interface DispatchToProps { onChangeFrame(frame: number, playing: boolean): void; onSwitchPlay(playing: boolean): void; + onSaveAnnotation(sessionInstance: any): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -28,12 +32,23 @@ function mapStateToProps(state: CombinedState): StateToProps { settings, } = state; + const { + playing, + saving, + savingStatuses, + canvasIsReady, + frame, + jobInstance, + } = annotation; + return { - jobInstance: annotation.jobInstance, - frame: annotation.frame as number, // is number when jobInstance specified frameStep: settings.player.frameStep, - playing: annotation.playing, - canvasIsReady: annotation.canvasIsReady, + playing, + saving, + savingStatuses, + canvasIsReady, + frame, + jobInstance, }; } @@ -45,30 +60,15 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSwitchPlay(playing: boolean): void { dispatch(switchPlayAction(playing)); }, + onSaveAnnotation(sessionInstance: any): void { + dispatch(saveAnnotationsAsync(sessionInstance)); + }, }; } function AnnotationTopBarContainer(props: StateToProps & DispatchToProps): JSX.Element { - const { - jobInstance, - frame, - frameStep, - playing, - canvasIsReady, - onChangeFrame, - onSwitchPlay, - } = props; - return ( - + ); } diff --git a/cvat-ui/src/containers/create-model-page/create-model-page.tsx b/cvat-ui/src/containers/create-model-page/create-model-page.tsx index a20b6fb5628..b6299aee67b 100644 --- a/cvat-ui/src/containers/create-model-page/create-model-page.tsx +++ b/cvat-ui/src/containers/create-model-page/create-model-page.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { connect } from 'react-redux'; -import CreateModelPageComponent from '../../components/create-model-page/create-model-page'; -import { createModelAsync } from '../../actions/models-actions'; +import CreateModelPageComponent from 'components/create-model-page/create-model-page'; +import { createModelAsync } from 'actions/models-actions'; import { ModelFiles, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; interface StateToProps { isAdmin: boolean; diff --git a/cvat-ui/src/containers/create-task-page/create-task-page.tsx b/cvat-ui/src/containers/create-task-page/create-task-page.tsx index aac98fa42ed..eab887051ca 100644 --- a/cvat-ui/src/containers/create-task-page/create-task-page.tsx +++ b/cvat-ui/src/containers/create-task-page/create-task-page.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; -import { CombinedState } from '../../reducers/interfaces'; -import CreateTaskComponent from '../../components/create-task-page/create-task-page'; -import { CreateTaskData } from '../../components/create-task-page/create-task-content'; -import { createTaskAsync } from '../../actions/tasks-actions'; +import { CombinedState } from 'reducers/interfaces'; +import CreateTaskComponent from 'components/create-task-page/create-task-page'; +import { CreateTaskData } from 'components/create-task-page/create-task-content'; +import { createTaskAsync } from 'actions/tasks-actions'; interface StateToProps { status: string; diff --git a/cvat-ui/src/containers/file-manager/file-manager.tsx b/cvat-ui/src/containers/file-manager/file-manager.tsx index bdfff58eaed..2fb55321af7 100644 --- a/cvat-ui/src/containers/file-manager/file-manager.tsx +++ b/cvat-ui/src/containers/file-manager/file-manager.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { connect } from 'react-redux'; import { TreeNodeNormal } from 'antd/lib/tree/Tree'; -import FileManagerComponent, { Files } from '../../components/file-manager/file-manager'; +import FileManagerComponent, { Files } from 'components/file-manager/file-manager'; -import { loadShareDataAsync } from '../../actions/share-actions'; +import { loadShareDataAsync } from 'actions/share-actions'; import { ShareItem, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; interface OwnProps { ref: any; diff --git a/cvat-ui/src/containers/header/header.tsx b/cvat-ui/src/containers/header/header.tsx index b434c18b6b1..e0de3939581 100644 --- a/cvat-ui/src/containers/header/header.tsx +++ b/cvat-ui/src/containers/header/header.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { connect } from 'react-redux'; -import { logoutAsync } from '../../actions/auth-actions'; +import { logoutAsync } from 'actions/auth-actions'; import { SupportedPlugins, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; -import HeaderComponent from '../../components/header/header'; +import HeaderComponent from 'components/header/header'; interface StateToProps { logoutFetching: boolean; diff --git a/cvat-ui/src/containers/login-page/login-page.tsx b/cvat-ui/src/containers/login-page/login-page.tsx index b6b9b57dee8..a9c7c56ae9b 100644 --- a/cvat-ui/src/containers/login-page/login-page.tsx +++ b/cvat-ui/src/containers/login-page/login-page.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { connect } from 'react-redux'; -import { loginAsync } from '../../actions/auth-actions'; -import LoginPageComponent from '../../components/login-page/login-page'; -import { CombinedState } from '../../reducers/interfaces'; +import { loginAsync } from 'actions/auth-actions'; +import LoginPageComponent from 'components/login-page/login-page'; +import { CombinedState } from 'reducers/interfaces'; interface StateToProps { fetching: boolean; diff --git a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx index 1da1386caef..e95b0da629e 100644 --- a/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx +++ b/cvat-ui/src/containers/model-runner-dialog/model-runner-dialog.tsx @@ -1,16 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; -import ModelRunnerModalComponent from '../../components/model-runner-modal/model-runner-modal'; +import ModelRunnerModalComponent from 'components/model-runner-modal/model-runner-modal'; import { Model, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; import { getModelsAsync, inferModelAsync, closeRunModelDialog, -} from '../../actions/models-actions'; +} from 'actions/models-actions'; interface StateToProps { diff --git a/cvat-ui/src/containers/models-page/models-page.tsx b/cvat-ui/src/containers/models-page/models-page.tsx index ba56bb97e25..07a2900aff8 100644 --- a/cvat-ui/src/containers/models-page/models-page.tsx +++ b/cvat-ui/src/containers/models-page/models-page.tsx @@ -1,15 +1,15 @@ import React from 'react'; import { connect } from 'react-redux'; -import ModelsPageComponent from '../../components/models-page/models-page'; +import ModelsPageComponent from 'components/models-page/models-page'; import { Model, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; import { getModelsAsync, deleteModelAsync, -} from '../../actions/models-actions'; +} from 'actions/models-actions'; interface StateToProps { installedAutoAnnotation: boolean; diff --git a/cvat-ui/src/containers/register-page/register-page.tsx b/cvat-ui/src/containers/register-page/register-page.tsx index c1131086544..a5487f32e81 100644 --- a/cvat-ui/src/containers/register-page/register-page.tsx +++ b/cvat-ui/src/containers/register-page/register-page.tsx @@ -1,8 +1,8 @@ import React from 'react'; import { connect } from 'react-redux'; -import { registerAsync } from '../../actions/auth-actions'; -import RegisterPageComponent from '../../components/register-page/register-page'; -import { CombinedState } from '../../reducers/interfaces'; +import { registerAsync } from 'actions/auth-actions'; +import RegisterPageComponent from 'components/register-page/register-page'; +import { CombinedState } from 'reducers/interfaces'; interface StateToProps { fetching: boolean; diff --git a/cvat-ui/src/containers/settings-page/player-settings.tsx b/cvat-ui/src/containers/settings-page/player-settings.tsx index be52e3dd429..73193b8f427 100644 --- a/cvat-ui/src/containers/settings-page/player-settings.tsx +++ b/cvat-ui/src/containers/settings-page/player-settings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import PlayerSettingsComponent from '../../components/settings-page/player-settings'; +import PlayerSettingsComponent from 'components/settings-page/player-settings'; import { switchRotateAll, @@ -9,13 +9,13 @@ import { changeGridSize, changeGridColor, changeGridOpacity, -} from '../../actions/settings-actions'; +} from 'actions/settings-actions'; import { CombinedState, FrameSpeed, GridColor, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; interface StateToProps { frameStep: number; diff --git a/cvat-ui/src/containers/settings-page/workspace-settings.tsx b/cvat-ui/src/containers/settings-page/workspace-settings.tsx index 48eac3cc125..0c5a597efbf 100644 --- a/cvat-ui/src/containers/settings-page/workspace-settings.tsx +++ b/cvat-ui/src/containers/settings-page/workspace-settings.tsx @@ -3,9 +3,9 @@ import { connect } from 'react-redux'; import { CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; -import WorkspaceSettingsComponent from '../../components/settings-page/workspace-settings'; +import WorkspaceSettingsComponent from 'components/settings-page/workspace-settings'; interface StateToProps { autoSave: boolean; diff --git a/cvat-ui/src/containers/task-page/details.tsx b/cvat-ui/src/containers/task-page/details.tsx index 913bbdf19dd..269041b5d0b 100644 --- a/cvat-ui/src/containers/task-page/details.tsx +++ b/cvat-ui/src/containers/task-page/details.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { connect } from 'react-redux'; -import DetailsComponent from '../../components/task-page/details'; -import { updateTaskAsync } from '../../actions/tasks-actions'; +import DetailsComponent from 'components/task-page/details'; +import { updateTaskAsync } from 'actions/tasks-actions'; import { Task, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; interface OwnProps { task: Task; diff --git a/cvat-ui/src/containers/task-page/job-list.tsx b/cvat-ui/src/containers/task-page/job-list.tsx index c7e32c589cc..04ca7ecdc09 100644 --- a/cvat-ui/src/containers/task-page/job-list.tsx +++ b/cvat-ui/src/containers/task-page/job-list.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { connect } from 'react-redux'; -import JobListComponent from '../../components/task-page/job-list'; -import { updateJobAsync } from '../../actions/tasks-actions'; +import JobListComponent from 'components/task-page/job-list'; +import { updateJobAsync } from 'actions/tasks-actions'; import { Task, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; interface OwnProps { task: Task; diff --git a/cvat-ui/src/containers/task-page/task-page.tsx b/cvat-ui/src/containers/task-page/task-page.tsx index da2f5aae93a..abad8db8588 100644 --- a/cvat-ui/src/containers/task-page/task-page.tsx +++ b/cvat-ui/src/containers/task-page/task-page.tsx @@ -3,13 +3,13 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router-dom'; import { RouteComponentProps } from 'react-router'; -import { getTasksAsync } from '../../actions/tasks-actions'; +import { getTasksAsync } from 'actions/tasks-actions'; -import TaskPageComponent from '../../components/task-page/task-page'; +import TaskPageComponent from 'components/task-page/task-page'; import { Task, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; type Props = RouteComponentProps<{id: string}>; diff --git a/cvat-ui/src/containers/tasks-page/task-item.tsx b/cvat-ui/src/containers/tasks-page/task-item.tsx index 9aa96727aca..3e115a3004a 100644 --- a/cvat-ui/src/containers/tasks-page/task-item.tsx +++ b/cvat-ui/src/containers/tasks-page/task-item.tsx @@ -5,13 +5,13 @@ import { TasksQuery, CombinedState, ActiveInference, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; -import TaskItemComponent from '../../components/tasks-page/task-item'; +import TaskItemComponent from 'components/tasks-page/task-item'; import { getTasksAsync, -} from '../../actions/tasks-actions'; +} from 'actions/tasks-actions'; interface StateToProps { deleted: boolean; diff --git a/cvat-ui/src/containers/tasks-page/tasks-list.tsx b/cvat-ui/src/containers/tasks-page/tasks-list.tsx index 7afd17b8de2..bbc79be446f 100644 --- a/cvat-ui/src/containers/tasks-page/tasks-list.tsx +++ b/cvat-ui/src/containers/tasks-page/tasks-list.tsx @@ -5,13 +5,13 @@ import { TasksState, TasksQuery, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; -import TasksListComponent from '../../components/tasks-page/task-list'; +import TasksListComponent from 'components/tasks-page/task-list'; import { getTasksAsync, -} from '../../actions/tasks-actions'; +} from 'actions/tasks-actions'; interface StateToProps { tasks: TasksState; diff --git a/cvat-ui/src/containers/tasks-page/tasks-page.tsx b/cvat-ui/src/containers/tasks-page/tasks-page.tsx index 7e404d0e87a..b713943e286 100644 --- a/cvat-ui/src/containers/tasks-page/tasks-page.tsx +++ b/cvat-ui/src/containers/tasks-page/tasks-page.tsx @@ -5,14 +5,14 @@ import { Task, TasksQuery, CombinedState, -} from '../../reducers/interfaces'; +} from 'reducers/interfaces'; -import TasksPageComponent from '../../components/tasks-page/tasks-page'; +import TasksPageComponent from 'components/tasks-page/tasks-page'; import { getTasksAsync, hideEmptyTasks, -} from '../../actions/tasks-actions'; +} from 'actions/tasks-actions'; interface StateToProps { tasksFetching: boolean; diff --git a/cvat-ui/src/canvas.ts b/cvat-ui/src/cvat-canvas.ts similarity index 100% rename from cvat-ui/src/canvas.ts rename to cvat-ui/src/cvat-canvas.ts diff --git a/cvat-ui/src/core.ts b/cvat-ui/src/cvat-core.ts similarity index 100% rename from cvat-ui/src/core.ts rename to cvat-ui/src/cvat-core.ts diff --git a/cvat-ui/src/store.ts b/cvat-ui/src/cvat-store.ts similarity index 100% rename from cvat-ui/src/store.ts rename to cvat-ui/src/cvat-store.ts diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index 3ef428246f1..2d01144eb2e 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -5,7 +5,7 @@ import { connect, Provider } from 'react-redux'; import CVATApplication from './components/cvat-app'; import createRootReducer from './reducers/root-reducer'; -import createCVATStore, { getCVATStore } from './store'; +import createCVATStore, { getCVATStore } from './cvat-store'; import { authorizedAsync } from './actions/auth-actions'; import { getFormatsAsync } from './actions/formats-actions'; diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index b4c1f91797f..599212ede7c 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -1,20 +1,33 @@ import { AnyAction } from 'redux'; -import { Canvas } from '../canvas'; +import { Canvas } from 'cvat-canvas'; +import { AnnotationActionTypes } from 'actions/annotation-actions'; +import { + AnnotationState, + ActiveControl, + ShapeType, + ObjectType, +} from './interfaces'; -import { AnnotationState } from './interfaces'; -import { AnnotationActionTypes } from '../actions/annotation-actions'; const defaultState: AnnotationState = { canvasInstance: new Canvas(), canvasIsReady: false, + activeControl: ActiveControl.CURSOR, jobInstance: null, frame: 0, playing: false, annotations: [], frameData: null, + saving: false, + savingStatuses: [], dataFetching: false, jobFetching: false, + drawing: { + activeShapeType: ShapeType.RECTANGLE, + activeLabelID: 0, + activeObjectType: ObjectType.SHAPE, + }, }; export default (state = defaultState, action: AnyAction): AnnotationState => { @@ -26,13 +39,25 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }; } case AnnotationActionTypes.GET_JOB_SUCCESS: { + const { + jobInstance, + frame, + frameData, + annotations, + } = action.payload; + return { ...defaultState, jobFetching: false, - jobInstance: action.payload.jobInstance, - frame: action.payload.frame, - frameData: action.payload.frameData, - annotations: action.payload.annotations, + jobInstance, + frame, + frameData, + annotations, + drawing: { + ...defaultState.drawing, + activeLabelID: jobInstance.task.labels[0].id, + activeObjectType: jobInstance.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE, + }, }; } case AnnotationActionTypes.GET_JOB_FAILED: { @@ -66,6 +91,31 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { dataFetching: false, }; // add notification if failed } + case AnnotationActionTypes.SAVE_ANNOTATIONS: { + return { + ...state, + saving: true, + savingStatuses: [], + }; + } + case AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS: { + return { + ...state, + saving: false, + }; + } + case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: { + return { + ...state, + saving: false, + }; // add notification if failed + } + case AnnotationActionTypes.SAVE_ANNOTATIONS_UPDATED_STATUS: { + return { + ...state, + savingStatuses: [...state.savingStatuses, action.payload.status], + }; + } case AnnotationActionTypes.SWITCH_PLAY: { return { ...state, @@ -78,6 +128,79 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { canvasIsReady: true, }; } + case AnnotationActionTypes.DRAG_CANVAS: { + const { enabled } = action.payload; + return { + ...state, + activeControl: enabled ? ActiveControl.DRAG_CANVAS : ActiveControl.CURSOR, + }; + } + case AnnotationActionTypes.ZOOM_CANVAS: { + const { enabled } = action.payload; + return { + ...state, + activeControl: enabled ? ActiveControl.ZOOM_CANVAS : ActiveControl.CURSOR, + }; + } + case AnnotationActionTypes.DRAW_SHAPE: { + const { + shapeType, + labelID, + objectType, + points, + activeControl, + } = action.payload; + + return { + ...state, + activeControl, + drawing: { + activeLabelID: labelID, + activeNumOfPoints: points, + activeObjectType: objectType, + activeShapeType: shapeType, + }, + }; + } + case AnnotationActionTypes.MERGE_OBJECTS: { + return { + ...state, + activeControl: ActiveControl.MERGE, + }; + } + case AnnotationActionTypes.GROUP_OBJECTS: { + return { + ...state, + activeControl: ActiveControl.GROUP, + }; + } + case AnnotationActionTypes.SPLIT_TRACK: { + return { + ...state, + activeControl: ActiveControl.SPLIT, + }; + } + case AnnotationActionTypes.OBJECTS_MERGED: + case AnnotationActionTypes.OBJECTS_GROUPPED: + case AnnotationActionTypes.TRACK_SPLITTED: + case AnnotationActionTypes.SHAPE_DRAWN: { + return { + ...state, + activeControl: ActiveControl.CURSOR, + }; + } + case AnnotationActionTypes.ANNOTATIONS_UPDATED: { + return { + ...state, + annotations: action.payload.annotations, + }; + } + case AnnotationActionTypes.RESET_CANVAS: { + return { + ...state, + activeControl: ActiveControl.CURSOR, + }; + } default: { return { ...state, diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index 63fecdf67ec..73a9bebd72c 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -1,5 +1,5 @@ import { AnyAction } from 'redux'; -import { AuthActionTypes } from '../actions/auth-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; import { AuthState } from './interfaces'; diff --git a/cvat-ui/src/reducers/formats-reducer.ts b/cvat-ui/src/reducers/formats-reducer.ts index 40fd2a80506..b66afab8bc0 100644 --- a/cvat-ui/src/reducers/formats-reducer.ts +++ b/cvat-ui/src/reducers/formats-reducer.ts @@ -1,6 +1,6 @@ import { AnyAction } from 'redux'; -import { FormatsActionTypes } from '../actions/formats-actions'; -import { AuthActionTypes } from '../actions/auth-actions'; +import { FormatsActionTypes } from 'actions/formats-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; import { FormatsState } from './interfaces'; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 7a3d70dbe04..dd9be592dd5 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -1,4 +1,8 @@ -import { Canvas } from '../canvas'; +import { Canvas } from 'cvat-canvas'; + +export type StringObject = { + [index: string]: string; +}; export interface AuthState { initialized: boolean; @@ -188,6 +192,11 @@ export interface NotificationsState { metaFetching: null | ErrorState; inferenceStatusFetching: null | ErrorState; }; + annotation: { + saving: null | ErrorState; + jobFetching: null | ErrorState; + frameFetching: null | ErrorState; + }; }; messages: { tasks: { @@ -199,16 +208,51 @@ export interface NotificationsState { }; } +export enum ActiveControl { + CURSOR = 'cursor', + DRAG_CANVAS = 'drag_canvas', + ZOOM_CANVAS = 'zoom_canvas', + DRAW_RECTANGLE = 'draw_rectangle', + DRAW_POLYGON = 'draw_polygon', + DRAW_POLYLINE = 'draw_polyline', + DRAW_POINTS = 'draw_points', + MERGE = 'merge', + GROUP = 'group', + SPLIT = 'split', +} + +export enum ShapeType { + RECTANGLE = 'rectangle', + POLYGON = 'polygon', + POLYLINE = 'polyline', + POINTS = 'points', +} + +export enum ObjectType { + SHAPE = 'shape', + TRACK = 'track', + TAG = 'tag', +} + export interface AnnotationState { canvasInstance: Canvas; canvasIsReady: boolean; + activeControl: ActiveControl; jobInstance: any | null | undefined; frameData: any | null; frame: number; playing: boolean; annotations: any[]; + saving: boolean; + savingStatuses: string[]; jobFetching: boolean; dataFetching: boolean; + drawing: { + activeShapeType: ShapeType; + activeNumOfPoints?: number; + activeLabelID: number; + activeObjectType: ObjectType; + }; } export enum GridColor { diff --git a/cvat-ui/src/reducers/models-reducer.ts b/cvat-ui/src/reducers/models-reducer.ts index 664622fecff..453885b4619 100644 --- a/cvat-ui/src/reducers/models-reducer.ts +++ b/cvat-ui/src/reducers/models-reducer.ts @@ -1,7 +1,7 @@ import { AnyAction } from 'redux'; -import { ModelsActionTypes } from '../actions/models-actions'; -import { AuthActionTypes } from '../actions/auth-actions'; +import { ModelsActionTypes } from 'actions/models-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; import { ModelsState } from './interfaces'; const defaultState: ModelsState = { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index e6fa3487386..b81f0a0758f 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1,12 +1,13 @@ import { AnyAction } from 'redux'; -import { AuthActionTypes } from '../actions/auth-actions'; -import { FormatsActionTypes } from '../actions/formats-actions'; -import { ModelsActionTypes } from '../actions/models-actions'; -import { ShareActionTypes } from '../actions/share-actions'; -import { TasksActionTypes } from '../actions/tasks-actions'; -import { UsersActionTypes } from '../actions/users-actions'; -import { NotificationsActionType } from '../actions/notification-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; +import { FormatsActionTypes } from 'actions/formats-actions'; +import { ModelsActionTypes } from 'actions/models-actions'; +import { ShareActionTypes } from 'actions/share-actions'; +import { TasksActionTypes } from 'actions/tasks-actions'; +import { UsersActionTypes } from 'actions/users-actions'; +import { AnnotationActionTypes } from 'actions/annotation-actions'; +import { NotificationsActionType } from 'actions/notification-actions'; import { NotificationsState } from './interfaces'; @@ -44,6 +45,11 @@ const defaultState: NotificationsState = { metaFetching: null, inferenceStatusFetching: null, }, + annotation: { + saving: null, + jobFetching: null, + frameFetching: null, + }, }, messages: { tasks: { @@ -405,6 +411,51 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case AnnotationActionTypes.GET_JOB_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + jobFetching: { + message: 'Error during fetching a job', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AnnotationActionTypes.CHANGE_FRAME_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + frameFetching: { + message: `Could not receive frame ${action.payload.frame}`, + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + saving: { + message: 'Could not save annotations', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } case NotificationsActionType.RESET_ERRORS: { return { ...state, diff --git a/cvat-ui/src/reducers/plugins-reducer.ts b/cvat-ui/src/reducers/plugins-reducer.ts index 1a7e1f16078..253cbcf720d 100644 --- a/cvat-ui/src/reducers/plugins-reducer.ts +++ b/cvat-ui/src/reducers/plugins-reducer.ts @@ -1,8 +1,8 @@ import { AnyAction } from 'redux'; -import { PluginsActionTypes } from '../actions/plugins-actions'; -import { AuthActionTypes } from '../actions/auth-actions'; -import { registerGitPlugin } from '../utils/git-utils'; +import { PluginsActionTypes } from 'actions/plugins-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; +import { registerGitPlugin } from 'utils/git-utils'; import { PluginsState, } from './interfaces'; diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 07e226936d3..d5da95d6581 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -1,5 +1,5 @@ import { AnyAction } from 'redux'; -import { SettingsActionTypes } from '../actions/settings-actions'; +import { SettingsActionTypes } from 'actions/settings-actions'; import { SettingsState, diff --git a/cvat-ui/src/reducers/share-reducer.ts b/cvat-ui/src/reducers/share-reducer.ts index e1c6dcae5c1..17bd58a1e2e 100644 --- a/cvat-ui/src/reducers/share-reducer.ts +++ b/cvat-ui/src/reducers/share-reducer.ts @@ -1,7 +1,7 @@ import { AnyAction } from 'redux'; -import { ShareActionTypes } from '../actions/share-actions'; -import { AuthActionTypes } from '../actions/auth-actions'; +import { ShareActionTypes } from 'actions/share-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; import { ShareState, ShareFileInfo, diff --git a/cvat-ui/src/reducers/tasks-reducer.ts b/cvat-ui/src/reducers/tasks-reducer.ts index eed41a78ac6..08301ff5242 100644 --- a/cvat-ui/src/reducers/tasks-reducer.ts +++ b/cvat-ui/src/reducers/tasks-reducer.ts @@ -1,6 +1,6 @@ import { AnyAction } from 'redux'; -import { TasksActionTypes } from '../actions/tasks-actions'; -import { AuthActionTypes } from '../actions/auth-actions'; +import { TasksActionTypes } from 'actions/tasks-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; import { TasksState, Task } from './interfaces'; diff --git a/cvat-ui/src/reducers/users-reducer.ts b/cvat-ui/src/reducers/users-reducer.ts index bb37e6dfc47..0b44a5d242d 100644 --- a/cvat-ui/src/reducers/users-reducer.ts +++ b/cvat-ui/src/reducers/users-reducer.ts @@ -1,8 +1,8 @@ import { AnyAction } from 'redux'; -import { UsersState } from './interfaces'; -import { AuthActionTypes } from '../actions/auth-actions'; -import { UsersActionTypes } from '../actions/users-actions'; +import { AuthActionTypes } from 'actions/auth-actions'; +import { UsersActionTypes } from 'actions/users-actions'; +import { UsersState } from './interfaces'; const defaultState: UsersState = { users: [], diff --git a/cvat-ui/src/utils/git-utils.ts b/cvat-ui/src/utils/git-utils.ts index ccc402a8fe1..7de60625965 100644 --- a/cvat-ui/src/utils/git-utils.ts +++ b/cvat-ui/src/utils/git-utils.ts @@ -1,4 +1,4 @@ -import getCore from '../core'; +import getCore from 'cvat-core'; const core = getCore(); const baseURL = core.config.backendAPI.slice(0, -7); diff --git a/cvat-ui/src/utils/plugin-checker.ts b/cvat-ui/src/utils/plugin-checker.ts index 0a31ef1a9d4..1c917470560 100644 --- a/cvat-ui/src/utils/plugin-checker.ts +++ b/cvat-ui/src/utils/plugin-checker.ts @@ -1,5 +1,5 @@ -import getCore from '../core'; -import { SupportedPlugins } from '../reducers/interfaces'; +import getCore from 'cvat-core'; +import { SupportedPlugins } from 'reducers/interfaces'; const core = getCore(); diff --git a/cvat-ui/tsconfig.json b/cvat-ui/tsconfig.json index a4261355634..388f07adab9 100644 --- a/cvat-ui/tsconfig.json +++ b/cvat-ui/tsconfig.json @@ -17,10 +17,12 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "preserve" + "jsx": "preserve", + "baseUrl": "src" }, "include": [ "./index.d.ts", - "src/index.tsx" + "src/index.tsx", + "src" ] } diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js index 721b14e204d..86b659672d0 100644 --- a/cvat-ui/webpack.config.js +++ b/cvat-ui/webpack.config.js @@ -6,6 +6,7 @@ /* eslint-disable */ const path = require('path'); const HtmlWebpackPlugin = require("html-webpack-plugin"); +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const Dotenv = require('dotenv-webpack'); module.exports = { @@ -26,6 +27,7 @@ module.exports = { }, resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], + plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })] }, module: { rules: [{