React UI: ZOrder implementation (#1176)

* Drawn z-order switcher

* Z layer was added to state

* Added ZLayer API method cvat-canvas

* Added sorting by Z

* Displaying points in top

* Removed old code

* Improved sort function

* Drawn a couple of icons

* Send to foreground / send to background

* Updated unit tests

* Added unit tests for filter parser

* Removed extra code

* Updated README.md
main
Boris Sekachev 6 years ago committed by GitHub
parent f329e14fe4
commit 9850094773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -71,6 +71,7 @@ Canvas itself handles:
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(rotation: Rotation, remember?: boolean): void;
@ -149,9 +150,6 @@ Standard JS events are used.
}); });
``` ```
## States
![](images/states.svg)
## API Reaction ## API Reaction
@ -172,3 +170,4 @@ Standard JS events are used.
| dragCanvas() | + | - | - | - | - | - | + | - | | dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + | | zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + | | cancel() | - | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + |

@ -34,6 +34,7 @@ const CanvasVersion = pjson.version;
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number | null, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(rotation: Rotation, remember?: boolean): void;
@ -69,6 +70,10 @@ class CanvasImpl implements Canvas {
return this.view.html(); return this.view.html();
} }
public setZLayer(zLayer: number | null): void {
this.model.setZLayer(zLayer);
}
public setup(frameData: any, objectStates: any[]): void { public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData, objectStates); this.model.setup(frameData, objectStates);
} }

@ -18,6 +18,7 @@ import {
export interface CanvasController { export interface CanvasController {
readonly objects: any[]; readonly objects: any[];
readonly zLayer: number | null;
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
readonly drawData: DrawData; readonly drawData: DrawData;
@ -105,6 +106,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.geometry = geometry; this.model.geometry = geometry;
} }
public get zLayer(): number | null {
return this.model.zLayer;
}
public get objects(): any[] { public get objects(): any[] {
return this.model.objects; return this.model.objects;
} }

@ -80,6 +80,7 @@ export enum UpdateReasons {
IMAGE_FITTED = 'image_fitted', IMAGE_FITTED = 'image_fitted',
IMAGE_MOVED = 'image_moved', IMAGE_MOVED = 'image_moved',
GRID_UPDATED = 'grid_updated', GRID_UPDATED = 'grid_updated',
SET_Z_LAYER = 'set_z_layer',
OBJECTS_UPDATED = 'objects_updated', OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated', SHAPE_ACTIVATED = 'shape_activated',
@ -113,6 +114,7 @@ export enum Mode {
export interface CanvasModel { export interface CanvasModel {
readonly image: HTMLImageElement | null; readonly image: HTMLImageElement | null;
readonly objects: any[]; readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size; readonly gridSize: Size;
readonly focusData: FocusData; readonly focusData: FocusData;
readonly activeElement: ActiveElement; readonly activeElement: ActiveElement;
@ -124,6 +126,7 @@ export interface CanvasModel {
geometry: Geometry; geometry: Geometry;
mode: Mode; mode: Mode;
setZLayer(zLayer: number | null): void;
zoom(x: number, y: number, direction: number): void; zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void; move(topOffset: number, leftOffset: number): void;
@ -163,6 +166,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
rememberAngle: boolean; rememberAngle: boolean;
scale: number; scale: number;
top: number; top: number;
zLayer: number | null;
drawData: DrawData; drawData: DrawData;
mergeData: MergeData; mergeData: MergeData;
groupData: GroupData; groupData: GroupData;
@ -204,6 +208,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
rememberAngle: false, rememberAngle: false,
scale: 1, scale: 1,
top: 0, top: 0,
zLayer: null,
drawData: { drawData: {
enabled: false, enabled: false,
initialState: null, initialState: null,
@ -222,6 +227,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}; };
} }
public setZLayer(zLayer: number | null): void {
this.data.zLayer = zLayer;
this.notify(UpdateReasons.SET_Z_LAYER);
}
public zoom(x: number, y: number, direction: number): void { public zoom(x: number, y: number, direction: number): void {
const oldScale: number = this.data.scale; const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6; const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
@ -515,11 +525,20 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
)); ));
} }
public get zLayer(): number | null {
return this.data.zLayer;
}
public get image(): HTMLImageElement | null { public get image(): HTMLImageElement | null {
return this.data.image; return this.data.image;
} }
public get objects(): any[] { public get objects(): any[] {
if (this.data.zLayer !== null) {
return this.data.objects
.filter((object: any): boolean => object.zOrder <= this.data.zLayer);
}
return this.data.objects; return this.data.objects;
} }

