Skip to content

Commit

Permalink
React UI: Attribute annotation mode (#1255)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bsekachev authored Mar 17, 2020
1 parent 6a7bf6d commit 1bb582f
Show file tree
Hide file tree
Showing 29 changed files with 1,518 additions and 445 deletions.
91 changes: 72 additions & 19 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
y + height / 2,
]);

const canvasOffset = this.canvas.getBoundingClientRect();
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
this.canvas.clientWidth / 2 + canvasOffset.left,
this.canvas.clientHeight / 2 + canvasOffset.top,
];

const dragged = {
Expand Down Expand Up @@ -725,7 +726,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (object) {
const bbox: SVG.BBox = object.bbox();
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) {
this.activate(this.controller.activeElement);
Expand Down Expand Up @@ -1014,7 +1015,26 @@ export class CanvasViewImpl implements CanvasView, Listener {
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) {
const { clientID } = this.activeElement;
const drawnState = this.drawnStates[clientID];
Expand Down Expand Up @@ -1047,29 +1067,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.sortObjects();

this.activeElement = {
...this.activeElement,
clientID: null,
attributeID: null,
};
}
}

private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement.clientID !== null) {
// Check if it is the same element
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
private deactivate(): void {
this.deactivateAttribute();
this.deactivateShape();
}

// Deactivate previous element
this.deactivate();
}
private activateAttribute(clientID: number, attributeID: number): void {
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;
if (clientID === null) {
return;
this.activeElement = {
...this.activeElement,
attributeID,
};
}
}

private activateShape(clientID: number): void {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);

Expand All @@ -1082,7 +1107,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
return;
}

this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];

let text = this.svgTexts[clientID];
Expand Down Expand Up @@ -1189,6 +1213,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
});

this.activeElement = {
...this.activeElement,
clientID,
};

this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false,
cancelable: true,
Expand All @@ -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.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox();
Expand Down
86 changes: 68 additions & 18 deletions cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ import {
Task,
FrameSpeed,
Rotation,
Workspace,
} from 'reducers/interfaces';

import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store';

interface AnnotationsParameters {
filters: string[];
frame: number;
showAllInterpolationTracks: boolean;
jobInstance: any;
}

const cvat = getCore();
let store: null | Store<CombinedState> = null;

Expand All @@ -34,19 +42,37 @@ function getStore(): Store<CombinedState> {
return store;
}

function receiveAnnotationsParameters():
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
function receiveAnnotationsParameters(): AnnotationsParameters {
if (store === null) {
store = getCVATStore();
}

const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations;
const frame = state.annotation.player.frame.number;
const { showAllInterpolationTracks } = state.settings.workspace;
const {
annotation: {
annotations: {
filters,
},
player: {
frame: {
number: frame,
},
},
job: {
instance: jobInstance,
},
},
settings: {
workspace: {
showAllInterpolationTracks,
},
},
} = state;

return {
filters,
frame,
jobInstance,
showAllInterpolationTracks,
};
}
Expand Down Expand Up @@ -138,11 +164,22 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
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 {
return {
type: AnnotationActionTypes.ADD_Z_LAYER,
payload: {},
};
}

Expand All @@ -155,12 +192,17 @@ export function switchZLayer(cur: number): AnyAction {
};
}

export function fetchAnnotationsAsync(sessionInstance: any):
export function fetchAnnotationsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const {
filters,
frame,
showAllInterpolationTracks,
jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);

Expand Down Expand Up @@ -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 {
type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: {
activatedStateID,
activatedAttributeID,
},
};
}
Expand Down Expand Up @@ -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> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
filters,
frame,
showAllInterpolationTracks,
} = receiveAnnotationsParameters();

try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
dispatch(activateObject(null, null));
}

const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
const history = await jobInstance.actions.get();
const [minZ, maxZ] = computeZRange(states);

dispatch({
Expand All @@ -933,8 +986,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
});
} catch (error) {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
Expand Down Expand Up @@ -1112,8 +1164,6 @@ export function changeLabelColorAsync(
}

export function changeGroupColorAsync(
sessionInstance: any,
frameNumber: number,
group: number,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
Expand All @@ -1123,9 +1173,9 @@ export function changeGroupColorAsync(
.filter((_state: any): boolean => _state.group.id === group);
if (groupStates.length) {
groupStates[0].group.color = color;
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates));
dispatch(updateAnnotationsAsync(groupStates));
} else {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, []));
dispatch(updateAnnotationsAsync([]));
}
};
}
Expand Down
19 changes: 16 additions & 3 deletions cvat-ui/src/components/annotation-page/annotation-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import {
Result,
} from 'antd';

import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';

interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
workspace: Workspace;
}


Expand All @@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job,
fetching,
getJob,
workspace,
} = props;


if (job === null) {
if (!fetching) {
getJob();
Expand All @@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {

return (
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceComponent />
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</Layout>
);
Expand Down
Loading

0 comments on commit 1bb582f

Please sign in to comment.