React UI: Attribute annotation mode (#1255)

* Done main work

* Fixed mount/unmount for canvas wrapper

* Refactoring, added filters

* Added missed file

* Removed unnecessary useEffect

* Removed extra code

* Max 9 attributes, inputNumber -> Input in aam

* Added blur

* Renamed component

* Fixed condition when validate number attribute

* Some minor fixes

* Fixed hotkeys config

* Fixed canvas zoom

* Improved behaviour of number & text

* Fixed attributes switching order

* Fix tags

* Fixed interval
main
Boris Sekachev 6 years ago committed by GitHub
parent 6a7bf6d267
commit 1bb582f7f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -264,9 +264,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
y + height / 2, y + height / 2,
]); ]);
const canvasOffset = this.canvas.getBoundingClientRect();
const [cx, cy] = [ const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft, this.canvas.clientWidth / 2 + canvasOffset.left,
this.canvas.clientHeight / 2 + this.canvas.offsetTop, this.canvas.clientHeight / 2 + canvasOffset.top,
]; ];
const dragged = { const dragged = {
@ -725,7 +726,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (object) { if (object) {
const bbox: SVG.BBox = object.bbox(); const bbox: SVG.BBox = object.bbox();
this.onFocusRegion(bbox.x - padding, bbox.y - padding, this.onFocusRegion(bbox.x - padding, bbox.y - padding,
bbox.width + padding, bbox.height + padding); bbox.width + padding * 2, bbox.height + padding * 2);
} }
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) { } else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement); this.activate(this.controller.activeElement);
@ -1014,7 +1015,26 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.prepend(...sorted.map((pair): SVGElement => pair[0])); this.content.prepend(...sorted.map((pair): SVGElement => pair[0]));
} }
private deactivate(): void { private deactivateAttribute(): void {
const { clientID, attributeID } = this.activeElement;
if (clientID !== null && attributeID !== null) {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = '';
}
}
this.activeElement = {
...this.activeElement,
attributeID: null,
};
}
}
private deactivateShape(): void {
if (this.activeElement.clientID !== null) { if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement; const { clientID } = this.activeElement;
const drawnState = this.drawnStates[clientID]; const drawnState = this.drawnStates[clientID];
@ -1047,29 +1067,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.sortObjects(); this.sortObjects();
this.activeElement = { this.activeElement = {
...this.activeElement,
clientID: null, clientID: null,
attributeID: null,
}; };
} }
} }
private activate(activeElement: ActiveElement): void { private deactivate(): void {
// Check if other element have been already activated this.deactivateAttribute();
if (this.activeElement.clientID !== null) { this.deactivateShape();
// Check if it is the same element }
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
// Deactivate previous element private activateAttribute(clientID: number, attributeID: number): void {
this.deactivate(); const text = this.svgTexts[clientID];
} if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = 'red';
}
const { clientID } = activeElement; this.activeElement = {
if (clientID === null) { ...this.activeElement,
return; attributeID,
};
} }
}
private activateShape(clientID: number): void {
const [state] = this.controller.objects const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID); .filter((_state: any): boolean => _state.clientID === clientID);
@ -1082,7 +1107,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
return; return;
} }
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID]; const shape = this.svgShapes[clientID];
let text = this.svgTexts[clientID]; let text = this.svgTexts[clientID];
@ -1189,6 +1213,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
}); });
this.activeElement = {
...this.activeElement,
clientID,
};
this.canvas.dispatchEvent(new CustomEvent('canvas.activated', { this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false, bubbles: false,
cancelable: true, cancelable: true,
@ -1198,6 +1227,30 @@ export class CanvasViewImpl implements CanvasView, Listener {
})); }));
} }
private activate(activeElement: ActiveElement): void {
// Check if another element have been already activated
if (this.activeElement.clientID !== null) {
if (this.activeElement.clientID !== activeElement.clientID) {
// Deactivate previous shape and attribute
this.deactivate();
} else if (this.activeElement.attributeID !== activeElement.attributeID) {
this.deactivateAttribute();
}
}
const { clientID, attributeID } = activeElement;
if (clientID !== null && this.activeElement.clientID !== clientID) {
this.activateShape(clientID);
}
if (clientID !== null
&& attributeID !== null
&& this.activeElement.attributeID !== attributeID
) {
this.activateAttribute(clientID, attributeID);
}
}
// Update text position after corresponding box has been moved, resized, etc. // Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox(); let box = (shape.node as any).getBBox();

@ -18,12 +18,20 @@ import {
Task, Task,
FrameSpeed, FrameSpeed,
Rotation, Rotation,
Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas'; import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
interface AnnotationsParameters {
filters: string[];
frame: number;
showAllInterpolationTracks: boolean;
jobInstance: any;
}
const cvat = getCore(); const cvat = getCore();
let store: null | Store<CombinedState> = null; let store: null | Store<CombinedState> = null;
@ -34,19 +42,37 @@ function getStore(): Store<CombinedState> {
return store; return store;
} }
function receiveAnnotationsParameters(): function receiveAnnotationsParameters(): AnnotationsParameters {
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
if (store === null) { if (store === null) {
store = getCVATStore(); store = getCVATStore();
} }
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations; const {
const frame = state.annotation.player.frame.number; annotation: {
const { showAllInterpolationTracks } = state.settings.workspace; annotations: {
filters,
},
player: {
frame: {
number: frame,
},
},
job: {
instance: jobInstance,
},
},
settings: {
workspace: {
showAllInterpolationTracks,
},
},
} = state;
return { return {
filters, filters,
frame, frame,
jobInstance,
showAllInterpolationTracks, showAllInterpolationTracks,
}; };
} }
@ -138,11 +164,22 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER', SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER', ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED', SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
}
export function changeWorkspace(workspace: Workspace): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_WORKSPACE,
payload: {
workspace,
},
};
} }
export function addZLayer(): AnyAction { export function addZLayer(): AnyAction {
return { return {
type: AnnotationActionTypes.ADD_Z_LAYER, type: AnnotationActionTypes.ADD_Z_LAYER,
payload: {},
}; };
} }
@ -155,12 +192,17 @@ export function switchZLayer(cur: number): AnyAction {
}; };
} }
export function fetchAnnotationsAsync(sessionInstance: any): export function fetchAnnotationsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters(); const {
const states = await sessionInstance.annotations filters,
frame,
showAllInterpolationTracks,
jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
@ -559,11 +601,15 @@ export function selectObjects(selectedStatesID: number[]): AnyAction {
}; };
} }
export function activateObject(activatedStateID: number | null): AnyAction { export function activateObject(
activatedStateID: number | null,
activatedAttributeID: number | null,
): AnyAction {
return { return {
type: AnnotationActionTypes.ACTIVATE_OBJECT, type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: { payload: {
activatedStateID, activatedStateID,
activatedAttributeID,
}, },
}; };
} }
@ -908,19 +954,26 @@ export function splitTrack(enabled: boolean): AnyAction {
}; };
} }
export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]): export function updateAnnotationsAsync(statesToUpdate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> { ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
filters,
frame,
showAllInterpolationTracks,
} = receiveAnnotationsParameters();
try { try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) { if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX) // deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null)); dispatch(activateObject(null, null));
} }
const promises = statesToUpdate const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save()); .map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises); const states = await Promise.all(promises);
const history = await sessionInstance.actions.get(); const history = await jobInstance.actions.get();
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
dispatch({ dispatch({
@ -933,8 +986,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}, },
}); });
} catch (error) { } catch (error) {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); const states = await jobInstance.annotations
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters); .get(frame, showAllInterpolationTracks, filters);
dispatch({ dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED, type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
@ -1112,8 +1164,6 @@ export function changeLabelColorAsync(
} }
export function changeGroupColorAsync( export function changeGroupColorAsync(
sessionInstance: any,
frameNumber: number,
group: number, group: number,
color: string, color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> { ): ThunkAction<Promise<void>, {}, {}, AnyAction> {
@ -1123,9 +1173,9 @@ export function changeGroupColorAsync(
.filter((_state: any): boolean => _state.group.id === group); .filter((_state: any): boolean => _state.group.id === group);
if (groupStates.length) { if (groupStates.length) {
groupStates[0].group.color = color; groupStates[0].group.color = color;
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates)); dispatch(updateAnnotationsAsync(groupStates));
} else { } else {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [])); dispatch(updateAnnotationsAsync([]));
} }
}; };
} }