@ -77,12 +77,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
private onDrawDone(data: object, continueDraw?: boolean): void { private onDrawDone(data: object, continueDraw?: boolean): void {
if (data) { if (data) {
const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', { const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
detail: { detail: {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
state: data, state: {
...data,
zOrder: zLayer || 0,
},
continue: continueDraw, continue: continueDraw,
}, },
}); });
@ -364,6 +368,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private setupObjects(states: any[]): void { private setupObjects(states: any[]): void {
const { offset } = this.controller.geometry; const { offset } = this.controller.geometry;
const translate = (points: number[]): number[] => points const translate = (points: number[]): number[] => points
@ -403,6 +408,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.addObjects(created, translate); this.addObjects(created, translate);
this.updateObjects(updated, translate); this.updateObjects(updated, translate);
this.sortObjects();
if (this.controller.activeElement.clientID !== null) { if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement; const { clientID } = this.controller.activeElement;
@ -685,12 +691,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.setupObjects([]); this.setupObjects([]);
this.moveCanvas(); this.moveCanvas();
this.resizeCanvas(); this.resizeCanvas();
} else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) { } else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas(); this.moveCanvas();
this.transformCanvas(); this.transformCanvas();
} else if (reason === UpdateReasons.IMAGE_MOVED) { } else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas(); this.moveCanvas();
} else if (reason === UpdateReasons.OBJECTS_UPDATED) { } else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
if (this.mode === Mode.GROUP) { if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects(); this.groupHandler.resetSelectedObjects();
} }
@ -833,6 +839,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shapeType: state.shapeType, shapeType: state.shapeType,
points: [...state.points], points: [...state.points],
attributes: { ...state.attributes }, attributes: { ...state.attributes },
zOrder: state.zOrder,
}; };
} }
@ -851,6 +858,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
if (drawnState.zOrder !== state.zOrder) {
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.attr('data-z-order', state.zOrder);
} else {
this.svgShapes[clientID].attr('data-z-order', state.zOrder);
}
}
if (drawnState.occluded !== state.occluded) { if (drawnState.occluded !== state.occluded) {
if (state.occluded) { if (state.occluded) {
this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded'); this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded');
@ -961,6 +977,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private sortObjects(): void {
// TODO: Can be significantly optimized
const states = Array.from(
this.content.getElementsByClassName('cvat_canvas_shape'),
).map((state: SVGElement): [SVGElement, number] => (
[state, +state.getAttribute('data-z-order')]
));
const needSort = states.some((pair): boolean => pair[1] !== states[0][1]);
if (!states.length || !needSort) {
return;
}
const sorted = states.sort((a, b): number => a[1] - b[1]);
sorted.forEach((pair): void => {
this.content.appendChild(pair[0]);
});
this.content.prepend(...sorted.map((pair): SVGElement => pair[0]));
}
private deactivate(): void { private deactivate(): void {
if (this.activeElement.clientID !== null) { if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement; const { clientID } = this.activeElement;
@ -989,6 +1026,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
delete this.svgTexts[clientID]; delete this.svgTexts[clientID];
} }
this.sortObjects();
this.activeElement = { this.activeElement = {
clientID: null, clientID: null,
attributeID: null, attributeID: null,
@ -1016,6 +1055,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
const [state] = this.controller.objects const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID); .filter((_state: any): boolean => _state.clientID === clientID);
if (!state) {
return;
}
if (state.shapeType === 'points') { if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested this.svgShapes[clientID].remember('_selectHandler').nested
.style('pointer-events', state.lock ? 'none' : ''); .style('pointer-events', state.lock ? 'none' : '');
@ -1040,7 +1083,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
const self = this; const self = this;
this.content.append(shape.node); if (state.shapeType === 'points') {
this.content.append(this.svgShapes[clientID]
.remember('_selectHandler').nested.node);
} else {
this.content.append(shape.node);
}
(shape as any).draggable().on('dragstart', (): void => { (shape as any).draggable().on('dragstart', (): void => {
this.mode = Mode.DRAG; this.mode = Mode.DRAG;
if (text) { if (text) {
@ -1197,7 +1246,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: state.color, stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, 'data-z-order': state.zOrder,
}).move(xtl, ytl) }).move(xtl, ytl)
.addClass('cvat_canvas_shape'); .addClass('cvat_canvas_shape');
@ -1221,7 +1270,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: state.color, stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, 'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape'); }).addClass('cvat_canvas_shape');
if (state.occluded) { if (state.occluded) {
@ -1244,7 +1293,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: state.color, stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, 'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape'); }).addClass('cvat_canvas_shape');
if (state.occluded) { if (state.occluded) {
@ -1264,9 +1313,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = basicPolyline.remember('_selectHandler').nested const group = basicPolyline.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({ .addClass('cvat_canvas_shape').attr({
clientID: state.clientID, clientID: state.clientID,
zOrder: state.zOrder,
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'data-z-order': state.zOrder,
}).style({ }).style({
'fill-opacity': 1, 'fill-opacity': 1,
}); });

@ -119,13 +119,11 @@
this.objects = {}; // key is a client id this.objects = {}; // key is a client id
this.count = 0; this.count = 0;
this.flush = false; this.flush = false;
this.collectionZ = {}; // key is a frame, {max, min} are values
this.groups = { this.groups = {
max: 0, max: 0,
}; // it is an object to we can pass it as an argument by a reference }; // it is an object to we can pass it as an argument by a reference
this.injection = { this.injection = {
labels: this.labels, labels: this.labels,
collectionZ: this.collectionZ,
groups: this.groups, groups: this.groups,
frameMeta: this.frameMeta, frameMeta: this.frameMeta,
history: this.history, history: this.history,
@ -461,7 +459,7 @@
points: [...objectState.points], points: [...objectState.points],
occluded: objectState.occluded, occluded: objectState.occluded,
outside: objectState.outside, outside: objectState.outside,
zOrder: 0, zOrder: objectState.zOrder,
attributes: Object.keys(objectState.attributes) attributes: Object.keys(objectState.attributes)
.reduce((accumulator, attrID) => { .reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) { if (!labelAttributes[attrID].mutable) {
@ -725,6 +723,7 @@
} else { } else {
checkObjectType('state occluded', state.occluded, 'boolean', null); checkObjectType('state occluded', state.occluded, 'boolean', null);
checkObjectType('state points', state.points, null, Array); checkObjectType('state points', state.points, null, Array);
checkObjectType('state zOrder', state.zOrder, 'integer', null);
for (const coord of state.points) { for (const coord of state.points) {
checkObjectType('point coordinate', coord, 'number', null); checkObjectType('point coordinate', coord, 'number', null);
@ -746,7 +745,7 @@
occluded: state.occluded || false, occluded: state.occluded || false,
points: [...state.points], points: [...state.points],
type: state.shapeType, type: state.shapeType,
z_order: 0, z_order: state.zOrder,
}); });
} else if (state.objectType === 'track') { } else if (state.objectType === 'track') {
constructed.tracks.push({ constructed.tracks.push({
@ -763,7 +762,7 @@
outside: false, outside: false,
points: [...state.points], points: [...state.points],
type: state.shapeType, type: state.shapeType,
z_order: 0, z_order: state.zOrder,
}], }],
}); });
} else { } else {

@ -210,6 +210,10 @@ class AnnotationsFilter {
toJSONQuery(filters) { toJSONQuery(filters) {
try { try {
if (!Array.isArray(filters) || filters.some((value) => typeof (value) !== 'string')) {
throw Error('Argument must be an array of strings');
}
if (!filters.length) { if (!filters.length) {
return [[], '$.objects[*].clientID']; return [[], '$.objects[*].clientID'];
} }

@ -38,8 +38,6 @@
objectState.__internal = { objectState.__internal = {
save: this.save.bind(this, frame, objectState), save: this.save.bind(this, frame, objectState),
delete: this.delete.bind(this), delete: this.delete.bind(this),
up: this.up.bind(this, frame, objectState),
down: this.down.bind(this, frame, objectState),
}; };
return objectState; return objectState;
@ -270,22 +268,12 @@
super(data, clientID, injection); super(data, clientID, injection);
this.frameMeta = injection.frameMeta; this.frameMeta = injection.frameMeta;
this.collectionZ = injection.collectionZ;
this.hidden = false; this.hidden = false;
this.color = color; this.color = color;
this.shapeType = null; this.shapeType = null;
} }
_getZ(frame) {
this.collectionZ[frame] = this.collectionZ[frame] || {
max: 0,
min: 0,
};
return this.collectionZ[frame];
}
_validateStateBeforeSave(frame, data, updated) { _validateStateBeforeSave(frame, data, updated) {
let fittedPoints = []; let fittedPoints = [];
@ -392,20 +380,6 @@
'Is not implemented', 'Is not implemented',
); );
} }
// Increase ZOrder within frame
up(frame, objectState) {
const z = this._getZ(frame);
z.max++;
objectState.zOrder = z.max;
}
// Decrease ZOrder within frame
down(frame, objectState) {
const z = this._getZ(frame);
z.min--;
objectState.zOrder = z.min;
}
} }
class Shape extends Drawn { class Shape extends Drawn {
@ -414,10 +388,6 @@
this.points = data.points; this.points = data.points;
this.occluded = data.occluded; this.occluded = data.occluded;
this.zOrder = data.z_order; this.zOrder = data.z_order;
const z = this._getZ(this.frame);
z.max = Math.max(z.max, this.zOrder || 0);
z.min = Math.min(z.min, this.zOrder || 0);
} }
// Method is used to export data to the server // Method is used to export data to the server
@ -582,10 +552,6 @@
}, {}), }, {}),
}; };
const z = this._getZ(value.frame);
z.max = Math.max(z.max, value.z_order);
z.min = Math.min(z.min, value.z_order);
return shapeAccumulator; return shapeAccumulator;
}, {}); }, {});
} }
@ -1064,7 +1030,7 @@
points: [...leftPosition.points], points: [...leftPosition.points],
occluded: leftPosition.occluded, occluded: leftPosition.occluded,
outside: leftPosition.outside, outside: leftPosition.outside,
zOrder: 0, zOrder: leftPosition.zOrder,
keyframe: targetFrame in this.shapes, keyframe: targetFrame in this.shapes,
}; };
} }
@ -1074,7 +1040,7 @@
points: [...rightPosition.points], points: [...rightPosition.points],
occluded: rightPosition.occluded, occluded: rightPosition.occluded,
outside: true, outside: true,
zOrder: 0, zOrder: rightPosition.zOrder,
keyframe: targetFrame in this.shapes, keyframe: targetFrame in this.shapes,
}; };
} }

