React UI: Annotation view enhancements (#1106)

* Keyframes navigation

* Synchronized objects on canvas and in side panel

* Fixed minor bug with collapse

* Fixed css property 'pointer-events'

* Drawn appearance block

* Removed extra force reflow

* Finished appearance block, fixed couple bugs

* Improved save() in cvat-core, changed approach to highlight shapes

* Fixed exception in edit function, fixed filling for polylines and points, fixed wrong image navigation, remove and copy

* Added lock

* Some fixes with points

* Minor appearance fixes

* Fixed insert for points

* Fixed unit tests

* Fixed control

* Fixed list size

* Added propagate

* Minor fix with attr saving

* Some div changed to buttons

* Locked some buttons for unimplemented functionalities

* Statistics modal, changing a job status

* Minor fix with shapes counting

* Couple of fixes to improve visibility

* Added fullscreen

* SVG Canvas -> HTML Canvas frame (#1113)

* SVG Frame -> HTML Canvas frame
main
Boris Sekachev 6 years ago committed by GitHub
parent 7f8b96d4e8
commit 42614c28a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -111,11 +111,12 @@ Canvas itself handles:
Standard JS events are used. Standard JS events are used.
```js ```js
- canvas.setup - canvas.setup
- canvas.activated => ObjectState - canvas.activated => {state: ObjectState}
- canvas.deactivated - canvas.clicked => {state: ObjectState}
- canvas.moved => {states: ObjectState[], x: number, y: number} - canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.find => {states: ObjectState[], x: number, y: number} - canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData} - canvas.drawn => {state: DrawnData}
- canvas.editstart
- canvas.edited => {state: ObjectState, points: number[]} - canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState} - canvas.splitted => {state: ObjectState}
- canvas.groupped => {states: ObjectState[]} - canvas.groupped => {states: ObjectState[]}

@ -3,15 +3,35 @@
} }
.cvat_canvas_shape { .cvat_canvas_shape {
fill-opacity: 0.03;
stroke-opacity: 1; stroke-opacity: 1;
} }
polyline.cvat_canvas_shape { polyline.cvat_canvas_shape {
fill-opacity: 0; fill-opacity: 0;
}
.cvat_shape_action_opacity {
fill-opacity: 0.5;
stroke-opacity: 1;
}
polyline.cvat_shape_action_opacity {
fill-opacity: 0;
}
.cvat_shape_drawing_opacity {
fill-opacity: 0.2;
stroke-opacity: 1; stroke-opacity: 1;
} }
polyline.cvat_shape_drawing_opacity {
fill-opacity: 0;
}
.cvat_shape_action_dasharray {
stroke-dasharray: 4 1 2 3;
}
.cvat_canvas_text { .cvat_canvas_text {
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 1.2em;
@ -27,51 +47,52 @@ polyline.cvat_canvas_shape {
stroke: red; stroke: red;
} }
.cvat_canvas_shape_activated {
fill-opacity: 0.3;
}
.cvat_canvas_shape_grouping { .cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: darkmagenta; fill: darkmagenta;
fill-opacity: 0.5;
} }
polyline.cvat_canvas_shape_grouping { polyline.cvat_canvas_shape_grouping {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: darkmagenta; stroke: darkmagenta;
stroke-opacity: 1;
} }
.cvat_canvas_shape_merging { .cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: blue; fill: blue;
fill-opacity: 0.5; }
polyline.cvat_canvas_shape_merging {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: blue;
} }
polyline.cvat_canvas_shape_splitting { polyline.cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
stroke: dodgerblue; stroke: dodgerblue;
stroke-opacity: 1;
} }
.cvat_canvas_shape_splitting { .cvat_canvas_shape_splitting {
@extend .cvat_shape_action_dasharray;
@extend .cvat_shape_action_opacity;
fill: dodgerblue; fill: dodgerblue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_merging {
stroke: blue;
stroke-opacity: 1;
} }
.cvat_canvas_shape_drawing { .cvat_canvas_shape_drawing {
fill-opacity: 0.1; @extend .cvat_shape_drawing_opacity;
stroke-opacity: 1;
fill: white; fill: white;
stroke: black; stroke: black;
} }
.cvat_canvas_zoom_selection { .cvat_canvas_zoom_selection {
@extend .cvat_shape_action_dasharray;
stroke: #096dd9; stroke: #096dd9;
fill-opacity: 0; fill-opacity: 0;
stroke-dasharray: 4;
} }
.cvat_canvas_shape_occluded { .cvat_canvas_shape_occluded {

@ -32,7 +32,7 @@ import '../scss/canvas.scss';
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void; activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(rotation: Rotation, remember?: boolean): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fit(): void; fit(): void;
@ -85,7 +85,7 @@ class CanvasImpl implements Canvas {
this.model.zoomCanvas(enable); this.model.zoomCanvas(enable);
} }
public activate(clientID: number, attributeID: number | null = null): void { public activate(clientID: number | null, attributeID: number | null = null): void {
this.model.activate(clientID, attributeID); this.model.activate(clientID, attributeID);
} }

@ -110,7 +110,7 @@ export enum Mode {
} }
export interface CanvasModel { export interface CanvasModel {
readonly image: string; readonly image: HTMLImageElement | null;
readonly objects: any[]; readonly objects: any[];
readonly gridSize: Size; readonly gridSize: Size;
readonly focusData: FocusData; readonly focusData: FocusData;
@ -127,7 +127,7 @@ export interface CanvasModel {
move(topOffset: number, leftOffset: number): void; move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID: number | null): void; activate(clientID: number | null, attributeID: number | null): void;
rotate(rotation: Rotation, remember: boolean): void; rotate(rotation: Rotation, remember: boolean): void;
focus(clientID: number, padding: number): void; focus(clientID: number, padding: number): void;
fit(): void; fit(): void;
@ -151,7 +151,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
activeElement: ActiveElement; activeElement: ActiveElement;
angle: number; angle: number;
canvasSize: Size; canvasSize: Size;
image: string; image: HTMLImageElement | null;
imageID: number | null; imageID: number | null;
imageOffset: number; imageOffset: number;
imageSize: Size; imageSize: Size;
@ -183,7 +183,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
height: 0, height: 0,
width: 0, width: 0,
}, },
image: '', image: null,
imageID: null, imageID: null,
imageOffset: 0, imageOffset: 0,
imageSize: { imageSize: {
@ -291,22 +291,33 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
public setup(frameData: any, objectStates: any[]): void { public setup(frameData: any, objectStates: any[]): void {
if (frameData.number === this.data.imageID) {
this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS_UPDATED);
return;
}
this.data.imageID = frameData.number;
frameData.data( frameData.data(
(): void => { (): void => {
this.data.image = ''; this.data.image = null;
this.notify(UpdateReasons.IMAGE_CHANGED); this.notify(UpdateReasons.IMAGE_CHANGED);
}, },
).then((data: string): void => { ).then((data: HTMLImageElement): void => {
if (frameData.number !== this.data.imageID) {
// already another image
return;
}
if (!this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageSize = { this.data.imageSize = {
height: (frameData.height as number), height: (frameData.height as number),
width: (frameData.width as number), width: (frameData.width as number),
}; };
if (this.data.imageID !== frameData.number && !this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageID = frameData.number;
this.data.image = data; this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED); this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.objects = objectStates; this.data.objects = objectStates;
@ -316,8 +327,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}); });
} }
public activate(clientID: number, attributeID: number | null): void { public activate(clientID: number | null, attributeID: number | null): void {
if (this.data.mode !== Mode.IDLE) { if (this.data.mode !== Mode.IDLE && clientID !== null) {
// Exception or just return? // Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
} }
@ -503,7 +514,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
)); ));
} }
public get image(): string { public get image(): HTMLImageElement | null {
return this.data.image; return this.data.image;
} }