@ -11,14 +11,17 @@ import {
Result, Result,
} from 'antd'; } from 'antd';
import { Workspace } from 'reducers/interfaces';
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 StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace'; import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';
interface Props { interface Props {
job: any | null | undefined; job: any | null | undefined;
fetching: boolean; fetching: boolean;
getJob(): void; getJob(): void;
workspace: Workspace;
} }
@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job, job,
fetching, fetching,
getJob, getJob,
workspace,
} = props; } = props;
if (job === null) { if (job === null) {
if (!fetching) { if (!fetching) {
getJob(); getJob();
@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return ( return (
<Layout className='cvat-annotation-page'> <Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer /> <Layout.Header className='cvat-annotation-header'>
<StandardWorkspaceComponent /> <AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer /> <StatisticsModalContainer />
</Layout> </Layout>
); );

@ -0,0 +1,92 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
import Icon from 'antd/lib/icon';
import {
changeAnnotationsFilters as changeAnnotationsFiltersAction,
fetchAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
annotationsFilters: string[];
annotationsFiltersHistory: string[];
}
interface DispatchToProps {
changeAnnotationsFilters(value: SelectValue): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory,
},
},
} = state;
return {
annotationsFilters,
annotationsFiltersHistory,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeAnnotationsFilters(value: SelectValue) {
if (typeof (value) === 'string') {
dispatch(changeAnnotationsFiltersAction([value]));
dispatch(fetchAnnotationsAsync());
} else if (Array.isArray(value)
&& value.every((element: string | number | LabeledValue): boolean => (
typeof (element) === 'string'
))
) {
dispatch(changeAnnotationsFiltersAction(value as string[]));
dispatch(fetchAnnotationsAsync());
}
},
};
}
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
changeAnnotationsFilters,
} = props;
return (
<Select
className='cvat-annotations-filters-input'
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
onChange={changeAnnotationsFilters}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationsFiltersInput);

@ -0,0 +1,300 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { connect } from 'react-redux';
import Layout, { SiderProps } from 'antd/lib/layout';
import { SelectValue } from 'antd/lib/select';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import ObjectSwitcher from './object-switcher';
import AttributeSwitcher from './attribute-switcher';
import ObjectBasicsEditor from './object-basics-edtior';
import AttributeEditor from './attribute-editor';
interface StateToProps {
activatedStateID: number | null;
activatedAttributeID: number | null;
states: any[];
labels: any[];
}
interface DispatchToProps {
activateObject(clientID: number | null, attrID: number | null): void;
updateAnnotations(statesToUpdate: any[]): void;
}
interface LabelAttrMap {
[index: number]: any;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
activatedStateID,
activatedAttributeID,
states,
},
job: {
labels,
},
},
} = state;
return {
labels,
activatedStateID,
activatedAttributeID,
states,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
activateObject(clientID: number, attrID: number): void {
dispatch(activateObjectAction(clientID, attrID));
},
updateAnnotations(states): void {
dispatch(updateAnnotationsAsync(states));
},
};
}
function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element {
const {
labels,
states,
activatedStateID,
activatedAttributeID,
updateAnnotations,
activateObject,
} = props;
const [labelAttrMap, setLabelAttrMap] = useState(
labels.reduce((acc, label): LabelAttrMap => {
acc[label.id] = label.attributes.length ? label.attributes[0] : null;
return acc;
}, {}),
);
const [activeObjectState] = activatedStateID === null
? [null] : states.filter((objectState: any): boolean => (
objectState.clientID === activatedStateID
));
const activeAttribute = activeObjectState
? labelAttrMap[activeObjectState.label.id]
: null;
if (activeObjectState) {
const attribute = labelAttrMap[activeObjectState.label.id];
if (attribute && attribute.id !== activatedAttributeID) {
activateObject(activatedStateID, attribute ? attribute.id : null);
}
} else if (states.length) {
const attribute = labelAttrMap[states[0].label.id];
activateObject(states[0].clientID, attribute ? attribute.id : null);
}
const nextObject = (step: number): void => {
if (states.length) {
const index = states.indexOf(activeObjectState);
let nextIndex = index + step;
if (nextIndex > states.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = states.length - 1;
}
if (nextIndex !== index) {
const attribute = labelAttrMap[states[nextIndex].label.id];
activateObject(states[nextIndex].clientID, attribute ? attribute.id : null);
}
}
};
const nextAttribute = (step: number): void => {
if (activeObjectState) {
const { label } = activeObjectState;
const { attributes } = label;
if (attributes.length) {
const index = attributes.indexOf(activeAttribute);
let nextIndex = index + step;
if (nextIndex > attributes.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = attributes.length - 1;
}
if (index !== nextIndex) {
const updatedLabelAttrMap = { ...labelAttrMap };
updatedLabelAttrMap[label.id] = attributes[nextIndex];
setLabelAttrMap(updatedLabelAttrMap);
}
}
}
};
useEffect(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, []);
const siderProps: SiderProps = {
className: 'attribute-annotation-sidebar',
theme: 'light',
width: 300,
collapsedWidth: 0,
reverseArrow: true,
collapsible: true,
trigger: null,
};
const keyMap = {
NEXT_ATTRIBUTE: {
name: 'Next attribute',
description: 'Go to the next attribute',
sequence: 'ArrowDown',
action: 'keydown',
},
PREVIOUS_ATTRIBUTE: {
name: 'Previous attribute',
description: 'Go to the previous attribute',
sequence: 'ArrowUp',
action: 'keydown',
},
NEXT_OBJECT: {
name: 'Next object',
description: 'Go to the next object',
sequence: 'Tab',
action: 'keydown',
},
PREVIOUS_OBJECT: {
name: 'Previous object',
description: 'Go to the previous object',
sequence: 'Shift+Tab',
action: 'keydown',
},
};
const handlers = {
NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextAttribute(1);
},
PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextAttribute(-1);
},
NEXT_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextObject(1);
},
PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextObject(-1);
},
};
if (activeObjectState) {
return (
<Layout.Sider {...siderProps}>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<Row>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
occluded={activeObjectState.occluded}
objectsCount={states.length}
currentIndex={states.indexOf(activeObjectState)}
nextObject={nextObject}
/>
<ObjectBasicsEditor
currentLabel={activeObjectState.label.name}
labels={labels}
occluded={activeObjectState.occluded}
changeLabel={(value: SelectValue): void => {
const labelName = value as string;
const [newLabel] = labels
.filter((_label): boolean => _label.name === labelName);
activeObjectState.label = newLabel;
updateAnnotations([activeObjectState]);
}}
setOccluded={(event: CheckboxChangeEvent): void => {
activeObjectState.occluded = event.target.checked;
updateAnnotations([activeObjectState]);
}}
/>
{
activeAttribute
? (
<>
<AttributeSwitcher
currentAttribute={activeAttribute.name}
currentIndex={activeObjectState.label.attributes
.indexOf(activeAttribute)}
attributesCount={activeObjectState.label.attributes.length}
nextAttribute={nextAttribute}
/>
<AttributeEditor
attribute={activeAttribute}
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);
}}
/>
</>
) : (
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No attributes found</Text>
</div>
)
}
</Layout.Sider>
);
}
return (
<Layout.Sider {...siderProps}>
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>
</Layout.Sider>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AttributeAnnotationSidebar);