@ -34,7 +34,7 @@
occluded: null, occluded: null,
keyframe: null, keyframe: null,
zOrder: null, zOrder: undefined,
lock: null, lock: null,
color: null, color: null,
hidden: null, hidden: null,
@ -372,36 +372,6 @@
.apiWrapper.call(this, ObjectState.prototype.delete, force); .apiWrapper.call(this, ObjectState.prototype.delete, force);
return result; return result;
} }
/**
* Set the highest ZOrder within a frame
* @method up
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async up() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.up);
return result;
}
/**
* Set the lowest ZOrder within a frame
* @method down
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async down() {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.down);
return result;
}
} }
// Updates element in collection which contains it // Updates element in collection which contains it
@ -422,22 +392,5 @@
return false; return false;
}; };
ObjectState.prototype.up.implementation = async function () {
if (this.__internal && this.__internal.up) {
return this.__internal.up();
}
return false;
};
ObjectState.prototype.down.implementation = async function () {
if (this.__internal && this.__internal.down) {
return this.__internal.down();
}
return false;
};
module.exports = ObjectState; module.exports = ObjectState;
})(); })();

@ -290,7 +290,7 @@
* <li> clientID == 50 </li> * <li> clientID == 50 </li>
* <li> (label=="car" & attr["parked"]==true) * <li> (label=="car" & attr["parked"]==true)
* | (label=="pedestrian" & width > 150) </li> * | (label=="pedestrian" & width > 150) </li>
* <li> (( label==["car \\"mazda\\""]) & * <li> (( label==["car \"mazda\""]) &
* (attr["sunglass ( help ) es"]==true | * (attr["sunglass ( help ) es"]==true |
* (width > 150 | height > 150 & (clientID == serverID))))) </li> * (width > 150 | height > 150 & (clientID == serverID))))) </li>
* </ul> * </ul>

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018 Intel Corporation * Copyright (C) 2018-2020 Intel Corporation
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -85,6 +85,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
await task.annotations.put([state]); await task.annotations.put([state]);
@ -104,6 +105,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.task.labels[0],
zOrder: 0,
}); });
await job.annotations.put([state]); await job.annotations.put([state]);
@ -123,6 +125,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
await task.annotations.put([state]); await task.annotations.put([state]);
@ -142,6 +145,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.task.labels[0],
zOrder: 0,
}); });
await job.annotations.put([state]); await job.annotations.put([state]);
@ -158,6 +162,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
@ -175,12 +180,45 @@ describe('Feature: put annotations', () => {
attributes: { 'bad key': 55 }, attributes: { 'bad key': 55 },
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError); .rejects.toThrow(window.cvat.exceptions.ArgumentError);
}); });
test('put shape with bad zOrder to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
const state = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
attributes: { 'bad key': 55 },
occluded: true,
label: task.labels[0],
zOrder: 'bad value',
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
const state1 = new window.cvat.classes.ObjectState({
frame: 1,
objectType: window.cvat.enums.ObjectType.SHAPE,
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
attributes: { 'bad key': 55 },
occluded: true,
label: task.labels[0],
zOrder: NaN,
});
expect(task.annotations.put([state1]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('put shape without points and with invalud points to a task', async () => { test('put shape without points and with invalud points to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0]; const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true); await task.annotations.clear(true);
@ -191,6 +229,7 @@ describe('Feature: put annotations', () => {
occluded: true, occluded: true,
points: [], points: [],
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
await expect(task.annotations.put([state])) await expect(task.annotations.put([state]))
@ -214,6 +253,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
@ -229,6 +269,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.POLYGON, shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
zOrder: 0,
}); });
await expect(task.annotations.put([state])) await expect(task.annotations.put([state]))
@ -253,6 +294,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.put([state])) expect(task.annotations.put([state]))
@ -296,6 +338,7 @@ describe('Feature: save annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(await task.annotations.hasUnsavedChanges()).toBe(false); expect(await task.annotations.hasUnsavedChanges()).toBe(false);
@ -341,6 +384,7 @@ describe('Feature: save annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: job.task.labels[0], label: job.task.labels[0],
zOrder: 0,
}); });
expect(await job.annotations.hasUnsavedChanges()).toBe(false); expect(await job.annotations.hasUnsavedChanges()).toBe(false);
@ -574,6 +618,7 @@ describe('Feature: group annotations', () => {
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: task.labels[0], label: task.labels[0],
zOrder: 0,
}); });
expect(task.annotations.group([state])) expect(task.annotations.group([state]))

@ -303,45 +303,3 @@ describe('Feature: delete object', () => {
expect(annotationsAfter).toHaveLength(length - 1); expect(annotationsAfter).toHaveLength(length - 1);
}); });
}); });
describe('Feature: change z order of an object', () => {
test('up z order for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.up();
expect(state.zOrder).toBeGreaterThan(zOrder);
});
test('up z order for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.up();
expect(state.zOrder).toBeGreaterThan(zOrder);
});
test('down z order for a shape', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.down();
expect(state.zOrder).toBeLessThan(zOrder);
});
test('down z order for a track', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
const state = annotations[0];
const { zOrder } = state;
await state.down();
expect(state.zOrder).toBeLessThan(zOrder);
});
});

@ -0,0 +1,124 @@
/*
* Copyright (C) 2018-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
jest:false
describe:false
*/
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
const AnnotationsFilter = require('../../src/annotations-filter');
// Initialize api
window.cvat = require('../../src/api');
// Test cases
describe('Feature: toJSONQuery', () => {
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter.toJSONQuery([]);
expect(Array.isArray(groups)).toBeTruthy();
expect(typeof (query)).toBe('string');
});
test('convert empty fitlers to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [, query] = annotationsFilter.toJSONQuery([]);
expect(query).toBe('$.objects[*].clientID');
});
test('convert wrong fitlers (empty string) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['']);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong fitlers (wrong number argument) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(1);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong fitlers (wrong array argument) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['clientID ==6', 1]);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert wrong filters (wrong expression) to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
expect(() => {
annotationsFilter.toJSONQuery(['clientID=5']);
}).toThrow(window.cvat.exceptions.ArgumentError);
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
expect(groups).toEqual([
['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]'],
]);
expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['label=="car" | width >= height & type=="track"']);
expect(groups).toEqual([
['label=="car"', '|', 'width >= height', '&', 'type=="track"'],
]);
expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]']);
expect(groups).toEqual([
['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]'],
]);
expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['label=="car" & attr["parked"]==true', 'label=="pedestrian" & width > 150']);
expect(groups).toEqual([
['label=="car"', '&', 'attr["parked"]==true'],
'|',
['label=="pedestrian"', '&', 'width > 150'],
]);
expect(query).toBe('$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID');
});
test('convert filters to a json query', () => {
const annotationsFilter = new AnnotationsFilter();
const [groups, query] = annotationsFilter
.toJSONQuery(['(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ']);
expect(groups).toEqual([[[
['label==["car `mazda`"]'],
'&',
['attr["sunglass ( help ) es"]==true', '|',
['width > 150', '|', 'height > 150', '&',
[
'clientID == serverID',
],
],
],
]]]);
expect(query).toBe('$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID');
});
});