@ -21,7 +21,6 @@ import consts from './consts';
import { import {
translateToSVG, translateToSVG,
translateFromSVG, translateFromSVG,
translateBetweenSVG,
pointsToArray, pointsToArray,
displayShapeSize, displayShapeSize,
ShapeSizeElement, ShapeSizeElement,
@ -44,25 +43,11 @@ export interface CanvasView {
html(): HTMLDivElement; html(): HTMLDivElement;
} }
function darker(color: string, percentage: number): string {
const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100));
const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100));
const B = Math.round(parseInt(color.slice(5, 7), 16) * (1 - percentage / 100));
const rHex = Math.max(0, R).toString(16);
const gHex = Math.max(0, G).toString(16);
const bHex = Math.max(0, B).toString(16);
return `#${rHex.length === 1 ? `0${rHex}` : rHex}`
+ `${gHex.length === 1 ? `0${gHex}` : gHex}`
+ `${bHex.length === 1 ? `0${bHex}` : bHex}`;
}
export class CanvasViewImpl implements CanvasView, Listener { export class CanvasViewImpl implements CanvasView, Listener {
private loadingAnimation: SVGSVGElement; private loadingAnimation: SVGSVGElement;
private text: SVGSVGElement; private text: SVGSVGElement;
private adoptedText: SVG.Container; private adoptedText: SVG.Container;
private background: SVGSVGElement; private background: HTMLCanvasElement;
private grid: SVGSVGElement; private grid: SVGSVGElement;
private content: SVGSVGElement; private content: SVGSVGElement;
private adoptedContent: SVG.Container; private adoptedContent: SVG.Container;
@ -80,10 +65,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private splitHandler: SplitHandler; private splitHandler: SplitHandler;
private groupHandler: GroupHandler; private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler; private zoomHandler: ZoomHandler;
private activeElement: { private activeElement: ActiveElement;
state: any;
attributeID: number;
} | null;
private set mode(value: Mode) { private set mode(value: Mode) {
this.controller.mode = value; this.controller.mode = value;
@ -93,7 +75,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode; return this.controller.mode;
} }
private onDrawDone(data: object): void { private onDrawDone(data: object, continueDraw?: boolean): void {
if (data) { if (data) {
const event: CustomEvent = new CustomEvent('canvas.drawn', { const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false, bubbles: false,
@ -101,6 +83,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
detail: { detail: {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
state: data, state: data,
continue: continueDraw,
}, },
}); });
@ -114,11 +97,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} }
this.controller.draw({ if (continueDraw) {
enabled: false, this.drawHandler.draw(
}); this.controller.drawData,
this.geometry,
);
} else {
this.controller.draw({
enabled: false,
});
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
}
} }
private onEditDone(state: any, points: number[]): void { private onEditDone(state: any, points: number[]): void {
@ -229,13 +219,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
private onFindObject(e: MouseEvent): void { private onFindObject(e: MouseEvent): void {
if (e.which === 1 || e.which === 0) { if (e.which === 1 || e.which === 0) {
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]); const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.find', { const event: CustomEvent = new CustomEvent('canvas.find', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
detail: { detail: {
x, x: x - offset,
y, y: y - offset,
states: this.controller.objects, states: this.controller.objects,
}, },
}); });
@ -339,11 +330,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const key in this.svgShapes) { for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) { if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) {
const object = this.svgShapes[key]; const object = this.svgShapes[key];
if (object.attr('stroke-width')) { object.attr({
object.attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, });
});
}
} }
} }
@ -376,7 +365,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private setupObjects(states: any[]): void { private setupObjects(states: any[]): void {
this.deactivate(); const { offset } = this.controller.geometry;
const translate = (points: number[]): number[] => points
.map((coord: number): number => coord + offset);
const created = []; const created = [];
const updated = []; const updated = [];
@ -394,17 +385,31 @@ export class CanvasViewImpl implements CanvasView, Listener {
const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID) const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID)
.filter((id: number): boolean => !newIDs.includes(id)) .filter((id: number): boolean => !newIDs.includes(id))
.map((id: number): any => this.drawnStates[id]); .map((id: number): any => this.drawnStates[id]);
if (this.activeElement.clientID !== null) {
this.deactivate();
}
for (const state of deleted) { for (const state of deleted) {
if (state.clientID in this.svgTexts) { if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove(); this.svgTexts[state.clientID].remove();
} }
this.svgShapes[state.clientID].off('click.canvas');
this.svgShapes[state.clientID].remove(); this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID]; delete this.drawnStates[state.clientID];
} }
this.addObjects(created); this.addObjects(created, translate);
this.updateObjects(updated); this.updateObjects(updated, translate);
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
if (states.map((state: any): number => state.clientID).includes(clientID)) {
this.activate(this.controller.activeElement);
}
}
} }
private selectize(value: boolean, shape: SVG.Element): void { private selectize(value: boolean, shape: SVG.Element): void {
@ -414,16 +419,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
const pointID = Array.prototype.indexOf const pointID = Array.prototype.indexOf
.call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target); .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
if (self.activeElement) { if (self.activeElement.clientID !== null) {
const [state] = self.controller.objects
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
if (e.ctrlKey) { if (e.ctrlKey) {
const { points } = self.activeElement.state; const { points } = state;
self.onEditDone( self.onEditDone(
self.activeElement.state, state,
points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)), points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)),
); );
} else if (e.shiftKey) { } else if (e.shiftKey) {
self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', {
bubbles: false,
cancelable: true,
}));
self.mode = Mode.EDIT; self.mode = Mode.EDIT;
const { state } = self.activeElement;
self.deactivate(); self.deactivate();
self.editHandler.edit({ self.editHandler.edit({
enabled: true, enabled: true,
@ -483,7 +496,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.svgShapes = {}; this.svgShapes = {};
this.svgTexts = {}; this.svgTexts = {};
this.drawnStates = {}; this.drawnStates = {};
this.activeElement = null; this.activeElement = {
clientID: null,
attributeID: null,
};
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
// Create HTML elements // Create HTML elements
@ -491,7 +507,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
.createElementNS('http://www.w3.org/2000/svg', 'svg'); .createElementNS('http://www.w3.org/2000/svg', 'svg');
this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container); this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container);
this.background = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.background = window.document.createElement('canvas');
// window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path'); this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path');
@ -560,12 +577,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.onDrawDone.bind(this), this.onDrawDone.bind(this),
this.adoptedContent, this.adoptedContent,
this.adoptedText, this.adoptedText,
this.background,
); );
this.editHandler = new EditHandlerImpl( this.editHandler = new EditHandlerImpl(
this.onEditDone.bind(this), this.onEditDone.bind(this),
this.adoptedContent, this.adoptedContent,
this.background,
); );
this.mergeHandler = new MergeHandlerImpl( this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this), this.onMergeDone.bind(this),
@ -613,8 +628,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
}); });
this.content.addEventListener('wheel', (event): void => { this.content.addEventListener('wheel', (event): void => {
const point = translateToSVG(self.background, [event.clientX, event.clientY]); const { offset } = this.controller.geometry;
self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1); const point = translateToSVG(this.content, [event.clientX, event.clientY]);
self.controller.zoom(point[0] - offset, point[1] - offset, event.deltaY > 0 ? -1 : 1);
this.canvas.dispatchEvent(new CustomEvent('canvas.zoom', { this.canvas.dispatchEvent(new CustomEvent('canvas.zoom', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
@ -628,13 +644,14 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (this.mode !== Mode.IDLE) return; if (this.mode !== Mode.IDLE) return;
if (e.ctrlKey || e.shiftKey) return; if (e.ctrlKey || e.shiftKey) return;
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]); const { offset } = this.controller.geometry;
const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.moved', { const event: CustomEvent = new CustomEvent('canvas.moved', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
detail: { detail: {
x, x: x - offset,
y, y: y - offset,
states: this.controller.objects, states: this.controller.objects,
}, },
}); });
@ -649,11 +666,17 @@ export class CanvasViewImpl implements CanvasView, Listener {
public notify(model: CanvasModel & Master, reason: UpdateReasons): void { public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
this.geometry = this.controller.geometry; this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE_CHANGED) { if (reason === UpdateReasons.IMAGE_CHANGED) {
if (!model.image.length) { const { image } = model;
if (!image) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden'); this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else { } else {
this.loadingAnimation.classList.add('cvat_canvas_hidden'); this.loadingAnimation.classList.add('cvat_canvas_hidden');
this.background.style.backgroundImage = `url("${model.image}")`; const ctx = this.background.getContext('2d');
this.background.setAttribute('width', `${image.width}px`);
this.background.setAttribute('height', `${image.height}px`);
if (ctx) {
ctx.drawImage(image, 0, 0);
}
this.moveCanvas(); this.moveCanvas();
this.resizeCanvas(); this.resizeCanvas();
this.transformCanvas(); this.transformCanvas();
@ -696,7 +719,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) { } else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement); this.activate(this.controller.activeElement);
} else if (reason === UpdateReasons.DRAG_CANVAS) { } else if (reason === UpdateReasons.DRAG_CANVAS) {
this.deactivate();
if (this.mode === Mode.DRAG_CANVAS) { if (this.mode === Mode.DRAG_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.dragstart', { this.canvas.dispatchEvent(new CustomEvent('canvas.dragstart', {
bubbles: false, bubbles: false,
@ -711,7 +733,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.style.cursor = ''; this.canvas.style.cursor = '';
} }
} else if (reason === UpdateReasons.ZOOM_CANVAS) { } else if (reason === UpdateReasons.ZOOM_CANVAS) {
this.deactivate();
if (this.mode === Mode.ZOOM_CANVAS) { if (this.mode === Mode.ZOOM_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstart', { this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstart', {
bubbles: false, bubbles: false,
@ -731,28 +752,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
const data: DrawData = this.controller.drawData; const data: DrawData = this.controller.drawData;
if (data.enabled) { if (data.enabled) {
this.mode = Mode.DRAW; this.mode = Mode.DRAW;
this.deactivate();
} }
this.drawHandler.draw(data, this.geometry); this.drawHandler.draw(data, this.geometry);
} else if (reason === UpdateReasons.MERGE) { } else if (reason === UpdateReasons.MERGE) {
const data: MergeData = this.controller.mergeData; const data: MergeData = this.controller.mergeData;
if (data.enabled) { if (data.enabled) {
this.mode = Mode.MERGE; this.mode = Mode.MERGE;
this.deactivate();
} }
this.mergeHandler.merge(data); this.mergeHandler.merge(data);
} else if (reason === UpdateReasons.SPLIT) { } else if (reason === UpdateReasons.SPLIT) {
const data: SplitData = this.controller.splitData; const data: SplitData = this.controller.splitData;
if (data.enabled) { if (data.enabled) {
this.mode = Mode.SPLIT; this.mode = Mode.SPLIT;
this.deactivate();
} }
this.splitHandler.split(data); this.splitHandler.split(data);
} else if (reason === UpdateReasons.GROUP) { } else if (reason === UpdateReasons.GROUP) {
const data: GroupData = this.controller.groupData; const data: GroupData = this.controller.groupData;
if (data.enabled) { if (data.enabled) {
this.mode = Mode.GROUP; this.mode = Mode.GROUP;
this.deactivate();
} }
this.groupHandler.group(data); this.groupHandler.group(data);
} else if (reason === UpdateReasons.SELECT) { } else if (reason === UpdateReasons.SELECT) {
@ -807,14 +824,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
}; };
} }
private updateObjects(states: any[]): void { private updateObjects(states: any[], translate: (points: number[]) => number[]): void {
for (const state of states) { for (const state of states) {
const { clientID } = state; const { clientID } = state;
const drawnState = this.drawnStates[clientID]; const drawnState = this.drawnStates[clientID];
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) { if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
const none = state.hidden || state.outside; const none = state.hidden || state.outside;
this.svgShapes[clientID].style('display', none ? 'none' : ''); if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('display', none ? 'none' : '');
} else {
this.svgShapes[clientID].style('display', none ? 'none' : '');
}
} }
if (drawnState.occluded !== state.occluded) { if (drawnState.occluded !== state.occluded) {
@ -825,12 +847,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
if (drawnState.points if (state.points
.some((p: number, id: number): boolean => p !== state.points[id]) .some((p: number, id: number): boolean => p !== drawnState.points[id])
) { ) {
const translatedPoints: number[] = translateBetweenSVG( const translatedPoints: number[] = translate(state.points);
this.background, this.content, state.points,
);
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translatedPoints; const [xtl, ytl, xbr, ybr] = translatedPoints;
@ -851,8 +871,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
return `${acc}${val},`; return `${acc}${val},`;
}, '', }, '',
); );
(this.svgShapes[clientID] as any).clear();
this.svgShapes[clientID].attr('points', stringified); this.svgShapes[clientID].attr('points', stringified);
if (state.shapeType === 'points') {
this.selectize(false, this.svgShapes[clientID]);
this.setupPoints(this.svgShapes[clientID] as SVG.PolyLine, state);
}
} }
} }
@ -874,15 +899,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private addObjects(states: any[]): void { private addObjects(states: any[], translate: (points: number[]) => number[]): void {
for (const state of states) { for (const state of states) {
if (state.objectType === 'tag') { if (state.objectType === 'tag') {
this.addTag(state); this.addTag(state);
} else { } else {
const points: number[] = (state.points as number[]); const points: number[] = (state.points as number[]);
const translatedPoints: number[] = translateBetweenSVG( const translatedPoints: number[] = translate(points);
this.background, this.content, points,
);
// TODO: Use enums after typification cvat-core // TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') { if (state.shapeType === 'rectangle') {
@ -910,6 +933,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addPoints(stringified, state); .addPoints(stringified, state);
} }
} }
this.svgShapes[state.clientID].on('click.canvas', (): void => {
this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}));
});
} }
this.saveState(state); this.saveState(state);
@ -917,9 +950,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
private deactivate(): void { private deactivate(): void {
if (this.activeElement) { if (this.activeElement.clientID !== null) {
const { state } = this.activeElement; const { clientID } = this.activeElement;
const shape = this.svgShapes[this.activeElement.state.clientID]; const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
const shape = this.svgShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_activated'); shape.removeClass('cvat_canvas_shape_activated');
(shape as any).off('dragstart'); (shape as any).off('dragstart');
@ -941,15 +976,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
text.remove(); text.remove();
delete this.svgTexts[state.clientID]; delete this.svgTexts[state.clientID];
} }
this.activeElement = null;
this.activeElement = {
clientID: null,
attributeID: null,
};
} }
} }
private activate(activeElement: ActiveElement): void { private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated // Check if other element have been already activated
if (this.activeElement) { if (this.activeElement.clientID !== null) {
// Check if it is the same element // Check if it is the same element
if (this.activeElement.state.clientID === activeElement.clientID) { if (this.activeElement.clientID === activeElement.clientID) {
return; return;
} }
@ -957,16 +996,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.deactivate(); this.deactivate();
} }
const state = this.controller.objects const { clientID } = activeElement;
.filter((el): boolean => el.clientID === activeElement.clientID)[0]; if (clientID === null) {
this.activeElement = { return;
attributeID: activeElement.attributeID, }
state,
}; const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('pointer-events', state.lock ? 'none' : '');
}
if (state.hidden || state.lock) {
return;
}
const shape = this.svgShapes[activeElement.clientID]; this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];
shape.addClass('cvat_canvas_shape_activated'); shape.addClass('cvat_canvas_shape_activated');
let text = this.svgTexts[activeElement.clientID]; let text = this.svgTexts[clientID];
// Draw text if it's hidden by default // Draw text if it's hidden by default
if (!text) { if (!text) {
text = this.addText(state); text = this.addText(state);
@ -998,14 +1048,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
const p1 = e.detail.handler.startPoints.point; const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p; const p2 = e.detail.p;
const delta = 1; const delta = 1;
const { offset } = this.controller.geometry;
if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) { if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) {
const points = pointsToArray( const points = pointsToArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},` + `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`, + `${shape.attr('y') + shape.attr('height')}`,
); ).map((x: number): number => x - offset);
this.onEditDone(state, translateBetweenSVG(this.content, this.background, points)); this.onEditDone(state, points);
} }
}); });
@ -1045,15 +1096,25 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
if (resized) { if (resized) {
const { offset } = this.controller.geometry;
const points = pointsToArray( const points = pointsToArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},` + `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`, + `${shape.attr('y') + shape.attr('height')}`,
); ).map((x: number): number => x - offset);
this.onEditDone(state, translateBetweenSVG(this.content, this.background, points)); this.onEditDone(state, points);
} }
}); });
this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false,
cancelable: true,
detail: {
state,
},
}));
} }
// Update text position after corresponding box has been moved, resized, etc. // Update text position after corresponding box has been moved, resized, etc.
@ -1122,7 +1183,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 20), stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, zOrder: state.zOrder,
}).move(xtl, ytl) }).move(xtl, ytl)
@ -1146,7 +1207,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 20), stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, zOrder: state.zOrder,
}).addClass('cvat_canvas_shape'); }).addClass('cvat_canvas_shape');
@ -1169,7 +1230,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 20), stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, zOrder: state.zOrder,
}).addClass('cvat_canvas_shape'); }).addClass('cvat_canvas_shape');
@ -1185,6 +1246,25 @@ export class CanvasViewImpl implements CanvasView, Listener {
return polyline; return polyline;
} }
private setupPoints(basicPolyline: SVG.PolyLine, state: any): any {
this.selectize(true, basicPolyline);
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,
}).style({
'fill-opacity': 1,
});
group.bbox = basicPolyline.bbox.bind(basicPolyline);
group.clone = basicPolyline.clone.bind(basicPolyline);
return group;
}
private addPoints(points: string, state: any): SVG.PolyLine { private addPoints(points: string, state: any): SVG.PolyLine {
const shape = this.adoptedContent.polyline(points).attr({ const shape = this.adoptedContent.polyline(points).attr({
'color-rendering': 'optimizeQuality', 'color-rendering': 'optimizeQuality',
@ -1196,25 +1276,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
opacity: 0, opacity: 0,
}); });
this.selectize(true, shape); const group = this.setupPoints(shape, state);
const group = shape.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
zOrder: state.zOrder,
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
}).style({
'fill-opacity': 1,
});
if (state.hidden || state.outside) { if (state.hidden || state.outside) {
group.style('display', 'none'); group.style('display', 'none');
} }
group.bbox = shape.bbox.bind(shape);
group.clone = shape.clone.bind(shape);
shape.remove = (): SVG.PolyLine => { shape.remove = (): SVG.PolyLine => {
this.selectize(false, shape); this.selectize(false, shape);
shape.constructor.prototype.remove.call(shape); shape.constructor.prototype.remove.call(shape);

@ -15,7 +15,6 @@ import {
import { import {
translateToSVG, translateToSVG,
translateBetweenSVG,
displayShapeSize, displayShapeSize,
ShapeSizeElement, ShapeSizeElement,
pointsToString, pointsToString,
@ -32,10 +31,9 @@ export interface DrawHandler {
export class DrawHandlerImpl implements DrawHandler { export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape // callback is used to notify about creating new shape
private onDrawDone: (data: object) => void; private onDrawDone: (data: object, continueDraw?: boolean) => void;
private canvas: SVG.Container; private canvas: SVG.Container;
private text: SVG.Container; private text: SVG.Container;
private background: SVGSVGElement;
private crosshair: { private crosshair: {
x: SVG.Line; x: SVG.Line;
y: SVG.Line; y: SVG.Line;
@ -46,17 +44,16 @@ export class DrawHandlerImpl implements DrawHandler {
// we should use any instead of SVG.Shape because svg plugins cannot change declared interface // we should use any instead of SVG.Shape because svg plugins cannot change declared interface
// so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist // so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist
private drawInstance: any; private drawInstance: any;
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement; private shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] { private getFinalRectCoordinates(bbox: BBox): number[] {
const frameWidth = this.geometry.image.width; const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height; const frameHeight = this.geometry.image.height;
const { offset } = this.geometry;
let [xtl, ytl, xbr, ybr] = translateBetweenSVG( let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height]
this.canvas.node as any as SVGSVGElement, .map((coord: number): number => coord - offset);
this.background,
[bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height],
);
xtl = Math.min(Math.max(xtl, 0), frameWidth); xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth); xbr = Math.min(Math.max(xbr, 0), frameWidth);
@ -70,12 +67,8 @@ export class DrawHandlerImpl implements DrawHandler {
points: number[]; points: number[];
box: Box; box: Box;
} { } {
const points = translateBetweenSVG( const { offset } = this.geometry;
this.canvas.node as any as SVGSVGElement, const points = targetPoints.map((coord: number): number => coord - offset);
this.background,
targetPoints,
);
const box = { const box = {
xtl: Number.MAX_SAFE_INTEGER, xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER, ytl: Number.MAX_SAFE_INTEGER,
@ -125,6 +118,11 @@ export class DrawHandlerImpl implements DrawHandler {
this.canvas.off('mousemove.draw'); this.canvas.off('mousemove.draw');
this.canvas.off('click.draw'); this.canvas.off('click.draw');
if (this.pointsGroup) {
this.pointsGroup.remove();
this.pointsGroup = null;
}
if (this.drawInstance) { if (this.drawInstance) {
// Draw plugin isn't activated when draw from initialState // Draw plugin isn't activated when draw from initialState
// So, we don't need to use any draw events // So, we don't need to use any draw events
@ -311,7 +309,7 @@ export class DrawHandlerImpl implements DrawHandler {
private drawPolygon(): void { private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon().draw({ this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1, snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({ }).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
@ -321,7 +319,7 @@ export class DrawHandlerImpl implements DrawHandler {
private drawPolyline(): void { private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline().draw({ this.drawInstance = (this.canvas as any).polyline().draw({
snapToGrid: 0.1, snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({ }).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0, 'fill-opacity': 0,
}); });
@ -332,7 +330,7 @@ export class DrawHandlerImpl implements DrawHandler {
private drawPoints(): void { private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon().draw({ this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1, snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({ }).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': 0, 'stroke-width': 0,
opacity: 0, opacity: 0,
}); });
@ -342,21 +340,22 @@ export class DrawHandlerImpl implements DrawHandler {
private pastePolyshape(): void { private pastePolyshape(): void {
this.canvas.on('click.draw', (e: MouseEvent): void => { this.canvas.on('click.draw', (e: MouseEvent): void => {
const targetPoints = (e.target as SVGElement) const targetPoints = this.drawInstance
.getAttribute('points') .attr('points')
.split(/[,\s]/g) .split(/[,\s]/g)
.map((coord): number => +coord); .map((coord: string): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints); const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
this.release(); this.release();
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points, points,
occluded: this.drawData.initialState.occluded, occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}); }, e.ctrlKey);
}); });
} }
@ -380,30 +379,31 @@ export class DrawHandlerImpl implements DrawHandler {
private pasteBox(box: BBox): void { private pasteBox(box: BBox): void {
this.drawInstance = (this.canvas as any).rect(box.width, box.height) this.drawInstance = (this.canvas as any).rect(box.width, box.height)
.move(box.x, box.y) .move(box.x, box.y)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
this.canvas.on('click.draw', (e: MouseEvent): void => { this.canvas.on('click.draw', (e: MouseEvent): void => {
const bbox = (e.target as SVGRectElement).getBBox(); const bbox = this.drawInstance.node.getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release(); this.release();
this.onDrawDone({ this.onDrawDone({
shapeType: this.drawData.shapeType, shapeType: this.drawData.initialState.shapeType,
objectType: this.drawData.initialState.objectType,
points: [xtl, ytl, xbr, ybr], points: [xtl, ytl, xbr, ybr],
occluded: this.drawData.initialState.occluded, occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes }, attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label, label: this.drawData.initialState.label,
color: this.drawData.initialState.color, color: this.drawData.initialState.color,
}); }, e.ctrlKey);
}); });
} }
private pastePolygon(points: string): void { private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points) this.drawInstance = (this.canvas as any).polygon(points)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
@ -412,7 +412,7 @@ export class DrawHandlerImpl implements DrawHandler {
private pastePolyline(points: string): void { private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points) this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}); });
this.pasteShape(); this.pasteShape();
@ -424,19 +424,52 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing').style({ .addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0, 'stroke-width': 0,
}); });
this.pasteShape();
this.pointsGroup = this.canvas.group();
for (const point of points.split(' ')) {
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale;
const [x, y] = point.split(',').map((coord: string): number => +coord);
this.pointsGroup.circle().move(x - radius / 2, y - radius / 2)
.fill('white').stroke('black').attr({
r: radius,
'stroke-width': stroke,
});
}
this.pointsGroup.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
this.canvas.on('mousemove.draw', (e: MouseEvent): void => {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
const bbox = this.drawInstance.bbox();
this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2);
const radius = consts.BASE_POINT_SIZE / this.geometry.scale;
const newPoints = this.drawInstance.attr('points').split(' ');
if (this.pointsGroup) {
this.pointsGroup.children()
.forEach((child: SVG.Element, idx: number): void => {
const [px, py] = newPoints[idx].split(',');
child.move(px - radius / 2, py - radius / 2);
});
}
});
this.pastePolyshape(); this.pastePolyshape();
} }
private startDraw(): void { private startDraw(): void {
// TODO: Use enums after typification cvat-core // TODO: Use enums after typification cvat-core
if (this.drawData.initialState) { if (this.drawData.initialState) {
const { offset } = this.geometry;
if (this.drawData.shapeType === 'rectangle') { if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translateBetweenSVG( const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points
this.background, .map((coord: number): number => coord + offset);
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
this.pasteBox({ this.pasteBox({
x: xtl, x: xtl,
@ -445,12 +478,8 @@ export class DrawHandlerImpl implements DrawHandler {
height: ybr - ytl, height: ybr - ytl,
}); });
} else { } else {
const points = translateBetweenSVG( const points = this.drawData.initialState.points
this.background, .map((coord: number): number => coord + offset);
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
const stringifiedPoints = pointsToString(points); const stringifiedPoints = pointsToString(points);
if (this.drawData.shapeType === 'polygon') { if (this.drawData.shapeType === 'polygon') {
@ -475,19 +504,18 @@ export class DrawHandlerImpl implements DrawHandler {
} }
public constructor( public constructor(
onDrawDone: (data: object) => void, onDrawDone: (data: object, continueDraw?: boolean) => void,
canvas: SVG.Container, canvas: SVG.Container,
text: SVG.Container, text: SVG.Container,
background: SVGSVGElement,
) { ) {
this.onDrawDone = onDrawDone; this.onDrawDone = onDrawDone;
this.canvas = canvas; this.canvas = canvas;
this.text = text; this.text = text;
this.background = background;
this.drawData = null; this.drawData = null;
this.geometry = null; this.geometry = null;
this.crosshair = null; this.crosshair = null;
this.drawInstance = null; this.drawInstance = null;
this.pointsGroup = null;
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => { this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
if (this.crosshair) { if (this.crosshair) {
@ -525,16 +553,25 @@ export class DrawHandlerImpl implements DrawHandler {
}); });
} }
if (this.pointsGroup) {
for (const point of this.pointsGroup.children()) {
point.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / geometry.scale,
r: consts.BASE_POINT_SIZE / geometry.scale,
});
}
}
if (this.drawInstance) { if (this.drawInstance) {
this.drawInstance.draw('transform'); this.drawInstance.draw('transform');
this.drawInstance.style({ this.drawInstance.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
}); });
const paintHandler = this.drawInstance.remember('_paintHandler'); const paintHandler = this.drawInstance.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) { for (const point of (paintHandler as any).set.members) {
point.style( point.attr(
'stroke-width', 'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`, `${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
); );

@ -9,7 +9,6 @@ import 'svg.select.js';
import consts from './consts'; import consts from './consts';
import { import {
translateFromSVG, translateFromSVG,
translateBetweenSVG,
pointsToArray, pointsToArray,
} from './shared'; } from './shared';
import { import {
@ -27,7 +26,6 @@ export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void; private onEditDone: (state: any, points: number[]) => void;
private geometry: Geometry; private geometry: Geometry;
private canvas: SVG.Container; private canvas: SVG.Container;
private background: SVGSVGElement;
private editData: EditData; private editData: EditData;
private editedShape: SVG.Shape; private editedShape: SVG.Shape;
private editLine: SVG.PolyLine; private editLine: SVG.PolyLine;
@ -94,21 +92,19 @@ export class EditHandlerImpl implements EditHandler {
} }
} }
private stopEdit(e: MouseEvent): void { private selectPolygon(shape: SVG.Polygon): void {
function selectPolygon(shape: SVG.Polygon): void { const { offset } = this.geometry;
const points = translateBetweenSVG( const points = pointsToArray(shape.attr('points'))
this.canvas.node as any as SVGSVGElement, .map((coord: number): number => coord - offset);
this.background,
pointsToArray(shape.attr('points')),
);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private stopEdit(e: MouseEvent): void {
if (!this.editLine) { if (!this.editLine) {
return; return;
} }
@ -154,7 +150,7 @@ export class EditHandlerImpl implements EditHandler {
} }
for (const clone of this.clones) { for (const clone of this.clones) {
clone.on('click', selectPolygon.bind(this, clone)); clone.on('click', (): void => this.selectPolygon(clone));
clone.on('mouseenter', (): void => { clone.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting'); clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => { }).on('mouseleave', (): void => {
@ -170,6 +166,7 @@ export class EditHandlerImpl implements EditHandler {
} }
let points = null; let points = null;
const { offset } = this.geometry;
if (this.editData.state.shapeType === 'polyline') { if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) { if (start !== this.editData.pointID) {
linePoints.reverse(); linePoints.reverse();
@ -181,11 +178,8 @@ export class EditHandlerImpl implements EditHandler {
points = oldPoints.concat(linePoints.slice(0, -1)); points = oldPoints.concat(linePoints.slice(0, -1));
} }
points = translateBetweenSVG( points = pointsToArray(points.join(' '))
this.canvas.node as any as SVGSVGElement, .map((coord: number): number => coord - offset);
this.background,
pointsToArray(points.join(' ')),
);
const { state } = this.editData; const { state } = this.editData;
this.edit({ this.edit({
@ -284,11 +278,9 @@ export class EditHandlerImpl implements EditHandler {
public constructor( public constructor(
onEditDone: (state: any, points: number[]) => void, onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container, canvas: SVG.Container,
background: SVGSVGElement,
) { ) {
this.onEditDone = onEditDone; this.onEditDone = onEditDone;
this.canvas = canvas; this.canvas = canvas;
this.background = background;
this.editData = null; this.editData = null;
this.editedShape = null; this.editedShape = null;
this.editLine = null; this.editLine = null;

@ -58,23 +58,13 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
return output; return output;
} }
// Translate point array from the first canvas coordinate system
// to another
export function translateBetweenSVG(
from: SVGSVGElement,
to: SVGSVGElement,
points: number[],
): number[] {
return translateToSVG(to, translateFromSVG(from, points));
}
export function pointsToString(points: number[]): string { export function pointsToString(points: number[]): string {
return points.reduce((acc, val, idx): string => { return points.reduce((acc, val, idx): string => {
if (idx % 2) { if (idx % 2) {
return `${acc},${val}`; return `${acc},${val}`;
} }
return `${acc} ${val}`; return `${acc} ${val}`.trim();
}, ''); }, '');
} }

@ -529,6 +529,10 @@
} }
for (const object of Object.values(this.objects)) { for (const object of Object.values(this.objects)) {
if (object.removed) {
continue;
}
let objectType = null; let objectType = null;
if (object instanceof Shape) { if (object instanceof Shape) {
objectType = 'shape'; objectType = 'shape';
@ -712,7 +716,7 @@
let minimumState = null; let minimumState = null;
for (const state of objectStates) { for (const state of objectStates) {
checkObjectType('object state', state, null, ObjectState); checkObjectType('object state', state, null, ObjectState);
if (state.outside) continue; if (state.outside || state.hidden) continue;
const object = this.objects[state.clientID]; const object = this.objects[state.clientID];
if (typeof (object) === 'undefined') { if (typeof (object) === 'undefined') {

@ -13,6 +13,7 @@
checkObjectType, checkObjectType,
} = require('./common'); } = require('./common');
const { const {
colors,
ObjectShape, ObjectShape,
ObjectType, ObjectType,
AttributeType, AttributeType,
@ -26,6 +27,8 @@
const { Label } = require('./labels'); const { Label } = require('./labels');
const defaultGroupColor = '#E0E0E0';
// Called with the Annotation context // Called with the Annotation context
function objectStateFactory(frame, data) { function objectStateFactory(frame, data) {
const objectState = new ObjectState(data); const objectState = new ObjectState(data);
@ -165,7 +168,7 @@
updateTimestamp(updated) { updateTimestamp(updated) {
const anyChanges = updated.label || updated.attributes || updated.points const anyChanges = updated.label || updated.attributes || updated.points
|| updated.outside || updated.occluded || updated.keyframe || updated.outside || updated.occluded || updated.keyframe
|| updated.group || updated.zOrder; || updated.zOrder;
if (anyChanges) { if (anyChanges) {
this.updated = Date.now(); this.updated = Date.now();
@ -202,6 +205,96 @@
return this.collectionZ[frame]; return this.collectionZ[frame];
} }
validateStateBeforeSave(frame, data) {
let fittedPoints = [];
const updated = data.updateFlags;
if (updated.label) {
checkObjectType('label', data.label, null, Label);
}
const labelAttributes = data.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`The label of the shape doesn't have the attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
fittedPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = false;
}
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
}
if (updated.outside) {
checkObjectType('outside', data.outside, 'boolean', null);
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
}
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
}
if (updated.keyframe) {
checkObjectType('keyframe', data.keyframe, 'boolean', null);
}
return fittedPoints;
}
save() { save() {
throw new ScriptingError( throw new ScriptingError(
'Is not implemented', 'Is not implemented',
@ -289,7 +382,10 @@
points: [...this.points], points: [...this.points],
attributes: { ...this.attributes }, attributes: { ...this.attributes },
label: this.label, label: this.label,
group: this.group, group: {
color: this.group ? colors[this.group % colors.length] : defaultGroupColor,
id: this.group,
},
color: this.color, color: this.color,
hidden: this.hidden, hidden: this.hidden,
updated: this.updated, updated: this.updated,
@ -308,111 +404,46 @@
return objectStateFactory.call(this, frame, this.get(frame)); return objectStateFactory.call(this, frame, this.get(frame));
} }
// All changes are done in this temporary object const fittedPoints = this.validateStateBeforeSave(frame, data);
const copy = this.get(frame);
const updated = data.updateFlags; const updated = data.updateFlags;
// Now when all fields are validated, we can apply them
if (updated.label) { if (updated.label) {
checkObjectType('label', data.label, null, Label); this.label = data.label;
copy.label = data.label; this.attributes = {};
copy.attributes = {}; this.appendDefaultAttributes(data.label);
this.appendDefaultAttributes.call(copy, copy.label);
} }
if (updated.attributes) { if (updated.attributes) {
const labelAttributes = copy.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) { for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID]; this.attributes[attrID] = data.attributes[attrID];
if (attrID in labelAttributes) {
if (validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
} else {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
}
} }
} }
if (updated.points) { if (updated.points && fittedPoints.length) {
checkObjectType('points', data.points, null, Array); this.points = [...fittedPoints];
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
const cutPoints = [];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
cutPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (checkShapeArea(this.shapeType, cutPoints)) {
copy.points = cutPoints;
}
} }
if (updated.occluded) { if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null); this.occluded = data.occluded;
copy.occluded = data.occluded;
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
} }
if (updated.zOrder) { if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null); this.zOrder = data.zOrder;
copy.zOrder = data.zOrder;
} }
if (updated.lock) { if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null); this.lock = data.lock;
copy.lock = data.lock;
} }
if (updated.color) { if (updated.color) {
checkObjectType('color', data.color, 'string', null); this.color = data.color;
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
copy.color = data.color;
} }
if (updated.hidden) { if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null); this.hidden = data.hidden;
copy.hidden = data.hidden;
} }
// Commit state
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
}
// Reset flags and commit all changes
this.updateTimestamp(updated); this.updateTimestamp(updated);
updated.reset(); updated.reset();
@ -496,10 +527,20 @@
// Method is used to construct ObjectState objects // Method is used to construct ObjectState objects
get(frame) { get(frame) {
const {
prev,
next,
first,
last,
} = this.boundedKeyframes(frame);
return { return {
...this.getPosition(frame), ...this.getPosition(frame, prev, next),
attributes: this.getAttributes(frame), attributes: this.getAttributes(frame),
group: this.group, group: {
color: this.group ? colors[this.group % colors.length] : defaultGroupColor,
id: this.group,
},
objectType: ObjectType.TRACK, objectType: ObjectType.TRACK,
shapeType: this.shapeType, shapeType: this.shapeType,
clientID: this.clientID, clientID: this.clientID,
@ -509,30 +550,48 @@
hidden: this.hidden, hidden: this.hidden,
updated: this.updated, updated: this.updated,
label: this.label, label: this.label,
keyframes: {
prev,
next,
first,
last,
},
frame, frame,
}; };
} }
neighborsFrames(targetFrame) { boundedKeyframes(targetFrame) {
const frames = Object.keys(this.shapes).map((frame) => +frame); const frames = Object.keys(this.shapes).map((frame) => +frame);
let lDiff = Number.MAX_SAFE_INTEGER; let lDiff = Number.MAX_SAFE_INTEGER;
let rDiff = Number.MAX_SAFE_INTEGER; let rDiff = Number.MAX_SAFE_INTEGER;
let first = Number.MAX_SAFE_INTEGER;
let last = Number.MIN_SAFE_INTEGER;
for (const frame of frames) { for (const frame of frames) {
if (frame < first) {
first = frame;
}
if (frame > last) {
last = frame;
}
const diff = Math.abs(targetFrame - frame); const diff = Math.abs(targetFrame - frame);
if (frame <= targetFrame && diff < lDiff) {
if (frame < targetFrame && diff < lDiff) {
lDiff = diff; lDiff = diff;
} else if (diff < rDiff) { } else if (frame > targetFrame && diff < rDiff) {
rDiff = diff; rDiff = diff;
} }
} }
const leftFrame = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff; const prev = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff;
const rightFrame = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff; const next = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff;
return { return {
leftFrame, prev,
rightFrame, next,
first,
last,
}; };
} }
@ -568,181 +627,80 @@
return objectStateFactory.call(this, frame, this.get(frame)); return objectStateFactory.call(this, frame, this.get(frame));
} }
// All changes are done in this temporary object const fittedPoints = this.validateStateBeforeSave(frame, data);
const copy = Object.assign(this.get(frame));
copy.attributes = Object.assign(copy.attributes);
copy.points = [...copy.points];
const updated = data.updateFlags; const updated = data.updateFlags;
let positionUpdated = false; const current = this.get(frame);
const labelAttributes = data.label.attributes
if (updated.label) {
checkObjectType('label', data.label, null, Label);
copy.label = data.label;
copy.attributes = {};
// Shape attributes will be removed later after all checks
this.appendDefaultAttributes.call(copy, copy.label);
}
const labelAttributes = copy.label.attributes
.reduce((accumulator, value) => { .reduce((accumulator, value) => {
accumulator[value.id] = value; accumulator[value.id] = value;
return accumulator; return accumulator;
}, {}); }, {});
if (updated.attributes) { if (updated.label) {
for (const attrID of Object.keys(data.attributes)) { this.label = data.label;
const value = data.attributes[attrID]; this.attributes = {};
if (attrID in labelAttributes) { for (const shape of Object.values(this.shapes)) {
if (validateAttributeValue(value, labelAttributes[attrID])) { shape.attributes = {};
copy.attributes[attrID] = value;
} else {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
}
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
const cutPoints = [];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
cutPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (checkShapeArea(this.shapeType, cutPoints)) {
copy.points = cutPoints;
positionUpdated = true;
}
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
copy.occluded = data.occluded;
positionUpdated = true;
}
if (updated.outside) {
checkObjectType('outside', data.outside, 'boolean', null);
copy.outside = data.outside;
positionUpdated = true;
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
copy.zOrder = data.zOrder;
positionUpdated = true;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
copy.color = data.color;
}
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
copy.hidden = data.hidden;
}
if (updated.keyframe) {
// Just check here
checkObjectType('keyframe', data.keyframe, 'boolean', null);
}
// Commit all changes
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
} }
this.appendDefaultAttributes(data.label);
} }
let mutableAttributesUpdated = false;
if (updated.attributes) { if (updated.attributes) {
// Mutable attributes will be updated below for (const attrID of Object.keys(data.attributes)) {
for (const attrID of Object.keys(copy.attributes)) {
if (!labelAttributes[attrID].mutable) { if (!labelAttributes[attrID].mutable) {
this.attributes[attrID] = data.attributes[attrID]; this.attributes[attrID] = data.attributes[attrID];
this.attributes[attrID] = data.attributes[attrID]; this.attributes[attrID] = data.attributes[attrID];
} else if (data.attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated
// not keyframe yet
|| !(frame in this.shapes)
// keyframe, but without this attrID
|| !(attrID in this.shapes[frame])
// keyframe with attrID, but with another value
|| (this.shapes[frame][attrID] !== data.attributes[attrID]);
} }
} }
} }
if (updated.label) { if (updated.lock) {
for (const shape of Object.values(this.shapes)) { this.lock = data.lock;
shape.attributes = {};
}
} }
// Remove keyframe if (updated.color) {
if (updated.keyframe && !data.keyframe) { this.color = data.color;
if (frame in this.shapes) {
if (Object.keys(this.shapes).length === 1) {
throw new DataError('You cannot remove the latest keyframe of a track');
}
delete this.shapes[frame];
this.updateTimestamp(updated);
updated.reset();
}
return objectStateFactory.call(this, frame, this.get(frame));
} }
// Add/update keyframe if (updated.hidden) {
if (positionUpdated || updated.attributes || (updated.keyframe && data.keyframe)) { this.hidden = data.hidden;
data.keyframe = true; }
if (updated.points || updated.keyframe || updated.outside
|| updated.occluded || updated.zOrder || mutableAttributesUpdated) {
const mutableAttributes = frame in this.shapes ? this.shapes[frame].attributes : {};
this.shapes[frame] = { this.shapes[frame] = {
frame, frame,
zOrder: copy.zOrder, zOrder: data.zOrder,
points: copy.points, points: updated.points && fittedPoints.length ? fittedPoints : current.points,
outside: copy.outside, outside: data.outside,
occluded: copy.occluded, occluded: data.occluded,
attributes: {}, attributes: mutableAttributes,
}; };
if (updated.attributes) { for (const attrID of Object.keys(data.attributes)) {
// Unmutable attributes were updated above if (labelAttributes[attrID].mutable
for (const attrID of Object.keys(copy.attributes)) { && data.attributes[attrID] !== current.attributes[attrID]) {
if (labelAttributes[attrID].mutable) { this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID]; this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID]; }
} }
if (updated.keyframe && !data.keyframe) {
if (Object.keys(this.shapes).length === 1) {
throw new DataError('You are not able to remove the latest keyframe for a track. '
+ 'Consider removing a track instead');
} else {
delete this.shapes[frame];
} }
} }
} }
@ -753,58 +711,45 @@
return objectStateFactory.call(this, frame, this.get(frame)); return objectStateFactory.call(this, frame, this.get(frame));
} }
getPosition(targetFrame) { getPosition(targetFrame, leftKeyframe, rightFrame) {
const { const leftFrame = targetFrame in this.shapes ? targetFrame : leftKeyframe;
leftFrame,
rightFrame,
} = this.neighborsFrames(targetFrame);
const rightPosition = Number.isInteger(rightFrame) ? this.shapes[rightFrame] : null; const rightPosition = Number.isInteger(rightFrame) ? this.shapes[rightFrame] : null;
const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null; const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null;
if (leftPosition && leftFrame === targetFrame) { if (leftPosition && rightPosition) {
return {
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
keyframe: true,
};
}
if (rightPosition && leftPosition) {
return { return {
...this.interpolatePosition( ...this.interpolatePosition(
leftPosition, leftPosition,
rightPosition, rightPosition,
(targetFrame - leftFrame) / (rightFrame - leftFrame), (targetFrame - leftFrame) / (rightFrame - leftFrame),
), ),
keyframe: false, keyframe: targetFrame in this.shapes,
}; };
} }
if (rightPosition) { if (leftPosition) {
return { return {
points: [...rightPosition.points], points: [...leftPosition.points],
occluded: rightPosition.occluded, occluded: leftPosition.occluded,
outside: true, outside: leftPosition.outside,
zOrder: 0, zOrder: 0,
keyframe: false, keyframe: targetFrame in this.shapes,
}; };
} }
if (leftPosition) { if (rightPosition) {
return { return {
points: [...leftPosition.points], points: [...rightPosition.points],
occluded: leftPosition.occluded, occluded: rightPosition.occluded,
outside: leftPosition.outside, outside: true,
zOrder: 0, zOrder: 0,
keyframe: false, keyframe: targetFrame in this.shapes,
}; };
} }
throw new ScriptingError( throw new DataError(
`No one neightbour frame found for the track with client ID: "${this.id}"`, 'No one left position or right position was found. '
+ `Interpolation impossible. Client ID: ${this.id}`,
); );
} }
@ -813,7 +758,7 @@
this.removed = true; this.removed = true;
} }
return true; return this.removed;
} }
} }
@ -873,46 +818,61 @@
return objectStateFactory.call(this, frame, this.get(frame)); return objectStateFactory.call(this, frame, this.get(frame));
} }
// All changes are done in this temporary object if (this.lock && data.lock) {
const copy = this.get(frame); return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags; const updated = data.updateFlags;
// First validate all the fields
if (updated.label) { if (updated.label) {
checkObjectType('label', data.label, null, Label); checkObjectType('label', data.label, null, Label);
copy.label = data.label;
copy.attributes = {};
this.appendDefaultAttributes.call(copy, copy.label);
} }
if (updated.attributes) { if (updated.attributes) {
const labelAttributes = copy.label const labelAttributes = data.label.attributes
.attributes.map((attr) => `${attr.id}`); .reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) { for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes.includes(attrID)) { const value = data.attributes[attrID];
copy.attributes[attrID] = data.attributes[attrID]; if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
);
} }
} }
} }
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.lock) { if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null); checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
} }
// Commit state // Now when all fields are validated, we can apply them
for (const prop of Object.keys(copy)) { if (updated.label) {
if (prop in this) { this.label = data.label;
this[prop] = copy[prop]; this.attributes = {};
this.appendDefaultAttributes(data.label);
}
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
this.attributes[attrID] = data.attributes[attrID];
} }
} }
// Reset flags and commit all changes if (updated.lock) {
this.lock = data.lock;
}
this.updateTimestamp(updated); this.updateTimestamp(updated);
updated.reset(); updated.reset();

@ -56,7 +56,7 @@
if (typeof (value) !== type) { if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS // specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) { if (type === 'integer' && Number.isInteger(value)) {
return; return true;
} }
throw new ArgumentError( throw new ArgumentError(
@ -77,6 +77,8 @@
); );
} }
} }
return true;
} }
module.exports = { module.exports = {

@ -167,7 +167,7 @@
}; };
/** /**
* Array of hex color * Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values * @type {module:API.cvat.classes.Loader[]} values
* @name colors * @name colors
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums

@ -100,9 +100,14 @@
} else if (isBrowser) { } else if (isBrowser) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
frameCache[this.tid][this.number] = reader.result; const image = new Image(frame.width, frame.height);
resolve(frameCache[this.tid][this.number]); image.onload = () => {
frameCache[this.tid][this.number] = image;
resolve(frameCache[this.tid][this.number]);
};
image.src = reader.result;
}; };
reader.readAsDataURL(frame); reader.readAsDataURL(frame);
} }
} }

@ -21,7 +21,7 @@
* initial information about an ObjectState; * initial information about an ObjectState;
* Necessary fields: objectType, shapeType, frame, updated * Necessary fields: objectType, shapeType, frame, updated
* Optional fields: points, group, zOrder, outside, occluded, hidden, * Optional fields: points, group, zOrder, outside, occluded, hidden,
* attributes, lock, label, mode, color, keyframe, clientID, serverID * attributes, lock, label, mode, color, keyframe, keyframes, clientID, serverID
* These fields can be set later via setters * These fields can be set later via setters
*/ */
constructor(serialized) { constructor(serialized) {
@ -34,11 +34,12 @@
occluded: null, occluded: null,
keyframe: null, keyframe: null,
group: null,
zOrder: null, zOrder: null,
lock: null, lock: null,
color: null, color: null,
hidden: null, hidden: null,
group: serialized.group,
keyframes: serialized.keyframes,
updated: serialized.updated, updated: serialized.updated,
clientID: serialized.clientID, clientID: serialized.clientID,
@ -61,7 +62,6 @@
this.occluded = false; this.occluded = false;
this.keyframe = false; this.keyframe = false;
this.group = false;
this.zOrder = false; this.zOrder = false;
this.lock = false; this.lock = false;
this.color = false; this.color = false;
@ -190,16 +190,14 @@
}, },
group: { group: {
/** /**
* Object with short group info { color, id }
* @name group * @name group
* @type {integer} * @type {object}
* @memberof module:API.cvat.classes.ObjectState * @memberof module:API.cvat.classes.ObjectState
* @instance * @instance
* @readonly
*/ */
get: () => data.group, get: () => data.group,
set: (group) => {
data.updateFlags.group = true;
data.group = group;
},
}, },
zOrder: { zOrder: {
/** /**
@ -240,6 +238,22 @@
data.keyframe = keyframe; data.keyframe = keyframe;
}, },
}, },
keyframes: {
/**
* Object of keyframes { first, prev, next, last }
* @name keyframes
* @type {object}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => {
if (data.keyframes) {
return { ...data.keyframes };
}
return null;
},
},
occluded: { occluded: {
/** /**
* @name occluded * @name occluded
@ -306,7 +320,6 @@
})); }));
this.label = serialized.label; this.label = serialized.label;
this.group = serialized.group;
this.zOrder = serialized.zOrder; this.zOrder = serialized.zOrder;
this.outside = serialized.outside; this.outside = serialized.outside;
this.keyframe = serialized.keyframe; this.keyframe = serialized.keyframe;

@ -548,7 +548,7 @@ describe('Feature: group annotations', () => {
expect(typeof (groupID)).toBe('number'); expect(typeof (groupID)).toBe('number');
annotations = await task.annotations.get(0); annotations = await task.annotations.get(0);
for (const state of annotations) { for (const state of annotations) {
expect(state.group).toBe(groupID); expect(state.group.id).toBe(groupID);
} }
}); });
@ -559,7 +559,7 @@ describe('Feature: group annotations', () => {
expect(typeof (groupID)).toBe('number'); expect(typeof (groupID)).toBe('number');
annotations = await job.annotations.get(0); annotations = await job.annotations.get(0);
for (const state of annotations) { for (const state of annotations) {
expect(state.group).toBe(groupID); expect(state.group.id).toBe(groupID);
} }
}); });

@ -32,6 +32,8 @@ export enum AnnotationActionTypes {
MERGE_OBJECTS = 'MERGE_OBJECTS', MERGE_OBJECTS = 'MERGE_OBJECTS',
GROUP_OBJECTS = 'GROUP_OBJECTS', GROUP_OBJECTS = 'GROUP_OBJECTS',
SPLIT_TRACK = 'SPLIT_TRACK', SPLIT_TRACK = 'SPLIT_TRACK',
COPY_SHAPE = 'COPY_SHAPE',
EDIT_SHAPE = 'EDIT_SHAPE',
DRAW_SHAPE = 'DRAW_SHAPE', DRAW_SHAPE = 'DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN', SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS', RESET_CANVAS = 'RESET_CANVAS',
@ -50,7 +52,215 @@ export enum AnnotationActionTypes {
UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT', UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT',
COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR', COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR',
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE', COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS' COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS',
ACTIVATE_OBJECT = 'ACTIVATE_OBJECT',
SELECT_OBJECTS = 'SELECT_OBJECTS',
REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS',
REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED',
PROPAGATE_OBJECT = 'PROPAGATE_OBJECT',
PROPAGATE_OBJECT_SUCCESS = 'PROPAGATE_OBJECT_SUCCESS',
PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED',
CHANGE_PROPAGATE_FRAMES = 'CHANGE_PROPAGATE_FRAMES',
SWITCH_SHOWING_STATISTICS = 'SWITCH_SHOWING_STATISTICS',
COLLECT_STATISTICS = 'COLLECT_STATISTICS',
COLLECT_STATISTICS_SUCCESS = 'COLLECT_STATISTICS_SUCCESS',
COLLECT_STATISTICS_FAILED = 'COLLECT_STATISTICS_FAILED',
CHANGE_JOB_STATUS = 'CHANGE_JOB_STATUS',
CHANGE_JOB_STATUS_SUCCESS = 'CHANGE_JOB_STATUS_SUCCESS',
CHANGE_JOB_STATUS_FAILED = 'CHANGE_JOB_STATUS_FAILED',
}
export function changeJobStatusAsync(jobInstance: any, status: string):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const oldStatus = jobInstance.status;
try {
dispatch({
type: AnnotationActionTypes.CHANGE_JOB_STATUS,
payload: {},
});
// eslint-disable-next-line no-param-reassign
jobInstance.status = status;
await jobInstance.save();
dispatch({
type: AnnotationActionTypes.CHANGE_JOB_STATUS_SUCCESS,
payload: {},
});
} catch (error) {
// eslint-disable-next-line no-param-reassign
jobInstance.status = oldStatus;
dispatch({
type: AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED,
payload: {
error,
},
});
}
};
}
export function collectStatisticsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch({
type: AnnotationActionTypes.COLLECT_STATISTICS,
payload: {},
});
const data = await sessionInstance.annotations.statistics();
dispatch({
type: AnnotationActionTypes.COLLECT_STATISTICS_SUCCESS,
payload: {
data,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.COLLECT_STATISTICS_FAILED,
payload: {
error,
},
});
}
};
}
export function showStatistics(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_SHOWING_STATISTICS,
payload: {
visible,
},
};
}
export function propagateObjectAsync(
sessionInstance: any,
objectState: any,
from: number,
to: number,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const copy = {
attributes: objectState.attributes,
points: objectState.points,
occluded: objectState.occluded,
objectType: objectState.objectType !== ObjectType.TRACK
? objectState.objectType : ObjectType.SHAPE,
shapeType: objectState.shapeType,
label: objectState.label,
frame: from,
};
const states = [];
for (let frame = from; frame <= to; frame++) {
copy.frame = frame;
const newState = new cvat.classes.ObjectState(copy);
states.push(newState);
}
await sessionInstance.annotations.put(states);
dispatch({
type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS,
payload: {
objectState,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.PROPAGATE_OBJECT_FAILED,
payload: {
error,
},
});
}
};
}
export function propagateObject(objectState: any | null): AnyAction {
return {
type: AnnotationActionTypes.PROPAGATE_OBJECT,
payload: {
objectState,
},
};
}
export function changePropagateFrames(frames: number): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_PROPAGATE_FRAMES,
payload: {
frames,
},
};
}
export function removeObjectAsync(objectState: any, force: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const removed = await objectState.delete(force);
if (removed) {
dispatch({
type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS,
payload: {
objectState,
},
});
} else {
throw new Error('Could not remove the object. Is it locked?');
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.REMOVE_OBJECT_FAILED,
payload: {
objectState,
},
});
}
};
}
export function editShape(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.EDIT_SHAPE,
payload: {
enabled,
},
};
}
export function copyShape(objectState: any): AnyAction {
return {
type: AnnotationActionTypes.COPY_SHAPE,
payload: {
objectState,
},
};
}
export function selectObjects(selectedStatesID: number[]): AnyAction {
return {
type: AnnotationActionTypes.SELECT_OBJECTS,
payload: {
selectedStatesID,
},
};
}
export function activateObject(activatedStateID: number | null): AnyAction {
return {
type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: {
activatedStateID,
},
};
} }
export function updateTabContentHeight(tabContentHeight: number): AnyAction { export function updateTabContentHeight(tabContentHeight: number): AnyAction {
@ -452,23 +662,32 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
export function changeLabelColor(label: any, color: string): AnyAction { export function changeLabelColorAsync(
try { sessionInstance: any,
const updatedLabel = label; frameNumber: number,
updatedLabel.color = color; label: any,
color: string,
return { ): ThunkAction<Promise<void>, {}, {}, AnyAction> {
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS, return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
payload: { try {
label: updatedLabel, const updatedLabel = label;
}, updatedLabel.color = color;
}; const states = await sessionInstance.annotations.get(frameNumber);
} catch (error) {
return { dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED, type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: { payload: {
error, label: updatedLabel,
}, states,
}; },
} });
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED,
payload: {
error,
},
});
}
};
} }

@ -1,6 +1,7 @@
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { import {
GridColor, GridColor,
ColorBy,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
export enum SettingsActionTypes { export enum SettingsActionTypes {
@ -9,6 +10,46 @@ export enum SettingsActionTypes {
CHANGE_GRID_SIZE = 'CHANGE_GRID_SIZE', CHANGE_GRID_SIZE = 'CHANGE_GRID_SIZE',
CHANGE_GRID_COLOR = 'CHANGE_GRID_COLOR', CHANGE_GRID_COLOR = 'CHANGE_GRID_COLOR',
CHANGE_GRID_OPACITY = 'CHANGE_GRID_OPACITY', CHANGE_GRID_OPACITY = 'CHANGE_GRID_OPACITY',
CHANGE_SHAPES_OPACITY = 'CHANGE_SHAPES_OPACITY',
CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY',
CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY',
CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS',
}
export function changeShapesOpacity(opacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_OPACITY,
payload: {
opacity,
},
};
}
export function changeSelectedShapesOpacity(selectedOpacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY,
payload: {
selectedOpacity,
},
};
}
export function changeShapesColorBy(colorBy: ColorBy): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_COLOR_BY,
payload: {
colorBy,
},
};
}
export function changeShapesBlackBorders(blackBorders: boolean): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS,
payload: {
blackBorders,
},
};
} }
export function switchRotateAll(rotateAll: boolean): AnyAction { export function switchRotateAll(rotateAll: boolean): AnyAction {

@ -17,5 +17,6 @@ $info-icon-color: #0074D9;
$objects-bar-tabs-color: #BEBEBE; $objects-bar-tabs-color: #BEBEBE;
$objects-bar-icons-color: #242424; // #6E6E6E $objects-bar-icons-color: #242424; // #6E6E6E
$active-object-item-background-color: #D8ECFF; $active-object-item-background-color: #D8ECFF;
$slider-color: #1890FF;
$monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; $monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;

@ -8,6 +8,7 @@ import {
} from 'antd'; } from 'antd';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace'; import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
interface Props { interface Props {
@ -46,6 +47,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
<Layout className='cvat-annotation-page'> <Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer /> <AnnotationTopBarContainer />
<StandardWorkspaceComponent /> <StandardWorkspaceComponent />
<StatisticsModalContainer />
</Layout> </Layout>
); );
} }

@ -5,6 +5,7 @@ import {
} from 'antd'; } from 'antd';
import { import {
ColorBy,
GridColor, GridColor,
ObjectType, ObjectType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
@ -23,9 +24,15 @@ interface Props {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
activatedStateID: number | null;
selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frame: number; frame: number;
opacity: number;
colorBy: ColorBy;
selectedOpacity: number;
blackBorders: boolean;
grid: boolean; grid: boolean;
gridSize: number; gridSize: number;
gridColor: GridColor; gridColor: GridColor;
@ -38,6 +45,7 @@ interface Props {
onMergeObjects: (enabled: boolean) => void; onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void; onShapeDrawn: () => void;
onResetCanvas: () => void; onResetCanvas: () => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
@ -45,6 +53,8 @@ interface Props {
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
} }
export default class CanvasWrapperComponent extends React.PureComponent<Props> { export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -65,12 +75,19 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
const { const {
opacity,
colorBy,
selectedOpacity,
blackBorders,
grid, grid,
gridSize, gridSize,
gridColor, gridColor,
gridOpacity, gridOpacity,
frameData,
annotations,
canvasInstance, canvasInstance,
sidebarCollapsed, sidebarCollapsed,
activatedStateID,
} = this.props; } = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) { if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -107,10 +124,32 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
} }
this.updateCanvas(); if (prevProps.activatedStateID !== null
&& prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null);
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity: opacity / 100 });
}
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas();
}
if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders
|| prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) {
this.updateShapesView();
}
this.activateOnCanvas();
} }
private async onShapeDrawn(event: any): Promise<void> { public componentWillUnmount(): void {
window.removeEventListener('resize', this.fitCanvas);
}
private onShapeDrawn(event: any): void {
const { const {
jobInstance, jobInstance,
activeLabelID, activeLabelID,
@ -120,7 +159,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onCreateAnnotations, onCreateAnnotations,
} = this.props; } = this.props;
onShapeDrawn(); if (!event.detail.continue) {
onShapeDrawn();
}
const { state } = event.detail; const { state } = event.detail;
if (!state.objectType) { if (!state.objectType) {
@ -132,7 +173,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
.filter((label: any) => label.id === activeLabelID); .filter((label: any) => label.id === activeLabelID);
} }
if (!state.occluded) { if (typeof (state.occluded) === 'undefined') {
state.occluded = false; state.occluded = false;
} }
@ -141,13 +182,16 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onCreateAnnotations(jobInstance, frame, [objectState]); onCreateAnnotations(jobInstance, frame, [objectState]);
} }
private async onShapeEdited(event: any): Promise<void> { private onShapeEdited(event: any): void {
const { const {
jobInstance, jobInstance,
frame, frame,
onEditShape,
onUpdateAnnotations, onUpdateAnnotations,
} = this.props; } = this.props;
onEditShape(false);
const { const {
state, state,
points, points,
@ -156,7 +200,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onUpdateAnnotations(jobInstance, frame, [state]); onUpdateAnnotations(jobInstance, frame, [state]);
} }
private async onObjectsMerged(event: any): Promise<void> { private onObjectsMerged(event: any): void {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -170,7 +214,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onMergeAnnotations(jobInstance, frame, states); onMergeAnnotations(jobInstance, frame, states);
} }
private async onObjectsGroupped(event: any): Promise<void> { private onObjectsGroupped(event: any): void {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -184,7 +228,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onGroupAnnotations(jobInstance, frame, states); onGroupAnnotations(jobInstance, frame, states);
} }
private async onTrackSplitted(event: any): Promise<void> { private onTrackSplitted(event: any): void {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -198,6 +242,61 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onSplitAnnotations(jobInstance, frame, state); onSplitAnnotations(jobInstance, frame, state);
} }
private fitCanvas = (): void => {
const { canvasInstance } = this.props;
canvasInstance.fitCanvas();
};
private activateOnCanvas(): void {
const {
activatedStateID,
canvasInstance,
selectedOpacity,
} = this.props;
if (activatedStateID !== null) {
canvasInstance.activate(activatedStateID);
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
(el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
}
}
}
private updateShapesView(): void {
const {
annotations,
opacity,
colorBy,
blackBorders,
} = this.props;
for (const state of annotations) {
let shapeColor = '';
if (colorBy === ColorBy.INSTANCE) {
shapeColor = state.color;
} else if (colorBy === ColorBy.GROUP) {
shapeColor = state.group.color;
} else if (colorBy === ColorBy.LABEL) {
shapeColor = state.label.color;
}
// TODO: In this approach CVAT-UI know details of implementations CVAT-CANVAS (svg.js)
const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`);
if (shapeView) {
if (['rect', 'polygon', 'polyline'].includes(shapeView.tagName)) {
(shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 });
(shapeView as any).instance.stroke({ color: blackBorders ? 'black' : shapeColor });
} else {
// group of points
for (const child of (shapeView as any).instance.children()) {
child.fill({ color: shapeColor });
}
}
}
}
}
private updateCanvas(): void { private updateCanvas(): void {
const { const {
annotations, annotations,
@ -222,10 +321,13 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onDragCanvas, onDragCanvas,
onZoomCanvas, onZoomCanvas,
onResetCanvas, onResetCanvas,
onActivateObject,
onEditShape,
} = this.props; } = this.props;
// Size // Size
canvasInstance.fitCanvas(); window.addEventListener('resize', this.fitCanvas);
this.fitCanvas();
// Grid // Grid
const gridElement = window.document.getElementById('cvat_canvas_grid'); const gridElement = window.document.getElementById('cvat_canvas_grid');
@ -240,8 +342,21 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.grid(gridSize, gridSize); canvasInstance.grid(gridSize, gridSize);
// Events // Events
canvasInstance.html().addEventListener('click', (e: MouseEvent): void => {
if ((e.target as HTMLElement).tagName === 'svg') {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('canvas.editstart', (): void => {
onActivateObject(null);
onEditShape(true);
});
canvasInstance.html().addEventListener('canvas.setup', (): void => { canvasInstance.html().addEventListener('canvas.setup', (): void => {
onSetupCanvas(); onSetupCanvas();
this.updateShapesView();
this.activateOnCanvas();
}); });
canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.html().addEventListener('canvas.setup', () => {
@ -268,7 +383,29 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onZoomCanvas(false); onZoomCanvas(false);
}); });
canvasInstance.html().addEventListener('canvas.clicked', (e: any) => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
.getElementById(`cvat-objects-sidebar-state-item-${clientID}`);
if (sidebarItem) {
sidebarItem.scrollIntoView();
}
});
canvasInstance.html().addEventListener('canvas.deactivated', (e: any): void => {
const { activatedStateID } = this.props;
const { state } = e.detail;
// when we activate element, canvas deactivates the previous
// and triggers this event
// in this case we do not need to update our state
if (state.clientID === activatedStateID) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise<void> => { canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise<void> => {
const { activatedStateID } = this.props;
const result = await jobInstance.annotations.select( const result = await jobInstance.annotations.select(
event.detail.states, event.detail.states,
event.detail.x, event.detail.x,
@ -282,7 +419,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
} }
canvasInstance.activate(result.state.clientID); if (activatedStateID !== result.state.clientID) {
onActivateObject(result.state.clientID);
}
} }
}); });

@ -87,7 +87,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
/> />
<Tooltip overlay='Setup a tag' placement='right'> <Tooltip overlay='Setup a tag' placement='right'>
<Icon component={TagIcon} /> <Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} />
</Tooltip> </Tooltip>
<hr /> <hr />

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl; activeControl: ActiveControl;
} }
const CursorControl = React.memo((props: Props): JSX.Element => { function CursorControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
activeControl, activeControl,
@ -43,6 +43,6 @@ const CursorControl = React.memo((props: Props): JSX.Element => {
/> />
</Tooltip> </Tooltip>
); );
}); }
export default CursorControl; export default React.memo(CursorControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean; isDrawing: boolean;
} }
const DrawPointsControl = React.memo((props: Props): JSX.Element => { function DrawPointsControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
isDrawing, isDrawing,
@ -49,6 +49,6 @@ const DrawPointsControl = React.memo((props: Props): JSX.Element => {
/> />
</Popover> </Popover>
); );
}); }
export default DrawPointsControl; export default React.memo(DrawPointsControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean; isDrawing: boolean;
} }
const DrawPolygonControl = React.memo((props: Props): JSX.Element => { function DrawPolygonControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
isDrawing, isDrawing,
@ -49,6 +49,6 @@ const DrawPolygonControl = React.memo((props: Props): JSX.Element => {
/> />
</Popover> </Popover>
); );
}); }
export default DrawPolygonControl; export default React.memo(DrawPolygonControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean; isDrawing: boolean;
} }
const DrawPolylineControl = React.memo((props: Props): JSX.Element => { function DrawPolylineControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
isDrawing, isDrawing,
@ -49,6 +49,6 @@ const DrawPolylineControl = React.memo((props: Props): JSX.Element => {
/> />
</Popover> </Popover>
); );
}); }
export default DrawPolylineControl; export default React.memo(DrawPolylineControl);

@ -15,7 +15,7 @@ interface Props {
isDrawing: boolean; isDrawing: boolean;
} }
const DrawRectangleControl = React.memo((props: Props): JSX.Element => { function DrawRectangleControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
isDrawing, isDrawing,
@ -49,6 +49,6 @@ const DrawRectangleControl = React.memo((props: Props): JSX.Element => {
/> />
</Popover> </Popover>
); );
}); }
export default DrawRectangleControl; export default React.memo(DrawRectangleControl);

@ -26,7 +26,7 @@ interface Props {
onDrawShape(): void; onDrawShape(): void;
} }
const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => { function DrawShapePopoverComponent(props: Props): JSX.Element {
const { const {
labels, labels,
shapeType, shapeType,
@ -106,6 +106,6 @@ const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => {
</Row> </Row>
</div> </div>
); );
}); }
export default DrawShapePopoverComponent; export default React.memo(DrawShapePopoverComponent);

@ -17,7 +17,7 @@ interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
} }
const FitControl = React.memo((props: Props): JSX.Element => { function FitControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
} = props; } = props;
@ -27,6 +27,6 @@ const FitControl = React.memo((props: Props): JSX.Element => {
<Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} /> <Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} />
</Tooltip> </Tooltip>
); );
}); }
export default FitControl; export default React.memo(FitControl);

@ -19,7 +19,7 @@ interface Props {
groupObjects(enabled: boolean): void; groupObjects(enabled: boolean): void;
} }
const GroupControl = React.memo((props: Props): JSX.Element => { function GroupControl(props: Props): JSX.Element {
const { const {
activeControl, activeControl,
canvasInstance, canvasInstance,
@ -46,6 +46,6 @@ const GroupControl = React.memo((props: Props): JSX.Element => {
<Icon {...dynamicIconProps} component={GroupIcon} /> <Icon {...dynamicIconProps} component={GroupIcon} />
</Tooltip> </Tooltip>
); );
}); }
export default GroupControl; export default React.memo(GroupControl);

@ -19,7 +19,7 @@ interface Props {
mergeObjects(enabled: boolean): void; mergeObjects(enabled: boolean): void;
} }
const MergeControl = React.memo((props: Props): JSX.Element => { function MergeControl(props: Props): JSX.Element {
const { const {
activeControl, activeControl,
canvasInstance, canvasInstance,
@ -46,6 +46,6 @@ const MergeControl = React.memo((props: Props): JSX.Element => {
<Icon {...dynamicIconProps} component={MergeIcon} /> <Icon {...dynamicIconProps} component={MergeIcon} />
</Tooltip> </Tooltip>
); );
}); }
export default MergeControl; export default React.memo(MergeControl);

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl; activeControl: ActiveControl;
} }
const MoveControl = React.memo((props: Props): JSX.Element => { function MoveControl(props: Props): JSX.Element {
const { const {
canvasInstance, canvasInstance,
activeControl, activeControl,
@ -46,6 +46,6 @@ const MoveControl = React.memo((props: Props): JSX.Element => {
/> />
</Tooltip> </Tooltip>
); );
}); }
export default MoveControl; export default React.memo(MoveControl);

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl; activeControl: ActiveControl;
} }
const ResizeControl = React.memo((props: Props): JSX.Element => { function ResizeControl(props: Props): JSX.Element {
const { const {
activeControl, activeControl,
canvasInstance, canvasInstance,
@ -46,6 +46,6 @@ const ResizeControl = React.memo((props: Props): JSX.Element => {
/> />
</Tooltip> </Tooltip>
); );
}); }
export default ResizeControl; export default React.memo(ResizeControl);

@ -20,7 +20,7 @@ interface Props {
rotateAll: boolean; rotateAll: boolean;
} }
const RotateControl = React.memo((props: Props): JSX.Element => { function RotateControl(props: Props): JSX.Element {
const { const {
rotateAll, rotateAll,
canvasInstance, canvasInstance,
@ -55,6 +55,6 @@ const RotateControl = React.memo((props: Props): JSX.Element => {
<Icon component={RotateIcon} /> <Icon component={RotateIcon} />
</Popover> </Popover>
); );
}); }
export default RotateControl; export default React.memo(RotateControl);

@ -19,7 +19,7 @@ interface Props {
splitTrack(enabled: boolean): void; splitTrack(enabled: boolean): void;
} }
const SplitControl = React.memo((props: Props): JSX.Element => { function SplitControl(props: Props): JSX.Element {
const { const {
activeControl, activeControl,
canvasInstance, canvasInstance,
@ -46,6 +46,6 @@ const SplitControl = React.memo((props: Props): JSX.Element => {
<Icon {...dynamicIconProps} component={SplitIcon} /> <Icon {...dynamicIconProps} component={SplitIcon} />
</Tooltip> </Tooltip>
); );
}); }
export default SplitControl; export default React.memo(SplitControl);

@ -0,0 +1,90 @@
import React from 'react';
import {
Checkbox,
Collapse,
Slider,
Radio,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { RadioChangeEvent } from 'antd/lib/radio';
import { SliderValue } from 'antd/lib/slider';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { ColorBy } from 'reducers/interfaces';
interface Props {
appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
collapseAppearance(): void;
changeShapesColorBy(event: RadioChangeEvent): void;
changeShapesOpacity(event: SliderValue): void;
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
}
function AppearanceBlock(props: Props): JSX.Element {
const {
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
collapseAppearance,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
} = props;
return (
<Collapse
onChange={collapseAppearance}
activeKey={appearanceCollapsed ? [] : ['appearance']}
className='cvat-objects-appearance-collapse'
>
<Collapse.Panel
header={
<Text strong>Appearance</Text>
}
key='appearance'
>
<div className='cvat-objects-appearance-content'>
<Text type='secondary'>Color by</Text>
<Radio.Group value={colorBy} onChange={changeShapesColorBy}>
<Radio.Button value={ColorBy.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button>
<Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button>
<Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button>
</Radio.Group>
<Text type='secondary'>Opacity</Text>
<Slider
onChange={changeShapesOpacity}
value={opacity}
min={0}
max={100}
/>
<Text type='secondary'>Selected opacity</Text>
<Slider
onChange={changeSelectedShapesOpacity}
value={selectedOpacity}
min={0}
max={100}
/>
<Checkbox
onChange={changeShapesBlackBorders}
checked={blackBorders}
>
Black borders
</Checkbox>
</div>
</Collapse.Panel>
</Collapse>
);
}
export default React.memo(AppearanceBlock);

@ -71,7 +71,7 @@ interface Props {
changeColor(color: string): void; changeColor(color: string): void;
} }
const LabelItemComponent = React.memo((props: Props): JSX.Element => { function LabelItemComponent(props: Props): JSX.Element {
const { const {
labelName, labelName,
labelColor, labelColor,
@ -125,6 +125,6 @@ const LabelItemComponent = React.memo((props: Props): JSX.Element => {
</Col> </Col>
</Row> </Row>
); );
}); }
export default LabelItemComponent; export default React.memo(LabelItemComponent);

@ -10,11 +10,15 @@ import {
Collapse, Collapse,
Checkbox, Checkbox,
InputNumber, InputNumber,
Dropdown,
Menu,
Button,
Modal,
} from 'antd'; } from 'antd';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { RadioChangeEvent } from 'antd/lib/radio'; import { RadioChangeEvent } from 'antd/lib/radio';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { import {
ObjectOutsideIcon, ObjectOutsideIcon,
@ -28,21 +32,72 @@ import {
ObjectType, ShapeType, ObjectType, ShapeType,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
interface ItemTopProps { function ItemMenu(
locked: boolean,
copy: (() => void),
remove: (() => void),
propagate: (() => void),
): JSX.Element {
return (
<Menu key='unique' className='cvat-object-item-menu'>
<Menu.Item>
<Button type='link' icon='copy' onClick={copy}>
Make a copy
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' icon='block' onClick={propagate}>
Propagate
</Button>
</Menu.Item>
<Menu.Item>
<Button
type='link'
icon='delete'
onClick={(): void => {
if (locked) {
Modal.confirm({
title: 'Object is locked',
content: 'Are you sure you want to remove it?',
onOk() {
remove();
},
});
} else {
remove();
}
}}
>
Remove
</Button>
</Menu.Item>
</Menu>
);
}
interface ItemTopComponentProps {
clientID: number; clientID: number;
labelID: number; labelID: number;
labels: any[]; labels: any[];
type: string; type: string;
locked: boolean;
changeLabel(labelID: string): void; changeLabel(labelID: string): void;
copy(): void;
remove(): void;
propagate(): void;
} }
const ItemTop = React.memo((props: ItemTopProps): JSX.Element => { function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
const { const {
clientID, clientID,
labelID, labelID,
labels, labels,
type, type,
locked,
changeLabel, changeLabel,
copy,
remove,
propagate,
} = props; } = props;
return ( return (
@ -62,13 +117,20 @@ const ItemTop = React.memo((props: ItemTopProps): JSX.Element => {
</Select> </Select>
</Col> </Col>
<Col span={2}> <Col span={2}>
<Icon type='more' /> <Dropdown
placement='bottomLeft'
overlay={ItemMenu(locked, copy, remove, propagate)}
>
<Icon type='more' />
</Dropdown>
</Col> </Col>
</Row> </Row>
); );
}); }
const ItemTop = React.memo(ItemTopComponent);
interface ItemButtonsProps { interface ItemButtonsComponentProps {
objectType: ObjectType; objectType: ObjectType;
occluded: boolean; occluded: boolean;
outside: boolean | undefined; outside: boolean | undefined;
@ -76,6 +138,11 @@ interface ItemButtonsProps {
hidden: boolean; hidden: boolean;
keyframe: boolean | undefined; keyframe: boolean | undefined;
navigateFirstKeyframe: null | (() => void);
navigatePrevKeyframe: null | (() => void);
navigateNextKeyframe: null | (() => void);
navigateLastKeyframe: null | (() => void);
setOccluded(): void; setOccluded(): void;
unsetOccluded(): void; unsetOccluded(): void;
setOutside(): void; setOutside(): void;
@ -88,7 +155,7 @@ interface ItemButtonsProps {
show(): void; show(): void;
} }
const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => { function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element {
const { const {
objectType, objectType,
occluded, occluded,
@ -96,6 +163,12 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
locked, locked,
hidden, hidden,
keyframe, keyframe,
navigateFirstKeyframe,
navigatePrevKeyframe,
navigateNextKeyframe,
navigateLastKeyframe,
setOccluded, setOccluded,
unsetOccluded, unsetOccluded,
setOutside, setOutside,
@ -114,16 +187,28 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
<Col span={20} style={{ textAlign: 'center' }}> <Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'> <Row type='flex' justify='space-around'>
<Col span={6}> <Col span={6}>
<Icon component={FirstIcon} /> { navigateFirstKeyframe
? <Icon component={FirstIcon} onClick={navigateFirstKeyframe} />
: <Icon component={FirstIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col> </Col>
<Col span={6}> <Col span={6}>
<Icon component={PreviousIcon} /> { navigatePrevKeyframe
? <Icon component={PreviousIcon} onClick={navigatePrevKeyframe} />
: <Icon component={PreviousIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col> </Col>
<Col span={6}> <Col span={6}>
<Icon component={NextIcon} /> { navigateNextKeyframe
? <Icon component={NextIcon} onClick={navigateNextKeyframe} />
: <Icon component={NextIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col> </Col>
<Col span={6}> <Col span={6}>
<Icon component={LastIcon} /> { navigateLastKeyframe
? <Icon component={LastIcon} onClick={navigateLastKeyframe} />
: <Icon component={LastIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />
}
</Col> </Col>
</Row> </Row>
<Row type='flex' justify='space-around'> <Row type='flex' justify='space-around'>
@ -189,9 +274,11 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
</Col> </Col>
</Row> </Row>
); );
}); }
interface ItemAttributeProps { const ItemButtons = React.memo(ItemButtonsComponent);
interface ItemAttributeComponentProps {
attrInputType: string; attrInputType: string;
attrValues: string[]; attrValues: string[];
attrValue: string; attrValue: string;
@ -200,7 +287,10 @@ interface ItemAttributeProps {
changeAttribute(attrID: number, value: string): void; changeAttribute(attrID: number, value: string): void;
} }
function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributeProps): boolean { function attrIsTheSame(
prevProps: ItemAttributeComponentProps,
nextProps: ItemAttributeComponentProps,
): boolean {
return nextProps.attrID === prevProps.attrID return nextProps.attrID === prevProps.attrID
&& nextProps.attrValue === prevProps.attrValue && nextProps.attrValue === prevProps.attrValue
&& nextProps.attrName === prevProps.attrName && nextProps.attrName === prevProps.attrName
@ -210,7 +300,7 @@ function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributePr
.every((value: boolean): boolean => value); .every((value: boolean): boolean => value);
} }
const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => { function ItemAttributeComponent(props: ItemAttributeComponentProps): JSX.Element {
const { const {
attrInputType, attrInputType,
attrValues, attrValues,
@ -332,10 +422,12 @@ const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => {
</Col> </Col>
</> </>
); );
}, attrIsTheSame); }
const ItemAttribute = React.memo(ItemAttributeComponent, attrIsTheSame);
interface ItemAttributesProps {
interface ItemAttributesComponentProps {
collapsed: boolean; collapsed: boolean;
attributes: any[]; attributes: any[];
values: Record<number, string>; values: Record<number, string>;
@ -352,13 +444,16 @@ function attrValuesAreEqual(next: Record<number, string>, prev: Record<number, s
.every((value: boolean) => value); .every((value: boolean) => value);
} }
function attrAreTheSame(prevProps: ItemAttributesProps, nextProps: ItemAttributesProps): boolean { function attrAreTheSame(
prevProps: ItemAttributesComponentProps,
nextProps: ItemAttributesComponentProps,
): boolean {
return nextProps.collapsed === prevProps.collapsed return nextProps.collapsed === prevProps.collapsed
&& nextProps.attributes === prevProps.attributes && nextProps.attributes === prevProps.attributes
&& attrValuesAreEqual(nextProps.values, prevProps.values); && attrValuesAreEqual(nextProps.values, prevProps.values);
} }
const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => { function ItemAttributesComponent(props: ItemAttributesComponentProps): JSX.Element {
const { const {
collapsed, collapsed,
attributes, attributes,
@ -403,9 +498,12 @@ const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => {
</Collapse> </Collapse>
</Row> </Row>
); );
}, attrAreTheSame); }
const ItemAttributes = React.memo(ItemAttributesComponent, attrAreTheSame);
interface Props { interface Props {
activated: boolean;
objectType: ObjectType; objectType: ObjectType;
shapeType: ShapeType; shapeType: ShapeType;
clientID: number; clientID: number;
@ -421,7 +519,15 @@ interface Props {
labels: any[]; labels: any[];
attributes: any[]; attributes: any[];
collapsed: boolean; collapsed: boolean;
navigateFirstKeyframe: null | (() => void);
navigatePrevKeyframe: null | (() => void);
navigateNextKeyframe: null | (() => void);
navigateLastKeyframe: null | (() => void);
activate(): void;
copy(): void;
propagate(): void;
remove(): void;
setOccluded(): void; setOccluded(): void;
unsetOccluded(): void; unsetOccluded(): void;
setOutside(): void; setOutside(): void;
@ -438,12 +544,13 @@ interface Props {
} }
function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
return nextProps.locked === prevProps.locked return nextProps.activated === prevProps.activated
&& nextProps.locked === prevProps.locked
&& nextProps.occluded === prevProps.occluded && nextProps.occluded === prevProps.occluded
&& nextProps.outside === prevProps.outside && nextProps.outside === prevProps.outside
&& nextProps.hidden === prevProps.hidden && nextProps.hidden === prevProps.hidden
&& nextProps.keyframe === prevProps.keyframe && nextProps.keyframe === prevProps.keyframe
&& nextProps.label === prevProps.label && nextProps.labelID === prevProps.labelID
&& nextProps.color === prevProps.color && nextProps.color === prevProps.color
&& nextProps.clientID === prevProps.clientID && nextProps.clientID === prevProps.clientID
&& nextProps.objectType === prevProps.objectType && nextProps.objectType === prevProps.objectType
@ -451,11 +558,16 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
&& nextProps.collapsed === prevProps.collapsed && nextProps.collapsed === prevProps.collapsed
&& nextProps.labels === prevProps.labels && nextProps.labels === prevProps.labels
&& nextProps.attributes === prevProps.attributes && nextProps.attributes === prevProps.attributes
&& nextProps.navigateFirstKeyframe === prevProps.navigateFirstKeyframe
&& nextProps.navigatePrevKeyframe === prevProps.navigatePrevKeyframe
&& nextProps.navigateNextKeyframe === prevProps.navigateNextKeyframe
&& nextProps.navigateLastKeyframe === prevProps.navigateLastKeyframe
&& attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues); && attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues);
} }
const ObjectItem = React.memo((props: Props): JSX.Element => { function ObjectItemComponent(props: Props): JSX.Element {
const { const {
activated,
objectType, objectType,
shapeType, shapeType,
clientID, clientID,
@ -471,7 +583,15 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
attributes, attributes,
labels, labels,
collapsed, collapsed,
navigateFirstKeyframe,
navigatePrevKeyframe,
navigateNextKeyframe,
navigateLastKeyframe,
activate,
copy,
propagate,
remove,
setOccluded, setOccluded,
unsetOccluded, unsetOccluded,
setOutside, setOutside,
@ -490,9 +610,14 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase() const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase()
: `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`; : `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`;
const className = !activated ? 'cvat-objects-sidebar-state-item'
: 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item';
return ( return (
<div <div
className='cvat-objects-sidebar-state-item' onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
className={className}
style={{ borderLeftStyle: 'solid', borderColor: ` ${color}` }} style={{ borderLeftStyle: 'solid', borderColor: ` ${color}` }}
> >
<ItemTop <ItemTop
@ -500,7 +625,11 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
labelID={labelID} labelID={labelID}
labels={labels} labels={labels}
type={type} type={type}
locked={locked}
changeLabel={changeLabel} changeLabel={changeLabel}
copy={copy}
remove={remove}
propagate={propagate}
/> />
<ItemButtons <ItemButtons
objectType={objectType} objectType={objectType}
@ -509,6 +638,10 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
locked={locked} locked={locked}
hidden={hidden} hidden={hidden}
keyframe={keyframe} keyframe={keyframe}
navigateFirstKeyframe={navigateFirstKeyframe}
navigatePrevKeyframe={navigatePrevKeyframe}
navigateNextKeyframe={navigateNextKeyframe}
navigateLastKeyframe={navigateLastKeyframe}
setOccluded={setOccluded} setOccluded={setOccluded}
unsetOccluded={unsetOccluded} unsetOccluded={unsetOccluded}
setOutside={setOutside} setOutside={setOutside}
@ -533,6 +666,6 @@ const ObjectItem = React.memo((props: Props): JSX.Element => {
} }
</div> </div>
); );
}, objectItemsAreEqual); }
export default ObjectItem; export default React.memo(ObjectItemComponent, objectItemsAreEqual);

@ -13,12 +13,12 @@ import Text from 'antd/lib/typography/Text';
import { StatesOrdering } from 'reducers/interfaces'; import { StatesOrdering } from 'reducers/interfaces';
interface StatesOrderingSelectorProps { interface StatesOrderingSelectorComponentProps {
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
} }
const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps): JSX.Element => { function StatesOrderingSelectorComponent(props: StatesOrderingSelectorComponentProps): JSX.Element {
const { const {
statesOrdering, statesOrdering,
changeStatesOrdering, changeStatesOrdering,
@ -49,7 +49,9 @@ const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps):
</Select> </Select>
</Col> </Col>
); );
}); }
const StatesOrderingSelector = React.memo(StatesOrderingSelectorComponent);
interface Props { interface Props {
statesHidden: boolean; statesHidden: boolean;
@ -65,7 +67,7 @@ interface Props {
showAllStates(): void; showAllStates(): void;
} }
const Header = React.memo((props: Props): JSX.Element => { function ObjectListHeader(props: Props): JSX.Element {
const { const {
statesHidden, statesHidden,
statesLocked, statesLocked,
@ -116,6 +118,6 @@ const Header = React.memo((props: Props): JSX.Element => {
</Row> </Row>
</div> </div>
); );
}); }
export default Header; export default React.memo(ObjectListHeader);

@ -3,7 +3,7 @@ import React from 'react';
import { StatesOrdering } from 'reducers/interfaces'; import { StatesOrdering } from 'reducers/interfaces';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import Header from './objects-list-header'; import ObjectListHeader from './objects-list-header';
interface Props { interface Props {
@ -22,7 +22,7 @@ interface Props {
showAllStates(): void; showAllStates(): void;
} }
const ObjectListComponent = React.memo((props: Props): JSX.Element => { function ObjectListComponent(props: Props): JSX.Element {
const { const {
listHeight, listHeight,
statesHidden, statesHidden,
@ -41,7 +41,7 @@ const ObjectListComponent = React.memo((props: Props): JSX.Element => {
return ( return (
<div style={{ height: listHeight }}> <div style={{ height: listHeight }}>
<Header <ObjectListHeader
statesHidden={statesHidden} statesHidden={statesHidden}
statesLocked={statesLocked} statesLocked={statesLocked}
statesCollapsed={statesCollapsed} statesCollapsed={statesCollapsed}
@ -61,6 +61,6 @@ const ObjectListComponent = React.memo((props: Props): JSX.Element => {
</div> </div>
</div> </div>
); );
}); }
export default ObjectListComponent; export default React.memo(ObjectListComponent);

@ -5,29 +5,66 @@ import {
Icon, Icon,
Tabs, Tabs,
Layout, Layout,
Collapse,
} from 'antd'; } from 'antd';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { RadioChangeEvent } from 'antd/lib/radio';
import { SliderValue } from 'antd/lib/slider';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { ColorBy } from 'reducers/interfaces';
import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import LabelsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/labels-list'; import LabelsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/labels-list';
import AppearanceBlock from './appearance-block';
interface Props { interface Props {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
collapseSidebar(): void; collapseSidebar(): void;
collapseAppearance(): void; collapseAppearance(): void;
changeShapesColorBy(event: RadioChangeEvent): void;
changeShapesOpacity(event: SliderValue): void;
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
} }
const ObjectsSideBar = React.memo((props: Props): JSX.Element => { function ObjectsSideBar(props: Props): JSX.Element {
const { const {
sidebarCollapsed, sidebarCollapsed,
appearanceCollapsed, appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
collapseSidebar, collapseSidebar,
collapseAppearance, collapseAppearance,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
} = props; } = props;
const appearanceProps = {
collapseAppearance,
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
};
return ( return (
<Layout.Sider <Layout.Sider
className='cvat-objects-sidebar' className='cvat-objects-sidebar'
@ -65,22 +102,9 @@ const ObjectsSideBar = React.memo((props: Props): JSX.Element => {
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>
<Collapse { !sidebarCollapsed && <AppearanceBlock {...appearanceProps} /> }
onChange={collapseAppearance}
activeKey={appearanceCollapsed ? [] : ['appearance']}
className='cvat-objects-appearance-collapse'
>
<Collapse.Panel
header={
<Text strong>Appearance</Text>
}
key='appearance'
>
</Collapse.Panel>
</Collapse>
</Layout.Sider> </Layout.Sider>
); );
}); }
export default ObjectsSideBar; export default React.memo(ObjectsSideBar);

@ -18,7 +18,7 @@
> .ant-collapse-content { > .ant-collapse-content {
background: $background-color-2; background: $background-color-2;
border-bottom: none; border-bottom: none;
height: 150px; height: 230px;
} }
} }
} }
@ -32,10 +32,6 @@
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);
} }
&:active {
transform: scale(1);
}
} }
.cvat-objects-sidebar-tabs.ant-tabs.ant-tabs-card { .cvat-objects-sidebar-tabs.ant-tabs.ant-tabs-card {
@ -144,10 +140,6 @@
> div:nth-child(3) { > div:nth-child(3) {
margin-top: 10px; margin-top: 10px;
} }
&:hover {
@extend .cvat-objects-sidebar-state-active-item;
}
} }
.cvat-objects-sidebar-state-item-collapse { .cvat-objects-sidebar-state-item-collapse {
@ -246,3 +238,28 @@
height: 20px; height: 20px;
border-radius: 5px; border-radius: 5px;
} }
.cvat-objects-appearance-content {
> div {
width: 100%;
> label {
text-align: center;
width: 33%;
}
}
}
.cvat-object-item-menu {
> li {
padding: 0px;
> button {
padding: 5px 32px;
color: $text-color;
width: 100%;
height: 100%;
text-align: left;
}
}
}

@ -0,0 +1,55 @@
import React from 'react';
import {
Modal,
InputNumber,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
visible: boolean;
propagateFrames: number;
propagateUpToFrame: number;
propagateObject(): void;
cancel(): void;
changePropagateFrames(value: number | undefined): void;
changeUpToFrame(value: number | undefined): void;
}
export default function PropagateConfirmComponent(props: Props): JSX.Element {
const {
visible,
propagateFrames,
propagateUpToFrame,
propagateObject,
changePropagateFrames,
changeUpToFrame,
cancel,
} = props;
return (
<Modal
okType='primary'
okText='Yes'
cancelText='Cancel'
onOk={propagateObject}
onCancel={cancel}
title='Confirm propagation'
visible={visible}
>
<div className='cvat-propagate-confirm'>
<Text>Do you want to make a copy of the object on</Text>
<InputNumber size='small' min={1} value={propagateFrames} onChange={changePropagateFrames} />
{
propagateFrames > 1
? <Text> frames </Text>
: <Text> frame </Text>
}
<Text>up to the </Text>
<InputNumber size='small' value={propagateUpToFrame} onChange={changeUpToFrame} />
<Text>frame</Text>
</div>
</Modal>
);
}

@ -8,7 +8,7 @@ import {
import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper'; import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm';
export default function StandardWorkspaceComponent(): JSX.Element { export default function StandardWorkspaceComponent(): JSX.Element {
return ( return (
@ -16,6 +16,7 @@ export default function StandardWorkspaceComponent(): JSX.Element {
<ControlsSideBarContainer /> <ControlsSideBarContainer />
<CanvasWrapperContainer /> <CanvasWrapperContainer />
<ObjectSideBarContainer /> <ObjectSideBarContainer />
<PropagateConfirmContainer />
</Layout> </Layout>
); );
} }

@ -104,3 +104,10 @@
} }
} }
} }
.cvat-propagate-confirm {
> .ant-input-number {
width: 70px;
margin: 0px 5px;
}
}

@ -12,9 +12,7 @@
} }
.cvat-annotation-header-left-group { .cvat-annotation-header-left-group {
height: 100%; > button:first-child {
> div:first-child {
filter: invert(0.9); filter: invert(0.9);
background: $background-color-1; background: $background-color-1;
border-radius: 0px; border-radius: 0px;
@ -22,15 +20,21 @@
} }
} }
.cvat-annotation-header-button { .ant-btn.cvat-annotation-header-button {
padding: 0px; padding: 0px;
width: 54px; width: 54px;
height: 54px; height: 54px;
float: left; float: left;
text-align: center; text-align: center;
user-select: none; user-select: none;
color: $text-color;
display: flex;
flex-direction: column;
align-items: center;
margin: 0px 3px;
> span { > span {
margin-left: 0px;
font-size: 10px; font-size: 10px;
} }
@ -40,7 +44,7 @@
} }
&:hover > i { &:hover > i {
transform: scale(0.9); transform: scale(0.85);
} }
&:active > i { &:active > i {
@ -119,38 +123,62 @@
} }
.cvat-annotation-header-right-group { .cvat-annotation-header-right-group {
height: 100%;
> div { > div {
height: 54px;
float: left; float: left;
text-align: center; display: block;
margin-right: 20px; height: 54px;
margin-right: 15px;
}
}
> span { .cvat-workspace-selector {
font-size: 10px; width: 150px;
} }
.cvat-job-info-modal-window {
> div {
margin-top: 10px;
}
> i { > div:nth-child(1) {
transform: scale(0.8); > div {
padding: 3px; > .ant-select, i {
margin-left: 10px;
}
} }
}
&:hover > i { > div:nth-child(2) {
transform: scale(0.9); > div {
> span {
font-size: 20px;
}
} }
}
&:active > i { > div:nth-child(3) {
transform: scale(0.8); > div {
display: grid;
} }
} }
> div:not(:nth-child(3)) > * { > .cvat-job-info-bug-tracker {
display: block; > div {
line-height: 0px; display: grid;
}
} }
}
.cvat-workspace-selector { > .cvat-job-info-statistics {
width: 150px; > div {
> span {
font-size: 20px;
}
.ant-table-thead {
> tr > th {
padding: 5px 5px;
}
}
}
}
} }

@ -4,6 +4,7 @@ import {
Col, Col,
Icon, Icon,
Modal, Modal,
Button,
Timeline, Timeline,
} from 'antd'; } from 'antd';
@ -20,7 +21,7 @@ interface Props {
onSaveAnnotation(): void; onSaveAnnotation(): void;
} }
const LeftGroup = React.memo((props: Props): JSX.Element => { function LeftGroup(props: Props): JSX.Element {
const { const {
saving, saving,
savingStatuses, savingStatuses,
@ -29,20 +30,20 @@ const LeftGroup = React.memo((props: Props): JSX.Element => {
return ( return (
<Col className='cvat-annotation-header-left-group'> <Col className='cvat-annotation-header-left-group'>
<div className='cvat-annotation-header-button'> <Button type='link' className='cvat-annotation-header-button'>
<Icon component={MainMenuIcon} /> <Icon component={MainMenuIcon} />
<span>Menu</span> Menu
</div> </Button>
<div <Button
onClick={saving ? undefined : onSaveAnnotation}
type='link'
className={saving className={saving
? 'cvat-annotation-disabled-header-button' ? 'cvat-annotation-disabled-header-button'
: 'cvat-annotation-header-button' : 'cvat-annotation-header-button'
} }
> >
<Icon component={SaveIcon} onClick={onSaveAnnotation} /> <Icon component={SaveIcon} />
<span> { saving ? 'Saving...' : 'Save' }
{ saving ? 'Saving...' : 'Save' }
</span>
<Modal <Modal
title='Saving changes on the server' title='Saving changes on the server'
visible={saving} visible={saving}
@ -60,17 +61,17 @@ const LeftGroup = React.memo((props: Props): JSX.Element => {
} }
</Timeline> </Timeline>
</Modal> </Modal>
</div> </Button>
<div className='cvat-annotation-header-button'> <Button disabled type='link' className='cvat-annotation-header-button'>
<Icon component={UndoIcon} /> <Icon component={UndoIcon} />
<span>Undo</span> Undo
</div> </Button>
<div className='cvat-annotation-header-button'> <Button disabled type='link' className='cvat-annotation-header-button'>
<Icon component={RedoIcon} /> <Icon component={RedoIcon} />
<span>Redo</span> Redo
</div> </Button>
</Col> </Col>
); );
}); }
export default LeftGroup; export default React.memo(LeftGroup);

@ -28,7 +28,7 @@ interface Props {
onLastFrame(): void; onLastFrame(): void;
} }
const PlayerButtons = React.memo((props: Props): JSX.Element => { function PlayerButtons(props: Props): JSX.Element {
const { const {
playing, playing,
onSwitchPlay, onSwitchPlay,
@ -82,6 +82,6 @@ const PlayerButtons = React.memo((props: Props): JSX.Element => {
</Tooltip> </Tooltip>
</Col> </Col>
); );
}); }
export default PlayerButtons; export default React.memo(PlayerButtons);

@ -19,7 +19,7 @@ interface Props {
onInputChange(value: number | undefined): void; onInputChange(value: number | undefined): void;
} }
const PlayerNavigation = React.memo((props: Props): JSX.Element => { function PlayerNavigation(props: Props): JSX.Element {
const { const {
startFrame, startFrame,
stopFrame, stopFrame,
@ -61,6 +61,6 @@ const PlayerNavigation = React.memo((props: Props): JSX.Element => {
</Col> </Col>
</> </>
); );
}); }
export default PlayerNavigation; export default React.memo(PlayerNavigation);

@ -4,6 +4,7 @@ import {
Col, Col,
Icon, Icon,
Select, Select,
Button,
} from 'antd'; } from 'antd';
import { import {
@ -11,23 +12,43 @@ import {
FullscreenIcon, FullscreenIcon,
} from '../../../icons'; } from '../../../icons';
const RightGroup = React.memo((): JSX.Element => ( interface Props {
<Col className='cvat-annotation-header-right-group'> showStatistics(): void;
<div className='cvat-annotation-header-button'> }
<Icon component={FullscreenIcon} />
<span>Fullscreen</span>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={InfoIcon} />
<span>Info</span>
</div>
<div>
<Select className='cvat-workspace-selector' defaultValue='standard'>
<Select.Option key='standard' value='standard'>Standard</Select.Option>
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option>
</Select>
</div>
</Col>
));
export default RightGroup; function RightGroup(props: Props): JSX.Element {
const { showStatistics } = props;
return (
<Col className='cvat-annotation-header-right-group'>
<Button
type='link'
className='cvat-annotation-header-button'
onClick={(): void => {
if (window.document.fullscreenEnabled) {
if (window.document.fullscreenElement) {
window.document.exitFullscreen();
} else {
window.document.documentElement.requestFullscreen();
}
}
}}
>
<Icon component={FullscreenIcon} />
Fullscreen
</Button>
<Button type='link' className='cvat-annotation-header-button' onClick={showStatistics}>
<Icon component={InfoIcon} />
Info
</Button>
<div>
<Select disabled className='cvat-workspace-selector' defaultValue='standard'>
<Select.Option key='standard' value='standard'>Standard</Select.Option>
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option>
</Select>
</div>
</Col>
);
}
export default React.memo(RightGroup);

@ -0,0 +1,203 @@
import React from 'react';
import {
Tooltip,
Select,
Table,
Modal,
Spin,
Icon,
Row,
Col,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
collecting: boolean;
data: any;
visible: boolean;
assignee: string;
startFrame: number;
stopFrame: number;
zOrder: boolean;
bugTracker: string;
jobStatus: string;
savingJobStatus: boolean;
closeStatistics(): void;
changeJobStatus(status: string): void;
}
export default function StatisticsModalComponent(props: Props): JSX.Element {
const {
collecting,
data,
visible,
jobStatus,
assignee,
startFrame,
stopFrame,
zOrder,
bugTracker,
closeStatistics,
changeJobStatus,
savingJobStatus,
} = props;
const baseProps = {
cancelButtonProps: { style: { display: 'none' } },
okButtonProps: { style: { width: 100 } },
onOk: closeStatistics,
width: 1000,
visible,
closable: false,
};
if (collecting || !data) {
return (
<Modal
{...baseProps}
>
<Spin style={{ margin: '0 50%' }} />
</Modal>
);
}
const rows = Object.keys(data.label).map((key: string) => ({
key,
label: key,
rectangle: `${data.label[key].rectangle.shape} / ${data.label[key].rectangle.track}`,
polygon: `${data.label[key].polygon.shape} / ${data.label[key].polygon.track}`,
polyline: `${data.label[key].polyline.shape} / ${data.label[key].polyline.track}`,
points: `${data.label[key].points.shape} / ${data.label[key].points.track}`,
tags: data.label[key].tags,
manually: data.label[key].manually,
interpolated: data.label[key].interpolated,
total: data.label[key].total,
}));
rows.push({
key: '___total',
label: 'Total',
rectangle: `${data.total.rectangle.shape} / ${data.total.rectangle.track}`,
polygon: `${data.total.polygon.shape} / ${data.total.polygon.track}`,
polyline: `${data.total.polyline.shape} / ${data.total.polyline.track}`,
points: `${data.total.points.shape} / ${data.total.points.track}`,
tags: data.total.tags,
manually: data.total.manually,
interpolated: data.total.interpolated,
total: data.total.total,
});
const makeShapesTracksTitle = (title: string): JSX.Element => (
<Tooltip overlay='Shapes / Tracks'>
<Text strong style={{ marginRight: 5 }}>{title}</Text>
<Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip>
);
const columns = [{
title: <Text strong> Label </Text>,
dataIndex: 'label',
key: 'label',
}, {
title: makeShapesTracksTitle('Rectangle'),
dataIndex: 'rectangle',
key: 'rectangle',
}, {
title: makeShapesTracksTitle('Polygon'),
dataIndex: 'polygon',
key: 'polygon',
}, {
title: makeShapesTracksTitle('Polyline'),
dataIndex: 'polyline',
key: 'polyline',
}, {
title: makeShapesTracksTitle('Points'),
dataIndex: 'points',
key: 'points',
}, {
title: <Text strong> Tags </Text>,
dataIndex: 'tags',
key: 'tags',
}, {
title: <Text strong> Manually </Text>,
dataIndex: 'manually',
key: 'manually',
}, {
title: <Text strong> Interpolated </Text>,
dataIndex: 'interpolated',
key: 'interpolated',
}, {
title: <Text strong> Total </Text>,
dataIndex: 'total',
key: 'total',
}];
return (
<Modal
{...baseProps}
>
<div className='cvat-job-info-modal-window'>
<Row type='flex' justify='start'>
<Col>
<Text strong className='cvat-text'>Job status</Text>
<Select value={jobStatus} onChange={changeJobStatus}>
<Select.Option key='1' value='annotation'>annotation</Select.Option>
<Select.Option key='2' value='validation'>validation</Select.Option>
<Select.Option key='3' value='completed'>completed</Select.Option>
</Select>
{savingJobStatus && <Icon type='loading' />}
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text'>Overview</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col span={5}>
<Text strong className='cvat-text'>Assignee</Text>
<Text className='cvat-text'>{assignee}</Text>
</Col>
<Col span={5}>
<Text strong className='cvat-text'>Start frame</Text>
<Text className='cvat-text'>{startFrame}</Text>
</Col>
<Col span={5}>
<Text strong className='cvat-text'>Stop frame</Text>
<Text className='cvat-text'>{stopFrame}</Text>
</Col>
<Col span={5}>
<Text strong className='cvat-text'>Frames</Text>
<Text className='cvat-text'>{stopFrame - startFrame + 1}</Text>
</Col>
<Col span={4}>
<Text strong className='cvat-text'>Z-Order</Text>
<Text className='cvat-text'>{zOrder.toString()}</Text>
</Col>
</Row>
{ !!bugTracker && (
<Row type='flex' justify='start' className='cvat-job-info-bug-tracker'>
<Col>
<Text strong className='cvat-text'>Bug tracker</Text>
<a href={bugTracker}>{bugTracker}</a>
</Col>
</Row>
)}
<Row type='flex' justify='space-around' className='cvat-job-info-statistics'>
<Col span={24}>
<Text className='cvat-text'>Annotations statistics</Text>
<Table
scroll={{ y: 400 }}
bordered
pagination={false}
columns={columns}
dataSource={rows}
/>
</Col>
</Row>
</div>
</Modal>
);
}

@ -20,6 +20,7 @@ interface Props {
frameNumber: number; frameNumber: number;
startFrame: number; startFrame: number;
stopFrame: number; stopFrame: number;
showStatistics(): void;
onSwitchPlay(): void; onSwitchPlay(): void;
onSaveAnnotation(): void; onSaveAnnotation(): void;
onPrevFrame(): void; onPrevFrame(): void;
@ -41,7 +42,7 @@ function propsAreEqual(curProps: Props, prevProps: Props): boolean {
&& curProps.savingStatuses.length === prevProps.savingStatuses.length; && curProps.savingStatuses.length === prevProps.savingStatuses.length;
} }
const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => { function AnnotationTopBarComponent(props: Props): JSX.Element {
const { const {
saving, saving,
savingStatuses, savingStatuses,
@ -49,6 +50,7 @@ const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => {
frameNumber, frameNumber,
startFrame, startFrame,
stopFrame, stopFrame,
showStatistics,
onSwitchPlay, onSwitchPlay,
onSaveAnnotation, onSaveAnnotation,
onPrevFrame, onPrevFrame,
@ -90,10 +92,10 @@ const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => {
/> />
</Row> </Row>
</Col> </Col>
<RightGroup /> <RightGroup showStatistics={showStatistics} />
</Row> </Row>
</Layout.Header> </Layout.Header>
); );
}, propsAreEqual); }
export default AnnotationTopBarComponent; export default React.memo(AnnotationTopBarComponent, propsAreEqual);

@ -289,7 +289,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
</Col> </Col>
<Col span={1} offset={1}> <Col span={1} offset={1}>
<Tooltip overlay='Specify a label mapping between model labels and task labels'> <Tooltip overlay='Specify a label mapping between model labels and task labels'>
<Icon className='cvat-run-model-dialog-info-icon' type='question-circle' /> <Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip> </Tooltip>
</Col> </Col>
</Row> </Row>

@ -4,10 +4,6 @@
margin-top: 10px; margin-top: 10px;
} }
.cvat-run-model-dialog-info-icon {
color: $info-icon-color;
}
.cvat-run-model-dialog-remove-mapping-icon { .cvat-run-model-dialog-remove-mapping-icon {
color: $danger-icon-color; color: $danger-icon-color;
} }

@ -61,8 +61,8 @@
} }
.cvat-player-settings-step > div > span > i { .cvat-player-settings-step > div > span > i {
vertical-align: -1em; margin: 0px 5px;
transform: scale(0.3); font-size: 10px;
} }
.cvat-player-settings-speed > div > .ant-select { .cvat-player-settings-speed > div > .ant-select {

@ -12,13 +12,17 @@ import {
mergeObjects, mergeObjects,
groupObjects, groupObjects,
splitTrack, splitTrack,
editShape,
updateAnnotationsAsync, updateAnnotationsAsync,
createAnnotationsAsync, createAnnotationsAsync,
mergeAnnotationsAsync, mergeAnnotationsAsync,
groupAnnotationsAsync, groupAnnotationsAsync,
splitAnnotationsAsync, splitAnnotationsAsync,
activateObject,
selectObjects,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { import {
ColorBy,
GridColor, GridColor,
ObjectType, ObjectType,
CombinedState, CombinedState,
@ -30,9 +34,15 @@ interface StateToProps {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
activatedStateID: number | null;
selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frame: number; frame: number;
opacity: number;
colorBy: ColorBy;
selectedOpacity: number;
blackBorders: boolean;
grid: boolean; grid: boolean;
gridSize: number; gridSize: number;
gridColor: GridColor; gridColor: GridColor;
@ -50,11 +60,14 @@ interface DispatchToProps {
onMergeObjects: (enabled: boolean) => void; onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -78,6 +91,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
}, },
annotations: { annotations: {
states: annotations, states: annotations,
activatedStateID,
selectedStatesID,
}, },
sidebarCollapsed, sidebarCollapsed,
}, },
@ -88,6 +103,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
gridColor, gridColor,
gridOpacity, gridOpacity,
}, },
shapes: {
opacity,
colorBy,
selectedOpacity,
blackBorders,
},
}, },
} = state; } = state;
@ -97,7 +118,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance, jobInstance,
frameData, frameData,
frame, frame,
activatedStateID,
selectedStatesID,
annotations, annotations,
opacity,
colorBy,
selectedOpacity,
blackBorders,
grid, grid,
gridSize, gridSize,
gridColor, gridColor,
@ -133,6 +160,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSplitTrack(enabled: boolean): void { onSplitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled)); dispatch(splitTrack(enabled));
}, },
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void { onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frame, states)); dispatch(updateAnnotationsAsync(sessionInstance, frame, states));
}, },
@ -148,6 +178,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void { onSplitAnnotations(sessionInstance: any, frame: number, state: any): void {
dispatch(splitAnnotationsAsync(sessionInstance, frame, state)); dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
}, },
onActivateObject(activatedStateID: number | null): void {
dispatch(activateObject(activatedStateID));
},
onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID));
},
}; };
} }

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
changeLabelColor as changeLabelColorAction, changeLabelColorAsync,
updateAnnotationsAsync, updateAnnotationsAsync,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
@ -26,7 +26,7 @@ interface StateToProps {
interface DispatchToProps { interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeLabelColor(label: any, color: string): void; changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void;
} }
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -36,8 +36,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
states: objectStates, states: objectStates,
}, },
job: { job: {
instance: jobInstance,
labels, labels,
instance: jobInstance,
}, },
player: { player: {
frame: { frame: {
@ -48,7 +48,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
}, },
} = state; } = state;
const [label] = labels.filter((_label: any) => _label.id === own.labelID); const [label] = labels
.filter((_label: any) => _label.id === own.labelID);
return { return {
label, label,
@ -66,8 +67,13 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
}, },
changeLabelColor(label: any, color: string): void { changeLabelColor(
dispatch(changeLabelColorAction(label, color)); sessionInstance: any,
frameNumber: number,
label: any,
color: string,
): void {
dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color));
}, },
}; };
} }
@ -139,9 +145,11 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
const { const {
changeLabelColor, changeLabelColor,
label, label,
frameNumber,
jobInstance,
} = this.props; } = this.props;
changeLabelColor(label, color); changeLabelColor(jobInstance, frameNumber, label, color);
}; };
private switchHidden(value: boolean): void { private switchHidden(value: boolean): void {

@ -1,11 +1,18 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
ActiveControl,
CombinedState, CombinedState,
ColorBy,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
collapseObjectItems, collapseObjectItems,
updateAnnotationsAsync, updateAnnotationsAsync,
changeFrameAsync,
removeObjectAsync,
copyShape as copyShapeAction,
activateObject as activateObjectAction,
propagateObject as propagateObjectAction,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
@ -21,11 +28,20 @@ interface StateToProps {
attributes: any[]; attributes: any[];
jobInstance: any; jobInstance: any;
frameNumber: number; frameNumber: number;
activated: boolean;
colorBy: ColorBy;
ready: boolean;
activeControl: ActiveControl;
} }
interface DispatchToProps { interface DispatchToProps {
changeFrame(frame: number): void;
updateState(sessionInstance: any, frameNumber: number, objectState: any): void; updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void;
removeObject: (objectState: any) => void;
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
} }
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -34,17 +50,27 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
annotations: { annotations: {
states, states,
collapsed: statesCollapsed, collapsed: statesCollapsed,
activatedStateID,
}, },
job: { job: {
labels,
attributes: jobAttributes, attributes: jobAttributes,
instance: jobInstance, instance: jobInstance,
labels,
}, },
player: { player: {
frame: { frame: {
number: frameNumber, number: frameNumber,
}, },
}, },
canvas: {
ready,
activeControl,
},
},
settings: {
shapes: {
colorBy,
},
}, },
} = state; } = state;
@ -60,24 +86,135 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
collapsed: collapsedState, collapsed: collapsedState,
attributes: jobAttributes[states[index].label.id], attributes: jobAttributes[states[index].label.id],
labels, labels,
ready,
activeControl,
colorBy,
jobInstance, jobInstance,
frameNumber, frameNumber,
activated: activatedStateID === own.clientID,
}; };
} }
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
updateState(sessionInstance: any, frameNumber: number, state: any): void { updateState(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state])); dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state]));
}, },
collapseOrExpand(objectStates: any[], collapsed: boolean): void { collapseOrExpand(objectStates: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(objectStates, collapsed)); dispatch(collapseObjectItems(objectStates, collapsed));
}, },
activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID));
},
removeObject(objectState: any): void {
dispatch(removeObjectAsync(objectState, true));
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
},
propagateObject(objectState: any): void {
dispatch(propagateObjectAction(objectState));
},
}; };
} }
type Props = StateToProps & DispatchToProps; type Props = StateToProps & DispatchToProps;
class ObjectItemContainer extends React.PureComponent<Props> { class ObjectItemContainer extends React.PureComponent<Props> {
private navigateFirstKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { first } = objectState.keyframes;
if (first !== frameNumber) {
changeFrame(first);
}
};
private navigatePrevKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { prev } = objectState.keyframes;
if (prev !== null && prev !== frameNumber) {
changeFrame(prev);
}
};
private navigateNextKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { next } = objectState.keyframes;
if (next !== null && next !== frameNumber) {
changeFrame(next);
}
};
private navigateLastKeyframe = (): void => {
const {
objectState,
changeFrame,
frameNumber,
} = this.props;
const { last } = objectState.keyframes;
if (last !== frameNumber) {
changeFrame(last);
}
};
private copy = (): void => {
const {
objectState,
copyShape,
} = this.props;
copyShape(objectState);
};
private propagate = (): void => {
const {
objectState,
propagateObject,
} = this.props;
propagateObject(objectState);
};
private remove = (): void => {
const {
objectState,
removeObject,
} = this.props;
removeObject(objectState);
};
private activate = (): void => {
const {
activateObject,
objectState,
ready,
activeControl,
} = this.props;
if (ready && activeControl === ActiveControl.CURSOR) {
activateObject(objectState.clientID);
}
};
private lock = (): void => { private lock = (): void => {
const { objectState } = this.props; const { objectState } = this.props;
objectState.lock = true; objectState.lock = true;
@ -184,10 +321,35 @@ class ObjectItemContainer extends React.PureComponent<Props> {
collapsed, collapsed,
labels, labels,
attributes, attributes,
frameNumber,
activated,
colorBy,
} = this.props; } = this.props;
const {
first,
prev,
next,
last,
} = objectState.keyframes || {
first: null, // shapes don't have keyframes, so we use null
prev: null,
next: null,
last: null,
};
let stateColor = '';
if (colorBy === ColorBy.INSTANCE) {
stateColor = objectState.color;
} else if (colorBy === ColorBy.GROUP) {
stateColor = objectState.group.color;
} else if (colorBy === ColorBy.LABEL) {
stateColor = objectState.label.color;
}
return ( return (
<ObjectStateItemComponent <ObjectStateItemComponent
activated={activated}
objectType={objectState.objectType} objectType={objectState.objectType}
shapeType={objectState.shapeType} shapeType={objectState.shapeType}
clientID={objectState.clientID} clientID={objectState.clientID}
@ -198,10 +360,30 @@ class ObjectItemContainer extends React.PureComponent<Props> {
keyframe={objectState.keyframe} keyframe={objectState.keyframe}
attrValues={{ ...objectState.attributes }} attrValues={{ ...objectState.attributes }}
labelID={objectState.label.id} labelID={objectState.label.id}
color={objectState.color} color={stateColor}
attributes={attributes} attributes={attributes}
labels={labels} labels={labels}
collapsed={collapsed} collapsed={collapsed}
navigateFirstKeyframe={
first >= frameNumber || first === null
? null : this.navigateFirstKeyframe
}
navigatePrevKeyframe={
prev === frameNumber || prev === null
? null : this.navigatePrevKeyframe
}
navigateNextKeyframe={
next === frameNumber || next === null
? null : this.navigateNextKeyframe
}
navigateLastKeyframe={
last <= frameNumber || last === null
? null : this.navigateLastKeyframe
}
activate={this.activate}
remove={this.remove}
copy={this.copy}
propagate={this.propagate}
setOccluded={this.setOccluded} setOccluded={this.setOccluded}
unsetOccluded={this.unsetOccluded} unsetOccluded={this.unsetOccluded}
setOutside={this.setOutside} setOutside={this.setOutside}

@ -2,23 +2,47 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RadioChangeEvent } from 'antd/lib/radio';
import { SliderValue } from 'antd/lib/slider';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import ObjectsSidebarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectsSidebarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import { CombinedState } from 'reducers/interfaces'; import {
CombinedState,
ColorBy,
} from 'reducers/interfaces';
import { import {
collapseSidebar as collapseSidebarAction, collapseSidebar as collapseSidebarAction,
collapseAppearance as collapseAppearanceAction, collapseAppearance as collapseAppearanceAction,
updateTabContentHeight as updateTabContentHeightAction, updateTabContentHeight as updateTabContentHeightAction,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import {
changeShapesColorBy as changeShapesColorByAction,
changeShapesOpacity as changeShapesOpacityAction,
changeSelectedShapesOpacity as changeSelectedShapesOpacityAction,
changeShapesBlackBorders as changeShapesBlackBordersAction,
} from 'actions/settings-actions';
interface StateToProps { interface StateToProps {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
} }
interface DispatchToProps { interface DispatchToProps {
collapseSidebar(): void; collapseSidebar(): void;
collapseAppearance(): void; collapseAppearance(): void;
updateTabContentHeight(): void; updateTabContentHeight(): void;
changeShapesColorBy(colorBy: ColorBy): void;
changeShapesOpacity(shapesOpacity: number): void;
changeSelectedShapesOpacity(selectedShapesOpacity: number): void;
changeShapesBlackBorders(blackBorders: boolean): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -27,11 +51,23 @@ function mapStateToProps(state: CombinedState): StateToProps {
sidebarCollapsed, sidebarCollapsed,
appearanceCollapsed, appearanceCollapsed,
}, },
settings: {
shapes: {
colorBy,
opacity,
selectedOpacity,
blackBorders,
},
},
} = state; } = state;
return { return {
sidebarCollapsed, sidebarCollapsed,
appearanceCollapsed, appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
}; };
} }
@ -80,19 +116,90 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
), ),
); );
}, },
changeShapesColorBy(colorBy: ColorBy): void {
dispatch(changeShapesColorByAction(colorBy));
},
changeShapesOpacity(shapesOpacity: number): void {
dispatch(changeShapesOpacityAction(shapesOpacity));
},
changeSelectedShapesOpacity(selectedShapesOpacity: number): void {
dispatch(changeSelectedShapesOpacityAction(selectedShapesOpacity));
},
changeShapesBlackBorders(blackBorders: boolean): void {
dispatch(changeShapesBlackBordersAction(blackBorders));
},
}; };
} }
type Props = StateToProps & DispatchToProps; type Props = StateToProps & DispatchToProps;
class ObjectsSideBarContainer extends React.PureComponent<Props> { class ObjectsSideBarContainer extends React.PureComponent<Props> {
public componentDidMount(): void { public componentDidMount(): void {
const { updateTabContentHeight } = this.props; window.addEventListener('resize', this.alignTabHeight);
updateTabContentHeight(); this.alignTabHeight();
} }
public componentWillUnmount(): void {
window.removeEventListener('resize', this.alignTabHeight);
}
private alignTabHeight = (): void => {
const {
sidebarCollapsed,
updateTabContentHeight,
} = this.props;
if (!sidebarCollapsed) {
updateTabContentHeight();
}
};
private changeShapesColorBy = (event: RadioChangeEvent): void => {
const { changeShapesColorBy } = this.props;
changeShapesColorBy(event.target.value);
};
private changeShapesOpacity = (value: SliderValue): void => {
const { changeShapesOpacity } = this.props;
changeShapesOpacity(value as number);
};
private changeSelectedShapesOpacity = (value: SliderValue): void => {
const { changeSelectedShapesOpacity } = this.props;
changeSelectedShapesOpacity(value as number);
};
private changeShapesBlackBorders = (event: CheckboxChangeEvent): void => {
const { changeShapesBlackBorders } = this.props;
changeShapesBlackBorders(event.target.checked);
};
public render(): JSX.Element { public render(): JSX.Element {
const {
sidebarCollapsed,
appearanceCollapsed,
colorBy,
opacity,
selectedOpacity,
blackBorders,
collapseSidebar,
collapseAppearance,
} = this.props;
return ( return (
<ObjectsSidebarComponent {...this.props} /> <ObjectsSidebarComponent
sidebarCollapsed={sidebarCollapsed}
appearanceCollapsed={appearanceCollapsed}
colorBy={colorBy}
opacity={opacity}
selectedOpacity={selectedOpacity}
blackBorders={blackBorders}
collapseSidebar={collapseSidebar}
collapseAppearance={collapseAppearance}
changeShapesColorBy={this.changeShapesColorBy}
changeShapesOpacity={this.changeShapesOpacity}
changeSelectedShapesOpacity={this.changeSelectedShapesOpacity}
changeShapesBlackBorders={this.changeShapesBlackBorders}
/>
); );
} }
} }

