Skip to content

Commit

Permalink
React UI: ZOrder implementation (#1176)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bsekachev authored Feb 25, 2020
1 parent f329e14 commit 9850094
Show file tree
Hide file tree
Showing 26 changed files with 633 additions and 154 deletions.
5 changes: 2 additions & 3 deletions cvat-canvas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Canvas itself handles:

interface Canvas {
html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
Expand Down Expand Up @@ -149,9 +150,6 @@ Standard JS events are used.
});
```

## States

![](images/states.svg)

## API Reaction

Expand All @@ -172,3 +170,4 @@ Standard JS events are used.
| dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + |
5 changes: 5 additions & 0 deletions cvat-canvas/src/typescript/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const CanvasVersion = pjson.version;

interface Canvas {
html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
Expand Down Expand Up @@ -69,6 +70,10 @@ class CanvasImpl implements Canvas {
return this.view.html();
}

public setZLayer(zLayer: number | null): void {
this.model.setZLayer(zLayer);
}

public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData, objectStates);
}
Expand Down
5 changes: 5 additions & 0 deletions cvat-canvas/src/typescript/canvasController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {

export interface CanvasController {
readonly objects: any[];
readonly zLayer: number | null;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
Expand Down Expand Up @@ -105,6 +106,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.geometry = geometry;
}

public get zLayer(): number | null {
return this.model.zLayer;
}

public get objects(): any[] {
return this.model.objects;
}
Expand Down
19 changes: 19 additions & 0 deletions cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export enum UpdateReasons {
IMAGE_FITTED = 'image_fitted',
IMAGE_MOVED = 'image_moved',
GRID_UPDATED = 'grid_updated',
SET_Z_LAYER = 'set_z_layer',

OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated',
Expand Down Expand Up @@ -113,6 +114,7 @@ export enum Mode {
export interface CanvasModel {
readonly image: HTMLImageElement | null;
readonly objects: any[];
readonly zLayer: number | null;
readonly gridSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
Expand All @@ -124,6 +126,7 @@ export interface CanvasModel {
geometry: Geometry;
mode: Mode;

setZLayer(zLayer: number | null): void;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;

Expand Down Expand Up @@ -163,6 +166,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
rememberAngle: boolean;
scale: number;
top: number;
zLayer: number | null;
drawData: DrawData;
mergeData: MergeData;
groupData: GroupData;
Expand Down Expand Up @@ -204,6 +208,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
rememberAngle: false,
scale: 1,
top: 0,
zLayer: null,
drawData: {
enabled: false,
initialState: null,
Expand All @@ -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 {
const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
Expand Down Expand Up @@ -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 {
return this.data.image;
}

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;
}

Expand Down
65 changes: 57 additions & 8 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,16 @@ export class CanvasViewImpl implements CanvasView, Listener {

private onDrawDone(data: object, continueDraw?: boolean): void {
if (data) {
const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false,
cancelable: true,
detail: {
// eslint-disable-next-line new-cap
state: data,
state: {
...data,
zOrder: zLayer || 0,
},
continue: continueDraw,
},
});
Expand Down Expand Up @@ -364,6 +368,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}


private setupObjects(states: any[]): void {
const { offset } = this.controller.geometry;
const translate = (points: number[]): number[] => points
Expand Down Expand Up @@ -403,6 +408,7 @@ export class CanvasViewImpl implements CanvasView, Listener {

this.addObjects(created, translate);
this.updateObjects(updated, translate);
this.sortObjects();

if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
Expand Down Expand Up @@ -685,12 +691,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.setupObjects([]);
this.moveCanvas();
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.transformCanvas();
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
} else if (reason === UpdateReasons.OBJECTS_UPDATED) {
} else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects();
}
Expand Down Expand Up @@ -833,6 +839,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shapeType: state.shapeType,
points: [...state.points],
attributes: { ...state.attributes },
zOrder: state.zOrder,
};
}

Expand All @@ -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 (state.occluded) {
this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded');
Expand Down Expand Up @@ -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 {
if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement;
Expand Down Expand Up @@ -989,6 +1026,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
delete this.svgTexts[clientID];
}

this.sortObjects();

this.activeElement = {
clientID: null,
attributeID: null,
Expand Down Expand Up @@ -1016,6 +1055,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);

if (!state) {
return;
}

if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('pointer-events', state.lock ? 'none' : '');
Expand All @@ -1040,7 +1083,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
}

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 => {
this.mode = Mode.DRAG;
if (text) {
Expand Down Expand Up @@ -1197,7 +1246,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
'data-z-order': state.zOrder,
}).move(xtl, ytl)
.addClass('cvat_canvas_shape');

Expand All @@ -1221,7 +1270,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape');

if (state.occluded) {
Expand All @@ -1244,7 +1293,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape');

if (state.occluded) {
Expand All @@ -1264,9 +1313,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = basicPolyline.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
zOrder: state.zOrder,
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'data-z-order': state.zOrder,
}).style({
'fill-opacity': 1,
});
Expand Down
9 changes: 4 additions & 5 deletions cvat-core/src/annotations-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,11 @@
this.objects = {}; // key is a client id
this.count = 0;
this.flush = false;
this.collectionZ = {}; // key is a frame, {max, min} are values
this.groups = {
max: 0,
}; // it is an object to we can pass it as an argument by a reference
this.injection = {
labels: this.labels,
collectionZ: this.collectionZ,
groups: this.groups,
frameMeta: this.frameMeta,
history: this.history,
Expand Down Expand Up @@ -461,7 +459,7 @@
points: [...objectState.points],
occluded: objectState.occluded,
outside: objectState.outside,
zOrder: 0,
zOrder: objectState.zOrder,
attributes: Object.keys(objectState.attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
Expand Down Expand Up @@ -725,6 +723,7 @@
} else {
checkObjectType('state occluded', state.occluded, 'boolean', null);
checkObjectType('state points', state.points, null, Array);
checkObjectType('state zOrder', state.zOrder, 'integer', null);

for (const coord of state.points) {
checkObjectType('point coordinate', coord, 'number', null);
Expand All @@ -746,7 +745,7 @@
occluded: state.occluded || false,
points: [...state.points],
type: state.shapeType,
z_order: 0,
z_order: state.zOrder,
});
} else if (state.objectType === 'track') {
constructed.tracks.push({
Expand All @@ -763,7 +762,7 @@
outside: false,
points: [...state.points],
type: state.shapeType,
z_order: 0,
z_order: state.zOrder,
}],
});
} else {
Expand Down
4 changes: 4 additions & 0 deletions cvat-core/src/annotations-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ class AnnotationsFilter {

toJSONQuery(filters) {
try {
if (!Array.isArray(filters) || filters.some((value) => typeof (value) !== 'string')) {
throw Error('Argument must be an array of strings');
}

if (!filters.length) {
return [[], '$.objects[*].clientID'];
}
Expand Down
Loading

0 comments on commit 9850094

Please sign in to comment.