@ -42,6 +42,17 @@ function receiveAnnotationsParameters(): { filters: string[]; frame: number } {
}; };
} }
function computeZRange(states: any[]): number[] {
let minZ = states.length ? states[0].zOrder : 0;
let maxZ = states.length ? states[0].zOrder : 0;
states.forEach((state: any): void => {
minZ = Math.min(minZ, state.zOrder);
maxZ = Math.max(maxZ, state.zOrder);
});
return [minZ, maxZ];
}
export enum AnnotationActionTypes { export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB', GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS', GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
@ -109,6 +120,23 @@ export enum AnnotationActionTypes {
CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS', CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS',
FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS', FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS',
FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED', FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED',
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
}
export function addZLayer(): AnyAction {
return {
type: AnnotationActionTypes.ADD_Z_LAYER,
};
}
export function switchZLayer(cur: number): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_Z_LAYER,
payload: {
cur,
},
};
} }
export function fetchAnnotationsAsync(sessionInstance: any): export function fetchAnnotationsAsync(sessionInstance: any):
@ -117,10 +145,14 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try { try {
const { filters, frame } = receiveAnnotationsParameters(); const { filters, frame } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations.get(frame, false, filters); const states = await sessionInstance.annotations.get(frame, false, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
type: AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
minZ,
maxZ,
}, },
}); });
} catch (error) { } catch (error) {
@ -153,12 +185,15 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
await sessionInstance.actions.undo(); await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, false, filters); const states = await sessionInstance.annotations.get(frame, false, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS, type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
payload: { payload: {
history, history,
states, states,
minZ,
maxZ,
}, },
}); });
} catch (error) { } catch (error) {
@ -182,12 +217,15 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
await sessionInstance.actions.redo(); await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, false, filters); const states = await sessionInstance.annotations.get(frame, false, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS, type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: { payload: {
history, history,
states, states,
minZ,
maxZ,
}, },
}); });
} catch (error) { } catch (error) {
@ -573,12 +611,15 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
const data = await job.frames.get(toFrame); const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame, false, filters); const states = await job.annotations.get(toFrame, false, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS, type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: { payload: {
number: toFrame, number: toFrame,
data, data,
states, states,
minZ,
maxZ,
}, },
}); });
} catch (error) { } catch (error) {
@ -661,6 +702,7 @@ export function getJobAsync(
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber); const frameData = await job.frames.get(frameNumber);
const states = await job.annotations.get(frameNumber, false, filters); const states = await job.annotations.get(frameNumber, false, filters);
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors]; const colors = [...cvat.enums.colors];
dispatch({ dispatch({
@ -672,6 +714,8 @@ export function getJobAsync(
frameData, frameData,
colors, colors,
filters, filters,
minZ,
maxZ,
}, },
}); });
} catch (error) { } catch (error) {
@ -789,12 +833,15 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
.map((objectState: any): Promise<any> => objectState.save()); .map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises); const states = await Promise.all(promises);
const history = await sessionInstance.actions.get(); const history = await sessionInstance.actions.get();
const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS,
payload: { payload: {
states, states,
history, history,
minZ,
maxZ,
}, },
}); });
} catch (error) { } catch (error) {

@ -0,0 +1,11 @@
<!--
Copyright (C) 2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
<svg height="1em" width="1em" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g>
<rect rx="12" height="166" width="167" y="25" x="25" stroke-width="15" stroke="#000" fill="none" stroke-dasharray="35" />
<rect rx="12" height="166" width="167" y="70" x="70" stroke-width="15" stroke="#000" fill="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@ -0,0 +1,11 @@
<!--
Copyright (C) 2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
<svg height="1em" width="1em" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g>
<rect rx="12" height="166" width="167" y="25" x="25" stroke-width="15" stroke="#000" fill="none" />
<rect rx="12" height="166" width="167" y="70" x="70" stroke-width="15" stroke="#000" fill="none" stroke-dasharray="35" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@ -6,6 +6,7 @@ $inprogress-progress-color: #1890FF;
$pending-progress-color: #C1C1C1; $pending-progress-color: #C1C1C1;
$border-color-1: #c3c3c3; $border-color-1: #c3c3c3;
$border-color-2: #d9d9d9; $border-color-2: #d9d9d9;
$border-color-3: #242424;
$border-color-hover: #40a9ff; $border-color-hover: #40a9ff;
$background-color-1: white; $background-color-1: white;
$background-color-2: #F1F1F1; $background-color-2: #F1F1F1;

@ -2,8 +2,13 @@ import React from 'react';
import { import {
Layout, Layout,
Slider,
Icon,
Tooltip,
} from 'antd'; } from 'antd';
import { SliderValue } from 'antd/lib//slider';
import { import {
ColorBy, ColorBy,
GridColor, GridColor,
@ -39,6 +44,9 @@ interface Props {
gridOpacity: number; gridOpacity: number;
activeLabelID: number; activeLabelID: number;
activeObjectType: ObjectType; activeObjectType: ObjectType;
curZLayer: number;
minZLayer: number;
maxZLayer: number;
onSetupCanvas: () => void; onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void; onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void;
@ -56,12 +64,15 @@ interface Props {
onActivateObject(activatedStateID: number | null): void; onActivateObject(activatedStateID: number | null): void;
onSelectObjects(selectedStatesID: number[]): void; onSelectObjects(selectedStatesID: number[]): void;
onUpdateContextMenu(visible: boolean, left: number, top: number): void; onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
} }
export default class CanvasWrapperComponent extends React.PureComponent<Props> { export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidMount(): void { public componentDidMount(): void {
const { const {
canvasInstance, canvasInstance,
curZLayer,
} = this.props; } = this.props;
// It's awful approach from the point of view React // It's awful approach from the point of view React
@ -70,6 +81,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
.getElementsByClassName('cvat-canvas-container'); .getElementsByClassName('cvat-canvas-container');
wrapper.appendChild(canvasInstance.html()); wrapper.appendChild(canvasInstance.html());
canvasInstance.setZLayer(curZLayer);
this.initialSetup(); this.initialSetup();
this.updateCanvas(); this.updateCanvas();
} }
@ -89,6 +101,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance, canvasInstance,
sidebarCollapsed, sidebarCollapsed,
activatedStateID, activatedStateID,
curZLayer,
} = this.props; } = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) { if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -143,6 +156,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView(); this.updateShapesView();
} }
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
this.activateOnCanvas(); this.activateOnCanvas();
} }
@ -462,13 +479,45 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
public render(): JSX.Element { public render(): JSX.Element {
const {
maxZLayer,
curZLayer,
minZLayer,
onSwitchZLayer,
onAddZLayer,
} = this.props;
return ( return (
// This element doesn't have any props <Layout.Content style={{ position: 'relative' }}>
// So, React isn't going to rerender it {/*
// And it's a reason why cvat-canvas appended in mount function works This element doesn't have any props
<Layout.Content So, React isn't going to rerender it
className='cvat-canvas-container' And it's a reason why cvat-canvas appended in mount function works
/> */}
<div
className='cvat-canvas-container'
style={{
overflow: 'hidden',
width: '100%',
height: '100%',
}}
/>
<div className='cvat-canvas-z-axis-wrapper'>
<Slider
disabled={minZLayer === maxZLayer}
min={minZLayer}
max={maxZLayer}
value={curZLayer}
vertical
reverse
defaultValue={0}
onChange={(value: SliderValue): void => onSwitchZLayer(value as number)}
/>
<Tooltip title={`Add new layer ${maxZLayer + 1} and switch to it`}>
<Icon type='plus-circle' onClick={onAddZLayer} />
</Tooltip>
</div>
</Layout.Content>
); );
} }
} }