@ -0,0 +1,134 @@
import React from 'react';
import { connect } from 'react-redux';
import {
propagateObject as propagateObjectAction,
changePropagateFrames as changePropagateFramesAction,
propagateObjectAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm';
interface StateToProps {
objectState: any | null;
frameNumber: number;
stopFrame: number;
propagateFrames: number;
jobInstance: any;
}
interface DispatchToProps {
cancel(): void;
propagateObject(sessionInstance: any, objectState: any, from: number, to: number): void;
changePropagateFrames(frames: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
propagate: {
objectState,
frames: propagateFrames,
},
job: {
instance: {
stopFrame,
},
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
},
} = state;
return {
objectState,
frameNumber,
stopFrame,
propagateFrames,
jobInstance,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
propagateObject(sessionInstance: any, objectState: any, from: number, to: number): void {
dispatch(propagateObjectAsync(sessionInstance, objectState, from, to));
},
changePropagateFrames(frames: number): void {
dispatch(changePropagateFramesAction(frames));
},
cancel(): void {
dispatch(propagateObjectAction(null));
},
};
}
type Props = StateToProps & DispatchToProps;
class PropagateConfirmContainer extends React.PureComponent<Props> {
private propagateObject = (): void => {
const {
propagateObject,
objectState,
propagateFrames,
frameNumber,
stopFrame,
jobInstance,
} = this.props;
const propagateUpToFrame = Math.min(frameNumber + propagateFrames, stopFrame);
propagateObject(jobInstance, objectState, frameNumber + 1, propagateUpToFrame);
};
private changePropagateFrames = (value: number | undefined): void => {
const { changePropagateFrames } = this.props;
if (typeof (value) !== 'undefined') {
changePropagateFrames(value);
}
};
private changeUpToFrame = (value: number | undefined): void => {
const {
stopFrame,
frameNumber,
changePropagateFrames,
} = this.props;
if (typeof (value) !== 'undefined') {
const propagateFrames = Math.max(0, Math.min(stopFrame, value)) - frameNumber;
changePropagateFrames(propagateFrames);
}
};
public render(): JSX.Element {
const {
frameNumber,
stopFrame,
propagateFrames,
cancel,
objectState,
} = this.props;
const propagateUpToFrame = Math.min(frameNumber + propagateFrames, stopFrame);
return (
<PropagateConfirmComponent
visible={objectState !== null}
propagateUpToFrame={propagateUpToFrame}
propagateFrames={propagateUpToFrame - frameNumber}
propagateObject={this.propagateObject}
changePropagateFrames={this.changePropagateFrames}
changeUpToFrame={this.changeUpToFrame}
cancel={cancel}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PropagateConfirmContainer);

@ -0,0 +1,108 @@
import React from 'react';
import { connect } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import {
showStatistics,
changeJobStatusAsync,
} from 'actions/annotation-actions';
import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal';
interface StateToProps {
visible: boolean;
collecting: boolean;
data: any;
jobInstance: any;
jobStatus: string;
savingJobStatus: boolean;
}
interface DispatchToProps {
changeJobStatus(jobInstance: any, status: string): void;
closeStatistics(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
statistics: {
visible,
collecting,
data,
},
job: {
saving: savingJobStatus,
instance: {
status: jobStatus,
},
instance: jobInstance,
},
},
} = state;
return {
visible,
collecting,
data,
jobInstance,
jobStatus,
savingJobStatus,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeJobStatus(jobInstance: any, status: string): void {
dispatch(changeJobStatusAsync(jobInstance, status));
},
closeStatistics(): void {
dispatch(showStatistics(false));
},
};
}
type Props = StateToProps & DispatchToProps;
class StatisticsModalContainer extends React.PureComponent<Props> {
private changeJobStatus = (status: string): void => {
const {
jobInstance,
changeJobStatus,
} = this.props;
changeJobStatus(jobInstance, status);
};
public render(): JSX.Element {
const {
jobInstance,
visible,
collecting,
data,
closeStatistics,
jobStatus,
savingJobStatus,
} = this.props;
return (
<StatisticsModalComponent
collecting={collecting}
data={data}
visible={visible}
jobStatus={jobStatus}
bugTracker={jobInstance.task.bugTracker}
zOrder={jobInstance.task.zOrder}
startFrame={jobInstance.startFrame}
stopFrame={jobInstance.stopFrame}
assignee={jobInstance.assignee ? jobInstance.assignee.username : 'Nobody'}
savingJobStatus={savingJobStatus}
closeStatistics={closeStatistics}
changeJobStatus={this.changeJobStatus}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(StatisticsModalContainer);

@ -7,6 +7,8 @@ import {
changeFrameAsync, changeFrameAsync,
switchPlay, switchPlay,
saveAnnotationsAsync, saveAnnotationsAsync,
collectStatisticsAsync,
showStatistics as showStatisticsAction,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -26,6 +28,7 @@ interface DispatchToProps {
onChangeFrame(frame: number): void; onChangeFrame(frame: number): void;
onSwitchPlay(playing: boolean): void; onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void; onSaveAnnotation(sessionInstance: any): void;
showStatistics(sessionInstance: any): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -79,6 +82,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSaveAnnotation(sessionInstance: any): void { onSaveAnnotation(sessionInstance: any): void {
dispatch(saveAnnotationsAsync(sessionInstance)); dispatch(saveAnnotationsAsync(sessionInstance));
}, },
showStatistics(sessionInstance: any): void {
dispatch(collectStatisticsAsync(sessionInstance));
dispatch(showStatisticsAction(true));
},
}; };
} }
@ -108,6 +115,15 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
} }
} }
private showStatistics = (): void => {
const {
jobInstance,
showStatistics,
} = this.props;
showStatistics(jobInstance);
};
private onSwitchPlay = (): void => { private onSwitchPlay = (): void => {
const { const {
frameNumber, frameNumber,
@ -288,6 +304,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
return ( return (
<AnnotationTopBarComponent <AnnotationTopBarComponent
showStatistics={this.showStatistics}
onSwitchPlay={this.onSwitchPlay} onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation} onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame} onPrevFrame={this.onPrevFrame}

@ -17,10 +17,11 @@ const defaultState: AnnotationState = {
activeControl: ActiveControl.CURSOR, activeControl: ActiveControl.CURSOR,
}, },
job: { job: {
instance: null,
labels: [], labels: [],
instance: null,
attributes: {}, attributes: {},
fetching: false, fetching: false,
saving: false,
}, },
player: { player: {
frame: { frame: {
@ -36,6 +37,8 @@ const defaultState: AnnotationState = {
activeObjectType: ObjectType.SHAPE, activeObjectType: ObjectType.SHAPE,
}, },
annotations: { annotations: {
selectedStatesID: [],
activatedStateID: null,
saving: { saving: {
uploading: false, uploading: false,
statuses: [], statuses: [],
@ -43,6 +46,15 @@ const defaultState: AnnotationState = {
collapsed: {}, collapsed: {},
states: [], states: [],
}, },
propagate: {
objectState: null,
frames: 50,
},
statistics: {
visible: false,
collecting: false,
data: null,
},
colors: [], colors: [],
sidebarCollapsed: false, sidebarCollapsed: false,
appearanceCollapsed: false, appearanceCollapsed: false,
@ -135,6 +147,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
states, states,
} = action.payload; } = action.payload;
const activatedStateID = states
.map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID)
? state.annotations.activatedStateID : null;
return { return {
...state, ...state,
player: { player: {
@ -147,6 +163,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
annotations: { annotations: {
...state.annotations, ...state.annotations,
activatedStateID,
states, states,
}, },
}; };
@ -279,6 +296,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return { return {
...state, ...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
@ -292,6 +313,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return { return {
...state, ...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
@ -309,6 +334,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return { return {
...state, ...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
@ -328,6 +357,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return { return {
...state, ...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
@ -341,6 +374,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return { return {
...state, ...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
@ -354,6 +391,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return { return {
...state, ...state,
annotations: {
...state.annotations,
activatedStateID: null,
},
canvas: { canvas: {
...state.canvas, ...state.canvas,
activeControl, activeControl,
@ -447,6 +488,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
case AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS: { case AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS: {
const { const {
label, label,
states,
} = action.payload; } = action.payload;
const { instance: job } = state.job; const { instance: job } = state.job;
@ -454,13 +496,202 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const index = labels.indexOf(label); const index = labels.indexOf(label);
labels[index] = label; labels[index] = label;
return { return {
...state, ...state,
job: { job: {
...state.job, ...state.job,
labels, labels,
}, },
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.ACTIVATE_OBJECT: {
const {
activatedStateID,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
activatedStateID,
},
};
}
case AnnotationActionTypes.SELECT_OBJECTS: {
const {
selectedStatesID,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
selectedStatesID,
},
};
}
case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: {
const {
objectState,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
activatedStateID: null,
states: state.annotations.states
.filter((_objectState: any) => (
_objectState.clientID !== objectState.clientID
)),
},
};
}
case AnnotationActionTypes.COPY_SHAPE: {
const {
objectState,
} = action.payload;
state.canvas.instance.cancel();
state.canvas.instance.draw({
enabled: true,
initialState: objectState,
});
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (objectState.shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (objectState.shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (objectState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
}
return {
...state,
canvas: {
...state.canvas,
activeControl,
},
annotations: {
...state.annotations,
activatedStateID: null,
},
};
}
case AnnotationActionTypes.EDIT_SHAPE: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.EDIT : ActiveControl.CURSOR;
return {
...state,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT: {
const { objectState } = action.payload;
return {
...state,
propagate: {
...state.propagate,
objectState,
},
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS: {
return {
...state,
propagate: {
...state.propagate,
objectState: null,
},
};
}
case AnnotationActionTypes.CHANGE_PROPAGATE_FRAMES: {
const { frames } = action.payload;
return {
...state,
propagate: {
...state.propagate,
frames,
},
};
}
case AnnotationActionTypes.SWITCH_SHOWING_STATISTICS: {
const { visible } = action.payload;
return {
...state,
statistics: {
...state.statistics,
visible,
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS: {
return {
...state,
statistics: {
...state.statistics,
collecting: true,
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS_SUCCESS: {
const { data } = action.payload;
return {
...state,
statistics: {
...state.statistics,
collecting: false,
data,
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS_FAILED: {
return {
...state,
statistics: {
...state.statistics,
collecting: false,
data: null,
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS: {
return {
...state,
job: {
...state.job,
saving: true,
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS_SUCCESS: {
return {
...state,
job: {
...state.job,
saving: false,
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED: {
return {
...state,
job: {
...state.job,
saving: false,
},
}; };
} }
case AnnotationActionTypes.RESET_CANVAS: { case AnnotationActionTypes.RESET_CANVAS: {

@ -211,6 +211,10 @@ export interface NotificationsState {
merging: null | ErrorState; merging: null | ErrorState;
grouping: null | ErrorState; grouping: null | ErrorState;
splitting: null | ErrorState; splitting: null | ErrorState;
removing: null | ErrorState;
propagating: null | ErrorState;
collectingStatistics: null | ErrorState;
savingJob: null | ErrorState;
}; };
[index: string]: any; [index: string]: any;
@ -238,6 +242,7 @@ export enum ActiveControl {
MERGE = 'merge', MERGE = 'merge',
GROUP = 'group', GROUP = 'group',
SPLIT = 'split', SPLIT = 'split',
EDIT = 'edit',
} }
export enum ShapeType { export enum ShapeType {
@ -266,10 +271,11 @@ export interface AnnotationState {
activeControl: ActiveControl; activeControl: ActiveControl;
}; };
job: { job: {
instance: any | null | undefined;
labels: any[]; labels: any[];
instance: any | null | undefined;
attributes: Record<number, any[]>; attributes: Record<number, any[]>;
fetching: boolean; fetching: boolean;
saving: boolean;
}; };
player: { player: {
frame: { frame: {
@ -286,6 +292,8 @@ export interface AnnotationState {
activeObjectType: ObjectType; activeObjectType: ObjectType;
}; };
annotations: { annotations: {
selectedStatesID: number[];
activatedStateID: number | null;
collapsed: Record<number, boolean>; collapsed: Record<number, boolean>;
states: any[]; states: any[];
saving: { saving: {
@ -293,6 +301,15 @@ export interface AnnotationState {
statuses: string[]; statuses: string[];
}; };
}; };
propagate: {
objectState: any | null;
frames: number;
};
statistics: {
collecting: boolean;
visible: boolean;
data: any;
};
colors: any[]; colors: any[];
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
@ -316,6 +333,12 @@ export enum FrameSpeed {
Slowest = 1, Slowest = 1,
} }
export enum ColorBy {
INSTANCE = 'Instance',
GROUP = 'Group',
LABEL = 'Label',
}
export interface PlayerSettingsState { export interface PlayerSettingsState {
frameStep: number; frameStep: number;
frameSpeed: FrameSpeed; frameSpeed: FrameSpeed;
@ -337,7 +360,15 @@ export interface WorkspaceSettingsState {
showAllInterpolationTracks: boolean; showAllInterpolationTracks: boolean;
} }
export interface ShapesSettingsState {
colorBy: ColorBy;
opacity: number;
selectedOpacity: number;
blackBorders: boolean;
}
export interface SettingsState { export interface SettingsState {
shapes: ShapesSettingsState;
workspace: WorkspaceSettingsState; workspace: WorkspaceSettingsState;
player: PlayerSettingsState; player: PlayerSettingsState;
} }

@ -59,6 +59,10 @@ const defaultState: NotificationsState = {
merging: null, merging: null,
grouping: null, grouping: null,
splitting: null, splitting: null,
removing: null,
propagating: null,
collectingStatistics: null,
savingJob: null,
}, },
}, },
messages: { messages: {
@ -564,7 +568,67 @@ export default function (state = defaultState, action: AnyAction): Notifications
annotation: { annotation: {
...state.errors.annotation, ...state.errors.annotation,
splitting: { splitting: {
message: 'Could not split a track', message: 'Could not split the track',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.REMOVE_OBJECT_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
removing: {
message: 'Could not remove the object',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
propagating: {
message: 'Could not propagate the object',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.COLLECT_STATISTICS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
collectingStatistics: {
message: 'Could not collect annotations statistics',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
savingJob: {
message: 'Could not save the job on the server',
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
}, },
}, },

@ -5,9 +5,16 @@ import {
SettingsState, SettingsState,
GridColor, GridColor,
FrameSpeed, FrameSpeed,
ColorBy,
} from './interfaces'; } from './interfaces';
const defaultState: SettingsState = { const defaultState: SettingsState = {
shapes: {
colorBy: ColorBy.INSTANCE,
opacity: 3,
selectedOpacity: 30,
blackBorders: false,
},
workspace: { workspace: {
autoSave: false, autoSave: false,
autoSaveInterval: 15 * 60 * 1000, autoSaveInterval: 15 * 60 * 1000,
@ -76,6 +83,42 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
}, },
}; };
} }
case SettingsActionTypes.CHANGE_SHAPES_COLOR_BY: {
return {
...state,
shapes: {
...state.shapes,
colorBy: action.payload.colorBy,
},
};
}
case SettingsActionTypes.CHANGE_SHAPES_OPACITY: {
return {
...state,
shapes: {
...state.shapes,
opacity: action.payload.opacity,
},
};
}
case SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY: {
return {
...state,
shapes: {
...state.shapes,
selectedOpacity: action.payload.selectedOpacity,
},
};
}
case SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS: {
return {
...state,
shapes: {
...state.shapes,
blackBorders: action.payload.blackBorders,
},
};
}
default: { default: {
return state; return state;
} }

@ -25,6 +25,20 @@ hr {
padding-top: 5px; padding-top: 5px;
} }
.ant-slider {
> .ant-slider-track {
background-color: $slider-color;
}
> .ant-slider-handle {
border-color: $slider-color;
}
}
.cvat-info-circle-icon {
color: $info-icon-color;
}
#root { #root {
width: 100%; width: 100%;
height: 100%; height: 100%;

Loading…
Cancel
Save