@ -0,0 +1,281 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import Text from 'antd/lib/typography/Text';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Select, { SelectValue } from 'antd/lib/select';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
interface InputElementParameters {
attrID: number;
inputType: string;
values: string[];
currentValue: string;
onChange(value: string): void;
ref: React.RefObject<Input | InputNumber>;
}
function renderInputElement(parameters: InputElementParameters): JSX.Element {
const {
inputType,
attrID,
values,
currentValue,
onChange,
ref,
} = parameters;
const renderCheckbox = (): JSX.Element => (
<>
<Text strong>Checkbox: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Checkbox
onChange={(event: CheckboxChangeEvent): void => (
onChange(event.target.checked ? 'true' : 'false')
)}
checked={currentValue === 'true'}
/>
</div>
</>
);
const renderSelect = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Select
value={currentValue}
style={{ width: '80%' }}
onChange={(value: SelectValue) => (
onChange(value as string)
)}
>
{values.map((value: string): JSX.Element => (
<Select.Option key={value} value={value}>{value}</Select.Option>
))}
</Select>
</div>
</>
);
const renderRadio = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Radio.Group
value={currentValue}
onChange={(event: RadioChangeEvent) => (
onChange(event.target.value)
)}
>
{values.map((value: string): JSX.Element => (
<Radio style={{ display: 'block' }} key={value} value={value}>{value}</Radio>
))}
</Radio.Group>
</div>
</>
);
const handleKeydown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (['ArrowDown', 'ArrowUp', 'ArrowLeft',
'ArrowRight', 'Tab', 'Shift', 'Control']
.includes(event.key)
) {
event.preventDefault();
const copyEvent = new KeyboardEvent('keydown', event);
window.document.dispatchEvent(copyEvent);
}
};
const renderText = (): JSX.Element => (
<>
{inputType === 'number' ? <Text strong>Number: </Text> : <Text strong>Text: </Text>}
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Input
autoFocus
key={attrID}
defaultValue={currentValue}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (inputType === 'number') {
if (value !== '') {
const numberValue = +value;
if (!Number.isNaN(numberValue)) {
onChange(`${numberValue}`);
}
}
} else {
onChange(value);
}
}}
onKeyDown={handleKeydown}
ref={ref as React.RefObject<Input>}
/>
</div>
</>
);
let element = null;
if (inputType === 'checkbox') {
element = renderCheckbox();
} else if (inputType === 'select') {
element = renderSelect();
} else if (inputType === 'radio') {
element = renderRadio();
} else {
element = renderText();
}
return (
<div className='attribute-annotation-sidebar-attr-editor'>
{element}
</div>
);
}
interface ListParameters {
inputType: string;
values: string[];
onChange(value: string): void;
}
function renderList(parameters: ListParameters): JSX.Element | null {
const { inputType, values, onChange } = parameters;
if (inputType === 'checkbox') {
const sortedValues = ['true', 'false'];
if (values[0].toLowerCase() !== 'true') {
sortedValues.reverse();
}
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
sortedValues.forEach((value: string, index: number): void => {
const key = `SET_${index}_VALUE`;
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onChange(value);
};
});
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<div>
<Text strong>0:</Text>
<Text>{` ${sortedValues[0]}`}</Text>
</div>
<div>
<Text strong>1:</Text>
<Text>{` ${sortedValues[1]}`}</Text>
</div>
</div>
);
}
if (inputType === 'radio' || inputType === 'select') {
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
values.slice(0, 10).forEach((value: string, index: number): void => {
const key = `SET_${index}_VALUE`;
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onChange(value);
};
});
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
{values.map((value: string, index: number): JSX.Element => (
<div key={value}>
<Text strong>{`${index}:`}</Text>
<Text>{` ${value}`}</Text>
</div>
))}
</div>
);
}
if (inputType === 'number') {
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<div>
<Text strong>From:</Text>
<Text>{` ${values[0]}`}</Text>
</div>
<div>
<Text strong>To:</Text>
<Text>{` ${values[1]}`}</Text>
</div>
<div>
<Text strong>Step:</Text>
<Text>{` ${values[2]}`}</Text>
</div>
</div>
);
}
return null;
}
interface Props {
attribute: any;
currentValue: string;
onChange(value: string): void;
}
function AttributeEditor(props: Props): JSX.Element {
const { attribute, currentValue, onChange } = props;
const { inputType, values, id: attrID } = attribute;
const ref = inputType === 'number' ? React.createRef<InputNumber>()
: React.createRef<Input>();
return (
<div>
{renderList({ values, inputType, onChange })}
<hr />
{renderInputElement({
attrID,
ref,
inputType,
currentValue,
values,
onChange,
})}
</div>
);
}
export default React.memo(AttributeEditor);

@ -0,0 +1,43 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
interface Props {
currentAttribute: string;
currentIndex: number;
attributesCount: number;
nextAttribute(step: number): void;
}
function AttributeSwitcher(props: Props): JSX.Element {
const {
currentAttribute,
currentIndex,
attributesCount,
nextAttribute,
} = props;
const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
<Icon type='left' />
</Button>
<Tooltip title={title}>
<Text className='cvat-text'>{currentAttribute}</Text>
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
</Tooltip>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
<Icon type='right' />
</Button>
</div>
);
}
export default React.memo(AttributeSwitcher);

@ -0,0 +1,43 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Select, { SelectValue } from 'antd/lib/select';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
interface Props {
currentLabel: string;
labels: any[];
occluded: boolean;
setOccluded(event: CheckboxChangeEvent): void;
changeLabel(value: SelectValue): void;
}
function ObjectBasicsEditor(props: Props): JSX.Element {
const {
currentLabel,
occluded,
labels,
setOccluded,
changeLabel,
} = props;
return (
<div className='attribute-annotation-sidebar-basics-editor'>
<Select value={currentLabel} onChange={changeLabel} style={{ width: '50%' }}>
{labels.map((label: any): JSX.Element => (
<Select.Option
value={label.name}
key={label.name}
>
{label.name}
</Select.Option>
))}
</Select>
<Checkbox checked={occluded} onChange={setOccluded}>Occluded</Checkbox>
</div>
);
}
export default React.memo(ObjectBasicsEditor);