@ -26,6 +26,8 @@ import {
LastIcon, LastIcon,
PreviousIcon, PreviousIcon,
NextIcon, NextIcon,
BackgroundIcon,
ForegroundIcon,
} from 'icons'; } from 'icons';
import { import {
@ -39,6 +41,8 @@ function ItemMenu(
remove: (() => void), remove: (() => void),
propagate: (() => void), propagate: (() => void),
createURL: (() => void), createURL: (() => void),
toBackground: (() => void),
toForeground: (() => void),
): JSX.Element { ): JSX.Element {
return ( return (
<Menu key='unique' className='cvat-object-item-menu'> <Menu key='unique' className='cvat-object-item-menu'>
@ -57,6 +61,18 @@ function ItemMenu(
Propagate Propagate
</Button> </Button>
</Menu.Item> </Menu.Item>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
<Menu.Item> <Menu.Item>
<Button <Button
type='link' type='link'
@ -94,6 +110,8 @@ interface ItemTopComponentProps {
remove(): void; remove(): void;
propagate(): void; propagate(): void;
createURL(): void; createURL(): void;
toBackground(): void;
toForeground(): void;
} }
function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
@ -109,6 +127,8 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
remove, remove,
propagate, propagate,
createURL, createURL,
toBackground,
toForeground,
} = props; } = props;
return ( return (
@ -130,7 +150,16 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
<Col span={2}> <Col span={2}>
<Dropdown <Dropdown
placement='bottomLeft' placement='bottomLeft'
overlay={ItemMenu(serverID, locked, copy, remove, propagate, createURL)} overlay={ItemMenu(
serverID,
locked,
copy,
remove,
propagate,
createURL,
toBackground,
toForeground,
)}
> >
<Icon type='more' /> <Icon type='more' />
</Dropdown> </Dropdown>
@ -528,6 +557,8 @@ interface Props {
copy(): void; copy(): void;
propagate(): void; propagate(): void;
createURL(): void; createURL(): void;
toBackground(): void;
toForeground(): void;
remove(): void; remove(): void;
setOccluded(): void; setOccluded(): void;
unsetOccluded(): void; unsetOccluded(): void;
@ -595,6 +626,8 @@ function ObjectItemComponent(props: Props): JSX.Element {
copy, copy,
propagate, propagate,
createURL, createURL,
toBackground,
toForeground,
remove, remove,
setOccluded, setOccluded,
unsetOccluded, unsetOccluded,
@ -636,6 +669,8 @@ function ObjectItemComponent(props: Props): JSX.Element {
remove={remove} remove={remove}
propagate={propagate} propagate={propagate}
createURL={createURL} createURL={createURL}
toBackground={toBackground}
toForeground={toForeground}
/> />
<ItemButtons <ItemButtons
objectType={objectType} objectType={objectType}

@ -123,4 +123,50 @@
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
} }
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}

@ -107,6 +107,7 @@
> .ant-slider-rail { > .ant-slider-rail {
background-color: $player-slider-color; background-color: $player-slider-color;
} }
} }
.cvat-player-filename-wrapper { .cvat-player-filename-wrapper {
@ -118,7 +119,7 @@
.cvat-player-frame-url-icon { .cvat-player-frame-url-icon {
opacity: 0.7; opacity: 0.7;
color: $info-icon-color; color: $objects-bar-icons-color;
&:hover { &:hover {
opacity: 1; opacity: 1;

@ -21,6 +21,8 @@ import {
activateObject, activateObject,
selectObjects, selectObjects,
updateCanvasContextMenu, updateCanvasContextMenu,
addZLayer,
switchZLayer,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { import {
ColorBy, ColorBy,
@ -50,6 +52,9 @@ interface StateToProps {
gridOpacity: number; gridOpacity: number;
activeLabelID: number; activeLabelID: number;
activeObjectType: ObjectType; activeObjectType: ObjectType;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
} }
interface DispatchToProps { interface DispatchToProps {
@ -70,6 +75,8 @@ interface DispatchToProps {
onActivateObject: (activatedStateID: number | null) => void; onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void; onSelectObjects: (selectedStatesID: number[]) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number): void; onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -95,6 +102,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
states: annotations, states: annotations,
activatedStateID, activatedStateID,
selectedStatesID, selectedStatesID,
zLayer: {
cur: curZLayer,
min: minZLayer,
max: maxZLayer,
},
}, },
sidebarCollapsed, sidebarCollapsed,
}, },
@ -133,6 +145,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
gridOpacity, gridOpacity,
activeLabelID, activeLabelID,
activeObjectType, activeObjectType,
curZLayer,
minZLayer,
maxZLayer,
}; };
} }
@ -193,6 +208,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onUpdateContextMenu(visible: boolean, left: number, top: number): void { onUpdateContextMenu(visible: boolean, left: number, top: number): void {
dispatch(updateCanvasContextMenu(visible, left, top)); dispatch(updateCanvasContextMenu(visible, left, top));
}, },
onAddZLayer(): void {
dispatch(addZLayer());
},
onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur));
},
}; };
} }

