diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index af08193894e1..d6b9825606c9 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -152,6 +152,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { angle: number; canvasSize: Size; image: string; + imageID: number | null; imageOffset: number; imageSize: Size; focusData: FocusData; @@ -183,6 +184,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { width: 0, }, image: '', + imageID: null, imageOffset: 0, imageSize: { height: 0, @@ -300,9 +302,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { width: (frameData.width as number), }; - if (!this.data.rememberAngle) { + if (this.data.imageID !== frameData.number && !this.data.rememberAngle) { this.data.angle = 0; } + this.data.imageID = frameData.number; this.data.image = data; this.notify(UpdateReasons.IMAGE_CHANGED); diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index f14606cea912..185a194b864d 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -44,14 +44,6 @@ export interface CanvasView { html(): HTMLDivElement; } -interface ShapeDict { - [index: number]: SVG.Shape; -} - -interface TextDict { - [index: number]: SVG.Text; -} - function darker(color: string, percentage: number): string { const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100)); const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100)); @@ -78,8 +70,9 @@ export class CanvasViewImpl implements CanvasView, Listener { private gridPath: SVGPathElement; private gridPattern: SVGPatternElement; private controller: CanvasController; - private svgShapes: ShapeDict; - private svgTexts: TextDict; + private svgShapes: Record; + private svgTexts: Record; + private drawnStates: Record; private geometry: Geometry; private drawHandler: DrawHandler; private editHandler: EditHandler; @@ -382,6 +375,38 @@ export class CanvasViewImpl implements CanvasView, Listener { } } + private setupObjects(states: any[]): void { + this.deactivate(); + + const created = []; + const updated = []; + for (const state of states) { + if (!(state.clientID in this.drawnStates)) { + created.push(state); + } else { + const drawnState = this.drawnStates[state.clientID]; + if (drawnState.updated !== state.updated || drawnState.frame !== state.frame) { + updated.push(state); + } + } + } + const newIDs = states.map((state: any): number => state.clientID); + const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID) + .filter((id: number): boolean => !newIDs.includes(id)) + .map((id: number): any => this.drawnStates[id]); + for (const state of deleted) { + if (state.clientID in this.svgTexts) { + this.svgTexts[state.clientID].remove(); + } + + this.svgShapes[state.clientID].remove(); + delete this.drawnStates[state.clientID]; + } + + this.addObjects(created); + this.updateObjects(updated); + } + private selectize(value: boolean, shape: SVG.Element): void { const self = this; @@ -457,6 +482,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.geometry = controller.geometry; this.svgShapes = {}; this.svgTexts = {}; + this.drawnStates = {}; this.activeElement = null; this.mode = Mode.IDLE; @@ -621,31 +647,6 @@ export class CanvasViewImpl implements CanvasView, Listener { } public notify(model: CanvasModel & Master, reason: UpdateReasons): void { - function setupObjects(objects: any[]): void { - const ctm = this.content.getScreenCTM() - .inverse().multiply(this.background.getScreenCTM()); - - this.deactivate(); - - // TODO: Compute difference - - // Instead of simple clearing let's remove all objects properly - for (const id of Object.keys(this.svgShapes)) { - if (id in this.svgTexts) { - this.svgTexts[id].remove(); - } - - this.svgShapes[id].remove(); - } - - this.svgTexts = {}; - this.svgShapes = {}; - - this.addObjects(ctm, objects); - // TODO: Update objects - // TODO: Delete objects - } - this.geometry = this.controller.geometry; if (reason === UpdateReasons.IMAGE_CHANGED) { if (!model.image.length) { @@ -658,6 +659,8 @@ export class CanvasViewImpl implements CanvasView, Listener { this.transformCanvas(); } } else if (reason === UpdateReasons.FITTED_CANVAS) { + // Canvas geometry is going to be changed. Old object positions aren't valid any more + this.setupObjects([]); this.moveCanvas(); this.resizeCanvas(); } else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) { @@ -669,7 +672,7 @@ export class CanvasViewImpl implements CanvasView, Listener { if (this.mode === Mode.GROUP) { this.groupHandler.resetSelectedObjects(); } - setupObjects.call(this, this.controller.objects); + this.setupObjects(this.controller.objects); if (this.mode === Mode.MERGE) { this.mergeHandler.repeatSelection(); } @@ -792,20 +795,94 @@ export class CanvasViewImpl implements CanvasView, Listener { return this.canvas; } - private addObjects(ctm: SVGMatrix, states: any[]): void { + private saveState(state: any): void { + this.drawnStates[state.clientID] = { + clientID: state.clientID, + outside: state.outside, + occluded: state.occluded, + hidden: state.hidden, + lock: state.lock, + points: [...state.points], + attributes: { ...state.attributes }, + }; + } + + private updateObjects(states: any[]): void { + for (const state of states) { + const { clientID } = state; + const drawnState = this.drawnStates[clientID]; + + if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) { + const none = state.hidden || state.outside; + this.svgShapes[clientID].style('display', none ? 'none' : ''); + } + + if (drawnState.occluded !== state.occluded) { + if (state.occluded) { + this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded'); + } else { + this.svgShapes[clientID].removeClass('cvat_canvas_shape_occluded'); + } + } + + if (drawnState.points + .some((p: number, id: number): boolean => p !== state.points[id]) + ) { + const translatedPoints: number[] = translateBetweenSVG( + this.background, this.content, state.points, + ); + + if (state.shapeType === 'rectangle') { + const [xtl, ytl, xbr, ybr] = translatedPoints; + + this.svgShapes[clientID].attr({ + x: xtl, + y: ytl, + width: xbr - xtl, + height: ybr - ytl, + }); + } else { + const stringified = translatedPoints.reduce( + (acc: string, val: number, idx: number): string => { + if (idx % 2) { + return `${acc}${val} `; + } + + return `${acc}${val},`; + }, '', + ); + + this.svgShapes[clientID].attr('points', stringified); + } + } + + for (const attrID of Object.keys(state.attributes)) { + if (state.attributes[attrID] !== drawnState.attributes[attrID]) { + const text = this.svgTexts[state.clientID]; + if (text) { + const [span] = this.svgTexts[state.clientID].node + .querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[]; + if (span && span.textContent) { + const prefix = span.textContent.split(':').slice(0, -1).join(':'); + span.textContent = `${prefix}: ${state.attributes[attrID]}`; + } + } + } + } + + this.saveState(state); + } + } + + private addObjects(states: any[]): void { for (const state of states) { if (state.objectType === 'tag') { this.addTag(state); } else { const points: number[] = (state.points as number[]); - const translatedPoints: number[] = []; - for (let i = 0; i <= points.length - 1; i += 2) { - let point: SVGPoint = this.background.createSVGPoint(); - point.x = points[i]; - point.y = points[i + 1]; - point = point.matrixTransform(ctm); - translatedPoints.push(point.x, point.y); - } + const translatedPoints: number[] = translateBetweenSVG( + this.background, this.content, points, + ); // TODO: Use enums after typification cvat-core if (state.shapeType === 'rectangle') { @@ -833,16 +910,9 @@ export class CanvasViewImpl implements CanvasView, Listener { .addPoints(stringified, state); } } - - // TODO: Use enums after typification cvat-core - if (state.visibility === 'all') { - this.svgTexts[state.clientID] = this.addText(state); - this.updateTextPosition( - this.svgTexts[state.clientID], - this.svgShapes[state.clientID], - ); - } } + + this.saveState(state); } } @@ -852,17 +922,22 @@ export class CanvasViewImpl implements CanvasView, Listener { const shape = this.svgShapes[this.activeElement.state.clientID]; shape.removeClass('cvat_canvas_shape_activated'); + (shape as any).off('dragstart'); + (shape as any).off('dragend'); (shape as any).draggable(false); if (state.shapeType !== 'points') { this.selectize(false, shape); } + (shape as any).off('resizestart'); + (shape as any).off('resizing'); + (shape as any).off('resizedone'); (shape as any).resize(false); - // Hide text only if it is hidden by settings + // TODO: Hide text only if it is hidden by settings const text = this.svgTexts[state.clientID]; - if (text && state.visibility === 'shape') { + if (text) { text.remove(); delete this.svgTexts[state.clientID]; } @@ -893,7 +968,7 @@ export class CanvasViewImpl implements CanvasView, Listener { shape.addClass('cvat_canvas_shape_activated'); let text = this.svgTexts[activeElement.clientID]; // Draw text if it's hidden by default - if (!text && state.visibility === 'shape') { + if (!text) { text = this.addText(state); this.svgTexts[state.clientID] = text; this.updateTextPosition( @@ -1057,6 +1132,10 @@ export class CanvasViewImpl implements CanvasView, Listener { rect.addClass('cvat_canvas_shape_occluded'); } + if (state.hidden || state.outside) { + rect.style('display', 'none'); + } + return rect; } @@ -1076,6 +1155,10 @@ export class CanvasViewImpl implements CanvasView, Listener { polygon.addClass('cvat_canvas_shape_occluded'); } + if (state.hidden || state.outside) { + polygon.style('display', 'none'); + } + return polygon; } @@ -1095,6 +1178,10 @@ export class CanvasViewImpl implements CanvasView, Listener { polyline.addClass('cvat_canvas_shape_occluded'); } + if (state.hidden || state.outside) { + polyline.style('display', 'none'); + } + return polyline; } @@ -1120,6 +1207,11 @@ export class CanvasViewImpl implements CanvasView, Listener { }).style({ 'fill-opacity': 1, }); + + if (state.hidden || state.outside) { + group.style('display', 'none'); + } + group.bbox = shape.bbox.bind(shape); group.clone = shape.clone.bind(shape); diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 873d2070f6b7..02a208eeaf47 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -34,13 +34,9 @@ const { ObjectShape, ObjectType, + colors, } = require('./enums'); const ObjectState = require('./object-state'); - const colors = [ - '#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; @@ -381,9 +377,6 @@ // Remove other shapes for (const object of objectsForMerge) { object.removed = true; - if (typeof (object.resetCache) === 'function') { - object.resetCache(); - } } } @@ -470,7 +463,6 @@ // Remove source object object.removed = true; - object.resetCache(); } group(objectStates, reset) { @@ -490,9 +482,6 @@ const groupIdx = reset ? 0 : ++this.groups.max; for (const object of objectsForGroup) { object.group = groupIdx; - if (typeof (object.resetCache) === 'function') { - object.resetCache(); - } } return groupIdx; diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 0f1a605202f7..1e05b1e91bf1 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -11,13 +11,11 @@ const ObjectState = require('./object-state'); const { checkObjectType, - isEnum, } = require('./common'); const { ObjectShape, ObjectType, AttributeType, - VisibleState, } = require('./enums'); const { @@ -32,7 +30,8 @@ function objectStateFactory(frame, data) { const objectState = new ObjectState(data); - objectState.hidden = { + // eslint-disable-next-line no-underscore-dangle + objectState.__internal = { save: this.save.bind(this, frame, objectState), delete: this.delete.bind(this), up: this.up.bind(this, frame, objectState), @@ -127,6 +126,10 @@ return ['true', 'false'].includes(value.toLowerCase()); } + if (type === AttributeType.TEXT) { + return true; + } + return values.includes(value); } @@ -140,6 +143,7 @@ this.frame = data.frame; this.removed = false; this.lock = false; + this.updated = Date.now(); this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { attributeAccumulator[attr.spec_id] = attr.value; return attributeAccumulator; @@ -158,6 +162,16 @@ } } + updateTimestamp(updated) { + const anyChanges = updated.label || updated.attributes || updated.points + || updated.outside || updated.occluded || updated.keyframe + || updated.group || updated.zOrder; + + if (anyChanges) { + this.updated = Date.now(); + } + } + delete(force) { if (!this.lock || force) { this.removed = true; @@ -173,7 +187,7 @@ this.frameMeta = injection.frameMeta; this.collectionZ = injection.collectionZ; - this.visibility = VisibleState.SHAPE; + this.hidden = false; this.color = color; this.shapeType = null; @@ -277,7 +291,8 @@ label: this.label, group: this.group, color: this.color, - visibility: this.visibility, + hidden: this.hidden, + updated: this.updated, frame, }; } @@ -313,9 +328,14 @@ for (const attrID of Object.keys(data.attributes)) { const value = data.attributes[attrID]; - if (attrID in labelAttributes - && validateAttributeValue(value, labelAttributes[attrID])) { - copy.attributes[attrID] = value; + if (attrID in labelAttributes) { + if (validateAttributeValue(value, labelAttributes[attrID])) { + copy.attributes[attrID] = value; + } else { + throw new ArgumentError( + `Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`, + ); + } } else { throw new ArgumentError( `Trying to save unknown attribute with id ${attrID} and value ${value}`, @@ -380,24 +400,22 @@ copy.color = data.color; } - if (updated.visibility) { - if (!isEnum.call(VisibleState, data.visibility)) { - throw new ArgumentError( - `Got invalid visibility value: "${data.visibility}"`, - ); - } - - copy.visibility = data.visibility; + if (updated.hidden) { + checkObjectType('hidden', data.hidden, 'boolean', null); + copy.hidden = data.hidden; } - // Reset flags and commit all changes - updated.reset(); + // Commit state for (const prop of Object.keys(copy)) { if (prop in this) { this[prop] = copy[prop]; } } + // Reset flags and commit all changes + this.updateTimestamp(updated); + updated.reset(); + return objectStateFactory.call(this, frame, this.get(frame)); } } @@ -424,8 +442,6 @@ return shapeAccumulator; }, {}); - - this.cache = {}; } // Method is used to export data to the server @@ -480,27 +496,21 @@ // Method is used to construct ObjectState objects get(frame) { - if (!(frame in this.cache)) { - const interpolation = { - ...this.getPosition(frame), - attributes: this.getAttributes(frame), - group: this.group, - objectType: ObjectType.TRACK, - shapeType: this.shapeType, - clientID: this.clientID, - serverID: this.serverID, - lock: this.lock, - color: this.color, - visibility: this.visibility, - frame, - }; - - this.cache[frame] = interpolation; - } - - const result = JSON.parse(JSON.stringify(this.cache[frame])); - result.label = this.label; - return result; + return { + ...this.getPosition(frame), + attributes: this.getAttributes(frame), + group: this.group, + objectType: ObjectType.TRACK, + shapeType: this.shapeType, + clientID: this.clientID, + serverID: this.serverID, + lock: this.lock, + color: this.color, + hidden: this.hidden, + updated: this.updated, + label: this.label, + frame, + }; } neighborsFrames(targetFrame) { @@ -584,9 +594,14 @@ if (updated.attributes) { for (const attrID of Object.keys(data.attributes)) { const value = data.attributes[attrID]; - if (attrID in labelAttributes - && validateAttributeValue(value, labelAttributes[attrID])) { - copy.attributes[attrID] = value; + if (attrID in labelAttributes) { + if (validateAttributeValue(value, labelAttributes[attrID])) { + copy.attributes[attrID] = value; + } else { + throw new ArgumentError( + `Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`, + ); + } } else { throw new ArgumentError( `Trying to save unknown attribute with id ${attrID} and value ${value}`, @@ -660,14 +675,9 @@ copy.color = data.color; } - if (updated.visibility) { - if (!isEnum.call(VisibleState, data.visibility)) { - throw new ArgumentError( - `Got invalid visibility value: "${data.visibility}"`, - ); - } - - copy.visibility = data.visibility; + if (updated.hidden) { + checkObjectType('hidden', data.hidden, 'boolean', null); + copy.hidden = data.hidden; } if (updated.keyframe) { @@ -680,16 +690,14 @@ if (prop in this) { this[prop] = copy[prop]; } - - this.cache[frame][prop] = copy[prop]; } if (updated.attributes) { // Mutable attributes will be updated below for (const attrID of Object.keys(copy.attributes)) { if (!labelAttributes[attrID].mutable) { - this.shapes[frame].attributes[attrID] = data.attributes[attrID]; - this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + this.attributes[attrID] = data.attributes[attrID]; + this.attributes[attrID] = data.attributes[attrID]; } } } @@ -702,42 +710,21 @@ // Remove keyframe if (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) { - delete this.cache[cacheFrame]; + if (frame in this.shapes) { + if (Object.keys(this.shapes).length === 1) { + throw new DataError('You cannot remove the latest keyframe of a track'); } - } - this.cache[frame].keyframe = false; - delete this.shapes[frame]; - updated.reset(); + delete this.shapes[frame]; + this.updateTimestamp(updated); + updated.reset(); + } return objectStateFactory.call(this, frame, this.get(frame)); } // Add/update keyframe - if (positionUpdated || (updated.keyframe && data.keyframe)) { - // 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]; - } - } - - this.cache[frame].keyframe = true; + if (positionUpdated || updated.attributes || (updated.keyframe && data.keyframe)) { data.keyframe = true; this.shapes[frame] = { @@ -760,6 +747,7 @@ } } + this.updateTimestamp(updated); updated.reset(); return objectStateFactory.call(this, frame, this.get(frame)); @@ -823,15 +811,10 @@ delete(force) { if (!this.lock || force) { this.removed = true; - this.resetCache(); } return true; } - - resetCache() { - this.cache = {}; - } } class Tag extends Annotation { @@ -874,6 +857,7 @@ attributes: { ...this.attributes }, label: this.label, group: this.group, + updated: this.updated, frame, }; } @@ -921,14 +905,17 @@ copy.lock = data.lock; } - // Reset flags and commit all changes - updated.reset(); + // Commit state for (const prop of Object.keys(copy)) { if (prop in this) { this[prop] = copy[prop]; } } + // Reset flags and commit all changes + this.updateTimestamp(updated); + updated.reset(); + return objectStateFactory.call(this, frame, this.get(frame)); } } diff --git a/cvat-core/src/annotations-saver.js b/cvat-core/src/annotations-saver.js index b4f9e0e73061..42a07ecb39d4 100644 --- a/cvat-core/src/annotations-saver.js +++ b/cvat-core/src/annotations-saver.js @@ -167,10 +167,6 @@ for (let i = 0; i < indexes[type].length; i++) { const clientID = indexes[type][i]; this.collection.objects[clientID].serverID = saved[type][i].id; - if (type === 'tracks') { - // We have to reset cache because of old value of serverID was saved there - this.collection.objects[clientID].resetCache(); - } } } } diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 9c720c70f5b6..6414e1737ec0 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -27,8 +27,8 @@ function build() { AttributeType, ObjectType, ObjectShape, - VisibleState, LogType, + colors, } = require('./enums'); const { @@ -497,8 +497,8 @@ function build() { AttributeType, ObjectType, ObjectShape, - VisibleState, LogType, + colors, }, /** * Namespace is used for access to exceptions diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index 241cca71a0c7..a0e7b2c41d47 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -102,22 +102,6 @@ POINTS: 'points', }); - /** - * Object visibility states - * @enum {string} - * @name ObjectShape - * @memberof module:API.cvat.enums - * @property {string} ALL 'all' - * @property {string} SHAPE 'shape' - * @property {string} NONE 'none' - * @readonly - */ - const VisibleState = Object.freeze({ - ALL: 'all', - SHAPE: 'shape', - NONE: 'none', - }); - /** * Event types * @enum {number} @@ -182,6 +166,21 @@ rotateImage: 26, }; + /** + * Array of hex color + * @type {module:API.cvat.classes.Loader[]} values + * @name colors + * @memberof module:API.cvat.enums + * @type {string[]} + * @readonly + */ + const colors = [ + '#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66', + '#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33', + '#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37', + '#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380', + ]; + module.exports = { ShareFileType, TaskStatus, @@ -189,7 +188,7 @@ AttributeType, ObjectType, ObjectShape, - VisibleState, LogType, + colors, }; })(); diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index e60d7a8db907..1ab2eaf21a94 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -52,6 +52,13 @@ value: tid, writable: false, }, + /** + * @name number + * @type {integer} + * @memberof module:API.cvat.classes.FrameData + * @readonly + * @instance + */ number: { value: number, writable: false, diff --git a/cvat-core/src/labels.js b/cvat-core/src/labels.js index 2730e6e3544d..fa92a61196ea 100644 --- a/cvat-core/src/labels.js +++ b/cvat-core/src/labels.js @@ -8,7 +8,10 @@ */ (() => { - const { AttributeType } = require('./enums'); + const { + AttributeType, + colors, + } = require('./enums'); const { ArgumentError } = require('./exceptions'); /** @@ -136,6 +139,7 @@ const data = { id: undefined, name: undefined, + color: undefined, }; for (const key in data) { @@ -146,6 +150,9 @@ } } + if (typeof (data.id) !== 'undefined') { + data.color = colors[data.id % colors.length]; + } data.attributes = []; if (Object.prototype.hasOwnProperty.call(initialData, 'attributes') @@ -176,6 +183,23 @@ name: { get: () => data.name, }, + /** + * @name color + * @type {string} + * @memberof module:API.cvat.classes.Label + * @readonly + * @instance + */ + color: { + get: () => data.color, + set: (color) => { + if (colors.includes(color)) { + data.color = color; + } else { + throw new ArgumentError('Trying to set unknown color'); + } + }, + }, /** * @name attributes * @type {module:API.cvat.classes.Attribute[]} diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js index a3ad09520e22..5d130d5e62bb 100644 --- a/cvat-core/src/object-state.js +++ b/cvat-core/src/object-state.js @@ -19,11 +19,8 @@ /** * @param {Object} serialized - is an dictionary which contains * initial information about an ObjectState; - * Necessary fields: objectType, shapeType - * (don't have setters) - * Necessary fields for objects which haven't been added to collection yet: frame - * (doesn't have setters) - * Optional fields: points, group, zOrder, outside, occluded, + * Necessary fields: objectType, shapeType, frame, updated + * Optional fields: points, group, zOrder, outside, occluded, hidden, * attributes, lock, label, mode, color, keyframe, clientID, serverID * These fields can be set later via setters */ @@ -41,7 +38,8 @@ zOrder: null, lock: null, color: null, - visibility: null, + hidden: null, + updated: serialized.updated, clientID: serialized.clientID, serverID: serialized.serverID, @@ -67,7 +65,9 @@ this.zOrder = false; this.lock = false; this.color = false; - this.visibility = false; + this.hidden = false; + + return reset; }, writable: false, }); @@ -153,17 +153,17 @@ data.color = color; }, }, - visibility: { + hidden: { /** - * @name visibility - * @type {module:API.cvat.enums.VisibleState} + * @name hidden + * @type {boolean} * @memberof module:API.cvat.classes.ObjectState * @instance */ - get: () => data.visibility, - set: (visibility) => { - data.updateFlags.visibility = true; - data.visibility = visibility; + get: () => data.hidden, + set: (hidden) => { + data.updateFlags.hidden = true; + data.hidden = hidden; }, }, points: { @@ -266,6 +266,17 @@ data.lock = lock; }, }, + updated: { + /** + * Timestamp of the latest updated of the object + * @name updated + * @type {number} + * @memberof module:API.cvat.classes.ObjectState + * @instance + * @readonly + */ + get: () => data.updated, + }, attributes: { /** * Object is id:value pairs where "id" is an integer @@ -302,7 +313,7 @@ this.occluded = serialized.occluded; this.color = serialized.color; this.lock = serialized.lock; - this.visibility = serialized.visibility; + this.hidden = serialized.hidden; // It can be undefined in a constructor and it can be defined later if (typeof (serialized.points) !== 'undefined') { @@ -382,8 +393,8 @@ // Updates element in collection which contains it ObjectState.prototype.save.implementation = async function () { - if (this.hidden && this.hidden.save) { - return this.hidden.save(); + if (this.__internal && this.__internal.save) { + return this.__internal.save(); } return this; @@ -391,24 +402,24 @@ // Delete element from a collection which contains it ObjectState.prototype.delete.implementation = async function (force) { - if (this.hidden && this.hidden.delete) { - return this.hidden.delete(force); + if (this.__internal && this.__internal.delete) { + return this.__internal.delete(force); } return false; }; ObjectState.prototype.up.implementation = async function () { - if (this.hidden && this.hidden.up) { - return this.hidden.up(); + if (this.__internal && this.__internal.up) { + return this.__internal.up(); } return false; }; ObjectState.prototype.down.implementation = async function () { - if (this.hidden && this.hidden.down) { - return this.hidden.down(); + if (this.__internal && this.__internal.down) { + return this.__internal.down(); } return false; diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index d7edd37f5a02..255e584f7074 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1094,6 +1094,27 @@ "@types/react": "*" } }, + "@types/redux-logger": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.7.tgz", + "integrity": "sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A==", + "requires": { + "redux": "^3.6.0" + }, + "dependencies": { + "redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "requires": { + "lodash": "^4.2.1", + "lodash-es": "^4.2.1", + "loose-envify": "^1.1.0", + "symbol-observable": "^1.0.3" + } + } + } + }, "@typescript-eslint/eslint-plugin": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz", @@ -3217,6 +3238,11 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=" + }, "deep-equal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz", @@ -6571,6 +6597,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -9510,6 +9541,14 @@ "symbol-observable": "^1.2.0" } }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "requires": { + "deep-diff": "^0.3.5" + } + }, "redux-thunk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 8d6f75f58ce2..5059ae7f9e4c 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -48,6 +48,7 @@ "@types/react-router": "^5.0.5", "@types/react-router-dom": "^5.1.0", "@types/react-share": "^3.0.1", + "@types/redux-logger": "^3.0.7", "antd": "^3.25.2", "copy-to-clipboard": "^3.2.0", "dotenv-webpack": "^1.7.0", @@ -60,6 +61,7 @@ "react-router-dom": "^5.1.0", "react-share": "^3.0.1", "redux": "^4.0.4", + "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0" } } diff --git a/cvat-ui/src/actions/about-actions.ts b/cvat-ui/src/actions/about-actions.ts index ce76e59faeb2..4315fa045989 100644 --- a/cvat-ui/src/actions/about-actions.ts +++ b/cvat-ui/src/actions/about-actions.ts @@ -20,10 +20,10 @@ function getAbout(): AnyAction { return action; } -function getAboutSuccess(about: any): AnyAction { +function getAboutSuccess(server: any): AnyAction { const action = { type: AboutActionTypes.GET_ABOUT_SUCCESS, - payload: { about }, + payload: { server }, }; return action; diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 11603ac51c39..80f51c0cdd26 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -24,21 +24,66 @@ export enum AnnotationActionTypes { SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS', SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS', SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED', - SAVE_ANNOTATIONS_UPDATED_STATUS = 'SAVE_ANNOTATIONS_UPDATED_STATUS', + SAVE_UPDATE_ANNOTATIONS_STATUS = 'SAVE_UPDATE_ANNOTATIONS_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', + DRAW_SHAPE = 'DRAW_SHAPE', + SHAPE_DRAWN = 'SHAPE_DRAWN', RESET_CANVAS = 'RESET_CANVAS', - ANNOTATIONS_UPDATED = 'ANNOTATIONS_UPDATED', + UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS', + UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED', + CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS', + CREATE_ANNOTATIONS_FAILED = 'CREATE_ANNOTATIONS_FAILED', + MERGE_ANNOTATIONS_SUCCESS = 'MERGE_ANNOTATIONS_SUCCESS', + MERGE_ANNOTATIONS_FAILED = 'MERGE_ANNOTATIONS_FAILED', + GROUP_ANNOTATIONS_SUCCESS = 'GROUP_ANNOTATIONS_SUCCESS', + GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED', + SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS', + SPLIT_ANNOTATIONS_FAILED = 'SPLIT_ANNOTATIONS_FAILED', + CHANGE_LABEL_COLOR_SUCCESS = 'CHANGE_LABEL_COLOR_SUCCESS', + CHANGE_LABEL_COLOR_FAILED = 'CHANGE_LABEL_COLOR_FAILED', + UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT', + COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR', + COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE', + COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS' +} + +export function updateTabContentHeight(tabContentHeight: number): AnyAction { + return { + type: AnnotationActionTypes.UPDATE_TAB_CONTENT_HEIGHT, + payload: { + tabContentHeight, + }, + }; +} + +export function collapseSidebar(): AnyAction { + return { + type: AnnotationActionTypes.COLLAPSE_SIDEBAR, + payload: {}, + }; +} + +export function collapseAppearance(): AnyAction { + return { + type: AnnotationActionTypes.COLLAPSE_APPEARANCE, + payload: {}, + }; +} + +export function collapseObjectItems(states: any[], collapsed: boolean): AnyAction { + return { + type: AnnotationActionTypes.COLLAPSE_OBJECT_ITEMS, + payload: { + states, + collapsed, + }, + }; } export function switchPlay(playing: boolean): AnyAction { @@ -50,47 +95,56 @@ export function switchPlay(playing: boolean): AnyAction { }; } -export function changeFrameAsync(toFrame: number, playing: boolean): +export function changeFrameAsync(toFrame: number): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { const store = getCVATStore(); const state: CombinedState = store.getState(); - const { jobInstance } = state.annotation; - const currentFrame = state.annotation.frame; + const { instance: job } = state.annotation.job; + const { number: frame } = state.annotation.player.frame; - const frame = Math.max( - Math.min(toFrame, jobInstance.stopFrame), - jobInstance.startFrame, - ); - - // !playing || state.annotation.playing prevents changing frame on the latest setTimeout - // after playing had become false - if (frame !== currentFrame && (!playing || state.annotation.playing)) { - dispatch({ - type: AnnotationActionTypes.CHANGE_FRAME, - payload: {}, - }); + try { + if (toFrame < job.startFrame || toFrame > job.stopFrame) { + throw Error(`Required frame ${toFrame} is out of the current job`); + } - try { - const frameData = await jobInstance.frames.get(frame); - const annotations = await jobInstance.annotations.get(frame); + if (toFrame === frame) { dispatch({ type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, payload: { - frame, - frameData, - annotations, - }, - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.CHANGE_FRAME_FAILED, - payload: { - frame, - error, + number: state.annotation.player.frame.number, + data: state.annotation.player.frame.data, + states: state.annotation.annotations.states, }, }); + + return; } + + // Start async requests + dispatch({ + type: AnnotationActionTypes.CHANGE_FRAME, + payload: {}, + }); + + const data = await job.frames.get(toFrame); + const states = await job.annotations.get(toFrame); + dispatch({ + type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, + payload: { + number: toFrame, + data, + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.CHANGE_FRAME_FAILED, + payload: { + number: toFrame, + error, + }, + }); } }; } @@ -138,30 +192,37 @@ ThunkAction, {}, {}, AnyAction> { try { const store = getCVATStore(); const state: CombinedState = store.getState(); + + // First check state if the task is already there let task = state.tasks.current .filter((_task: Task) => _task.instance.id === tid) .map((_task: Task) => _task.instance)[0]; + + // If there aren't the task, get it from the server if (!task) { [task] = await cvat.tasks.get({ id: tid }); } + // Finally get the job from the task const job = task.jobs .filter((_job: any) => _job.id === jid)[0]; if (!job) { - throw new Error('Job with specified id does not exist'); + throw new Error(`Task ${tid} doesn't contain the job ${jid}`); } - const frame = Math.min(0, job.startFrame); - const frameData = await job.frames.get(frame); - const annotations = await job.annotations.get(frame); + const frameNumber = Math.max(0, job.startFrame); + const frameData = await job.frames.get(frameNumber); + const states = await job.annotations.get(frameNumber); + const colors = [...cvat.enums.colors]; dispatch({ type: AnnotationActionTypes.GET_JOB_SUCCESS, payload: { - jobInstance: job, + job, + states, + frameNumber, frameData, - annotations, - frame, + colors, }, }); } catch (error) { @@ -186,7 +247,7 @@ ThunkAction, {}, {}, AnyAction> { try { await sessionInstance.annotations.save((status: string) => { dispatch({ - type: AnnotationActionTypes.SAVE_ANNOTATIONS_UPDATED_STATUS, + type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS, payload: { status, }, @@ -242,53 +303,172 @@ export function shapeDrawn(): AnyAction { }; } -export function mergeObjects(): AnyAction { +export function mergeObjects(enabled: boolean): AnyAction { return { type: AnnotationActionTypes.MERGE_OBJECTS, - payload: {}, + payload: { + enabled, + }, }; } -export function objectsMerged(): AnyAction { +export function groupObjects(enabled: boolean): AnyAction { return { - type: AnnotationActionTypes.OBJECTS_MERGED, - payload: {}, + type: AnnotationActionTypes.GROUP_OBJECTS, + payload: { + enabled, + }, }; } -export function groupObjects(): AnyAction { +export function splitTrack(enabled: boolean): AnyAction { return { - type: AnnotationActionTypes.GROUP_OBJECTS, - payload: {}, + type: AnnotationActionTypes.SPLIT_TRACK, + payload: { + enabled, + }, }; } -export function objectsGroupped(): AnyAction { - return { - type: AnnotationActionTypes.OBJECTS_GROUPPED, - payload: {}, +export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + const promises = statesToUpdate.map((state: any): Promise => state.save()); + const states = await Promise.all(promises); + + dispatch({ + type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS, + payload: { + states, + }, + }); + } catch (error) { + const states = await sessionInstance.annotations.get(frame); + dispatch({ + type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED, + payload: { + error, + states, + }, + }); + } }; } -export function splitTrack(): AnyAction { - return { - type: AnnotationActionTypes.SPLIT_TRACK, - payload: {}, +export function createAnnotationsAsync(sessionInstance: any, frame: number, statesToCreate: any[]): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + await sessionInstance.annotations.put(statesToCreate); + const states = await sessionInstance.annotations.get(frame); + + dispatch({ + type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS, + payload: { + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.CREATE_ANNOTATIONS_FAILED, + payload: { + error, + }, + }); + } }; } -export function trackSplitted(): AnyAction { - return { - type: AnnotationActionTypes.TRACK_SPLITTED, - payload: {}, +export function mergeAnnotationsAsync(sessionInstance: any, frame: number, statesToMerge: any[]): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + await sessionInstance.annotations.merge(statesToMerge); + const states = await sessionInstance.annotations.get(frame); + + dispatch({ + type: AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS, + payload: { + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.MERGE_ANNOTATIONS_FAILED, + payload: { + error, + }, + }); + } }; } -export function annotationsUpdated(annotations: any[]): AnyAction { - return { - type: AnnotationActionTypes.ANNOTATIONS_UPDATED, - payload: { - annotations, - }, +export function groupAnnotationsAsync(sessionInstance: any, frame: number, statesToGroup: any[]): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + await sessionInstance.annotations.group(statesToGroup); + const states = await sessionInstance.annotations.get(frame); + + dispatch({ + type: AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS, + payload: { + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.GROUP_ANNOTATIONS_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + await sessionInstance.annotations.split(stateToSplit, frame); + const states = await sessionInstance.annotations.get(frame); + + dispatch({ + type: AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS, + payload: { + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.SPLIT_ANNOTATIONS_FAILED, + payload: { + error, + }, + }); + } }; } + +export function changeLabelColor(label: any, color: string): AnyAction { + try { + const updatedLabel = label; + updatedLabel.color = color; + + return { + type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS, + payload: { + label: updatedLabel, + }, + }; + } catch (error) { + return { + type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED, + payload: { + error, + }, + }; + } +} diff --git a/cvat-ui/src/actions/models-actions.ts b/cvat-ui/src/actions/models-actions.ts index f120d11d923b..c44f57675c60 100644 --- a/cvat-ui/src/actions/models-actions.ts +++ b/cvat-ui/src/actions/models-actions.ts @@ -76,9 +76,9 @@ ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { const store = getCVATStore(); const state: CombinedState = store.getState(); - const OpenVINO = state.plugins.plugins.AUTO_ANNOTATION; - const RCNN = state.plugins.plugins.TF_ANNOTATION; - const MaskRCNN = state.plugins.plugins.TF_SEGMENTATION; + const OpenVINO = state.plugins.list.AUTO_ANNOTATION; + const RCNN = state.plugins.list.TF_ANNOTATION; + const MaskRCNN = state.plugins.list.TF_SEGMENTATION; dispatch(getModels()); const models: Model[] = []; @@ -468,9 +468,9 @@ ThunkAction, {}, {}, AnyAction> { const store = getCVATStore(); const state: CombinedState = store.getState(); - const OpenVINO = state.plugins.plugins.AUTO_ANNOTATION; - const RCNN = state.plugins.plugins.TF_ANNOTATION; - const MaskRCNN = state.plugins.plugins.TF_SEGMENTATION; + const OpenVINO = state.plugins.list.AUTO_ANNOTATION; + const RCNN = state.plugins.list.TF_ANNOTATION; + const MaskRCNN = state.plugins.list.TF_SEGMENTATION; try { if (OpenVINO) { diff --git a/cvat-ui/src/actions/plugins-actions.ts b/cvat-ui/src/actions/plugins-actions.ts index 26e9faaac727..a102db714d72 100644 --- a/cvat-ui/src/actions/plugins-actions.ts +++ b/cvat-ui/src/actions/plugins-actions.ts @@ -21,11 +21,11 @@ function checkPlugins(): AnyAction { return action; } -function checkedAllPlugins(plugins: PluginObjects): AnyAction { +function checkedAllPlugins(list: PluginObjects): AnyAction { const action = { type: PluginsActionTypes.CHECKED_ALL_PLUGINS, payload: { - plugins, + list, }, }; diff --git a/cvat-ui/src/assets/back-jump-icon.svg b/cvat-ui/src/assets/back-jump-icon.svg new file mode 100644 index 000000000000..bf099f5d0222 --- /dev/null +++ b/cvat-ui/src/assets/back-jump-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/assets/expand-all-objects-icon.svg b/cvat-ui/src/assets/expand-all-objects-icon.svg deleted file mode 100644 index f811fb1bfc79..000000000000 --- a/cvat-ui/src/assets/expand-all-objects-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/first-icon.svg b/cvat-ui/src/assets/first-icon.svg new file mode 100644 index 000000000000..46eabc106745 --- /dev/null +++ b/cvat-ui/src/assets/first-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/assets/forward-jump-icon.svg b/cvat-ui/src/assets/forward-jump-icon.svg new file mode 100644 index 000000000000..e6e0f18e7986 --- /dev/null +++ b/cvat-ui/src/assets/forward-jump-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/assets/icon-object-video-hide-frames.svg b/cvat-ui/src/assets/icon-object-video-hide-frames.svg deleted file mode 100644 index 2d6940768d03..000000000000 --- a/cvat-ui/src/assets/icon-object-video-hide-frames.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/last-icon.svg b/cvat-ui/src/assets/last-icon.svg new file mode 100644 index 000000000000..710b62713da9 --- /dev/null +++ b/cvat-ui/src/assets/last-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/assets/next-icon.svg b/cvat-ui/src/assets/next-icon.svg new file mode 100644 index 000000000000..27af75f87720 --- /dev/null +++ b/cvat-ui/src/assets/next-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/assets/object-inside-icon.svg b/cvat-ui/src/assets/object-inside-icon.svg new file mode 100644 index 000000000000..f7e1236ebe4f --- /dev/null +++ b/cvat-ui/src/assets/object-inside-icon.svg @@ -0,0 +1,7 @@ + diff --git a/cvat-ui/src/assets/object-outside-icon.svg b/cvat-ui/src/assets/object-outside-icon.svg new file mode 100644 index 000000000000..dae8de9fa499 --- /dev/null +++ b/cvat-ui/src/assets/object-outside-icon.svg @@ -0,0 +1,8 @@ + diff --git a/cvat-ui/src/assets/pause-icon.svg b/cvat-ui/src/assets/pause-icon.svg new file mode 100644 index 000000000000..255d8a9c7f95 --- /dev/null +++ b/cvat-ui/src/assets/pause-icon.svg @@ -0,0 +1,8 @@ + diff --git a/cvat-ui/src/assets/play-icon.svg b/cvat-ui/src/assets/play-icon.svg new file mode 100644 index 000000000000..fcd0b868b7fe --- /dev/null +++ b/cvat-ui/src/assets/play-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/assets/playcontrol-back-jump-icon.svg b/cvat-ui/src/assets/playcontrol-back-jump-icon.svg deleted file mode 100644 index 97a8d64199a1..000000000000 --- a/cvat-ui/src/assets/playcontrol-back-jump-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/playcontrol-first-icon.svg b/cvat-ui/src/assets/playcontrol-first-icon.svg deleted file mode 100644 index 1b3b39269407..000000000000 --- a/cvat-ui/src/assets/playcontrol-first-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/playcontrol-forward-jump-icon.svg b/cvat-ui/src/assets/playcontrol-forward-jump-icon.svg deleted file mode 100644 index af47e5e096fb..000000000000 --- a/cvat-ui/src/assets/playcontrol-forward-jump-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/playcontrol-last-icon.svg b/cvat-ui/src/assets/playcontrol-last-icon.svg deleted file mode 100644 index fd96914fdec9..000000000000 --- a/cvat-ui/src/assets/playcontrol-last-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/playcontrol-next-icon.svg b/cvat-ui/src/assets/playcontrol-next-icon.svg deleted file mode 100644 index 47751840cf77..000000000000 --- a/cvat-ui/src/assets/playcontrol-next-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/playcontrol-pause-icon.svg b/cvat-ui/src/assets/playcontrol-pause-icon.svg deleted file mode 100644 index 58e161d01129..000000000000 --- a/cvat-ui/src/assets/playcontrol-pause-icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/cvat-ui/src/assets/playcontrol-play-icon.svg b/cvat-ui/src/assets/playcontrol-play-icon.svg deleted file mode 100644 index 570327975e92..000000000000 --- a/cvat-ui/src/assets/playcontrol-play-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/playcontrol-previous-icon.svg b/cvat-ui/src/assets/playcontrol-previous-icon.svg deleted file mode 100644 index 154b66bb6bc8..000000000000 --- a/cvat-ui/src/assets/playcontrol-previous-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/cvat-ui/src/assets/previous-icon.svg b/cvat-ui/src/assets/previous-icon.svg new file mode 100644 index 000000000000..3143484da1ba --- /dev/null +++ b/cvat-ui/src/assets/previous-icon.svg @@ -0,0 +1,5 @@ + diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 17348718f067..9d43c1ad0ff7 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -11,7 +11,11 @@ $background-color-1: white; $background-color-2: #F1F1F1; $transparent-color: rgba(0, 0, 0, 0); $player-slider-color: #979797; +$player-buttons-color: #242424; $danger-icon-color: #FF4136; $info-icon-color: #0074D9; +$objects-bar-tabs-color: #BEBEBE; +$objects-bar-icons-color: #242424; // #6E6E6E +$active-object-item-background-color: #D8ECFF; $monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 11e32c1349c4..842b1b0217c0 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -8,22 +8,22 @@ import { } from 'antd'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; -import StandardWorkspaceContainer from 'containers/annotation-page/standard-workspace/standard-workspace'; +import StandardWorkspaceComponent from './standard-workspace/standard-workspace'; interface Props { - jobInstance: any | null | undefined; + job: any | null | undefined; fetching: boolean; getJob(): void; } export default function AnnotationPageComponent(props: Props): JSX.Element { const { - jobInstance, + job, fetching, getJob, } = props; - if (jobInstance === null) { + if (job === null) { if (!fetching) { getJob(); } @@ -31,7 +31,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { return ; } - if (typeof (jobInstance) === 'undefined') { + if (typeof (job) === 'undefined') { return ( - + ); } 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 947e264becda..f070d61d9a1f 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 @@ -20,6 +20,7 @@ const cvat = getCore(); const MAX_DISTANCE_TO_OPEN_SHAPE = 50; interface Props { + sidebarCollapsed: boolean; canvasInstance: Canvas; jobInstance: any; annotations: any[]; @@ -34,12 +35,16 @@ interface Props { onSetupCanvas: () => void; onDragCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void; + onMergeObjects: (enabled: boolean) => void; + onGroupObjects: (enabled: boolean) => void; + onSplitTrack: (enabled: boolean) => void; onShapeDrawn: () => void; - onObjectsMerged: () => void; - onObjectsGroupped: () => void; - onTrackSplitted: () => void; onResetCanvas: () => void; - onAnnotationsUpdated: (annotations: any[]) => void; + onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; } export default class CanvasWrapperComponent extends React.PureComponent { @@ -51,7 +56,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { // It's awful approach from the point of view React // But we do not have another way because cvat-canvas returns regular DOM element const [wrapper] = window.document - .getElementsByClassName('cvat-annotation-page-canvas-container'); + .getElementsByClassName('cvat-canvas-container'); wrapper.appendChild(canvasInstance.html()); this.initialSetup(); @@ -65,8 +70,18 @@ export default class CanvasWrapperComponent extends React.PureComponent { gridColor, gridOpacity, canvasInstance, + sidebarCollapsed, } = this.props; + if (prevProps.sidebarCollapsed !== sidebarCollapsed) { + const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar'); + if (sidebar) { + sidebar.addEventListener('transitionend', () => { + canvasInstance.fitCanvas(); + }, { once: true }); + } + } + if (prevProps.grid !== grid) { const gridElement = window.document.getElementById('cvat_canvas_grid'); if (gridElement) { @@ -102,7 +117,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { activeObjectType, frame, onShapeDrawn, - onAnnotationsUpdated, + onCreateAnnotations, } = this.props; onShapeDrawn(); @@ -123,17 +138,14 @@ export default class CanvasWrapperComponent extends React.PureComponent { state.frame = frame; const objectState = new cvat.classes.ObjectState(state); - await jobInstance.annotations.put([objectState]); - - const annotations = await jobInstance.annotations.get(frame); - onAnnotationsUpdated(annotations); + onCreateAnnotations(jobInstance, frame, [objectState]); } private async onShapeEdited(event: any): Promise { const { jobInstance, frame, - onAnnotationsUpdated, + onUpdateAnnotations, } = this.props; const { @@ -141,58 +153,49 @@ export default class CanvasWrapperComponent extends React.PureComponent { points, } = event.detail; state.points = points; - state.save(); - - const annotations = await jobInstance.annotations.get(frame); - onAnnotationsUpdated(annotations); + onUpdateAnnotations(jobInstance, frame, [state]); } private async onObjectsMerged(event: any): Promise { const { jobInstance, frame, - onAnnotationsUpdated, - onObjectsMerged, + onMergeAnnotations, + onMergeObjects, } = this.props; - onObjectsMerged(); + onMergeObjects(false); const { states } = event.detail; - await jobInstance.annotations.merge(states); - const annotations = await jobInstance.annotations.get(frame); - onAnnotationsUpdated(annotations); + onMergeAnnotations(jobInstance, frame, states); } private async onObjectsGroupped(event: any): Promise { const { jobInstance, frame, - onAnnotationsUpdated, - onObjectsGroupped, + onGroupAnnotations, + onGroupObjects, } = this.props; - onObjectsGroupped(); + onGroupObjects(false); const { states } = event.detail; - await jobInstance.annotations.group(states); - const annotations = await jobInstance.annotations.get(frame); - onAnnotationsUpdated(annotations); + onGroupAnnotations(jobInstance, frame, states); } private async onTrackSplitted(event: any): Promise { const { jobInstance, frame, - onAnnotationsUpdated, - onTrackSplitted, + onSplitAnnotations, + onSplitTrack, } = this.props; - onTrackSplitted(); + onSplitTrack(false); const { state } = event.detail; - await jobInstance.annotations.split(state, frame); - const annotations = await jobInstance.annotations.get(frame); - onAnnotationsUpdated(annotations); + onSplitAnnotations(jobInstance, frame, state); } private updateCanvas(): void { @@ -239,9 +242,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { // Events canvasInstance.html().addEventListener('canvas.setup', (): void => { onSetupCanvas(); - if (jobInstance.task.mode === 'annotation') { - canvasInstance.fit(); - } }); canvasInstance.html().addEventListener('canvas.setup', () => { @@ -314,7 +314,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { // So, React isn't going to rerender it // And it's a reason why cvat-canvas appended in mount function works ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index ddbb6db407ec..7bb0c4c4f5e7 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx @@ -36,33 +36,55 @@ interface Props { rotateAll: boolean; activeControl: ActiveControl; - onMergeStart(): void; - onGroupStart(): void; - onSplitStart(): void; + mergeObjects(enabled: boolean): void; + groupObjects(enabled: boolean): void; + splitTrack(enabled: boolean): void; } export default function ControlsSideBarComponent(props: Props): JSX.Element { + const { + canvasInstance, + activeControl, + rotateAll, + + mergeObjects, + groupObjects, + splitTrack, + } = props; + return ( - - - + + +
- - + +
- - - - + + + + @@ -70,9 +92,21 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
- - - + + +
); } 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 index f8c53e1f563b..dcb3c03e56fe 100644 --- 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 @@ -22,7 +22,7 @@ interface Props { activeControl: ActiveControl; } -export default function CursorControl(props: Props): JSX.Element { +const CursorControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, activeControl, @@ -33,14 +33,16 @@ export default function CursorControl(props: Props): JSX.Element { canvasInstance.cancel() + : undefined } - onClick={(): void => { - if (activeControl !== ActiveControl.CURSOR) { - canvasInstance.cancel(); - } - }} /> ); -} +}); + +export default CursorControl; 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 index 0c6ba7e0b9e6..b83e547e5d02 100644 --- 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 @@ -6,38 +6,33 @@ import { import { Canvas } from 'cvat-canvas'; import { PointIcon } from 'icons'; -import { - ShapeType, - ActiveControl, -} from 'reducers/interfaces'; +import { ShapeType } from 'reducers/interfaces'; import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; interface Props { canvasInstance: Canvas; - activeControl: ActiveControl; + isDrawing: boolean; } -export default function DrawRectangleControl(props: Props): JSX.Element { +const DrawPointsControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, - activeControl, + isDrawing, } = 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 }); - }, - } : {}; + const dynamcPopoverPros = isDrawing ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = isDrawing ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; return ( ); -} +}); + +export default DrawPointsControl; 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 index ca9cc605de72..837254d5e73a 100644 --- 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 @@ -6,38 +6,33 @@ import { import { Canvas } from 'cvat-canvas'; import { PolygonIcon } from 'icons'; -import { - ShapeType, - ActiveControl, -} from 'reducers/interfaces'; +import { ShapeType } from 'reducers/interfaces'; import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; interface Props { canvasInstance: Canvas; - activeControl: ActiveControl; + isDrawing: boolean; } -export default function DrawRectangleControl(props: Props): JSX.Element { +const DrawPolygonControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, - activeControl, + isDrawing, } = 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 }); - }, - } : {}; + const dynamcPopoverPros = isDrawing ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = isDrawing ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; return ( ); -} +}); + +export default DrawPolygonControl; 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 index 56b3a51cb669..961a520aeab9 100644 --- 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 @@ -6,38 +6,33 @@ import { import { Canvas } from 'cvat-canvas'; import { PolylineIcon } from 'icons'; -import { - ShapeType, - ActiveControl, -} from 'reducers/interfaces'; +import { ShapeType } from 'reducers/interfaces'; import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; interface Props { canvasInstance: Canvas; - activeControl: ActiveControl; + isDrawing: boolean; } -export default function DrawRectangleControl(props: Props): JSX.Element { +const DrawPolylineControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, - activeControl, + isDrawing, } = 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 }); - }, - } : {}; + const dynamcPopoverPros = isDrawing ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = isDrawing ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; return ( ); -} +}); + +export default DrawPolylineControl; 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 index c762c593d2c6..10e1eb1d8f00 100644 --- 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 @@ -6,38 +6,33 @@ import { import { Canvas } from 'cvat-canvas'; import { RectangleIcon } from 'icons'; -import { - ShapeType, - ActiveControl, -} from 'reducers/interfaces'; +import { ShapeType } from 'reducers/interfaces'; import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover'; interface Props { canvasInstance: Canvas; - activeControl: ActiveControl; + isDrawing: boolean; } -export default function DrawRectangleControl(props: Props): JSX.Element { +const DrawRectangleControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, - activeControl, + isDrawing, } = 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 }); - }, - } : {}; + const dynamcPopoverPros = isDrawing ? { + overlayStyle: { + display: 'none', + }, + } : {}; + + const dynamicIconProps = isDrawing ? { + className: 'cvat-active-canvas-control', + onClick: (): void => { + canvasInstance.draw({ enabled: false }); + }, + } : {}; return ( ); -} +}); + +export default DrawRectangleControl; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 3af282a35ce6..efad75e5072a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -12,175 +12,100 @@ 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 { + labels: any[]; + minimumPoints: number; numberOfPoints?: number; selectedLabeID: number; + onChangeLabel(value: string): void; + onChangePoints(value: number | undefined): void; + onDrawTrack(): void; + onDrawShape(): void; } -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: - - - - - - ) - } - - - - - - - - -
- ); - } -} +const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => { + const { + labels, + shapeType, + minimumPoints, + selectedLabeID, + numberOfPoints, + onDrawTrack, + onDrawShape, + onChangeLabel, + onChangePoints, + } = props; + + return ( +
+ + + {`Draw new ${shapeType}`} + + + + + Label + + + + + + + + { + shapeType !== ShapeType.RECTANGLE && ( + + + Number of points: + + + + + + ) + } + + + + + + + + +
+ ); +}); + +export default DrawShapePopoverComponent; 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 index dc35d4d578a7..998d00820a8b 100644 --- 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 @@ -17,7 +17,7 @@ interface Props { canvasInstance: Canvas; } -export default function FitControl(props: Props): JSX.Element { +const FitControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, } = props; @@ -27,4 +27,6 @@ export default function FitControl(props: Props): JSX.Element { canvasInstance.fit()} /> ); -} +}); + +export default FitControl; 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 index 5d35c483afd2..1c2732f8f284 100644 --- 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 @@ -16,27 +16,28 @@ interface Props { canvasInstance: Canvas; activeControl: ActiveControl; - onGroupStart(): void; + groupObjects(enabled: boolean): void; } -export default function GroupControl(props: Props): JSX.Element { +const GroupControl = React.memo((props: Props): JSX.Element => { const { activeControl, canvasInstance, - onGroupStart, + groupObjects, } = props; const dynamicIconProps = activeControl === ActiveControl.GROUP ? { - className: 'cvat-annotation-page-active-control', + className: 'cvat-active-canvas-control', onClick: (): void => { canvasInstance.group({ enabled: false }); + groupObjects(false); }, } : { onClick: (): void => { canvasInstance.cancel(); canvasInstance.group({ enabled: true }); - onGroupStart(); + groupObjects(true); }, }; @@ -45,4 +46,6 @@ export default function GroupControl(props: Props): JSX.Element { ); -} +}); + +export default GroupControl; 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 index 5a2f8d53e428..b4dcc0076f5d 100644 --- 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 @@ -16,27 +16,28 @@ interface Props { canvasInstance: Canvas; activeControl: ActiveControl; - onMergeStart(): void; + mergeObjects(enabled: boolean): void; } -export default function MergeControl(props: Props): JSX.Element { +const MergeControl = React.memo((props: Props): JSX.Element => { const { activeControl, canvasInstance, - onMergeStart, + mergeObjects, } = props; const dynamicIconProps = activeControl === ActiveControl.MERGE ? { - className: 'cvat-annotation-page-active-control', + className: 'cvat-active-canvas-control', onClick: (): void => { canvasInstance.merge({ enabled: false }); + mergeObjects(false); }, } : { onClick: (): void => { canvasInstance.cancel(); canvasInstance.merge({ enabled: true }); - onMergeStart(); + mergeObjects(true); }, }; @@ -45,4 +46,6 @@ export default function MergeControl(props: Props): JSX.Element { ); -} +}); + +export default MergeControl; 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 index 82fafb5f58c5..89aa6542cb1c 100644 --- 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 @@ -22,7 +22,7 @@ interface Props { activeControl: ActiveControl; } -export default function MoveControl(props: Props): JSX.Element { +const MoveControl = React.memo((props: Props): JSX.Element => { const { canvasInstance, activeControl, @@ -33,7 +33,7 @@ export default function MoveControl(props: Props): JSX.Element { { if (activeControl === ActiveControl.DRAG_CANVAS) { @@ -46,4 +46,6 @@ export default function MoveControl(props: Props): JSX.Element { /> ); -} +}); + +export default MoveControl; 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 index 6922195c095f..819b3c70c272 100644 --- 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 @@ -22,7 +22,7 @@ interface Props { activeControl: ActiveControl; } -export default function ResizeControl(props: Props): JSX.Element { +const ResizeControl = React.memo((props: Props): JSX.Element => { const { activeControl, canvasInstance, @@ -33,7 +33,7 @@ export default function ResizeControl(props: Props): JSX.Element { { if (activeControl === ActiveControl.ZOOM_CANVAS) { @@ -46,4 +46,6 @@ export default function ResizeControl(props: Props): JSX.Element { /> ); -} +}); + +export default ResizeControl; 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 index 10cfcb765e96..d0acee42b5ca 100644 --- 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 @@ -20,7 +20,7 @@ interface Props { rotateAll: boolean; } -export default function RotateControl(props: Props): JSX.Element { +const RotateControl = React.memo((props: Props): JSX.Element => { const { rotateAll, canvasInstance, @@ -28,13 +28,13 @@ export default function RotateControl(props: Props): JSX.Element { return ( canvasInstance .rotate(Rotation.ANTICLOCKWISE90, rotateAll)} component={RotateIcon} @@ -42,7 +42,7 @@ export default function RotateControl(props: Props): JSX.Element { canvasInstance .rotate(Rotation.CLOCKWISE90, rotateAll)} component={RotateIcon} @@ -55,4 +55,6 @@ export default function RotateControl(props: Props): JSX.Element { ); -} +}); + +export default RotateControl; 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 index 8f41dc51fb17..c4725f7a77d4 100644 --- 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 @@ -16,27 +16,28 @@ interface Props { canvasInstance: Canvas; activeControl: ActiveControl; - onSplitStart(): void; + splitTrack(enabled: boolean): void; } -export default function SplitControl(props: Props): JSX.Element { +const SplitControl = React.memo((props: Props): JSX.Element => { const { activeControl, canvasInstance, - onSplitStart, + splitTrack, } = props; const dynamicIconProps = activeControl === ActiveControl.SPLIT ? { - className: 'cvat-annotation-page-active-control', + className: 'cvat-active-canvas-control', onClick: (): void => { canvasInstance.split({ enabled: false }); + splitTrack(false); }, } : { onClick: (): void => { canvasInstance.cancel(); canvasInstance.split({ enabled: true }); - onSplitStart(); + splitTrack(true); }, }; @@ -45,4 +46,6 @@ export default function SplitControl(props: Props): JSX.Element { ); -} +}); + +export default SplitControl; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx new file mode 100644 index 000000000000..90284e39bb98 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -0,0 +1,130 @@ +import React from 'react'; + +import { + Row, + Col, + Icon, + Popover, + Button, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +interface PopoverContentProps { + colors: string[]; + changeColor(color: string): void; +} + +function PopoverContent(props: PopoverContentProps): JSX.Element { + const { + colors, + changeColor, + } = props; + + const cols = 6; + const rows = Math.ceil(colors.length / cols); + + const antdRows = []; + for (let row = 0; row < rows; row++) { + const antdCols = []; + for (let col = 0; col < cols; col++) { + const idx = row * cols + col; + if (idx >= colors.length) { + break; + } + const color = colors[idx]; + antdCols.push( + +