React UI: Player in annotation view & settings page (#1018)

* Active player controls
* Setup packages
* Playing
* Fold/unfold sidebar, minor issues
* Improved cvat-canvas integration
* Resolved some issues
* Added cvat-canvas to Dockerfile.ui
* Fit canvas method
* Added annotation reducer
* Added annotation actions
* Added containers
* Added components
* cvat-canvas removed from dockerignore
* Added settings page
* Minor improvements
* Container for canvas wrapper
* Configurable grid
* Rotation
* fitCanvas added to readme
* Aligned table
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent 67239b6148
commit 0277547dc3

@ -6,6 +6,5 @@
/.vscode /.vscode
/db.sqlite3 /db.sqlite3
/keys /keys
/cvat-canvas
**/node_modules **/node_modules

@ -22,22 +22,28 @@ RUN apt update && apt install -yq nodejs npm curl && \
npm install -g n && n 10.16.3 npm install -g n && n 10.16.3
# Create output directories # Create output directories
RUN mkdir /tmp/cvat-ui /tmp/cvat-core RUN mkdir /tmp/cvat-ui /tmp/cvat-core /tmp/cvat-canvas
# Install dependencies # Install dependencies
COPY cvat-core/package*.json /tmp/cvat-core/ COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/ COPY cvat-ui/package*.json /tmp/cvat-ui/
# Install cvat-core dependencies # Install cvat-core dependencies
WORKDIR /tmp/cvat-core/ WORKDIR /tmp/cvat-core/
RUN npm install RUN npm install
# Install cvat-canvas dependencies
WORKDIR /tmp/cvat-canvas/
RUN npm install
# Install cvat-ui dependencies # Install cvat-ui dependencies
WORKDIR /tmp/cvat-ui/ WORKDIR /tmp/cvat-ui/
RUN npm install RUN npm install
# Build source code # Build source code
COPY cvat-core/ /tmp/cvat-core/ COPY cvat-core/ /tmp/cvat-core/
COPY cvat-canvas/ /tmp/cvat-canvas/
COPY cvat-ui/ /tmp/cvat-ui/ COPY cvat-ui/ /tmp/cvat-ui/
RUN npm run build RUN npm run build

@ -74,6 +74,7 @@ Canvas itself handles:
activate(clientID: number, attributeID?: number): void; activate(clientID: number, 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;
fitCanvas(): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -99,7 +100,7 @@ Canvas itself handles:
- Drawn texts have the class ```cvat_canvas_text``` - Drawn texts have the class ```cvat_canvas_text```
- Tags have the class ```cvat_canvas_tag``` - Tags have the class ```cvat_canvas_tag```
- Canvas image has ID ```cvat_canvas_image``` - Canvas image has ID ```cvat_canvas_image```
- Grid on the canvas has ID ```cvat_canvas_grid_pattern``` - Grid on the canvas has ID ```cvat_canvas_grid``` and ```cvat_canvas_grid_pattern```
- Crosshair during a draw has class ```cvat_canvas_crosshair``` - Crosshair during a draw has class ```cvat_canvas_crosshair```
### Events ### Events
@ -126,6 +127,7 @@ Standard JS events are used.
// Put canvas to a html container // Put canvas to a html container
htmlContainer.appendChild(canvas.html()); htmlContainer.appendChild(canvas.html());
canvas.fitCanvas();
// Next you can use its API methods. For example: // Next you can use its API methods. For example:
canvas.rotate(window.Canvas.Rotation.CLOCKWISE90); canvas.rotate(window.Canvas.Rotation.CLOCKWISE90);
@ -182,17 +184,18 @@ Than you can use it in TypeScript:
## API Reaction ## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | | | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING |
|------------|------|----------|-----------|---------|---------|---------| |-------------|------|----------|-----------|---------|---------|---------|
| html() | + | + | + | + | + | + | | html() | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - | | setup() | + | + | + | + | + | - |
| activate() | + | - | - | - | - | - | | activate() | + | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | | rotate() | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | | focus() | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | | fit() | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | | fitCanvas() | + | + | + | + | + | + |
| draw() | + | - | - | - | - | - | | grid() | + | + | + | + | + | + |
| split() | + | - | + | - | - | - | | draw() | + | - | - | - | - | - |
| group | + | + | - | - | - | - | | split() | + | - | + | - | - | - |
| merge() | + | - | - | - | + | - | | group | + | + | - | - | - | - |
| cancel() | - | + | + | + | + | + | | merge() | + | - | - | - | + | - |
| cancel() | - | + | + | + | + | + |

File diff suppressed because it is too large Load Diff

@ -31,7 +31,11 @@
"eslint-config-airbnb-typescript": "^4.0.1", "eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17", "eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"node-sass": "^4.13.0",
"nodemon": "^1.19.1", "nodemon": "^1.19.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"webpack": "^4.36.1", "webpack": "^4.36.1",

@ -0,0 +1,9 @@
/* eslint-disable */
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

@ -78,8 +78,9 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_wrapper { #cvat_canvas_wrapper {
width: 100%; width: 98%;
height: 93%; height: 98%;
margin: 10px;
border-radius: 5px; border-radius: 5px;
background-color: white; background-color: white;
overflow: hidden; overflow: hidden;

@ -27,9 +27,7 @@ import {
CanvasViewImpl, CanvasViewImpl,
} from './canvasView'; } from './canvasView';
import '../scss/canvas.scss';
import '../css/canvas.css';
interface Canvas { interface Canvas {
html(): HTMLDivElement; html(): HTMLDivElement;
@ -68,6 +66,13 @@ class CanvasImpl implements Canvas {
this.model.setup(frameData, objectStates); this.model.setup(frameData, objectStates);
} }
public fitCanvas(): void {
this.model.fitCanvas(
this.view.html().clientWidth,
this.view.html().clientHeight,
);
}
public activate(clientID: number, attributeID: number = null): void { public activate(clientID: number, attributeID: number = null): void {
this.model.activate(clientID, attributeID); this.model.activate(clientID, attributeID);
} }

@ -3,9 +3,6 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
// Disable till full implementation
/* eslint class-methods-use-this: "off" */
import { MasterImpl } from './master'; import { MasterImpl } from './master';
@ -81,6 +78,7 @@ export enum UpdateReasons {
OBJECTS = 'objects', OBJECTS = 'objects',
ZOOM = 'zoom', ZOOM = 'zoom',
FIT = 'fit', FIT = 'fit',
FIT_CANVAS = 'fit_canvas',
MOVE = 'move', MOVE = 'move',
GRID = 'grid', GRID = 'grid',
FOCUS = 'focus', FOCUS = 'focus',
@ -126,6 +124,7 @@ export interface CanvasModel {
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;
fitCanvas(width: number, height: number): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
@ -242,6 +241,19 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.MOVE); this.notify(UpdateReasons.MOVE);
} }
public fitCanvas(width: number, height: number): void {
this.data.canvasSize.height = height;
this.data.canvasSize.width = width;
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
this.notify(UpdateReasons.FIT_CANVAS);
this.notify(UpdateReasons.OBJECTS);
}
public setup(frameData: any, objectStates: any[]): void { public setup(frameData: any, objectStates: any[]): void {
frameData.data( frameData.data(
(): void => { (): void => {

@ -396,24 +396,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.appendChild(this.content); this.canvas.appendChild(this.content);
// A little hack to get size after first mounting
// http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/
const self = this; const self = this;
const canvasFirstMounted = (event: AnimationEvent): void => {
if (event.animationName === 'loadingAnimation') {
const { geometry } = this.controller;
geometry.canvas = {
height: self.canvas.clientHeight,
width: self.canvas.clientWidth,
};
this.controller.geometry = geometry;
this.geometry = geometry;
self.canvas.removeEventListener('animationstart', canvasFirstMounted);
}
};
this.canvas.addEventListener('animationstart', canvasFirstMounted);
// Setup API handlers // Setup API handlers
this.drawHandler = new DrawHandlerImpl( this.drawHandler = new DrawHandlerImpl(
@ -667,6 +650,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
resize.call(this); resize.call(this);
transform.call(this); transform.call(this);
} }
} else if (reason === UpdateReasons.FIT_CANVAS) {
move.call(this);
resize.call(this);
} else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) { } else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) {
move.call(this); move.call(this);
transform.call(this); transform.call(this);

@ -1,3 +1,8 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* eslint-disable */ /* eslint-disable */
const path = require('path'); const path = require('path');
const DtsBundleWebpack = require('dts-bundle-webpack') const DtsBundleWebpack = require('dts-bundle-webpack')
@ -70,15 +75,23 @@ const webConfig = {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
presets: [ presets: [
['@babel/preset-env'], ['@babel/preset-env', {
targets: '> 2.5%', // https://github.com/browserslist/browserslist
}],
['@babel/typescript'], ['@babel/typescript'],
], ],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
}, { }, {
test: /\.css$/, test: /\.scss$/,
use: ['style-loader', 'css-loader'] exclude: /node_modules/,
use: ['style-loader', {
loader: 'css-loader',
options: {
importLoaders: 2,
},
}, 'postcss-loader', 'sass-loader']
}], }],
}, },
plugins: [ plugins: [

@ -18,7 +18,6 @@
"airbnb": "0.0.2", "airbnb": "0.0.2",
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"core-js": "^3.0.1",
"coveralls": "^3.0.5", "coveralls": "^3.0.5",
"eslint": "6.1.0", "eslint": "6.1.0",
"eslint-config-airbnb-base": "14.0.0", "eslint-config-airbnb-base": "14.0.0",

@ -480,20 +480,18 @@
// Method is used to construct ObjectState objects // Method is used to construct ObjectState objects
get(frame) { get(frame) {
if (!(frame in this.cache)) { if (!(frame in this.cache)) {
const interpolation = Object.assign( const interpolation = {
{}, this.getPosition(frame), ...this.getPosition(frame),
{ attributes: this.getAttributes(frame),
attributes: this.getAttributes(frame), group: this.group,
group: this.group, objectType: ObjectType.TRACK,
objectType: ObjectType.TRACK, shapeType: this.shapeType,
shapeType: this.shapeType, clientID: this.clientID,
clientID: this.clientID, serverID: this.serverID,
serverID: this.serverID, lock: this.lock,
lock: this.lock, color: this.color,
color: this.color, visibility: this.visibility,
visibility: this.visibility, };
},
);
this.cache[frame] = interpolation; this.cache[frame] = interpolation;
} }
@ -504,7 +502,7 @@
} }
neighborsFrames(targetFrame) { neighborsFrames(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;
@ -773,13 +771,14 @@
} }
if (rightPosition && leftPosition) { if (rightPosition && leftPosition) {
return Object.assign({}, this.interpolatePosition( return {
leftPosition, ...this.interpolatePosition(
rightPosition, leftPosition,
(targetFrame - leftFrame) / (rightFrame - leftFrame), rightPosition,
), { (targetFrame - leftFrame) / (rightFrame - leftFrame),
),
keyframe: false, keyframe: false,
}); };
} }
if (rightPosition) { if (rightPosition) {
@ -858,7 +857,7 @@
clientID: this.clientID, clientID: this.clientID,
serverID: this.serverID, serverID: this.serverID,
lock: this.lock, lock: this.lock,
attributes: Object.assign({}, this.attributes), attributes: { ...this.attributes },
label: this.label, label: this.label,
group: this.group, group: this.group,
}; };
@ -888,7 +887,7 @@
if (updated.attributes) { if (updated.attributes) {
const labelAttributes = copy.label const labelAttributes = copy.label
.attributes.map(attr => `${attr.id}`); .attributes.map((attr) => `${attr.id}`);
for (const attrID of Object.keys(data.attributes)) { for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes.includes(attrID)) { if (labelAttributes.includes(attrID)) {
@ -1399,8 +1398,8 @@
// some points from source and target can absent in mapping // some points from source and target can absent in mapping
// source, target - arrays of points. Target array size >= sourse array size // source, target - arrays of points. Target array size >= sourse array size
appendMapping(mapping, source, target) { appendMapping(mapping, source, target) {
const targetMatched = Object.values(mapping).map(x => +x); const targetMatched = Object.values(mapping).map((x) => +x);
const sourceMatched = Object.keys(mapping).map(x => +x); const sourceMatched = Object.keys(mapping).map((x) => +x);
const orderForReceive = []; const orderForReceive = [];
function findNeighbors(point) { function findNeighbors(point) {

@ -127,8 +127,9 @@
async function getFrame(taskID, mode, frame) { async function getFrame(taskID, mode, frame) {
if (!(taskID in frameDataCache)) { if (!(taskID in frameDataCache)) {
frameDataCache[taskID] = {}; frameDataCache[taskID] = {
frameDataCache[taskID].meta = await serverProxy.frames.getMeta(taskID); meta: await serverProxy.frames.getMeta(taskID),
};
frameCache[taskID] = {}; frameCache[taskID] = {};
} }

@ -192,7 +192,7 @@
toJSON() { toJSON() {
const object = { const object = {
name: this.name, name: this.name,
attributes: [...this.attributes.map(el => el.toJSON())], attributes: [...this.attributes.map((el) => el.toJSON())],
}; };
if (typeof (this.id) !== 'undefined') { if (typeof (this.id) !== 'undefined') {

@ -17,7 +17,7 @@
const pluginList = await PluginRegistry.list(); const pluginList = await PluginRegistry.list();
for (const plugin of pluginList) { for (const plugin of pluginList) {
const pluginDecorators = plugin.functions const pluginDecorators = plugin.functions
.filter(obj => obj.callback === wrappedFunc)[0]; .filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.enter) { if (pluginDecorators && pluginDecorators.enter) {
try { try {
await pluginDecorators.enter.call(this, plugin, ...args); await pluginDecorators.enter.call(this, plugin, ...args);
@ -35,7 +35,7 @@
for (const plugin of pluginList) { for (const plugin of pluginList) {
const pluginDecorators = plugin.functions const pluginDecorators = plugin.functions
.filter(obj => obj.callback === wrappedFunc)[0]; .filter((obj) => obj.callback === wrappedFunc)[0];
if (pluginDecorators && pluginDecorators.leave) { if (pluginDecorators && pluginDecorators.leave) {
try { try {
result = await pluginDecorators.leave.call(this, plugin, result, ...args); result = await pluginDecorators.leave.call(this, plugin, result, ...args);

@ -46,16 +46,7 @@ const webConfig = {
options: { options: {
presets: [ presets: [
['@babel/preset-env', { ['@babel/preset-env', {
targets: { targets: '> 2.5%', // https://github.com/browserslist/browserslist
chrome: 58,
},
useBuiltIns: 'usage',
corejs: 3,
loose: false,
spec: false,
debug: false,
include: [],
exclude: [],
}], }],
], ],
sourceType: 'unambiguous', sourceType: 'unambiguous',

@ -3,7 +3,7 @@ module.exports = {
parser: false, parser: false,
plugins: { plugins: {
'postcss-preset-env': { 'postcss-preset-env': {
browsers: '> 2%', // https://github.com/browserslist/browserslist browsers: '> 2.5%', // https://github.com/browserslist/browserslist
}, },
}, },
}; };

@ -0,0 +1,132 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import {
CombinedState,
Task,
} from '../reducers/interfaces';
import getCore from '../core';
import { getCVATStore } from '../store';
const cvat = getCore();
export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
GET_JOB_FAILED = 'GET_JOB_FAILED',
CHANGE_FRAME = 'CHANGE_FRAME',
CHANGE_FRAME_SUCCESS = 'CHANGE_FRAME_SUCCESS',
CHANGE_FRAME_FAILED = 'CHANGE_FRAME_FAILED',
SWITCH_PLAY = 'SWITCH_PLAY',
CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY',
}
export function switchPlay(playing: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_PLAY,
payload: {
playing,
},
};
}
export function changeFrameAsync(toFrame: number, playing: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const store = getCVATStore();
const state: CombinedState = store.getState();
const { jobInstance } = state.annotation;
const currentFrame = state.annotation.frame;
const frame = Math.max(
Math.min(toFrame, jobInstance.stopFrame),
jobInstance.startFrame,
);
// !playing || state.annotation.playing prevents changing frame on the latest setTimeout
// after playing had become false
if (frame !== currentFrame && (!playing || state.annotation.playing)) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME,
payload: {},
});
try {
const frameData = await jobInstance.frames.get(frame);
const annotations = await jobInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
frame,
frameData,
annotations,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_FAILED,
payload: {
frame,
error,
},
});
}
}
};
}
export function confirmCanvasReady(): AnyAction {
return {
type: AnnotationActionTypes.CONFIRM_CANVAS_READY,
payload: {},
};
}
export function getJobAsync(tid: number, jid: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
});
try {
const store = getCVATStore();
const state: CombinedState = store.getState();
let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0];
if (!task) {
[task] = await cvat.tasks.get({ id: tid });
}
const job = task.jobs
.filter((_job: any) => _job.id === jid)[0];
if (!job) {
throw new Error('Job with specified id does not exist');
}
const frame = Math.min(0, job.startFrame);
const frameData = await job.frames.get(frame);
const annotations = await job.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.GET_JOB_SUCCESS,
payload: {
jobInstance: job,
frameData,
annotations,
frame,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.GET_JOB_FAILED,
payload: {
error,
},
});
}
};
}

@ -0,0 +1,57 @@
import { AnyAction } from 'redux';
import {
GridColor,
} from '../reducers/interfaces';
export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',
SWITCH_GRID = 'SWITCH_GRID',
CHANGE_GRID_SIZE = 'CHANGE_GRID_SIZE',
CHANGE_GRID_COLOR = 'CHANGE_GRID_COLOR',
CHANGE_GRID_OPACITY = 'CHANGE_GRID_OPACITY',
}
export function switchRotateAll(rotateAll: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_ROTATE_ALL,
payload: {
rotateAll,
},
};
}
export function switchGrid(grid: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_GRID,
payload: {
grid,
},
};
}
export function changeGridSize(gridSize: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_GRID_SIZE,
payload: {
gridSize,
},
};
}
export function changeGridColor(gridColor: GridColor): AnyAction {
return {
type: SettingsActionTypes.CHANGE_GRID_COLOR,
payload: {
gridColor,
},
};
}
export function changeGridOpacity(gridOpacity: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_GRID_OPACITY,
payload: {
gridOpacity,
},
};
}

@ -0,0 +1,7 @@
<!-- Drawn in https://www.iconfinder.com/editor/ -->
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(0.07812)" transform-origin="bottom">
<path d="m201.350937,473.371796l0,-434.86451c0,-8.10006 -6.528412,-14.628468 -14.749344,-14.628468l-86.561874,0c-8.220955,0 -14.749359,6.528408 -14.749359,14.628468l0,434.86454c0,8.100037 6.528404,14.749359 14.749359,14.749359l86.561874,0c8.220932,0 14.749344,-6.528412 14.749344,-14.74939z" />
<path d="m423.967224,473.371796l0,-434.86451c0,-8.10006 -6.528381,-14.628468 -14.749329,-14.628468l-86.56189,0c-8.220947,0 -14.749329,6.528408 -14.749329,14.628468l0,434.86454c0,8.100037 6.528381,14.749359 14.749329,14.749359l86.56189,0c8.220947,0 14.749329,-6.528412 14.749329,-14.74939z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 798 B

@ -0,0 +1,9 @@
import {
Canvas,
Rotation,
} from '../../cvat-canvas/src/typescript/canvas';
export {
Canvas,
Rotation,
};

@ -7,8 +7,8 @@ import {
Result, Result,
} from 'antd'; } from 'antd';
import AnnotationTopBarComponent from './top-bar/top-bar'; import AnnotationTopBarContainer from '../../containers/annotation-page/top-bar/top-bar';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace'; import StandardWorkspaceContainer from '../../containers/annotation-page/standard-workspace/standard-workspace';
interface Props { interface Props {
jobInstance: any | null | undefined; jobInstance: any | null | undefined;
@ -44,8 +44,8 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return ( return (
<Layout className='cvat-annotation-page'> <Layout className='cvat-annotation-page'>
<AnnotationTopBarComponent /> <AnnotationTopBarContainer />
<StandardWorkspaceComponent /> <StandardWorkspaceContainer />
</Layout> </Layout>
); );
} }

@ -1,15 +0,0 @@
import React from 'react';
import {
Layout,
} from 'antd';
export default function CanvasWrapperComponent(): JSX.Element {
return (
<Layout.Content
className='cvat-annotation-page-canvas-container'
>
main content
</Layout.Content>
);
}

@ -0,0 +1,139 @@
import React from 'react';
import {
Layout,
} from 'antd';
import {
GridColor,
} from '../../../reducers/interfaces';
import { Canvas } from '../../../canvas';
interface Props {
canvasInstance: Canvas;
jobInstance: any;
annotations: any[];
frameData: any;
grid: boolean;
gridSize: number;
gridColor: GridColor;
gridOpacity: number;
onSetupCanvas: () => void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidMount(): void {
const {
canvasInstance,
} = this.props;
// It's awful approach from the point of view React
// But we do not have another way because cvat-canvas returns regular DOM element
const [wrapper] = window.document
.getElementsByClassName('cvat-annotation-page-canvas-container');
wrapper.appendChild(canvasInstance.html());
this.initialSetup();
this.updateCanvas();
}
public componentDidUpdate(prevProps: Props): void {
const {
grid,
gridSize,
gridColor,
gridOpacity,
canvasInstance,
} = this.props;
if (prevProps.grid !== grid) {
const gridElement = window.document.getElementById('cvat_canvas_grid');
if (gridElement) {
gridElement.style.display = grid ? 'block' : 'none';
}
}
if (prevProps.gridSize !== gridSize) {
canvasInstance.grid(gridSize, gridSize);
}
if (prevProps.gridColor !== gridColor) {
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
}
}
if (prevProps.gridOpacity !== gridOpacity) {
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridPattern) {
gridPattern.style.opacity = `${gridOpacity / 100}`;
}
}
this.updateCanvas();
}
private initialSetup(): void {
const {
grid,
gridSize,
gridColor,
gridOpacity,
canvasInstance,
jobInstance,
onSetupCanvas,
} = this.props;
// Size
canvasInstance.fitCanvas();
// Grid
const gridElement = window.document.getElementById('cvat_canvas_grid');
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridElement) {
gridElement.style.display = grid ? 'block' : 'none';
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
}
canvasInstance.grid(gridSize, gridSize);
// Events
canvasInstance.html().addEventListener('canvas.setup', (): void => {
onSetupCanvas();
if (jobInstance.task.mode === 'annotation') {
canvasInstance.fit();
}
});
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
}, { once: true });
}
private updateCanvas(): void {
const {
annotations,
frameData,
canvasInstance,
} = this.props;
if (frameData !== null) {
canvasInstance.setup(frameData, annotations);
}
}
public render(): JSX.Element {
return (
// This element doesn't have any props
// So, React isn't going to rerender it
// And it's a reason why cvat-canvas appended in mount function works
<Layout.Content
className='cvat-annotation-page-canvas-container'
/>
);
}
}

@ -4,6 +4,7 @@ import {
Icon, Icon,
Layout, Layout,
Tooltip, Tooltip,
Popover,
} from 'antd'; } from 'antd';
import { import {
@ -22,8 +23,22 @@ import {
SplitIcon, SplitIcon,
} from '../../../icons'; } from '../../../icons';
import {
Canvas,
Rotation,
} from '../../../canvas';
interface Props {
canvasInstance: Canvas;
rotateAll: boolean;
}
export default function ControlsSideBarComponent(props: Props): JSX.Element {
const {
rotateAll,
canvasInstance,
} = props;
export default function ControlsSideBarComponent(): JSX.Element {
return ( return (
<Layout.Sider <Layout.Sider
className='cvat-annotation-page-controls-sidebar' className='cvat-annotation-page-controls-sidebar'
@ -38,9 +53,31 @@ export default function ControlsSideBarComponent(): JSX.Element {
<Icon component={MoveIcon} /> <Icon component={MoveIcon} />
</Tooltip> </Tooltip>
<Tooltip overlay='Rotate the image' placement='right'> <Popover
<Icon component={RotateIcon} /> overlayClassName='cvat-annotation-page-controls-rotate'
</Tooltip> placement='right'
content={(
<>
<Icon
className='cvat-annotation-page-controls-rotate-left'
onClick={(): void => canvasInstance
.rotate(Rotation.ANTICLOCKWISE90, rotateAll)}
component={RotateIcon}
/>
<Icon
className='cvat-annotation-page-controls-rotate-right'
onClick={(): void => canvasInstance
.rotate(Rotation.CLOCKWISE90, rotateAll)}
component={RotateIcon}
/>
</>
)}
trigger='hover'
>
<Tooltip overlay='Rotate the image' placement='topRight'>
<Icon component={RotateIcon} />
</Tooltip>
</Popover>
<hr /> <hr />

@ -5,11 +5,15 @@ import {
Layout, Layout,
} from 'antd'; } from 'antd';
interface Props {
onSidebarFoldUnfold(): void;
}
interface State { interface State {
collapsed: boolean; collapsed: boolean;
} }
export default class StandardWorkspaceComponent extends React.PureComponent<{}, State> { export default class StandardWorkspaceComponent extends React.PureComponent<Props, State> {
public constructor(props: any) { public constructor(props: any) {
super(props); super(props);
this.state = { this.state = {
@ -19,6 +23,8 @@ export default class StandardWorkspaceComponent extends React.PureComponent<{},
public render(): JSX.Element { public render(): JSX.Element {
const { collapsed } = this.state; const { collapsed } = this.state;
const { onSidebarFoldUnfold } = this.props;
return ( return (
<Layout.Sider <Layout.Sider
className='cvat-annotation-page-objects-sidebar' className='cvat-annotation-page-objects-sidebar'
@ -35,13 +41,19 @@ export default class StandardWorkspaceComponent extends React.PureComponent<{},
className={`cvat-annotation-page-objects-sidebar className={`cvat-annotation-page-objects-sidebar
ant-layout-sider-zero-width-trigger ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`} ant-layout-sider-zero-width-trigger-left`}
onClick={ onClick={(): void => {
(): void => this.setState( this.setState(
(prevState: State): State => ({ (prevState: State): State => ({
collapsed: !prevState.collapsed, collapsed: !prevState.collapsed,
}), }),
) );
}
const [sidebar] = window.document
.getElementsByClassName('cvat-annotation-page-objects-sidebar');
sidebar.addEventListener('transitionend', () => {
onSidebarFoldUnfold();
}, { once: true });
}}
> >
{collapsed ? <Icon type='menu-fold' title='Show' /> {collapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />} : <Icon type='menu-unfold' title='Hide' />}

@ -5,16 +5,30 @@ import {
Layout, Layout,
} from 'antd'; } from 'antd';
import ControlsSideBarComponent from './controls-side-bar'; import { Canvas } from '../../../canvas';
import CanvasWrapperComponent from './canvas-wrapper-component';
import CanvasWrapperContainer from '../../../containers/annotation-page/standard-workspace/canvas-wrapper';
import ControlsSideBarContainer from '../../../containers/annotation-page/standard-workspace/controls-side-bar';
import ObjectSideBarComponent from './objects-side-bar/objects-side-bar'; import ObjectSideBarComponent from './objects-side-bar/objects-side-bar';
export default function StandardWorkspaceComponent(): JSX.Element { interface Props {
canvasInstance: Canvas;
}
export default function StandardWorkspaceComponent(props: Props): JSX.Element {
const {
canvasInstance,
} = props;
return ( return (
<Layout> <Layout hasSider>
<ControlsSideBarComponent /> <ControlsSideBarContainer />
<CanvasWrapperComponent /> <CanvasWrapperContainer />
<ObjectSideBarComponent /> <ObjectSideBarComponent
onSidebarFoldUnfold={(): void => {
canvasInstance.fitCanvas();
}}
/>
</Layout> </Layout>
); );
} }

@ -10,6 +10,7 @@
left: auto; left: auto;
background-color: $background-color-2; background-color: $background-color-2;
border-left: 1px solid $border-color-1; border-left: 1px solid $border-color-1;
z-index: 2;
} }
.cvat-annotation-page-controls-sidebar { .cvat-annotation-page-controls-sidebar {
@ -37,3 +38,28 @@
} }
} }
} }
.cvat-annotation-page-controls-rotate-left,
.cvat-annotation-page-controls-rotate-right {
transform: scale(0.65);
border-radius: 5px;
&:hover {
background: $header-color;
transform: scale(0.75);
}
&:active {
transform: scale(0.65);
}
}
.cvat-annotation-page-controls-rotate >
.ant-popover-content >
.ant-popover-inner > div >
.ant-popover-inner-content {
padding: 0px;
}
.cvat-annotation-page-controls-rotate-right > svg {
transform: scaleX(-1);
}

@ -11,6 +11,7 @@ import {
Select, Select,
} from 'antd'; } from 'antd';
import { SliderValue } from 'antd/lib/slider';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { import {
@ -22,6 +23,7 @@ import {
PlaycontrolBackJumpIcon, PlaycontrolBackJumpIcon,
PlaycontrolPreviousIcon, PlaycontrolPreviousIcon,
PlaycontrolPlayIcon, PlaycontrolPlayIcon,
PlaycontrolPauseIcon,
PlaycontrolNextIcon, PlaycontrolNextIcon,
PlaycontrolForwardJumpIcon, PlaycontrolForwardJumpIcon,
PlaycontrolLastIcon, PlaycontrolLastIcon,
@ -29,7 +31,37 @@ import {
FullscreenIcon, FullscreenIcon,
} from '../../../icons'; } from '../../../icons';
export default function AnnotationPageComponent(): JSX.Element { interface Props {
jobInstance: any;
frame: number;
frameStep: number;
playing: boolean;
canvasIsReady: boolean;
onChangeFrame(frame: number, playing: boolean): void;
onSwitchPlay(playing: boolean): void;
}
export default function AnnotationTopBarComponent(props: Props): JSX.Element {
const {
jobInstance,
frame,
frameStep,
playing,
canvasIsReady,
onChangeFrame,
onSwitchPlay,
} = props;
if (playing && canvasIsReady) {
if (frame < jobInstance.stopFrame) {
setTimeout(() => {
onChangeFrame(frame + 1, true);
}, 30);
} else {
onSwitchPlay(false);
}
}
return ( return (
<Layout.Header className='cvat-annotation-page-header'> <Layout.Header className='cvat-annotation-page-header'>
<Row type='flex' justify='space-between'> <Row type='flex' justify='space-between'>
@ -54,18 +86,120 @@ export default function AnnotationPageComponent(): JSX.Element {
<Col className='cvat-annotation-header-player-group'> <Col className='cvat-annotation-header-player-group'>
<Row type='flex' align='middle'> <Row type='flex' align='middle'>
<Col className='cvat-annotation-header-player-buttons'> <Col className='cvat-annotation-header-player-buttons'>
<Icon component={PlaycontrolFirstIcon} /> <Tooltip overlay='Go to the first frame'>
<Icon component={PlaycontrolBackJumpIcon} /> <Icon
<Icon component={PlaycontrolPreviousIcon} /> component={PlaycontrolFirstIcon}
<Icon component={PlaycontrolPlayIcon} /> onClick={(): void => {
<Icon component={PlaycontrolNextIcon} /> if (jobInstance.startFrame !== frame) {
<Icon component={PlaycontrolForwardJumpIcon} /> onSwitchPlay(false);
<Icon component={PlaycontrolLastIcon} /> onChangeFrame(jobInstance.startFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go back with a step'>
<Icon
component={PlaycontrolBackJumpIcon}
onClick={(): void => {
const newFrame = Math
.max(jobInstance.startFrame, frame - frameStep);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go back'>
<Icon
component={PlaycontrolPreviousIcon}
onClick={(): void => {
const newFrame = Math
.max(jobInstance.startFrame, frame - 1);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
{!playing
? (
<Tooltip overlay='Play'>
<Icon
component={PlaycontrolPlayIcon}
onClick={(): void => {
if (frame < jobInstance.stopFrame) {
onSwitchPlay(true);
}
}}
/>
</Tooltip>
)
: (
<Tooltip overlay='Pause'>
<Icon
component={PlaycontrolPauseIcon}
onClick={(): void => {
onSwitchPlay(false);
}}
/>
</Tooltip>
)
}
<Tooltip overlay='Go next'>
<Icon
component={PlaycontrolNextIcon}
onClick={(): void => {
const newFrame = Math
.min(jobInstance.stopFrame, frame + 1);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go next with a step'>
<Icon
component={PlaycontrolForwardJumpIcon}
onClick={(): void => {
const newFrame = Math
.min(jobInstance.stopFrame, frame + frameStep);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go to the last frame'>
<Icon
component={PlaycontrolLastIcon}
onClick={(): void => {
if (jobInstance.stopFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(jobInstance.stopFrame, false);
}
}}
/>
</Tooltip>
</Col> </Col>
<Col className='cvat-annotation-header-player-controls'> <Col className='cvat-annotation-header-player-controls'>
<Row type='flex'> <Row type='flex'>
<Col> <Col>
<Slider className='cvat-annotation-header-player-slider' tipFormatter={null} /> <Slider
className='cvat-annotation-header-player-slider'
min={jobInstance.startFrame}
max={jobInstance.stopFrame}
value={frame || 0}
onChange={(value: SliderValue): void => {
onSwitchPlay(false);
onChangeFrame(value as number, false);
}}
/>
</Col> </Col>
</Row> </Row>
<Row type='flex' justify='space-around'> <Row type='flex' justify='space-around'>
@ -77,7 +211,16 @@ export default function AnnotationPageComponent(): JSX.Element {
</Row> </Row>
</Col> </Col>
<Col> <Col>
<Input className='cvat-annotation-header-frame-selector' type='number' /> <Input
className='cvat-annotation-header-frame-selector'
type='number'
value={frame || 0}
// https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => {
onSwitchPlay(false);
onChangeFrame(+e.target.value, false);
}}
/>
</Col> </Col>
</Row> </Row>
</Col> </Col>

@ -13,6 +13,7 @@ import {
notification, notification,
} from 'antd'; } from 'antd';
import SettingsPageComponent from './settings-page/settings-page';
import TasksPageContainer from '../containers/tasks-page/tasks-page'; import TasksPageContainer from '../containers/tasks-page/tasks-page';
import CreateTaskPageContainer from '../containers/create-task-page/create-task-page'; import CreateTaskPageContainer from '../containers/create-task-page/create-task-page';
import TaskPageContainer from '../containers/task-page/task-page'; import TaskPageContainer from '../containers/task-page/task-page';
@ -259,6 +260,7 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
<HeaderContainer> </HeaderContainer> <HeaderContainer> </HeaderContainer>
<Layout.Content> <Layout.Content>
<Switch> <Switch>
<Route exact path='/settings' component={SettingsPageComponent} />
<Route exact path='/tasks' component={TasksPageContainer} /> <Route exact path='/tasks' component={TasksPageContainer} />
<Route exact path='/tasks/create' component={CreateTaskPageContainer} /> <Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} /> <Route exact path='/tasks/:id' component={TaskPageContainer} />

@ -52,7 +52,11 @@ function HeaderContainer(props: Props): JSX.Element {
const menu = ( const menu = (
<Menu className='cvat-header-menu' mode='vertical'> <Menu className='cvat-header-menu' mode='vertical'>
<Menu.Item> <Menu.Item
onClick={
(): void => props.history.push('/settings')
}
>
<Icon type='setting' /> <Icon type='setting' />
Settings Settings
</Menu.Item> </Menu.Item>

@ -0,0 +1,217 @@
import React from 'react';
import {
Row,
Col,
Checkbox,
Slider,
Select,
InputNumber,
Icon,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import {
PlaycontrolBackJumpIcon,
PlaycontrolForwardJumpIcon,
} from '../../icons';
import {
FrameSpeed,
GridColor,
} from '../../reducers/interfaces';
interface Props {
frameStep: number;
frameSpeed: FrameSpeed;
resetZoom: boolean;
rotateAll: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
gridOpacity: number;
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
onChangeFrameStep(step: number): void;
onChangeFrameSpeed(speed: FrameSpeed): void;
onSwitchResetZoom(enabled: boolean): void;
onSwitchRotateAll(rotateAll: boolean): void;
onSwitchGrid(grid: boolean): void;
onChangeGridSize(gridSize: number): void;
onChangeGridColor(gridColor: GridColor): void;
onChangeGridOpacity(gridOpacity: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
}
export default function PlayerSettingsComponent(props: Props): JSX.Element {
const {
frameStep,
frameSpeed,
resetZoom,
rotateAll,
grid,
gridSize,
gridColor,
gridOpacity,
brightnessLevel,
contrastLevel,
saturationLevel,
onSwitchRotateAll,
onSwitchGrid,
onChangeGridSize,
onChangeGridColor,
onChangeGridOpacity,
} = props;
return (
<div className='cvat-player-settings'>
<Row type='flex' align='bottom' className='cvat-player-settings-step'>
<Col>
<Text className='cvat-text-color'> Player step </Text>
<InputNumber min={2} max={1000} value={frameStep} />
</Col>
<Col offset={1}>
<Text type='secondary'>
Number of frames skipped when selecting
<Icon component={PlaycontrolBackJumpIcon} />
or
<Icon component={PlaycontrolForwardJumpIcon} />
</Text>
</Col>
</Row>
<Row type='flex' align='middle' className='cvat-player-settings-speed'>
<Col>
<Text className='cvat-text-color'> Player speed </Text>
<Select value={frameSpeed}>
<Select.Option key='fastest' value={FrameSpeed.Fastest}>Fastest</Select.Option>
<Select.Option key='fast' value={FrameSpeed.Fast}>Fast</Select.Option>
<Select.Option key='usual' value={FrameSpeed.Usual}>Usual</Select.Option>
<Select.Option key='slow' value={FrameSpeed.Slow}>Slow</Select.Option>
<Select.Option key='slower' value={FrameSpeed.Slower}>Slower</Select.Option>
<Select.Option key='slowest' value={FrameSpeed.Slowest}>Slowest</Select.Option>
</Select>
</Col>
</Row>
<Row type='flex'>
<Col>
<Checkbox
className='cvat-text-color cvat-player-settings-grid'
checked={grid}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchGrid(event.target.checked);
}}
>
Show grid
</Checkbox>
</Col>
</Row>
<Row type='flex' justify='space-between'>
<Col span={8} className='cvat-player-settings-grid-size'>
<Text className='cvat-text-color'> Grid size </Text>
<InputNumber
min={5}
max={1000}
step={1}
value={gridSize}
onChange={(value: number | undefined): void => {
if (value) {
onChangeGridSize(value);
}
}}
/>
</Col>
<Col span={8} className='cvat-player-settings-grid-color'>
<Text className='cvat-text-color'> Grid color </Text>
<Select
value={gridColor}
onChange={(color: GridColor): void => {
onChangeGridColor(color);
}}
>
<Select.Option key='white' value={GridColor.White}>White</Select.Option>
<Select.Option key='black' value={GridColor.Black}>Black</Select.Option>
<Select.Option key='red' value={GridColor.Red}>Red</Select.Option>
<Select.Option key='green' value={GridColor.Green}>Green</Select.Option>
<Select.Option key='blue' value={GridColor.Blue}>Blue</Select.Option>
</Select>
</Col>
<Col span={8} className='cvat-player-settings-grid-opacity'>
<Text className='cvat-text-color'> Grid opacity </Text>
<Slider
min={0}
max={100}
value={gridOpacity}
onChange={(value: number | [number, number]): void => {
onChangeGridOpacity(value as number);
}}
/>
<Text className='cvat-text-color'>{`${gridOpacity} %`}</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Row className='cvat-player-settings-reset-zoom'>
<Col className='cvat-player-settings-reset-zoom-checkbox'>
<Checkbox
className='cvat-text-color'
checked={resetZoom}
>
Reset zoom
</Checkbox>
</Col>
<Col>
<Text type='secondary'> Fit image after changing frame </Text>
</Col>
</Row>
</Col>
<Col offset={5}>
<Row className='cvat-player-settings-rotate-all'>
<Col className='cvat-player-settings-rotate-all-checkbox'>
<Checkbox
className='cvat-text-color'
checked={rotateAll}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchRotateAll(event.target.checked);
}}
>
Rotate all images
</Checkbox>
</Col>
<Col>
<Text type='secondary'> Rotate all images simultaneously </Text>
</Col>
</Row>
</Col>
</Row>
<Row className='cvat-player-settings-brightness'>
<Col className='cvat-text-color'>
Brightness
</Col>
<Col>
<Slider min={0} max={100} value={brightnessLevel} />
</Col>
</Row>
<Row className='cvat-player-settings-contrast'>
<Col className='cvat-text-color'>
Contrast
</Col>
<Col>
<Slider min={0} max={100} value={contrastLevel} />
</Col>
</Row>
<Row className='cvat-player-settings-saturation'>
<Col className='cvat-text-color'>
Saturation
</Col>
<Col>
<Slider min={0} max={100} value={saturationLevel} />
</Col>
</Row>
</div>
);
}

@ -0,0 +1,79 @@
import './styles.scss';
import React from 'react';
import {
Row,
Col,
Tabs,
Icon,
Button,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import WorkspaceSettingsContainer from '../../containers/settings-page/workspace-settings';
import PlayerSettingsContainer from '../../containers/settings-page/player-settings';
function SettingsPage(props: RouteComponentProps): JSX.Element {
return (
<div className='cvat-settings-page'>
<Row type='flex' justify='center'>
<Col>
<Text className='cvat-title'> Settings </Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col md={14} lg={12} xl={10} xxl={9}>
<Tabs
type='card'
tabBarStyle={{ marginBottom: '0px', marginLeft: '-1px' }}
>
<Tabs.TabPane
tab={
(
<span>
<Icon type='play-circle' />
<Text>Player</Text>
</span>
)
}
key='player'
>
<PlayerSettingsContainer />
</Tabs.TabPane>
<Tabs.TabPane
tab={
(
<span>
<Icon type='laptop' />
<Text>Workspace</Text>
</span>
)
}
key='workspace'
>
<WorkspaceSettingsContainer />
</Tabs.TabPane>
</Tabs>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col md={14} lg={12} xl={10} xxl={9} className='cvat-settings-page-back-button-wrapper'>
<Button
className='cvat-settings-page-back-button'
type='primary'
onClick={(): void => {
props.history.goBack();
}}
>
Go Back
</Button>
</Col>
</Row>
</div>
);
}
export default withRouter(SettingsPage);

@ -0,0 +1,86 @@
@import '../../base.scss';
.cvat-settings-page {
> div:nth-child(1) {
margin-top: 30px;
margin-bottom: 10px;
}
}
.cvat-workspace-settings, .cvat-player-settings {
width: 100%;
height: max-content;
background: $background-color-1;
padding: 50px;
}
.cvat-player-settings-grid,
.cvat-workspace-settings-auto-save,
.cvat-workspace-settings-show-interpolated-checkbox {
margin-bottom: 10px;
}
.cvat-player-settings-grid-size,
.cvat-player-settings-grid-color,
.cvat-player-settings-grid-opacity,
.cvat-player-settings-step,
.cvat-player-settings-speed,
.cvat-player-settings-reset-zoom,
.cvat-player-settings-rotate-all,
.cvat-workspace-settings-show-interpolated,
.cvat-workspace-settings-aam-zoom-margin,
.cvat-workspace-settings-auto-save-interval {
margin-bottom: 25px;
}
.cvat-player-settings-grid-size,
.cvat-player-settings-grid-color,
.cvat-player-settings-grid-opacity {
display: grid;
justify-items: start;
}
.cvat-player-settings-grid-color {
> .ant-select {
width: 150px;
}
}
.cvat-player-settings-grid-opacity {
> .ant-slider {
width: 150px;
}
}
.cvat-player-settings-step,
.cvat-player-settings-speed {
> div {
display: grid;
justify-items: start;
}
}
.cvat-player-settings-step > div > span > i {
vertical-align: -1em;
transform: scale(0.3);
}
.cvat-player-settings-speed > div > .ant-select {
width: 90px;
}
.cvat-player-settings-brightness,
.cvat-player-settings-contrast,
.cvat-player-settings-saturation {
width: 40%;
}
.cvat-settings-page-back-button {
width: 100px;
margin-top: 15px;
}
.cvat-settings-page-back-button-wrapper {
display: flex;
justify-content: flex-end;
}

@ -0,0 +1,76 @@
import React from 'react';
import {
Row,
Col,
Checkbox,
InputNumber,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface Props {
autoSave: boolean;
autoSaveInterval: number;
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
onSwitchAutoSave(enabled: boolean): void;
onChangeAutoSaveInterval(interval: number): void;
onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void;
}
export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
const {
autoSave,
autoSaveInterval,
aamZoomMargin,
showAllInterpolationTracks,
} = props;
return (
<div className='cvat-workspace-settings'>
<Row type='flex'>
<Col>
<Checkbox
className='cvat-text-color cvat-workspace-settings-auto-save'
checked={autoSave}
>
Enable auto save
</Checkbox>
</Col>
</Row>
<Row type='flex'>
<Col className='cvat-workspace-settings-auto-save-interval'>
<Text type='secondary'> Auto save every </Text>
<InputNumber
min={5}
max={60}
step={1}
value={Math.round(autoSaveInterval / (60 * 1000))}
/>
<Text type='secondary'> minutes </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-show-interpolated'>
<Col className='cvat-workspace-settings-show-interpolated-checkbox'>
<Checkbox
className='cvat-text-color'
checked={showAllInterpolationTracks}
>
Show all interpolation tracks
</Checkbox>
</Col>
<Col>
<Text type='secondary'> Show hidden interpolated objects in the side panel </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-aam-zoom-margin'>
<Col>
<Text className='cvat-text-color'> Attribute annotation mode (AAM) zoom margin </Text>
<InputNumber min={0} max={1000} value={aamZoomMargin} />
</Col>
</Row>
</div>
);
}

@ -27,8 +27,6 @@ interface TaskPageComponentProps {
type Props = TaskPageComponentProps & RouteComponentProps<{id: string}>; type Props = TaskPageComponentProps & RouteComponentProps<{id: string}>;
class TaskPageComponent extends React.PureComponent<Props> { class TaskPageComponent extends React.PureComponent<Props> {
private attempts = 0;
public componentDidUpdate(): void { public componentDidUpdate(): void {
const { const {
deleteActivity, deleteActivity,

@ -4,11 +4,10 @@ import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from '../../components/annotation-page/annotation-page'; import AnnotationPageComponent from '../../components/annotation-page/annotation-page';
import { getTasksAsync } from '../../actions/tasks-actions'; import { getJobAsync } from '../../actions/annotation-actions';
import { import {
CombinedState, CombinedState,
Task,
} from '../../reducers/interfaces'; } from '../../reducers/interfaces';
type OwnProps = RouteComponentProps<{ type OwnProps = RouteComponentProps<{
@ -25,46 +24,23 @@ interface DispatchToProps {
getJob(): void; getJob(): void;
} }
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { tasks } = state; const { annotation } = state;
const {
gettingQuery,
current,
} = tasks;
const { params } = own.match;
const taskID = +params.tid;
const jobID = +params.jid;
const filteredTasks = current
.filter((_task: Task) => _task.instance.id === taskID);
const task = filteredTasks[0] || (gettingQuery.id === taskID || Number.isNaN(taskID)
? undefined : null);
const job = task ? task.instance.jobs
.filter((_job: any) => _job.id === jobID)[0] : task;
return { return {
jobInstance: job, jobInstance: annotation.jobInstance,
fetching: tasks.fetching, fetching: annotation.jobFetching,
}; };
} }
function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
const { params } = own.match; const { params } = own.match;
const taskID = +params.tid; const taskID = +params.tid;
const jobID = +params.jid;
return { return {
getJob(): void { getJob(): void {
dispatch(getTasksAsync({ dispatch(getJobAsync(taskID, jobID));
id: taskID,
page: 1,
search: null,
owner: null,
assignee: null,
name: null,
status: null,
mode: null,
}));
}, },
}; };
} }

@ -0,0 +1,75 @@
import React from 'react';
import { connect } from 'react-redux';
import CanvasWrapperComponent from '../../../components/annotation-page/standard-workspace/canvas-wrapper';
import {
confirmCanvasReady,
} from '../../../actions/annotation-actions';
import {
GridColor,
CombinedState,
} from '../../../reducers/interfaces';
import { Canvas } from '../../../canvas';
interface StateToProps {
canvasInstance: Canvas;
jobInstance: any;
annotations: any[];
frameData: any;
grid: boolean;
gridSize: number;
gridColor: GridColor;
gridOpacity: number;
}
interface DispatchToProps {
onSetupCanvas(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
canvasInstance,
jobInstance,
frameData,
annotations,
} = state.annotation;
const {
grid,
gridSize,
gridColor,
gridOpacity,
} = state.settings.player;
return {
canvasInstance,
jobInstance,
frameData,
annotations,
grid,
gridSize,
gridColor,
gridOpacity,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onSetupCanvas(): void {
dispatch(confirmCanvasReady());
},
};
}
function CanvasWrapperContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CanvasWrapperComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CanvasWrapperContainer);

@ -0,0 +1,35 @@
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from '../../../canvas';
import ControlsSideBarComponent from '../../../components/annotation-page/standard-workspace/controls-side-bar';
import { CombinedState } from '../../../reducers/interfaces';
interface StateToProps {
canvasInstance: Canvas;
rotateAll: boolean;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation,
settings,
} = state;
return {
rotateAll: settings.player.rotateAll,
canvasInstance: annotation.canvasInstance,
};
}
function StandardWorkspaceContainer(props: StateToProps): JSX.Element {
return (
<ControlsSideBarComponent {...props} />
);
}
export default connect(
mapStateToProps,
)(StandardWorkspaceContainer);

@ -0,0 +1,35 @@
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from '../../../canvas';
import StandardWorkspaceComponent from '../../../components/annotation-page/standard-workspace/standard-workspace';
import { CombinedState } from '../../../reducers/interfaces';
interface StateToProps {
canvasInstance: Canvas;
}
function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state;
return {
canvasInstance: annotation.canvasInstance,
};
}
function StandardWorkspaceContainer(props: StateToProps): JSX.Element {
const {
canvasInstance,
} = props;
return (
<StandardWorkspaceComponent
canvasInstance={canvasInstance}
/>
);
}
export default connect(
mapStateToProps,
)(StandardWorkspaceContainer);

@ -0,0 +1,78 @@
import React from 'react';
import { connect } from 'react-redux';
import {
changeFrameAsync,
switchPlay as switchPlayAction,
} from '../../../actions/annotation-actions';
import AnnotationTopBarComponent from '../../../components/annotation-page/top-bar/top-bar';
import { CombinedState } from '../../../reducers/interfaces';
interface StateToProps {
jobInstance: any;
frame: number;
frameStep: number;
playing: boolean;
canvasIsReady: boolean;
}
interface DispatchToProps {
onChangeFrame(frame: number, playing: boolean): void;
onSwitchPlay(playing: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation,
settings,
} = state;
return {
jobInstance: annotation.jobInstance,
frame: annotation.frame as number, // is number when jobInstance specified
frameStep: settings.player.frameStep,
playing: annotation.playing,
canvasIsReady: annotation.canvasIsReady,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onChangeFrame(frame: number, playing: boolean): void {
dispatch(changeFrameAsync(frame, playing));
},
onSwitchPlay(playing: boolean): void {
dispatch(switchPlayAction(playing));
},
};
}
function AnnotationTopBarContainer(props: StateToProps & DispatchToProps): JSX.Element {
const {
jobInstance,
frame,
frameStep,
playing,
canvasIsReady,
onChangeFrame,
onSwitchPlay,
} = props;
return (
<AnnotationTopBarComponent
jobInstance={jobInstance}
frame={frame}
frameStep={frameStep}
playing={playing}
canvasIsReady={canvasIsReady}
onChangeFrame={onChangeFrame}
onSwitchPlay={onSwitchPlay}
/>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationTopBarContainer);

@ -0,0 +1,114 @@
import React from 'react';
import { connect } from 'react-redux';
import PlayerSettingsComponent from '../../components/settings-page/player-settings';
import {
switchRotateAll,
switchGrid,
changeGridSize,
changeGridColor,
changeGridOpacity,
} from '../../actions/settings-actions';
import {
CombinedState,
FrameSpeed,
GridColor,
} from '../../reducers/interfaces';
interface StateToProps {
frameStep: number;
frameSpeed: FrameSpeed;
resetZoom: boolean;
rotateAll: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
gridOpacity: number;
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
}
interface DispatchToProps {
onChangeFrameStep(step: number): void;
onChangeFrameSpeed(speed: FrameSpeed): void;
onSwitchResetZoom(enabled: boolean): void;
onSwitchRotateAll(rotateAll: boolean): void;
onSwitchGrid(grid: boolean): void;
onChangeGridSize(gridSize: number): void;
onChangeGridColor(gridColor: GridColor): void;
onChangeGridOpacity(gridOpacity: number): void;
onChangeBrightnessLevel(level: number): void;
onChangeContrastLevel(level: number): void;
onChangeSaturationLevel(level: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const { player } = state.settings;
return {
...player,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
// will be implemented
// eslint-disable-next-line
onChangeFrameStep(step: number): void {
},
// will be implemented
// eslint-disable-next-line
onChangeFrameSpeed(speed: FrameSpeed): void {
},
// will be implemented
// eslint-disable-next-line
onSwitchResetZoom(enabled: boolean): void {
},
onSwitchRotateAll(rotateAll: boolean): void {
dispatch(switchRotateAll(rotateAll));
},
onSwitchGrid(grid: boolean): void {
dispatch(switchGrid(grid));
},
onChangeGridSize(gridSize: number): void {
dispatch(changeGridSize(gridSize));
},
onChangeGridColor(gridColor: GridColor): void {
dispatch(changeGridColor(gridColor));
},
onChangeGridOpacity(gridOpacity: number): void {
dispatch(changeGridOpacity(gridOpacity));
},
// will be implemented
// eslint-disable-next-line
onChangeBrightnessLevel(level: number): void {
},
// will be implemented
// eslint-disable-next-line
onChangeContrastLevel(level: number): void {
},
// will be implemented
// eslint-disable-next-line
onChangeSaturationLevel(level: number): void {
},
};
}
function PlayerSettingsContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<PlayerSettingsComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(PlayerSettingsContainer);

@ -0,0 +1,75 @@
import React from 'react';
import { connect } from 'react-redux';
import {
CombinedState,
} from '../../reducers/interfaces';
import WorkspaceSettingsComponent from '../../components/settings-page/workspace-settings';
interface StateToProps {
autoSave: boolean;
autoSaveInterval: number;
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
}
interface DispatchToProps {
onSwitchAutoSave(enabled: boolean): void;
onChangeAutoSaveInterval(interval: number): void;
onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const { workspace } = state.settings;
const {
autoSave,
autoSaveInterval,
aamZoomMargin,
showAllInterpolationTracks,
} = workspace;
return {
autoSave,
autoSaveInterval,
aamZoomMargin,
showAllInterpolationTracks,
};
}
function mapDispatchToProps(): DispatchToProps {
return {
// will be implemented
// eslint-disable-next-line
onSwitchAutoSave(enabled: boolean): void {
},
// will be implemented
// eslint-disable-next-line
onChangeAutoSaveInterval(interval: number): void {
},
// will be implemented
// eslint-disable-next-line
onChangeAAMZoomMargin(margin: number): void {
},
// will be implemented
// eslint-disable-next-line
onSwitchShowingInterpolatedTracks(enabled: boolean): void {
},
};
}
function WorkspaceSettingsContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<WorkspaceSettingsComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(WorkspaceSettingsContainer);

@ -25,13 +25,13 @@ import SVGPlaycontrolFirstIcon from './assets/playcontrol-first-icon.svg';
import SVGPlaycontrolBackJumpIcon from './assets/playcontrol-back-jump-icon.svg'; import SVGPlaycontrolBackJumpIcon from './assets/playcontrol-back-jump-icon.svg';
import SVGPlaycontrolPreviousIcon from './assets/playcontrol-previous-icon.svg'; import SVGPlaycontrolPreviousIcon from './assets/playcontrol-previous-icon.svg';
import SVGPlaycontrolPlayIcon from './assets/playcontrol-play-icon.svg'; import SVGPlaycontrolPlayIcon from './assets/playcontrol-play-icon.svg';
import SVGPlaycontrolPauseIcon from './assets/playcontrol-pause-icon.svg';
import SVGPlaycontrolNextIcon from './assets/playcontrol-next-icon.svg'; import SVGPlaycontrolNextIcon from './assets/playcontrol-next-icon.svg';
import SVGPlaycontrolForwardJumpIcon from './assets/playcontrol-forward-jump-icon.svg'; import SVGPlaycontrolForwardJumpIcon from './assets/playcontrol-forward-jump-icon.svg';
import SVGPlaycontrolLastIcon from './assets/playcontrol-last-icon.svg'; import SVGPlaycontrolLastIcon from './assets/playcontrol-last-icon.svg';
import SVGInfoIcon from './assets/info-icon.svg'; import SVGInfoIcon from './assets/info-icon.svg';
import SVGFullscreenIcon from './assets/fullscreen-icon.svg'; import SVGFullscreenIcon from './assets/fullscreen-icon.svg';
export const CVATLogo = (): JSX.Element => <SVGCVATLogo />; export const CVATLogo = (): JSX.Element => <SVGCVATLogo />;
export const AccountIcon = (): JSX.Element => <SVGAccountIcon />; export const AccountIcon = (): JSX.Element => <SVGAccountIcon />;
export const EmptyTasksIcon = (): JSX.Element => <SVGEmptyTasksIcon />; export const EmptyTasksIcon = (): JSX.Element => <SVGEmptyTasksIcon />;
@ -56,6 +56,7 @@ export const RedoIcon = (): JSX.Element => <SVGRedoIcon />;
export const PlaycontrolFirstIcon = (): JSX.Element => <SVGPlaycontrolFirstIcon />; export const PlaycontrolFirstIcon = (): JSX.Element => <SVGPlaycontrolFirstIcon />;
export const PlaycontrolBackJumpIcon = (): JSX.Element => <SVGPlaycontrolBackJumpIcon />; export const PlaycontrolBackJumpIcon = (): JSX.Element => <SVGPlaycontrolBackJumpIcon />;
export const PlaycontrolPreviousIcon = (): JSX.Element => <SVGPlaycontrolPreviousIcon />; export const PlaycontrolPreviousIcon = (): JSX.Element => <SVGPlaycontrolPreviousIcon />;
export const PlaycontrolPauseIcon = (): JSX.Element => <SVGPlaycontrolPauseIcon />;
export const PlaycontrolPlayIcon = (): JSX.Element => <SVGPlaycontrolPlayIcon />; export const PlaycontrolPlayIcon = (): JSX.Element => <SVGPlaycontrolPlayIcon />;
export const PlaycontrolNextIcon = (): JSX.Element => <SVGPlaycontrolNextIcon />; export const PlaycontrolNextIcon = (): JSX.Element => <SVGPlaycontrolNextIcon />;
export const PlaycontrolForwardJumpIcon = (): JSX.Element => <SVGPlaycontrolForwardJumpIcon />; export const PlaycontrolForwardJumpIcon = (): JSX.Element => <SVGPlaycontrolForwardJumpIcon />;

@ -0,0 +1,87 @@
import { AnyAction } from 'redux';
import { Canvas } from '../canvas';
import { AnnotationState } from './interfaces';
import { AnnotationActionTypes } from '../actions/annotation-actions';
const defaultState: AnnotationState = {
canvasInstance: new Canvas(),
canvasIsReady: false,
jobInstance: null,
frame: 0,
playing: false,
annotations: [],
frameData: null,
dataFetching: false,
jobFetching: false,
};
export default (state = defaultState, action: AnyAction): AnnotationState => {
switch (action.type) {
case AnnotationActionTypes.GET_JOB: {
return {
...defaultState,
jobFetching: true,
};
}
case AnnotationActionTypes.GET_JOB_SUCCESS: {
return {
...defaultState,
jobFetching: false,
jobInstance: action.payload.jobInstance,
frame: action.payload.frame,
frameData: action.payload.frameData,
annotations: action.payload.annotations,
};
}
case AnnotationActionTypes.GET_JOB_FAILED: {
return {
...state,
jobInstance: undefined,
jobFetching: false,
};
}
case AnnotationActionTypes.CHANGE_FRAME: {
return {
...state,
frameData: null,
annotations: [],
dataFetching: true,
canvasIsReady: false,
};
}
case AnnotationActionTypes.CHANGE_FRAME_SUCCESS: {
return {
...state,
frame: action.payload.frame,
annotations: action.payload.annotations,
frameData: action.payload.frameData,
dataFetching: false,
};
}
case AnnotationActionTypes.CHANGE_FRAME_FAILED: {
return {
...state,
dataFetching: false,
}; // add notification if failed
}
case AnnotationActionTypes.SWITCH_PLAY: {
return {
...state,
playing: action.payload.playing,
};
}
case AnnotationActionTypes.CONFIRM_CANVAS_READY: {
return {
...state,
canvasIsReady: true,
};
}
default: {
return {
...state,
};
}
}
};

@ -1,3 +1,5 @@
import { Canvas } from '../canvas';
export interface AuthState { export interface AuthState {
initialized: boolean; initialized: boolean;
fetching: boolean; fetching: boolean;
@ -197,6 +199,61 @@ export interface NotificationsState {
}; };
} }
export interface AnnotationState {
canvasInstance: Canvas;
canvasIsReady: boolean;
jobInstance: any | null | undefined;
frameData: any | null;
frame: number;
playing: boolean;
annotations: any[];
jobFetching: boolean;
dataFetching: boolean;
}
export enum GridColor {
White = 'White',
Black = 'Black',
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}
export enum FrameSpeed {
Fastest = 100,
Fast = 50,
Usual = 25,
Slow = 15,
Slower = 12,
Slowest = 1,
}
export interface PlayerSettingsState {
frameStep: number;
frameSpeed: FrameSpeed;
resetZoom: boolean;
rotateAll: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
gridOpacity: number; // in %
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
}
export interface WorkspaceSettingsState {
autoSave: boolean;
autoSaveInterval: number; // in ms
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
}
export interface SettingsState {
workspace: WorkspaceSettingsState;
player: PlayerSettingsState;
}
export interface CombinedState { export interface CombinedState {
auth: AuthState; auth: AuthState;
tasks: TasksState; tasks: TasksState;
@ -206,4 +263,6 @@ export interface CombinedState {
plugins: PluginsState; plugins: PluginsState;
models: ModelsState; models: ModelsState;
notifications: NotificationsState; notifications: NotificationsState;
annotation: AnnotationState;
settings: SettingsState;
} }

@ -7,6 +7,8 @@ import formatsReducer from './formats-reducer';
import pluginsReducer from './plugins-reducer'; import pluginsReducer from './plugins-reducer';
import modelsReducer from './models-reducer'; import modelsReducer from './models-reducer';
import notificationsReducer from './notifications-reducer'; import notificationsReducer from './notifications-reducer';
import annotationReducer from './annotation-reducer';
import settingsReducer from './settings-reducer';
export default function createRootReducer(): Reducer { export default function createRootReducer(): Reducer {
return combineReducers({ return combineReducers({
@ -18,5 +20,7 @@ export default function createRootReducer(): Reducer {
plugins: pluginsReducer, plugins: pluginsReducer,
models: modelsReducer, models: modelsReducer,
notifications: notificationsReducer, notifications: notificationsReducer,
annotation: annotationReducer,
settings: settingsReducer,
}); });
} }

@ -0,0 +1,85 @@
import { AnyAction } from 'redux';
import { SettingsActionTypes } from '../actions/settings-actions';
import {
SettingsState,
GridColor,
FrameSpeed,
} from './interfaces';
const defaultState: SettingsState = {
workspace: {
autoSave: false,
autoSaveInterval: 15 * 60 * 1000,
aamZoomMargin: 100,
showAllInterpolationTracks: false,
},
player: {
frameStep: 10,
frameSpeed: FrameSpeed.Usual,
resetZoom: false,
rotateAll: false,
grid: false,
gridSize: 100,
gridColor: GridColor.White,
gridOpacity: 0,
brightnessLevel: 50,
contrastLevel: 50,
saturationLevel: 50,
},
};
export default (state = defaultState, action: AnyAction): SettingsState => {
switch (action.type) {
case SettingsActionTypes.SWITCH_ROTATE_ALL: {
return {
...state,
player: {
...state.player,
rotateAll: action.payload.rotateAll,
},
};
}
case SettingsActionTypes.SWITCH_GRID: {
return {
...state,
player: {
...state.player,
grid: action.payload.grid,
},
};
}
case SettingsActionTypes.CHANGE_GRID_SIZE: {
return {
...state,
player: {
...state.player,
gridSize: action.payload.gridSize,
},
};
}
case SettingsActionTypes.CHANGE_GRID_COLOR: {
return {
...state,
player: {
...state.player,
gridColor: action.payload.gridColor,
},
};
}
case SettingsActionTypes.CHANGE_GRID_OPACITY: {
return {
...state,
player: {
...state.player,
gridOpacity: action.payload.gridOpacity,
},
};
}
default: {
return {
...state,
};
}
}
};

@ -39,7 +39,7 @@ module.exports = {
}]], }]],
presets: [ presets: [
['@babel/preset-env', { ['@babel/preset-env', {
targets: '> 3%', // https://github.com/browserslist/browserslist targets: '> 2.5%', // https://github.com/browserslist/browserslist
}], }],
['@babel/preset-react'], ['@babel/preset-react'],
['@babel/typescript'], ['@babel/typescript'],

Loading…
Cancel
Save