diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4ce4c536f827..63b26e1dc52c 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -850,6 +850,7 @@ export class CanvasViewImpl implements CanvasView, Listener { points: [...state.points], attributes: { ...state.attributes }, zOrder: state.zOrder, + pinned: state.pinned, }; } @@ -885,6 +886,12 @@ export class CanvasViewImpl implements CanvasView, Listener { } } + if (drawnState.pinned !== state.pinned && this.activeElement.clientID !== null) { + const activeElement = { ...this.activeElement }; + this.deactivate(); + this.activate(activeElement); + } + if (state.points .some((p: number, id: number): boolean => p !== drawnState.points[id]) ) { @@ -1016,9 +1023,11 @@ export class CanvasViewImpl implements CanvasView, Listener { shape.removeClass('cvat_canvas_shape_activated'); - (shape as any).off('dragstart'); - (shape as any).off('dragend'); - (shape as any).draggable(false); + if (!drawnState.pinned) { + (shape as any).off('dragstart'); + (shape as any).off('dragend'); + (shape as any).draggable(false); + } if (drawnState.shapeType !== 'points') { this.selectize(false, shape); @@ -1098,37 +1107,38 @@ export class CanvasViewImpl implements CanvasView, Listener { this.content.append(shape.node); } - (shape as any).draggable().on('dragstart', (): void => { - this.mode = Mode.DRAG; - if (text) { - text.addClass('cvat_canvas_hidden'); - } - }).on('dragend', (e: CustomEvent): void => { - if (text) { - text.removeClass('cvat_canvas_hidden'); - this.updateTextPosition( - text, - shape, - ); - } - - this.mode = Mode.IDLE; - - const p1 = e.detail.handler.startPoints.point; - const p2 = e.detail.p; - const delta = 1; - const { offset } = this.controller.geometry; - if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) { - const points = pointsToArray( - shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` - + `${shape.attr('x') + shape.attr('width')},` - + `${shape.attr('y') + shape.attr('height')}`, - ).map((x: number): number => x - offset); + if (!state.pinned) { + (shape as any).draggable().on('dragstart', (): void => { + this.mode = Mode.DRAG; + if (text) { + text.addClass('cvat_canvas_hidden'); + } + }).on('dragend', (e: CustomEvent): void => { + if (text) { + text.removeClass('cvat_canvas_hidden'); + this.updateTextPosition( + text, + shape, + ); + } - this.drawnStates[state.clientID].points = points; - this.onEditDone(state, points); - } - }); + this.mode = Mode.IDLE; + const p1 = e.detail.handler.startPoints.point; + const p2 = e.detail.p; + const delta = 1; + const { offset } = this.controller.geometry; + if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) { + const points = pointsToArray( + shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + + `${shape.attr('x') + shape.attr('width')},` + + `${shape.attr('y') + shape.attr('height')}`, + ).map((x: number): number => x - offset); + + this.drawnStates[state.clientID].points = points; + this.onEditDone(state, points); + } + }); + } if (state.shapeType !== 'points') { this.selectize(true, shape); diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 74813dfae927..f86e92ee861f 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -320,6 +320,10 @@ checkObjectType('lock', data.lock, 'boolean', null); } + if (updated.pinned) { + checkObjectType('pinned', data.pinned, 'boolean', null); + } + if (updated.color) { checkObjectType('color', data.color, 'string', null); if (!/^#[0-9A-F]{6}$/i.test(data.color)) { @@ -379,9 +383,23 @@ super(data, clientID, color, injection); this.frameMeta = injection.frameMeta; this.hidden = false; + this.pinned = true; this.shapeType = null; } + _savePinned(pinned) { + const undoPinned = this.pinned; + const redoPinned = pinned; + + this.history.do(HistoryActions.CHANGED_PINNED, () => { + this.pinned = undoPinned; + }, () => { + this.pinned = redoPinned; + }, [this.clientID]); + + this.pinned = pinned; + } + save() { throw new ScriptingError( 'Is not implemented', @@ -455,6 +473,7 @@ color: this.color, hidden: this.hidden, updated: this.updated, + pinned: this.pinned, frame, }; } @@ -537,6 +556,10 @@ this._saveLock(data.lock); } + if (updated.pinned) { + this._savePinned(data.pinned); + } + if (updated.color) { this._saveColor(data.color); } @@ -644,6 +667,7 @@ hidden: this.hidden, updated: this.updated, label: this.label, + pinned: this.pinned, keyframes: { prev, next, @@ -984,6 +1008,10 @@ this._saveLock(data.lock); } + if (updated.pinned) { + this._savePinned(data.pinned); + } + if (updated.color) { this._saveColor(data.color); } @@ -1148,6 +1176,7 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = ObjectShape.RECTANGLE; + this.pinned = false; checkNumberOfPoints(this.shapeType, this.points); } @@ -1315,6 +1344,7 @@ constructor(data, clientID, color, injection) { super(data, clientID, color, injection); this.shapeType = ObjectShape.RECTANGLE; + this.pinned = false; for (const shape of Object.values(this.shapes)) { checkNumberOfPoints(this.shapeType, shape.points); } diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index df7c5ca4e9fa..2c34290876bf 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -196,6 +196,7 @@ CHANGED_ZORDER: 'Changed z-order', CHANGED_KEYFRAME: 'Changed keyframe', CHANGED_LOCK: 'Changed lock', + CHANGED_PINNED: 'Changed pinned', CHANGED_COLOR: 'Changed color', CHANGED_HIDDEN: 'Changed hidden', MERGED_OBJECTS: 'Merged objects', diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js index 282ab344e619..806a29099796 100644 --- a/cvat-core/src/object-state.js +++ b/cvat-core/src/object-state.js @@ -19,10 +19,10 @@ /** * @param {Object} serialized - is an dictionary which contains * initial information about an ObjectState; - * Necessary fields: objectType, shapeType, frame, updated - * Optional fields: points, group, zOrder, outside, occluded, hidden, - * attributes, lock, label, mode, color, keyframe, keyframes, clientID, serverID - * These fields can be set later via setters + *
Necessary fields: objectType, shapeType, frame, updated, group + *
Optional fields: keyframes, clientID, serverID + *
Optional fields which can be set later: points, zOrder, outside, + * occluded, hidden, attributes, lock, label, color, keyframe */ constructor(serialized) { const data = { @@ -34,12 +34,13 @@ occluded: null, keyframe: null, - zOrder: undefined, + zOrder: null, lock: null, color: null, hidden: null, + pinned: null, + keyframes: null, group: serialized.group, - keyframes: serialized.keyframes, updated: serialized.updated, clientID: serialized.clientID, @@ -63,6 +64,7 @@ this.keyframe = false; this.zOrder = false; + this.pinned = false; this.lock = false; this.color = false; this.hidden = false; @@ -202,7 +204,7 @@ zOrder: { /** * @name zOrder - * @type {integer} + * @type {integer | null} * @memberof module:API.cvat.classes.ObjectState * @instance */ @@ -242,15 +244,16 @@ /** * Object of keyframes { first, prev, next, last } * @name keyframes - * @type {object} + * @type {object | null} * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance */ get: () => { - if (data.keyframes) { + if (typeof (data.keyframes) === 'object') { return { ...data.keyframes }; } + return null; }, }, @@ -280,6 +283,25 @@ data.lock = lock; }, }, + pinned: { + /** + * @name pinned + * @type {boolean | null} + * @memberof module:API.cvat.classes.ObjectState + * @instance + */ + get: () => { + if (typeof (data.pinned) === 'boolean') { + return data.pinned; + } + + return null; + }, + set: (pinned) => { + data.updateFlags.pinned = true; + data.pinned = pinned; + }, + }, updated: { /** * Timestamp of the latest updated of the object @@ -320,19 +342,33 @@ })); this.label = serialized.label; - this.zOrder = serialized.zOrder; - this.outside = serialized.outside; - this.keyframe = serialized.keyframe; - this.occluded = serialized.occluded; - this.color = serialized.color; this.lock = serialized.lock; - this.hidden = serialized.hidden; - // It can be undefined in a constructor and it can be defined later - if (typeof (serialized.points) !== 'undefined') { + if (typeof (serialized.zOrder) === 'number') { + this.zOrder = serialized.zOrder; + } + if (typeof (serialized.occluded) === 'boolean') { + this.occluded = serialized.occluded; + } + if (typeof (serialized.outside) === 'boolean') { + this.outside = serialized.outside; + } + if (typeof (serialized.keyframe) === 'boolean') { + this.keyframe = serialized.keyframe; + } + if (typeof (serialized.pinned) === 'boolean') { + this.pinned = serialized.pinned; + } + if (typeof (serialized.hidden) === 'boolean') { + this.hidden = serialized.hidden; + } + if (typeof (serialized.color) === 'string') { + this.color = serialized.color; + } + if (Array.isArray(serialized.points)) { this.points = serialized.points; } - if (typeof (serialized.attributes) !== 'undefined') { + if (typeof (serialized.attributes) === 'object') { this.attributes = serialized.attributes; } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index d45847fd1a39..284fc6130094 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -178,9 +178,11 @@ const ItemTop = React.memo(ItemTopComponent); interface ItemButtonsComponentProps { objectType: ObjectType; + shapeType: ShapeType; occluded: boolean; outside: boolean | undefined; locked: boolean; + pinned: boolean; hidden: boolean; keyframe: boolean | undefined; @@ -197,6 +199,8 @@ interface ItemButtonsComponentProps { unsetKeyframe(): void; lock(): void; unlock(): void; + pin(): void; + unpin(): void; hide(): void; show(): void; } @@ -204,9 +208,11 @@ interface ItemButtonsComponentProps { function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element { const { objectType, + shapeType, occluded, outside, locked, + pinned, hidden, keyframe, @@ -223,6 +229,8 @@ function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element { unsetKeyframe, lock, unlock, + pin, + unpin, hide, show, } = props; @@ -232,53 +240,62 @@ function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element { - + { navigateFirstKeyframe ? : } - + { navigatePrevKeyframe ? : } - + { navigateNextKeyframe ? : } - + { navigateLastKeyframe ? : } - + { outside ? : } - + { locked ? : } - + { occluded ? : } - + { hidden ? : } - + { keyframe ? : } + { + shapeType !== ShapeType.POINTS && ( + + { pinned + ? + : } + + ) + } @@ -289,21 +306,30 @@ function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element { - + { locked ? : } - + { occluded ? : } - + { hidden ? : } + { + shapeType !== ShapeType.POINTS && ( + + { pinned + ? + : } + + ) + } @@ -550,6 +576,7 @@ interface Props { occluded: boolean; outside: boolean | undefined; locked: boolean; + pinned: boolean; hidden: boolean; keyframe: boolean | undefined; attrValues: Record; @@ -579,6 +606,8 @@ interface Props { unsetKeyframe(): void; lock(): void; unlock(): void; + pin(): void; + unpin(): void; hide(): void; show(): void; changeLabel(labelID: string): void; @@ -590,6 +619,7 @@ interface Props { function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { return nextProps.activated === prevProps.activated && nextProps.locked === prevProps.locked + && nextProps.pinned === prevProps.pinned && nextProps.occluded === prevProps.occluded && nextProps.outside === prevProps.outside && nextProps.hidden === prevProps.hidden @@ -620,6 +650,7 @@ function ObjectItemComponent(props: Props): JSX.Element { occluded, outside, locked, + pinned, hidden, keyframe, attrValues, @@ -650,6 +681,8 @@ function ObjectItemComponent(props: Props): JSX.Element { unsetKeyframe, lock, unlock, + pin, + unpin, hide, show, changeLabel, @@ -704,10 +737,12 @@ function ObjectItemComponent(props: Props): JSX.Element { toForeground={toForeground} />