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}
/>
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index ce03be583e83..6ced57029d23 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -300,6 +300,18 @@ class ObjectItemContainer extends React.PureComponent {
this.commit();
};
+ private pin = (): void => {
+ const { objectState } = this.props;
+ objectState.pinned = true;
+ this.commit();
+ };
+
+ private unpin = (): void => {
+ const { objectState } = this.props;
+ objectState.pinned = false;
+ this.commit();
+ };
+
private show = (): void => {
const { objectState } = this.props;
objectState.hidden = false;
@@ -451,6 +463,7 @@ class ObjectItemContainer extends React.PureComponent {
occluded={objectState.occluded}
outside={objectState.outside}
locked={objectState.lock}
+ pinned={objectState.pinned}
hidden={objectState.hidden}
keyframe={objectState.keyframe}
attrValues={{ ...objectState.attributes }}
@@ -491,6 +504,8 @@ class ObjectItemContainer extends React.PureComponent {
unsetKeyframe={this.unsetKeyframe}
lock={this.lock}
unlock={this.unlock}
+ pin={this.pin}
+ unpin={this.unpin}
hide={this.hide}
show={this.show}
changeColor={this.changeColor}