@ -33,6 +33,8 @@ interface StateToProps {
colorBy: ColorBy; colorBy: ColorBy;
ready: boolean; ready: boolean;
activeControl: ActiveControl; activeControl: ActiveControl;
minZLayer: number;
maxZLayer: number;
} }
interface DispatchToProps { interface DispatchToProps {
@ -52,6 +54,10 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
states, states,
collapsed: statesCollapsed, collapsed: statesCollapsed,
activatedStateID, activatedStateID,
zLayer: {
min: minZLayer,
max: maxZLayer,
},
}, },
job: { job: {
attributes: jobAttributes, attributes: jobAttributes,
@ -93,6 +99,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
jobInstance, jobInstance,
frameNumber, frameNumber,
activated: activatedStateID === own.clientID, activated: activatedStateID === own.clientID,
minZLayer,
maxZLayer,
}; };
} }
@ -220,6 +228,30 @@ class ObjectItemContainer extends React.PureComponent<Props> {
copy(url); copy(url);
}; };
private toBackground = (): void => {
const {
objectState,
minZLayer,
} = this.props;
if (objectState.zOrder !== minZLayer) {
objectState.zOrder = minZLayer - 1;
this.commit();
}
};
private toForeground = (): void => {
const {
objectState,
maxZLayer,
} = this.props;
if (objectState.zOrder !== maxZLayer) {
objectState.zOrder = maxZLayer + 1;
this.commit();
}
};
private activate = (): void => { private activate = (): void => {
const { const {
activateObject, activateObject,
@ -404,6 +436,8 @@ class ObjectItemContainer extends React.PureComponent<Props> {
copy={this.copy} copy={this.copy}
propagate={this.propagate} propagate={this.propagate}
createURL={this.createURL} createURL={this.createURL}
toBackground={this.toBackground}
toForeground={this.toForeground}
setOccluded={this.setOccluded} setOccluded={this.setOccluded}
unsetOccluded={this.unsetOccluded} unsetOccluded={this.unsetOccluded}
setOutside={this.setOutside} setOutside={this.setOutside}

@ -33,6 +33,8 @@ import SVGInfoIcon from './assets/info-icon.svg';
import SVGFullscreenIcon from './assets/fullscreen-icon.svg'; import SVGFullscreenIcon from './assets/fullscreen-icon.svg';
import SVGObjectOutsideIcon from './assets/object-outside-icon.svg'; import SVGObjectOutsideIcon from './assets/object-outside-icon.svg';
import SVGObjectInsideIcon from './assets/object-inside-icon.svg'; import SVGObjectInsideIcon from './assets/object-inside-icon.svg';
import SVGBackgroundIcon from './assets/background-icon.svg';
import SVGForegroundIcon from './assets/foreground-icon.svg';
export const CVATLogo = React.memo( export const CVATLogo = React.memo(
(): JSX.Element => <SVGCVATLogo />, (): JSX.Element => <SVGCVATLogo />,
@ -133,3 +135,9 @@ export const ObjectOutsideIcon = React.memo(
export const ObjectInsideIcon = React.memo( export const ObjectInsideIcon = React.memo(
(): JSX.Element => <SVGObjectInsideIcon />, (): JSX.Element => <SVGObjectInsideIcon />,
); );
export const BackgroundIcon = React.memo(
(): JSX.Element => <SVGBackgroundIcon />,
);
export const ForegroundIcon = React.memo(
(): JSX.Element => <SVGForegroundIcon />,
);

@ -58,6 +58,11 @@ const defaultState: AnnotationState = {
undo: [], undo: [],
redo: [], redo: [],
}, },
zLayer: {
min: 0,
max: 0,
cur: 0,
},
}, },
propagate: { propagate: {
objectState: null, objectState: null,
@ -93,6 +98,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
colors, colors,
filters, filters,
frameData: data, frameData: data,
minZ,
maxZ,
} = action.payload; } = action.payload;
return { return {
@ -112,6 +119,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations, ...state.annotations,
states, states,
filters, filters,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
}, },
player: { player: {
...state.player, ...state.player,
@ -160,6 +172,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
number, number,
data, data,
states, states,
minZ,
maxZ,
} = action.payload; } = action.payload;
const activatedStateID = states const activatedStateID = states
@ -180,6 +194,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations, ...state.annotations,
activatedStateID, activatedStateID,
states, states,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
}, },
}; };
} }
@ -431,6 +450,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const { const {
history, history,
states: updatedStates, states: updatedStates,
minZ,
maxZ,
} = action.payload; } = action.payload;
const { states: prevStates } = state.annotations; const { states: prevStates } = state.annotations;
const nextStates = [...prevStates]; const nextStates = [...prevStates];
@ -443,10 +464,18 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
} }
} }
const maxZLayer = Math.max(state.annotations.zLayer.max, maxZ);
const minZLayer = Math.min(state.annotations.zLayer.min, minZ);
return { return {
...state, ...state,
annotations: { annotations: {
...state.annotations, ...state.annotations,
zLayer: {
min: minZLayer,
max: maxZLayer,
cur: maxZLayer,
},
states: nextStates, states: nextStates,
history, history,
}, },
@ -841,6 +870,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const { const {
history, history,
states, states,
minZ,
maxZ,
} = action.payload; } = action.payload;
const activatedStateID = states const activatedStateID = states
@ -854,11 +885,16 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
activatedStateID, activatedStateID,
states, states,
history, history,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
}, },
}; };
} }
case AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS: { case AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS: {
const { states } = action.payload; const { states, minZ, maxZ } = action.payload;
const activatedStateID = states const activatedStateID = states
.map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID) .map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID)
? state.annotations.activatedStateID : null; ? state.annotations.activatedStateID : null;
@ -869,6 +905,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations, ...state.annotations,
activatedStateID, activatedStateID,
states, states,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
}, },
}; };
} }
@ -882,6 +923,49 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.SWITCH_Z_LAYER: {
const { cur } = action.payload;
const { max, min } = state.annotations.zLayer;
let { activatedStateID } = state.annotations;
if (activatedStateID !== null) {
const idx = state.annotations.states
.map((_state: any) => _state.clientID).indexOf(activatedStateID);
if (idx !== -1) {
if (state.annotations.states[idx].zOrder > cur) {
activatedStateID = null;
}
} else {
activatedStateID = null;
}
}
return {
...state,
annotations: {
...state.annotations,
activatedStateID,
zLayer: {
...state.annotations.zLayer,
cur: Math.max(Math.min(cur, max), min),
},
},
};
}
case AnnotationActionTypes.ADD_Z_LAYER: {
const { max } = state.annotations.zLayer;
return {
...state,
annotations: {
...state.annotations,
zLayer: {
...state.annotations.zLayer,
max: max + 1,
cur: max + 1,
},
},
};
}
case AnnotationActionTypes.RESET_CANVAS: { case AnnotationActionTypes.RESET_CANVAS: {
return { return {
...state, ...state,

@ -329,6 +329,11 @@ export interface AnnotationState {
uploading: boolean; uploading: boolean;
statuses: string[]; statuses: string[];
}; };
zLayer: {
min: number;
max: number;
cur: number;
};
}; };
propagate: { propagate: {
objectState: any | null; objectState: any | null;

Loading…
Cancel
Save