@ -0,0 +1,48 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
interface Props {
currentLabel: string;
clientID: number;
occluded: boolean;
objectsCount: number;
currentIndex: number;
nextObject(step: number): void;
}
function ObjectSwitcher(props: Props): JSX.Element {
const {
currentLabel,
clientID,
objectsCount,
currentIndex,
nextObject,
} = props;
const title = `${currentLabel} ${clientID} [${currentIndex + 1}/${objectsCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(-1)}>
<Icon type='left' />
</Button>
<Tooltip title={title}>
<Text className='cvat-text'>{currentLabel}</Text>
<Text className='cvat-text'>{` ${clientID} `}</Text>
<Text strong>{`[${currentIndex + 1}/${objectsCount}]`}</Text>
</Tooltip>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(1)}>
<Icon type='right' />
</Button>
</div>
);
}
export default React.memo(ObjectSwitcher);

@ -0,0 +1,19 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import AttributeAnnotationSidebar from './attribute-annotation-sidebar/attribute-annotation-sidebar';
export default function AttributeAnnotationWorkspace(): JSX.Element {
return (
<Layout hasSider className='attribute-annotation-workspace'>
<CanvasWrapperContainer />
<AttributeAnnotationSidebar />
</Layout>
);
}

@ -0,0 +1,66 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.attribute-annotation-workspace.ant-layout {
height: 100%;
}
.attribute-annotation-sidebar {
background: $background-color-2;
padding: 5px;
}
.attribute-annotation-sidebar-switcher {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 18px;
margin-top: 10px;
> span {
max-width: 60%;
text-overflow: ellipsis;
overflow: hidden;
}
> button > i {
color: $objects-bar-icons-color;
}
}
.attribute-annotation-sidebar-basics-editor {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 18px;
margin: 10px 0px;
}
.attribute-annotations-sidebar-not-found-wrapper {
margin-top: 20px;
text-align: center;
}
.attribute-annotation-sidebar-attr-list-wrapper {
margin: 10px 0px 10px 10px;
}
.attribute-annotation-sidebar-attr-elem-wrapper {
display: inline-block;
width: 60%;
}
.attribute-annotation-sidebar-number-list {
display: flex;
justify-content: space-around;
}
.attribute-annotation-sidebar-attr-editor {
display: flex;
align-items: center;
justify-content: space-around;
}

@ -4,18 +4,19 @@
import React from 'react'; import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import Slider, { SliderValue } from 'antd/lib/slider';
import Layout from 'antd/lib/layout';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import {
Layout,
Slider,
Icon,
Tooltip,
} from 'antd';
import { SliderValue } from 'antd/lib//slider';
import { ColorBy, GridColor, ObjectType } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas'; import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core'; import getCore from 'cvat-core';
import {
ColorBy,
GridColor,
ObjectType,
Workspace,
} from 'reducers/interfaces';
const cvat = getCore(); const cvat = getCore();
@ -26,6 +27,7 @@ interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[]; selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
@ -48,6 +50,8 @@ interface Props {
contrastLevel: number; contrastLevel: number;
saturationLevel: number; saturationLevel: number;
resetZoom: boolean; resetZoom: boolean;
aamZoomMargin: number;
workspace: Workspace;
onSetupCanvas: () => void; onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void; onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void;
@ -57,7 +61,7 @@ interface Props {
onEditShape: (enabled: boolean) => void; onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void; onShapeDrawn: () => void;
onResetCanvas: () => void; onResetCanvas: () => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onUpdateAnnotations(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;
@ -113,6 +117,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
workspace,
} = this.props; } = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) { if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -161,11 +166,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
} }
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) { if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas(); this.updateCanvas();
} }
if (prevProps.frame !== frameData.number && resetZoom) { if (prevProps.frame !== frameData.number
&& resetZoom
&& workspace !== Workspace.ATTRIBUTE_ANNOTATION
) {
canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit(); canvasInstance.fit();
}, { once: true }); }, { once: true });
@ -176,10 +188,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView(); this.updateShapesView();
} }
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.frameAngle !== frameAngle) { if (prevProps.frameAngle !== frameAngle) {
canvasInstance.rotate(frameAngle); canvasInstance.rotate(frameAngle);
} }
@ -188,10 +196,34 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
canvasInstance.html().removeEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart);
canvasInstance.html().removeEventListener('canvas.dragstop', this.onCanvasDragDone);
canvasInstance.html().removeEventListener('canvas.zoomstart', this.onCanvasZoomStart);
canvasInstance.html().removeEventListener('canvas.zoomstop', this.onCanvasZoomDone);
canvasInstance.html().removeEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().removeEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().removeEventListener('canvas.find', this.onCanvasFindObject);
canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
window.removeEventListener('resize', this.fitCanvas); window.removeEventListener('resize', this.fitCanvas);
} }
private onShapeDrawn(event: any): void { private onCanvasShapeDrawn = (event: any): void => {
const { const {
jobInstance, jobInstance,
activeLabelID, activeLabelID,
@ -222,27 +254,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
state.frame = frame; state.frame = frame;
const objectState = new cvat.classes.ObjectState(state); const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]); onCreateAnnotations(jobInstance, frame, [objectState]);
} };
private onShapeEdited(event: any): void {
const {
jobInstance,
frame,
onEditShape,
onUpdateAnnotations,
} = this.props;
onEditShape(false);
const {
state,
points,
} = event.detail;
state.points = points;
onUpdateAnnotations(jobInstance, frame, [state]);
}
private onObjectsMerged(event: any): void { private onCanvasObjectsMerged = (event: any): void => {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -254,9 +268,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail; const { states } = event.detail;
onMergeAnnotations(jobInstance, frame, states); onMergeAnnotations(jobInstance, frame, states);
} };
private onObjectsGroupped(event: any): void { private onCanvasObjectsGroupped = (event: any): void => {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -268,9 +282,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail; const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states); onGroupAnnotations(jobInstance, frame, states);
} };
private onTrackSplitted(event: any): void { private onCanvasTrackSplitted = (event: any): void => {
const { const {
jobInstance, jobInstance,
frame, frame,
@ -282,22 +296,179 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { state } = event.detail; const { state } = event.detail;
onSplitAnnotations(jobInstance, frame, state); onSplitAnnotations(jobInstance, frame, state);
} };
private fitCanvas = (): void => { private fitCanvas = (): void => {
const { canvasInstance } = this.props; const { canvasInstance } = this.props;
canvasInstance.fitCanvas(); canvasInstance.fitCanvas();
}; };
private onCanvasMouseDown = (e: MouseEvent): void => {
const { workspace, activatedStateID, onActivateObject } = this.props;
if ((e.target as HTMLElement).tagName === 'svg') {
if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) {
onActivateObject(null);
}
}
};
private onCanvasClicked = (): void => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
private onCanvasContextMenu = (e: MouseEvent): void => {
const { activatedStateID, onUpdateContextMenu } = this.props;
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
};
private onCanvasShapeClicked = (e: any): void => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
.getElementById(`cvat-objects-sidebar-state-item-${clientID}`);
if (sidebarItem) {
sidebarItem.scrollIntoView();
}
};
private onCanvasShapeDeactivated = (e: any): void => {
const { onActivateObject, 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);
}
};
private onCanvasCursorMoved = async (event: any): Promise<void> => {
const {
jobInstance,
activatedStateID,
workspace,
onActivateObject,
} = this.props;
if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
return;
}
const result = await jobInstance.annotations.select(
event.detail.states,
event.detail.x,
event.detail.y,
);
if (result && result.state) {
if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') {
if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) {
return;
}
}
if (activatedStateID !== result.state.clientID) {
onActivateObject(result.state.clientID);
}
}
};
private onCanvasEditStart = (): void => {
const { onActivateObject, onEditShape } = this.props;
onActivateObject(null);
onEditShape(true);
};
private onCanvasEditDone = (event: any): void => {
const {
onEditShape,
onUpdateAnnotations,
} = this.props;
onEditShape(false);
const {
state,
points,
} = event.detail;
state.points = points;
onUpdateAnnotations([state]);
};
private onCanvasDragStart = (): void => {
const { onDragCanvas } = this.props;
onDragCanvas(true);
};
private onCanvasDragDone = (): void => {
const { onDragCanvas } = this.props;
onDragCanvas(false);
};
private onCanvasZoomStart = (): void => {
const { onZoomCanvas } = this.props;
onZoomCanvas(true);
};
private onCanvasZoomDone = (): void => {
const { onZoomCanvas } = this.props;
onZoomCanvas(false);
};
private onCanvasSetup = (): void => {
const { onSetupCanvas } = this.props;
onSetupCanvas();
this.updateShapesView();
this.activateOnCanvas();
};
private onCanvasCancel = (): void => {
const { onResetCanvas } = this.props;
onResetCanvas();
};
private onCanvasFindObject = async (e: any): Promise<void> => {
const { jobInstance, canvasInstance } = this.props;
const result = await jobInstance.annotations
.select(e.detail.states, e.detail.x, e.detail.y);
if (result && result.state) {
if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') {
if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) {
return;
}
}
canvasInstance.select(result.state);
}
};
private activateOnCanvas(): void { private activateOnCanvas(): void {
const { const {
activatedStateID, activatedStateID,
activatedAttributeID,
canvasInstance, canvasInstance,
selectedOpacity, selectedOpacity,
aamZoomMargin,
workspace,
annotations,
} = this.props; } = this.props;
if (activatedStateID !== null) { if (activatedStateID !== null) {
canvasInstance.activate(activatedStateID); if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
const [activatedState] = annotations
.filter((state: any): boolean => state.clientID === activatedStateID);
if (activatedState.objectType !== ObjectType.TAG) {
canvasInstance.focus(activatedStateID, aamZoomMargin);
} else {
canvasInstance.fit();
}
}
canvasInstance.activate(activatedStateID, activatedAttributeID);
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) { if (el) {
(el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`); (el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
@ -358,14 +529,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridColor, gridColor,
gridOpacity, gridOpacity,
canvasInstance, canvasInstance,
jobInstance,
onSetupCanvas,
onDragCanvas,
onZoomCanvas,
onResetCanvas,
onActivateObject,
onUpdateContextMenu,
onEditShape,
brightnessLevel, brightnessLevel,
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
@ -396,127 +559,33 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
// Events // Events
canvasInstance.html().addEventListener('mousedown', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
if ((e.target as HTMLElement).tagName === 'svg' && activatedStateID !== null) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('click', (): void => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
}
});
canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
});
canvasInstance.html().addEventListener('canvas.editstart', (): void => {
onActivateObject(null);
onEditShape(true);
});
canvasInstance.html().addEventListener('canvas.setup', (): void => {
onSetupCanvas();
this.updateShapesView();
this.activateOnCanvas();
});
canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.html().addEventListener('canvas.setup', () => {
const { activatedStateID, activatedAttributeID } = this.props;
canvasInstance.fit(); canvasInstance.fit();
canvasInstance.activate(activatedStateID, activatedAttributeID);
}, { once: true }); }, { once: true });
canvasInstance.html().addEventListener('canvas.canceled', () => { canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown);
onResetCanvas(); canvasInstance.html().addEventListener('click', this.onCanvasClicked);
}); canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().addEventListener('canvas.dragstart', () => { canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone);
onDragCanvas(true); canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart);
}); canvasInstance.html().addEventListener('canvas.dragstop', this.onCanvasDragDone);
canvasInstance.html().addEventListener('canvas.zoomstart', this.onCanvasZoomStart);
canvasInstance.html().addEventListener('canvas.dragstop', () => { canvasInstance.html().addEventListener('canvas.zoomstop', this.onCanvasZoomDone);
onDragCanvas(false);
}); canvasInstance.html().addEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().addEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().addEventListener('canvas.zoomstart', () => { canvasInstance.html().addEventListener('canvas.find', this.onCanvasFindObject);
onZoomCanvas(true); canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
}); canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().addEventListener('canvas.zoomstop', () => { canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked);
onZoomCanvas(false); canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn);
}); canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.clicked', (e: any) => { canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
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> => {
const { activatedStateID } = this.props;
const result = await jobInstance.annotations.select(
event.detail.states,
event.detail.x,
event.detail.y,
);
if (result && result.state) {
if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') {
if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) {
return;
}
}
if (activatedStateID !== result.state.clientID) {
onActivateObject(result.state.clientID);
}
}
});
canvasInstance.html().addEventListener('canvas.find', async (e: any) => {
const result = await jobInstance.annotations
.select(e.detail.states, e.detail.x, e.detail.y);
if (result && result.state) {
if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') {
if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) {
return;
}
}
canvasInstance.select(result.state);
}
});
canvasInstance.html().addEventListener('canvas.edited', this.onShapeEdited.bind(this));
canvasInstance.html().addEventListener('canvas.drawn', this.onShapeDrawn.bind(this));
canvasInstance.html().addEventListener('canvas.merged', this.onObjectsMerged.bind(this));
canvasInstance.html().addEventListener('canvas.groupped', this.onObjectsGroupped.bind(this));
canvasInstance.html().addEventListener('canvas.splitted', this.onTrackSplitted.bind(this));
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -3,20 +3,14 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import { import Icon from 'antd/lib/icon';
Row, import Select from 'antd/lib/select';
Col,
Icon,
Select,
} from 'antd';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { SelectValue } from 'antd/lib/select';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import { StatesOrdering } from 'reducers/interfaces'; import { StatesOrdering } from 'reducers/interfaces';
interface StatesOrderingSelectorComponentProps { interface StatesOrderingSelectorComponentProps {
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
@ -62,10 +56,7 @@ interface Props {
statesLocked: boolean; statesLocked: boolean;
statesCollapsed: boolean; statesCollapsed: boolean;
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void; lockAllStates(): void;
unlockAllStates(): void; unlockAllStates(): void;
collapseAllStates(): void; collapseAllStates(): void;
@ -76,8 +67,6 @@ interface Props {
function ObjectListHeader(props: Props): JSX.Element { function ObjectListHeader(props: Props): JSX.Element {
const { const {
annotationsFilters,
annotationsFiltersHistory,
statesHidden, statesHidden,
statesLocked, statesLocked,
statesCollapsed, statesCollapsed,
@ -89,30 +78,13 @@ function ObjectListHeader(props: Props): JSX.Element {
expandAllStates, expandAllStates,
hideAllStates, hideAllStates,
showAllStates, showAllStates,
changeAnnotationsFilters,
} = props; } = props;
return ( return (
<div className='cvat-objects-sidebar-states-header'> <div className='cvat-objects-sidebar-states-header'>
<Row> <Row>
<Col> <Col>
<Select <AnnotationsFiltersInput />
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
onChange={changeAnnotationsFilters}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
</Col> </Col>
</Row> </Row>
<Row type='flex' justify='space-between' align='middle'> <Row type='flex' justify='space-between' align='middle'>

@ -4,7 +4,6 @@
import React from 'react'; import React from 'react';
import { SelectValue } from 'antd/lib/select';
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 ObjectListHeader from './objects-list-header'; import ObjectListHeader from './objects-list-header';
@ -17,10 +16,7 @@ interface Props {
statesCollapsed: boolean; statesCollapsed: boolean;
statesOrdering: StatesOrdering; statesOrdering: StatesOrdering;
sortedStatesID: number[]; sortedStatesID: number[];
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void; changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void; lockAllStates(): void;
unlockAllStates(): void; unlockAllStates(): void;
collapseAllStates(): void; collapseAllStates(): void;
@ -37,10 +33,7 @@ function ObjectListComponent(props: Props): JSX.Element {
statesCollapsed, statesCollapsed,
statesOrdering, statesOrdering,
sortedStatesID, sortedStatesID,
annotationsFilters,
annotationsFiltersHistory,
changeStatesOrdering, changeStatesOrdering,
changeAnnotationsFilters,
lockAllStates, lockAllStates,
unlockAllStates, unlockAllStates,
collapseAllStates, collapseAllStates,
@ -56,16 +49,13 @@ function ObjectListComponent(props: Props): JSX.Element {
statesLocked={statesLocked} statesLocked={statesLocked}
statesCollapsed={statesCollapsed} statesCollapsed={statesCollapsed}
statesOrdering={statesOrdering} statesOrdering={statesOrdering}
annotationsFilters={annotationsFilters}
changeStatesOrdering={changeStatesOrdering} changeStatesOrdering={changeStatesOrdering}
changeAnnotationsFilters={changeAnnotationsFilters}
lockAllStates={lockAllStates} lockAllStates={lockAllStates}
unlockAllStates={unlockAllStates} unlockAllStates={unlockAllStates}
collapseAllStates={collapseAllStates} collapseAllStates={collapseAllStates}
expandAllStates={expandAllStates} expandAllStates={expandAllStates}
hideAllStates={hideAllStates} hideAllStates={hideAllStates}
showAllStates={showAllStates} showAllStates={showAllStates}
annotationsFiltersHistory={annotationsFiltersHistory}
/> />
<div className='cvat-objects-sidebar-states-list'> <div className='cvat-objects-sidebar-states-list'>
{ sortedStatesID.map((id: number): JSX.Element => ( { sortedStatesID.map((id: number): JSX.Element => (

@ -69,16 +69,6 @@
> div:nth-child(1) > div:nth-child(1) { > div:nth-child(1) > div:nth-child(1) {
height: 32px; height: 32px;
> .ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}
} }
> div:nth-child(2) { > div:nth-child(2) {

@ -17,7 +17,7 @@ import CanvasContextMenuContainer from 'containers/annotation-page/standard-work
export default function StandardWorkspaceComponent(): JSX.Element { export default function StandardWorkspaceComponent(): JSX.Element {
return ( return (
<Layout hasSider> <Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer /> <ControlsSideBarContainer />
<CanvasWrapperContainer /> <CanvasWrapperContainer />
<ObjectSideBarContainer /> <ObjectSideBarContainer />

@ -4,6 +4,10 @@
@import 'base.scss'; @import 'base.scss';
.cvat-standard-workspace.ant-layout {
height: 100%
}
.cvat-canvas-container { .cvat-canvas-container {
background-color: $background-color-1; background-color: $background-color-1;
} }
@ -115,62 +119,3 @@
margin: 0px 5px; margin: 0px 5px;
} }
} }
.cvat-canvas-context-menu {
opacity: 0.6;
position: fixed;
width: 300px;
z-index: 10;
max-height: 50%;
overflow-y: auto;
&:hover {
opacity: 1;
}
}
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}

@ -213,3 +213,75 @@
width: 15em; width: 15em;
} }
} }
// TODO: Move canvas from standard workspace and create its own .scss
.cvat-canvas-context-menu {
opacity: 0.6;
position: fixed;
width: 300px;
z-index: 10;
max-height: 50%;
overflow-y: auto;
&:hover {
opacity: 1;
}
}
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}
.cvat-annotations-filters-input.ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}

@ -11,17 +11,17 @@ import {
Button, Button,
} from 'antd'; } from 'antd';
import { import { Workspace } from 'reducers/interfaces';
InfoIcon, import { InfoIcon, FullscreenIcon } from '../../../icons';
FullscreenIcon,
} from '../../../icons';
interface Props { interface Props {
workspace: Workspace;
showStatistics(): void; showStatistics(): void;
changeWorkspace(workspace: Workspace): void;
} }
function RightGroup(props: Props): JSX.Element { function RightGroup(props: Props): JSX.Element {
const { showStatistics } = props; const { showStatistics, changeWorkspace, workspace } = props;
return ( return (
<Col className='cvat-annotation-header-right-group'> <Col className='cvat-annotation-header-right-group'>
@ -46,9 +46,23 @@ function RightGroup(props: Props): JSX.Element {
Info Info
</Button> </Button>
<div> <div>
<Select disabled className='cvat-workspace-selector' defaultValue='standard'> <Select
<Select.Option key='standard' value='standard'>Standard</Select.Option> className='cvat-workspace-selector'
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option> onChange={changeWorkspace}
value={workspace}
>
<Select.Option
key={Workspace.STANDARD}
value={Workspace.STANDARD}
>
{Workspace.STANDARD}
</Select.Option>
<Select.Option
key={Workspace.ATTRIBUTE_ANNOTATION}
value={Workspace.ATTRIBUTE_ANNOTATION}
>
{Workspace.ATTRIBUTE_ANNOTATION}
</Select.Option>
</Select> </Select>
</div> </div>
</Col> </Col>

@ -7,12 +7,12 @@ import React from 'react';
import { import {
Row, Row,
Col, Col,
Layout,
InputNumber, InputNumber,
} from 'antd'; } from 'antd';
import { SliderValue } from 'antd/lib/slider'; import { SliderValue } from 'antd/lib/slider';
import { Workspace } from 'reducers/interfaces';
import LeftGroup from './left-group'; import LeftGroup from './left-group';
import RightGroup from './right-group'; import RightGroup from './right-group';
import PlayerNavigation from './player-navigation'; import PlayerNavigation from './player-navigation';
@ -28,6 +28,8 @@ interface Props {
stopFrame: number; stopFrame: number;
undoAction?: string; undoAction?: string;
redoAction?: string; redoAction?: string;
workspace: Workspace;
changeWorkspace(workspace: Workspace): void;
showStatistics(): void; showStatistics(): void;
onSwitchPlay(): void; onSwitchPlay(): void;
onSaveAnnotation(): void; onSaveAnnotation(): void;
@ -55,7 +57,9 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
inputFrameRef, inputFrameRef,
startFrame, startFrame,
stopFrame, stopFrame,
workspace,
showStatistics, showStatistics,
changeWorkspace,
onSwitchPlay, onSwitchPlay,
onSaveAnnotation, onSaveAnnotation,
onPrevFrame, onPrevFrame,
@ -72,42 +76,44 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
} = props; } = props;
return ( return (
<Layout.Header className='cvat-annotation-header'> <Row type='flex' justify='space-between'>
<Row type='flex' justify='space-between'> <LeftGroup
<LeftGroup saving={saving}
saving={saving} savingStatuses={savingStatuses}
savingStatuses={savingStatuses} onSaveAnnotation={onSaveAnnotation}
onSaveAnnotation={onSaveAnnotation} undoAction={undoAction}
undoAction={undoAction} redoAction={redoAction}
redoAction={redoAction} onUndoClick={onUndoClick}
onUndoClick={onUndoClick} onRedoClick={onRedoClick}
onRedoClick={onRedoClick} />
/> <Col className='cvat-annotation-header-player-group'>
<Col className='cvat-annotation-header-player-group'> <Row type='flex' align='middle'>
<Row type='flex' align='middle'> <PlayerButtons
<PlayerButtons playing={playing}
playing={playing} onPrevFrame={onPrevFrame}
onPrevFrame={onPrevFrame} onNextFrame={onNextFrame}
onNextFrame={onNextFrame} onForward={onForward}
onForward={onForward} onBackward={onBackward}
onBackward={onBackward} onFirstFrame={onFirstFrame}
onFirstFrame={onFirstFrame} onLastFrame={onLastFrame}
onLastFrame={onLastFrame} onSwitchPlay={onSwitchPlay}
onSwitchPlay={onSwitchPlay} />
/> <PlayerNavigation
<PlayerNavigation startFrame={startFrame}
startFrame={startFrame} stopFrame={stopFrame}
stopFrame={stopFrame} frameNumber={frameNumber}
frameNumber={frameNumber} inputFrameRef={inputFrameRef}
inputFrameRef={inputFrameRef} onSliderChange={onSliderChange}
onSliderChange={onSliderChange} onInputChange={onInputChange}
onInputChange={onInputChange} onURLIconClick={onURLIconClick}
onURLIconClick={onURLIconClick} />
/> </Row>
</Row> </Col>
</Col> <RightGroup
<RightGroup showStatistics={showStatistics} /> workspace={workspace}
</Row> changeWorkspace={changeWorkspace}
</Layout.Header> showStatistics={showStatistics}
/>
</Row>
); );
} }

@ -93,7 +93,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
max={1000} max={1000}
value={aamZoomMargin} value={aamZoomMargin}
onChange={(value: number | undefined): void => { onChange={(value: number | undefined): void => {
if (value) { if (typeof (value) === 'number') {
onChangeAAMZoomMargin(value); onChangeAAMZoomMargin(value);
} }
}} }}

@ -9,9 +9,7 @@ import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page'; import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync } from 'actions/annotation-actions'; import { getJobAsync } from 'actions/annotation-actions';
import { import { CombinedState, Workspace } from 'reducers/interfaces';
CombinedState,
} from 'reducers/interfaces';
type OwnProps = RouteComponentProps<{ type OwnProps = RouteComponentProps<{
tid: string; tid: string;
@ -21,6 +19,7 @@ type OwnProps = RouteComponentProps<{
interface StateToProps { interface StateToProps {
job: any | null | undefined; job: any | null | undefined;
fetching: boolean; fetching: boolean;
workspace: Workspace;
} }
interface DispatchToProps { interface DispatchToProps {
@ -36,12 +35,14 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
instance: job, instance: job,
fetching, fetching,
}, },
workspace,
}, },
} = state; } = state;
return { return {
job: !job || jobID === job.id ? job : null, job: !job || jobID === job.id ? job : null,
fetching, fetching,
workspace,
}; };
} }

@ -39,6 +39,7 @@ import {
GridColor, GridColor,
ObjectType, ObjectType,
CombinedState, CombinedState,
Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas'; import { Canvas } from 'cvat-canvas';
@ -48,6 +49,7 @@ interface StateToProps {
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[]; selectedStatesID: number[];
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
@ -67,6 +69,8 @@ interface StateToProps {
contrastLevel: number; contrastLevel: number;
saturationLevel: number; saturationLevel: number;
resetZoom: boolean; resetZoom: boolean;
aamZoomMargin: number;
workspace: Workspace;
minZLayer: number; minZLayer: number;
maxZLayer: number; maxZLayer: number;
curZLayer: number; curZLayer: number;
@ -82,7 +86,7 @@ interface DispatchToProps {
onGroupObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void; onEditShape: (enabled: boolean) => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onUpdateAnnotations(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;
@ -123,6 +127,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotations: { annotations: {
states: annotations, states: annotations,
activatedStateID, activatedStateID,
activatedAttributeID,
selectedStatesID, selectedStatesID,
zLayer: { zLayer: {
cur: curZLayer, cur: curZLayer,
@ -131,6 +136,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
}, },
}, },
sidebarCollapsed, sidebarCollapsed,
workspace,
}, },
settings: { settings: {
player: { player: {
@ -143,6 +149,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
saturationLevel, saturationLevel,
resetZoom, resetZoom,
}, },
workspace: {
aamZoomMargin,
},
shapes: { shapes: {
opacity, opacity,
colorBy, colorBy,
@ -160,6 +169,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameAngle: frameAngles[frame - jobInstance.startFrame], frameAngle: frameAngles[frame - jobInstance.startFrame],
frame, frame,
activatedStateID, activatedStateID,
activatedAttributeID,
selectedStatesID, selectedStatesID,
annotations, annotations,
opacity, opacity,
@ -176,9 +186,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
contrastLevel, contrastLevel,
saturationLevel, saturationLevel,
resetZoom, resetZoom,
aamZoomMargin,
curZLayer, curZLayer,
minZLayer, minZLayer,
maxZLayer, maxZLayer,
workspace,
}; };
} }
@ -211,8 +223,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onEditShape(enabled: boolean): void { onEditShape(enabled: boolean): void {
dispatch(editShape(enabled)); dispatch(editShape(enabled));
}, },
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void { onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frame, states)); dispatch(updateAnnotationsAsync(states));
}, },
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states)); dispatch(createAnnotationsAsync(sessionInstance, frame, states));
@ -231,7 +243,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(updateCanvasContextMenu(false, 0, 0)); dispatch(updateCanvasContextMenu(false, 0, 0));
} }
dispatch(activateObject(activatedStateID)); dispatch(activateObject(activatedStateID, null));
}, },
onSelectObjects(selectedStatesID: number[]): void { onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID)); dispatch(selectObjects(selectedStatesID));

@ -29,7 +29,7 @@ interface StateToProps {
} }
interface DispatchToProps { interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; updateAnnotations(states: any[]): void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void; changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void;
} }
@ -68,8 +68,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); dispatch(updateAnnotationsAsync(states));
}, },
changeLabelColor( changeLabelColor(
sessionInstance: any, sessionInstance: any,
@ -162,8 +162,6 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
private switchHidden(value: boolean): void { private switchHidden(value: boolean): void {
const { const {
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
const { ownObjectStates } = this.state; const { ownObjectStates } = this.state;
@ -171,14 +169,12 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
state.hidden = value; state.hidden = value;
} }
updateAnnotations(jobInstance, frameNumber, ownObjectStates); updateAnnotations(ownObjectStates);
} }
private switchLock(value: boolean): void { private switchLock(value: boolean): void {
const { const {
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
const { ownObjectStates } = this.state; const { ownObjectStates } = this.state;
@ -186,7 +182,7 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
state.lock = value; state.lock = value;
} }
updateAnnotations(jobInstance, frameNumber, ownObjectStates); updateAnnotations(ownObjectStates);
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -49,7 +49,7 @@ interface StateToProps {
interface DispatchToProps { interface DispatchToProps {
changeFrame(frame: number): void; changeFrame(frame: number): void;
updateState(sessionInstance: any, frameNumber: number, objectState: any): void; updateState(objectState: any): void;
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void; createAnnotations(sessionInstance: any, frameNumber: number, state: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void; activateObject: (activatedStateID: number | null) => void;
@ -57,7 +57,7 @@ interface DispatchToProps {
copyShape: (objectState: any) => void; copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void; propagateObject: (objectState: any) => void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void; changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void;
changeGroupColor(sessionInstance: any, frameNumber: number, group: number, color: string): void; changeGroupColor(group: number, color: string): void;
} }
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -124,8 +124,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
changeFrame(frame: number): void { changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame)); dispatch(changeFrameAsync(frame));
}, },
updateState(sessionInstance: any, frameNumber: number, state: any): void { updateState(state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state])); dispatch(updateAnnotationsAsync([state]));
}, },
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void { createAnnotations(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(createAnnotationsAsync(sessionInstance, frameNumber, state)); dispatch(createAnnotationsAsync(sessionInstance, frameNumber, state));
@ -134,7 +134,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(collapseObjectItems(objectStates, collapsed)); dispatch(collapseObjectItems(objectStates, collapsed));
}, },
activateObject(activatedStateID: number | null): void { activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID)); dispatch(activateObjectAction(activatedStateID, null));
}, },
removeObject(sessionInstance: any, objectState: any): void { removeObject(sessionInstance: any, objectState: any): void {
dispatch(removeObjectAsync(sessionInstance, objectState, true)); dispatch(removeObjectAsync(sessionInstance, objectState, true));
@ -154,13 +154,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
): void { ): void {
dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color)); dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color));
}, },
changeGroupColor( changeGroupColor(group: number, color: string): void {
sessionInstance: any, dispatch(changeGroupColorAsync(group, color));
frameNumber: number,
group: number,
color: string,
): void {
dispatch(changeGroupColorAsync(sessionInstance, frameNumber, group, color));
}, },
}; };
} }
@ -392,7 +387,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
objectState.color = color; objectState.color = color;
this.commit(); this.commit();
} else if (colorBy === ColorBy.GROUP) { } else if (colorBy === ColorBy.GROUP) {
changeGroupColor(jobInstance, frameNumber, objectState.group.id, color); changeGroupColor(objectState.group.id, color);
} else if (colorBy === ColorBy.LABEL) { } else if (colorBy === ColorBy.LABEL) {
changeLabelColor(jobInstance, frameNumber, objectState.label, color); changeLabelColor(jobInstance, frameNumber, objectState.label, color);
} }
@ -421,11 +416,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
const { const {
objectState, objectState,
updateState, updateState,
jobInstance,
frameNumber,
} = this.props; } = this.props;
updateState(jobInstance, frameNumber, objectState); updateState(objectState);
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -6,15 +6,11 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys'; import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { SelectValue } from 'antd/lib/select';
import ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list'; import ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list';
import { import {
updateAnnotationsAsync, updateAnnotationsAsync,
fetchAnnotationsAsync,
removeObjectAsync, removeObjectAsync,
changeFrameAsync, changeFrameAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems, collapseObjectItems,
copyShape as copyShapeAction, copyShape as copyShapeAction,
propagateObject as propagateObjectAction, propagateObject as propagateObjectAction,
@ -42,8 +38,7 @@ interface StateToProps {
} }
interface DispatchToProps { interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; updateAnnotations(states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
collapseStates(states: any[], value: boolean): void; collapseStates(states: any[], value: boolean): void;
removeObject: (sessionInstance: any, objectState: any, force: boolean) => void; removeObject: (sessionInstance: any, objectState: any, force: boolean) => void;
copyShape: (objectState: any) => void; copyShape: (objectState: any) => void;
@ -111,19 +106,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); dispatch(updateAnnotationsAsync(states));
}, },
collapseStates(states: any[], collapsed: boolean): void { collapseStates(states: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(states, collapsed)); dispatch(collapseObjectItems(states, collapsed));
}, },
changeAnnotationsFilters(
sessionInstance: any,
filters: string[],
): void {
dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance));
},
removeObject(sessionInstance: any, objectState: any, force: boolean): void { removeObject(sessionInstance: any, objectState: any, force: boolean): void {
dispatch(removeObjectAsync(sessionInstance, objectState, force)); dispatch(removeObjectAsync(sessionInstance, objectState, force));
}, },
@ -190,15 +178,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
}); });
}; };
private onChangeAnnotationsFilters = (value: SelectValue): void => {
const {
jobInstance,
changeAnnotationsFilters,
} = this.props;
const filters = value as string[];
changeAnnotationsFilters(jobInstance, filters);
};
private onLockAllStates = (): void => { private onLockAllStates = (): void => {
this.lockAllStates(true); this.lockAllStates(true);
}; };
@ -227,28 +206,24 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const { const {
objectStates, objectStates,
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
for (const objectState of objectStates) { for (const objectState of objectStates) {
objectState.lock = locked; objectState.lock = locked;
} }
updateAnnotations(jobInstance, frameNumber, objectStates); updateAnnotations(objectStates);
} }
private hideAllStates(hidden: boolean): void { private hideAllStates(hidden: boolean): void {
const { const {
objectStates, objectStates,
updateAnnotations, updateAnnotations,
jobInstance,
frameNumber,
} = this.props; } = this.props;
for (const objectState of objectStates) { for (const objectState of objectStates) {
objectState.hidden = hidden; objectState.hidden = hidden;
} }
updateAnnotations(jobInstance, frameNumber, objectStates); updateAnnotations(objectStates);
} }
private collapseAllStates(collapsed: boolean): void { private collapseAllStates(collapsed: boolean): void {
@ -262,12 +237,10 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element { public render(): JSX.Element {
const { const {
annotationsFilters,
statesHidden, statesHidden,
statesLocked, statesLocked,
activatedStateID, activatedStateID,
objectStates, objectStates,
frameNumber,
jobInstance, jobInstance,
updateAnnotations, updateAnnotations,
removeObject, removeObject,
@ -276,7 +249,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
changeFrame, changeFrame,
maxZLayer, maxZLayer,
minZLayer, minZLayer,
annotationsFiltersHistory,
} = this.props; } = this.props;
const { const {
sortedStatesID, sortedStatesID,
@ -399,7 +371,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state) { if (state) {
state.lock = !state.lock; state.lock = !state.lock;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => { SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => {
@ -411,7 +383,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state) { if (state) {
state.hidden = !state.hidden; state.hidden = !state.hidden;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => { SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
@ -419,7 +391,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state && state.objectType !== ObjectType.TAG) {
state.occluded = !state.occluded; state.occluded = !state.occluded;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => { SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => {
@ -427,7 +399,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) { if (state && state.objectType === ObjectType.TRACK) {
state.keyframe = !state.keyframe; state.keyframe = !state.keyframe;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => { SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => {
@ -435,7 +407,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) { if (state && state.objectType === ObjectType.TRACK) {
state.outside = !state.outside; state.outside = !state.outside;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
DELETE_OBJECT: (event: KeyboardEvent | undefined) => { DELETE_OBJECT: (event: KeyboardEvent | undefined) => {
@ -450,7 +422,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = minZLayer - 1; state.zOrder = minZLayer - 1;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
TO_FOREGROUND: (event: KeyboardEvent | undefined) => { TO_FOREGROUND: (event: KeyboardEvent | undefined) => {
@ -458,7 +430,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated(); const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) { if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = maxZLayer + 1; state.zOrder = maxZLayer + 1;
updateAnnotations(jobInstance, frameNumber, [state]); updateAnnotations([state]);
} }
}, },
COPY_SHAPE: (event: KeyboardEvent | undefined) => { COPY_SHAPE: (event: KeyboardEvent | undefined) => {
@ -506,10 +478,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
{...this.props} {...this.props}
statesOrdering={statesOrdering} statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID} sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering} changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
annotationsFiltersHistory={annotationsFiltersHistory}
lockAllStates={this.onLockAllStates} lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates} unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates} collapseAllStates={this.onCollapseAllStates}

@ -22,10 +22,12 @@ import {
undoActionAsync, undoActionAsync,
redoActionAsync, redoActionAsync,
searchAnnotationsAsync, searchAnnotationsAsync,
changeWorkspace as changeWorkspaceAction,
activateObject,
} 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';
import { CombinedState, FrameSpeed } from 'reducers/interfaces'; import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
jobInstance: any; jobInstance: any;
@ -41,6 +43,7 @@ interface StateToProps {
redoAction?: string; redoAction?: string;
autoSave: boolean; autoSave: boolean;
autoSaveInterval: number; autoSaveInterval: number;
workspace: Workspace;
} }
interface DispatchToProps { interface DispatchToProps {
@ -51,6 +54,7 @@ interface DispatchToProps {
undo(sessionInstance: any, frameNumber: any): void; undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void; redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void; searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
changeWorkspace(workspace: Workspace): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -76,6 +80,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
canvas: { canvas: {
ready: canvasIsReady, ready: canvasIsReady,
}, },
workspace,
}, },
settings: { settings: {
player: { player: {
@ -103,6 +108,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
redoAction: history.redo[history.redo.length - 1], redoAction: history.redo[history.redo.length - 1],
autoSave, autoSave,
autoSaveInterval, autoSaveInterval,
workspace,
}; };
} }
@ -130,6 +136,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void { searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo)); dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
}, },
changeWorkspace(workspace: Workspace): void {
dispatch(activateObject(null, null));
dispatch(changeWorkspaceAction(workspace));
},
}; };
} }
@ -442,8 +452,10 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
frameNumber, frameNumber,
undoAction, undoAction,
redoAction, redoAction,
searchAnnotations, workspace,
canvasIsReady, canvasIsReady,
searchAnnotations,
changeWorkspace,
} = this.props; } = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => { const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -602,6 +614,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
onSliderChange={this.onChangePlayerSliderValue} onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue} onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick} onURLIconClick={this.onURLIconClick}
changeWorkspace={changeWorkspace}
workspace={workspace}
playing={playing} playing={playing}
saving={saving} saving={saving}
savingStatuses={savingStatuses} savingStatuses={savingStatuses}

@ -12,6 +12,7 @@ import {
ActiveControl, ActiveControl,
ShapeType, ShapeType,
ObjectType, ObjectType,
Workspace,
} from './interfaces'; } from './interfaces';
const defaultState: AnnotationState = { const defaultState: AnnotationState = {
@ -54,6 +55,7 @@ const defaultState: AnnotationState = {
annotations: { annotations: {
selectedStatesID: [], selectedStatesID: [],
activatedStateID: null, activatedStateID: null,
activatedAttributeID: null,
saving: { saving: {
uploading: false, uploading: false,
statuses: [], statuses: [],
@ -88,6 +90,7 @@ const defaultState: AnnotationState = {
sidebarCollapsed: false, sidebarCollapsed: false,
appearanceCollapsed: false, appearanceCollapsed: false,
tabContentHeight: 0, tabContentHeight: 0,
workspace: Workspace.STANDARD,
}; };
export default (state = defaultState, action: AnyAction): AnnotationState => { export default (state = defaultState, action: AnyAction): AnnotationState => {
@ -646,7 +649,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}; };
} }
case AnnotationActionTypes.ACTIVATE_OBJECT: { case AnnotationActionTypes.ACTIVATE_OBJECT: {
const { activatedStateID } = action.payload; const {
activatedStateID,
activatedAttributeID,
} = action.payload;
const { const {
canvas: { canvas: {
activeControl, activeControl,
@ -663,6 +670,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: { annotations: {
...state.annotations, ...state.annotations,
activatedStateID, activatedStateID,
activatedAttributeID,
}, },
}; };
} }
@ -1041,6 +1049,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.CHANGE_WORKSPACE: {
const { workspace } = action.payload;
return {
...state,
workspace,
};
}
case AnnotationActionTypes.RESET_CANVAS: { case AnnotationActionTypes.RESET_CANVAS: {
return { return {
...state, ...state,

@ -335,6 +335,7 @@ export interface AnnotationState {
annotations: { annotations: {
selectedStatesID: number[]; selectedStatesID: number[];
activatedStateID: number | null; activatedStateID: number | null;
activatedAttributeID: number | null;
collapsed: Record<number, boolean>; collapsed: Record<number, boolean>;
states: any[]; states: any[];
filters: string[]; filters: string[];
@ -367,6 +368,12 @@ export interface AnnotationState {
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
tabContentHeight: number; tabContentHeight: number;
workspace: Workspace;
}
export enum Workspace {
STANDARD = 'Standard',
ATTRIBUTE_ANNOTATION = 'Attribute annotation',
} }
export enum GridColor { export enum GridColor {

Loading…
Cancel
Save