From 7a791f9f8814eebe0ffca83f9ecea9d7a8b5cf01 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 27 Jan 2020 17:18:22 +0300 Subject: [PATCH 01/25] Keyframes navigation --- cvat-core/src/annotations-objects.js | 88 +++++++++++-------- cvat-core/src/enums.js | 2 +- cvat-core/src/object-state.js | 19 +++- .../objects-side-bar/object-item.tsx | 49 +++++++++-- .../objects-side-bar/object-item.tsx | 81 +++++++++++++++++ 5 files changed, 194 insertions(+), 45 deletions(-) diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 1e05b1e91bf1..257d6694dfcd 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -496,8 +496,15 @@ // Method is used to construct ObjectState objects get(frame) { + const { + prev, + next, + first, + last, + } = this.boundedKeyframes(frame); + return { - ...this.getPosition(frame), + ...this.getPosition(frame, prev, next), attributes: this.getAttributes(frame), group: this.group, objectType: ObjectType.TRACK, @@ -509,30 +516,48 @@ hidden: this.hidden, updated: this.updated, label: this.label, + keyframes: { + prev, + next, + first, + last, + }, frame, }; } - neighborsFrames(targetFrame) { + boundedKeyframes(targetFrame) { const frames = Object.keys(this.shapes).map((frame) => +frame); let lDiff = Number.MAX_SAFE_INTEGER; let rDiff = Number.MAX_SAFE_INTEGER; + let first = Number.MAX_SAFE_INTEGER; + let last = Number.MIN_SAFE_INTEGER; for (const frame of frames) { + if (frame < first) { + first = frame; + } + if (frame > last) { + last = frame; + } + const diff = Math.abs(targetFrame - frame); - if (frame <= targetFrame && diff < lDiff) { + + if (frame < targetFrame && diff < lDiff) { lDiff = diff; - } else if (diff < rDiff) { + } else if (frame > targetFrame && diff < rDiff) { rDiff = diff; } } - const leftFrame = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff; - const rightFrame = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff; + const prev = lDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame - lDiff; + const next = rDiff === Number.MAX_SAFE_INTEGER ? null : targetFrame + rDiff; return { - leftFrame, - rightFrame, + prev, + next, + first, + last, }; } @@ -753,58 +778,45 @@ return objectStateFactory.call(this, frame, this.get(frame)); } - getPosition(targetFrame) { - const { - leftFrame, - rightFrame, - } = this.neighborsFrames(targetFrame); - + getPosition(targetFrame, leftKeyframe, rightFrame) { + const leftFrame = targetFrame in this.shapes ? targetFrame : leftKeyframe; const rightPosition = Number.isInteger(rightFrame) ? this.shapes[rightFrame] : null; const leftPosition = Number.isInteger(leftFrame) ? this.shapes[leftFrame] : null; - if (leftPosition && leftFrame === targetFrame) { - return { - points: [...leftPosition.points], - occluded: leftPosition.occluded, - outside: leftPosition.outside, - zOrder: leftPosition.zOrder, - keyframe: true, - }; - } - - if (rightPosition && leftPosition) { + if (leftPosition && rightPosition) { return { ...this.interpolatePosition( leftPosition, rightPosition, (targetFrame - leftFrame) / (rightFrame - leftFrame), ), - keyframe: false, + keyframe: targetFrame in this.shapes, }; } - if (rightPosition) { + if (leftPosition) { return { - points: [...rightPosition.points], - occluded: rightPosition.occluded, - outside: true, + points: [...leftPosition.points], + occluded: leftPosition.occluded, + outside: leftPosition.outside, zOrder: 0, - keyframe: false, + keyframe: targetFrame in this.shapes, }; } - if (leftPosition) { + if (rightPosition) { return { - points: [...leftPosition.points], - occluded: leftPosition.occluded, - outside: leftPosition.outside, + points: [...rightPosition.points], + occluded: rightPosition.occluded, + outside: true, zOrder: 0, - keyframe: false, + keyframe: targetFrame in this.shapes, }; } - throw new ScriptingError( - `No one neightbour frame found for the track with client ID: "${this.id}"`, + throw new DataError( + 'No one left position or right position was found. ' + + `Interpolation impossible. Client ID: ${this.id}`, ); } diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js index a0e7b2c41d47..0e9b066b31db 100644 --- a/cvat-core/src/enums.js +++ b/cvat-core/src/enums.js @@ -167,7 +167,7 @@ }; /** - * Array of hex color + * Array of hex colors * @type {module:API.cvat.classes.Loader[]} values * @name colors * @memberof module:API.cvat.enums diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js index 5d130d5e62bb..6bf7e2bc05b8 100644 --- a/cvat-core/src/object-state.js +++ b/cvat-core/src/object-state.js @@ -21,7 +21,7 @@ * initial information about an ObjectState; * Necessary fields: objectType, shapeType, frame, updated * Optional fields: points, group, zOrder, outside, occluded, hidden, - * attributes, lock, label, mode, color, keyframe, clientID, serverID + * attributes, lock, label, mode, color, keyframe, keyframes, clientID, serverID * These fields can be set later via setters */ constructor(serialized) { @@ -39,6 +39,7 @@ lock: null, color: null, hidden: null, + keyframes: serialized.keyframes, updated: serialized.updated, clientID: serialized.clientID, @@ -240,6 +241,22 @@ data.keyframe = keyframe; }, }, + keyframes: { + /** + * Object of keyframes { first, prev, next, last } + * @name keyframes + * @type {object} + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + */ + get: () => { + if (data.keyframes) { + return { ...data.keyframes }; + } + return null; + }, + }, occluded: { /** * @name occluded diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index d45c16afe58d..db3597c0c1ee 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -76,6 +76,11 @@ interface ItemButtonsProps { hidden: boolean; keyframe: boolean | undefined; + navigateFirstKeyframe: null | (() => void); + navigatePrevKeyframe: null | (() => void); + navigateNextKeyframe: null | (() => void); + navigateLastKeyframe: null | (() => void); + setOccluded(): void; unsetOccluded(): void; setOutside(): void; @@ -96,6 +101,12 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => { locked, hidden, keyframe, + + navigateFirstKeyframe, + navigatePrevKeyframe, + navigateNextKeyframe, + navigateLastKeyframe, + setOccluded, unsetOccluded, setOutside, @@ -114,16 +125,28 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => { - + { navigateFirstKeyframe + ? + : + } - + { navigatePrevKeyframe + ? + : + } - + { navigateNextKeyframe + ? + : + } - + { navigateLastKeyframe + ? + : + } @@ -422,6 +445,10 @@ interface Props { attributes: any[]; collapsed: boolean; + navigateFirstKeyframe: null | (() => void); + navigatePrevKeyframe: null | (() => void); + navigateNextKeyframe: null | (() => void); + navigateLastKeyframe: null | (() => void); setOccluded(): void; unsetOccluded(): void; setOutside(): void; @@ -443,7 +470,7 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { && nextProps.outside === prevProps.outside && nextProps.hidden === prevProps.hidden && nextProps.keyframe === prevProps.keyframe - && nextProps.label === prevProps.label + && nextProps.labelID === prevProps.labelID && nextProps.color === prevProps.color && nextProps.clientID === prevProps.clientID && nextProps.objectType === prevProps.objectType @@ -451,6 +478,10 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { && nextProps.collapsed === prevProps.collapsed && nextProps.labels === prevProps.labels && nextProps.attributes === prevProps.attributes + && nextProps.navigateFirstKeyframe === prevProps.navigateFirstKeyframe + && nextProps.navigatePrevKeyframe === prevProps.navigatePrevKeyframe + && nextProps.navigateNextKeyframe === prevProps.navigateNextKeyframe + && nextProps.navigateLastKeyframe === prevProps.navigateLastKeyframe && attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues); } @@ -471,6 +502,10 @@ const ObjectItem = React.memo((props: Props): JSX.Element => { attributes, labels, collapsed, + navigateFirstKeyframe, + navigatePrevKeyframe, + navigateNextKeyframe, + navigateLastKeyframe, setOccluded, unsetOccluded, @@ -509,6 +544,10 @@ const ObjectItem = React.memo((props: Props): JSX.Element => { locked={locked} hidden={hidden} keyframe={keyframe} + navigateFirstKeyframe={navigateFirstKeyframe} + navigatePrevKeyframe={navigatePrevKeyframe} + navigateNextKeyframe={navigateNextKeyframe} + navigateLastKeyframe={navigateLastKeyframe} setOccluded={setOccluded} unsetOccluded={unsetOccluded} setOutside={setOutside} diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 0c8c7f3be084..92067e2caf1b 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -6,6 +6,7 @@ import { import { collapseObjectItems, updateAnnotationsAsync, + changeFrameAsync, } from 'actions/annotation-actions'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; @@ -24,6 +25,7 @@ interface StateToProps { } interface DispatchToProps { + changeFrame(frame: number): void; updateState(sessionInstance: any, frameNumber: number, objectState: any): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void; } @@ -67,6 +69,9 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { function mapDispatchToProps(dispatch: any): DispatchToProps { return { + changeFrame(frame: number): void { + dispatch(changeFrameAsync(frame)); + }, updateState(sessionInstance: any, frameNumber: number, state: any): void { dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state])); }, @@ -84,6 +89,58 @@ class ObjectItemContainer extends React.PureComponent { this.commit(); }; + private navigateFirstKeyframe = (): void => { + const { + objectState, + changeFrame, + frameNumber, + } = this.props; + + const { first } = objectState.keyframes; + if (first !== frameNumber) { + changeFrame(first); + } + }; + + private navigatePrevKeyframe = (): void => { + const { + objectState, + changeFrame, + frameNumber, + } = this.props; + + const { prev } = objectState.keyframes; + if (prev !== null && prev !== frameNumber) { + changeFrame(prev); + } + }; + + private navigateNextKeyframe = (): void => { + const { + objectState, + changeFrame, + frameNumber, + } = this.props; + + const { next } = objectState.keyframes; + if (next !== null && next !== frameNumber) { + changeFrame(next); + } + }; + + private navigateLastKeyframe = (): void => { + const { + objectState, + changeFrame, + frameNumber, + } = this.props; + + const { last } = objectState.keyframes; + if (last !== frameNumber) { + changeFrame(last); + } + }; + private unlock = (): void => { const { objectState } = this.props; objectState.lock = false; @@ -184,8 +241,16 @@ class ObjectItemContainer extends React.PureComponent { collapsed, labels, attributes, + frameNumber, } = this.props; + const { + first, + prev, + next, + last, + } = objectState.keyframes; + return ( { attributes={attributes} labels={labels} collapsed={collapsed} + navigateFirstKeyframe={ + first === frameNumber + ? null : this.navigateFirstKeyframe + } + navigatePrevKeyframe={ + prev === frameNumber || prev === null + ? null : this.navigatePrevKeyframe + } + navigateNextKeyframe={ + next === frameNumber || next === null + ? null : this.navigateNextKeyframe + } + navigateLastKeyframe={ + last <= frameNumber + ? null : this.navigateLastKeyframe + } setOccluded={this.setOccluded} unsetOccluded={this.unsetOccluded} setOutside={this.setOutside} From 3795a24481225c5f4aefa5937d64eebdb2506531 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 27 Jan 2020 19:49:39 +0300 Subject: [PATCH 02/25] Synchronized objects on canvas and in side panel --- cvat-canvas/README.md | 3 +- cvat-canvas/src/typescript/canvasModel.ts | 2 +- cvat-canvas/src/typescript/canvasView.ts | 38 ++++++++++++++- cvat-ui/src/actions/annotation-actions.ts | 22 ++++++++- .../standard-workspace/canvas-wrapper.tsx | 46 ++++++++++++++++++- .../objects-side-bar/object-item.tsx | 16 +++++-- .../objects-side-bar/styles.scss | 4 -- .../standard-workspace/canvas-wrapper.tsx | 16 +++++++ .../objects-side-bar/object-item.tsx | 32 ++++++++++--- cvat-ui/src/reducers/annotation-reducer.ts | 28 +++++++++++ cvat-ui/src/reducers/interfaces.ts | 2 + 11 files changed, 190 insertions(+), 19 deletions(-) diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index a7def23c98ee..1757a5dc3a8e 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -111,7 +111,8 @@ Canvas itself handles: Standard JS events are used. ```js - canvas.setup - - canvas.activated => ObjectState + - canvas.activated => {state: ObjectState} + - canvas.clicked => {state: ObjectState} - canvas.deactivated - canvas.moved => {states: ObjectState[], x: number, y: number} - canvas.find => {states: ObjectState[], x: number, y: number} diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index d6b9825606c9..c390fb74cf05 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -33,7 +33,7 @@ export interface FocusData { } export interface ActiveElement { - clientID: number | null; + clientID: number; attributeID: number | null; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 185a194b864d..156dd2d4bbea 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -82,7 +82,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private zoomHandler: ZoomHandler; private activeElement: { state: any; - attributeID: number; + attributeID: number | null; } | null; private set mode(value: Mode) { @@ -399,6 +399,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.svgTexts[state.clientID].remove(); } + this.svgShapes[state.clientID].off('click.canvas'); this.svgShapes[state.clientID].remove(); delete this.drawnStates[state.clientID]; } @@ -590,6 +591,12 @@ export class CanvasViewImpl implements CanvasView, Listener { ); // Setup event handlers + this.canvas.addEventListener('mouseleave', (e: MouseEvent): void => { + if (!e.ctrlKey) { + this.deactivate(); + } + }); + this.content.addEventListener('dblclick', (e: MouseEvent): void => { if (e.ctrlKey || e.shiftKey) return; self.controller.fit(); @@ -598,6 +605,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.content.addEventListener('mousedown', (event): void => { if ([1, 2].includes(event.which)) { + this.deactivate(); if ([Mode.DRAG_CANVAS, Mode.IDLE].includes(this.mode)) { self.controller.enableDrag(event.clientX, event.clientY); } else if (this.mode === Mode.ZOOM_CANVAS && event.which === 2) { @@ -764,6 +772,7 @@ export class CanvasViewImpl implements CanvasView, Listener { this.groupHandler.select(this.controller.selected); } } else if (reason === UpdateReasons.CANCEL) { + this.deactivate(); if (this.mode === Mode.DRAW) { this.drawHandler.cancel(); } else if (this.mode === Mode.MERGE) { @@ -910,6 +919,16 @@ export class CanvasViewImpl implements CanvasView, Listener { .addPoints(stringified, state); } } + + this.svgShapes[state.clientID].on('click.canvas', (): void => { + this.canvas.dispatchEvent(new CustomEvent('canvas.clicked', { + bubbles: false, + cancelable: true, + detail: { + state, + }, + })); + }); } this.saveState(state); @@ -935,6 +954,15 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).off('resizedone'); (shape as any).resize(false); + this.canvas.dispatchEvent(new CustomEvent('canvas.deactivated', { + bubbles: false, + cancelable: true, + detail: { + state, + }, + })); + + // TODO: Hide text only if it is hidden by settings const text = this.svgTexts[state.clientID]; if (text) { @@ -1054,6 +1082,14 @@ export class CanvasViewImpl implements CanvasView, Listener { this.onEditDone(state, translateBetweenSVG(this.content, this.background, points)); } }); + + this.canvas.dispatchEvent(new CustomEvent('canvas.activated', { + bubbles: false, + cancelable: true, + detail: { + state, + }, + })); } // Update text position after corresponding box has been moved, resized, etc. diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 80f51c0cdd26..78892a6e9cf2 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -50,7 +50,27 @@ export enum AnnotationActionTypes { UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT', COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR', COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE', - COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS' + COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS', + ACTIVATE_OBJECT = 'ACTIVATE_OBJECT', + SELECT_OBJECTS = 'SELECT_OBJECTS', +} + +export function selectObjects(selectedStatesID: number[]): AnyAction { + return { + type: AnnotationActionTypes.SELECT_OBJECTS, + payload: { + selectedStatesID, + }, + }; +} + +export function activateObject(activatedStateID: number | null): AnyAction { + return { + type: AnnotationActionTypes.ACTIVATE_OBJECT, + payload: { + activatedStateID, + }, + }; } export function updateTabContentHeight(tabContentHeight: number): AnyAction { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index f070d61d9a1f..98d2aae90e99 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -23,6 +23,8 @@ interface Props { sidebarCollapsed: boolean; canvasInstance: Canvas; jobInstance: any; + activatedStateID: number | null; + selectedStatesID: number[]; annotations: any[]; frameData: any; frame: number; @@ -45,6 +47,8 @@ interface Props { onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; + onActivateObject: (activatedStateID: number | null) => void; + onSelectObjects: (selectedStatesID: number[]) => void; } export default class CanvasWrapperComponent extends React.PureComponent { @@ -69,8 +73,11 @@ export default class CanvasWrapperComponent extends React.PureComponent { gridSize, gridColor, gridOpacity, + frameData, + annotations, canvasInstance, sidebarCollapsed, + activatedStateID, } = this.props; if (prevProps.sidebarCollapsed !== sidebarCollapsed) { @@ -107,7 +114,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { } } - this.updateCanvas(); + if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) { + this.updateCanvas(); + } + + if (prevProps.activatedStateID !== activatedStateID) { + if (activatedStateID !== null) { + canvasInstance.activate(activatedStateID); + } else { + canvasInstance.cancel(); + } + } } private async onShapeDrawn(event: any): Promise { @@ -222,6 +239,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { onDragCanvas, onZoomCanvas, onResetCanvas, + onActivateObject, } = this.props; // Size @@ -268,7 +286,29 @@ export default class CanvasWrapperComponent extends React.PureComponent { onZoomCanvas(false); }); + canvasInstance.html().addEventListener('canvas.clicked', (e: any) => { + const { clientID } = e.detail.state; + const sidebarItem = window.document + .getElementById(`cvat-objects-sidebar-state-item-${clientID}`); + if (sidebarItem) { + sidebarItem.scrollIntoView(); + } + }); + + canvasInstance.html().addEventListener('canvas.deactivated', (e: any): void => { + const { activatedStateID } = this.props; + const { state } = e.detail; + + // when we activate element, canvas deactivates the previous + // and triggers this event + // in this case we do not need to update our state + if (state.clientID === activatedStateID) { + onActivateObject(null); + } + }); + canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise => { + const { activatedStateID } = this.props; const result = await jobInstance.annotations.select( event.detail.states, event.detail.x, @@ -282,7 +322,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { } } - canvasInstance.activate(result.state.clientID); + if (activatedStateID !== result.state.clientID) { + onActivateObject(result.state.clientID); + } } }); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index db3597c0c1ee..a939ea486904 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -429,6 +429,7 @@ const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => { }, attrAreTheSame); interface Props { + activated: boolean; objectType: ObjectType; shapeType: ShapeType; clientID: number; @@ -444,11 +445,12 @@ interface Props { labels: any[]; attributes: any[]; collapsed: boolean; - navigateFirstKeyframe: null | (() => void); navigatePrevKeyframe: null | (() => void); navigateNextKeyframe: null | (() => void); navigateLastKeyframe: null | (() => void); + + activate(): void; setOccluded(): void; unsetOccluded(): void; setOutside(): void; @@ -465,7 +467,8 @@ interface Props { } function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { - return nextProps.locked === prevProps.locked + return nextProps.activated === prevProps.activated + && nextProps.locked === prevProps.locked && nextProps.occluded === prevProps.occluded && nextProps.outside === prevProps.outside && nextProps.hidden === prevProps.hidden @@ -487,6 +490,7 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { const ObjectItem = React.memo((props: Props): JSX.Element => { const { + activated, objectType, shapeType, clientID, @@ -507,6 +511,7 @@ const ObjectItem = React.memo((props: Props): JSX.Element => { navigateNextKeyframe, navigateLastKeyframe, + activate, setOccluded, unsetOccluded, setOutside, @@ -525,9 +530,14 @@ const ObjectItem = React.memo((props: Props): JSX.Element => { const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase() : `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`; + const className = !activated ? 'cvat-objects-sidebar-state-item' + : 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item'; + return (
div:nth-child(3) { margin-top: 10px; } - - &:hover { - @extend .cvat-objects-sidebar-state-active-item; - } } .cvat-objects-sidebar-state-item-collapse { diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index 0aaacba28575..be6e12ee41c9 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -17,6 +17,8 @@ import { mergeAnnotationsAsync, groupAnnotationsAsync, splitAnnotationsAsync, + activateObject, + selectObjects, } from 'actions/annotation-actions'; import { GridColor, @@ -30,6 +32,8 @@ interface StateToProps { sidebarCollapsed: boolean; canvasInstance: Canvas; jobInstance: any; + activatedStateID: number | null; + selectedStatesID: number[]; annotations: any[]; frameData: any; frame: number; @@ -55,6 +59,8 @@ interface DispatchToProps { onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; + onActivateObject: (activatedStateID: number | null) => void; + onSelectObjects: (selectedStatesID: number[]) => void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -78,6 +84,8 @@ function mapStateToProps(state: CombinedState): StateToProps { }, annotations: { states: annotations, + activatedStateID, + selectedStatesID, }, sidebarCollapsed, }, @@ -97,6 +105,8 @@ function mapStateToProps(state: CombinedState): StateToProps { jobInstance, frameData, frame, + activatedStateID, + selectedStatesID, annotations, grid, gridSize, @@ -148,6 +158,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSplitAnnotations(sessionInstance: any, frame: number, state: any): void { dispatch(splitAnnotationsAsync(sessionInstance, frame, state)); }, + onActivateObject(activatedStateID: number | null): void { + dispatch(activateObject(activatedStateID)); + }, + onSelectObjects(selectedStatesID: number[]): void { + dispatch(selectObjects(selectedStatesID)); + }, }; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 92067e2caf1b..8abd5e959a37 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -7,6 +7,7 @@ import { collapseObjectItems, updateAnnotationsAsync, changeFrameAsync, + activateObject as activateObjectAction, } from 'actions/annotation-actions'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; @@ -22,12 +23,14 @@ interface StateToProps { attributes: any[]; jobInstance: any; frameNumber: number; + activated: boolean; } interface DispatchToProps { changeFrame(frame: number): void; updateState(sessionInstance: any, frameNumber: number, objectState: any): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void; + activateObject: (activatedStateID: number | null) => void; } function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { @@ -36,6 +39,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { annotations: { states, collapsed: statesCollapsed, + activatedStateID, }, job: { labels, @@ -64,6 +68,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { labels, jobInstance, frameNumber, + activated: activatedStateID === own.clientID, }; } @@ -78,17 +83,14 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { collapseOrExpand(objectStates: any[], collapsed: boolean): void { dispatch(collapseObjectItems(objectStates, collapsed)); }, + activateObject(activatedStateID: number | null): void { + dispatch(activateObjectAction(activatedStateID)); + }, }; } type Props = StateToProps & DispatchToProps; class ObjectItemContainer extends React.PureComponent { - private lock = (): void => { - const { objectState } = this.props; - objectState.lock = true; - this.commit(); - }; - private navigateFirstKeyframe = (): void => { const { objectState, @@ -141,6 +143,21 @@ class ObjectItemContainer extends React.PureComponent { } }; + private activate = (): void => { + const { + activateObject, + objectState, + } = this.props; + + activateObject(objectState.clientID); + }; + + private lock = (): void => { + const { objectState } = this.props; + objectState.lock = true; + this.commit(); + }; + private unlock = (): void => { const { objectState } = this.props; objectState.lock = false; @@ -242,6 +259,7 @@ class ObjectItemContainer extends React.PureComponent { labels, attributes, frameNumber, + activated, } = this.props; const { @@ -253,6 +271,7 @@ class ObjectItemContainer extends React.PureComponent { return ( { last <= frameNumber ? null : this.navigateLastKeyframe } + activate={this.activate} setOccluded={this.setOccluded} unsetOccluded={this.unsetOccluded} setOutside={this.setOutside} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index cf3cea7a7ca8..fcef79b55a82 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -36,6 +36,8 @@ const defaultState: AnnotationState = { activeObjectType: ObjectType.SHAPE, }, annotations: { + selectedStatesID: [], + activatedStateID: null, saving: { uploading: false, statuses: [], @@ -463,6 +465,32 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.ACTIVATE_OBJECT: { + const { + activatedStateID, + } = action.payload; + + return { + ...state, + annotations: { + ...state.annotations, + activatedStateID, + }, + }; + } + case AnnotationActionTypes.SELECT_OBJECTS: { + const { + selectedStatesID, + } = action.payload; + + return { + ...state, + annotations: { + ...state.annotations, + selectedStatesID, + }, + }; + } case AnnotationActionTypes.RESET_CANVAS: { return { ...state, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index d7589ab36400..c1bcbbfae88b 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -286,6 +286,8 @@ export interface AnnotationState { activeObjectType: ObjectType; }; annotations: { + selectedStatesID: number[]; + activatedStateID: number | null; collapsed: Record; states: any[]; saving: { From 1c7573644da84aedd7947a220203c346e8afe1a4 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 27 Jan 2020 20:17:46 +0300 Subject: [PATCH 03/25] Fixed minor bug with collapse --- .../objects-side-bar/objects-side-bar.tsx | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index 982fa5f2deb5..ec2b453831d0 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -65,20 +65,24 @@ const ObjectsSideBar = React.memo((props: Props): JSX.Element => { - - Appearance - } - key='appearance' - > + { !sidebarCollapsed + && ( + + Appearance + } + key='appearance' + > - - + + + ) + } ); }); From 71a247857f170d31a6ba2406481d0054beefda15 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 27 Jan 2020 20:23:55 +0300 Subject: [PATCH 04/25] Fixed css property 'pointer-events' --- .../standard-workspace/objects-side-bar/object-item.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index a939ea486904..1f143baa5b68 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -127,25 +127,25 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => { { navigateFirstKeyframe ? - : + : } { navigatePrevKeyframe ? - : + : } { navigateNextKeyframe ? - : + : } { navigateLastKeyframe ? - : + : } From f7fd38e50528981dfa094f9943206c3a1950230f Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 27 Jan 2020 21:42:25 +0300 Subject: [PATCH 05/25] Drawn appearance block --- cvat-ui/src/actions/settings-actions.ts | 41 ++++++++ .../objects-side-bar/appearance-block.tsx | 90 +++++++++++++++++ .../objects-side-bar/object-item.tsx | 2 +- .../objects-side-bar/objects-side-bar.tsx | 58 +++++++---- .../objects-side-bar/styles.scss | 15 ++- .../objects-side-bar/objects-side-bar.tsx | 96 ++++++++++++++++++- cvat-ui/src/reducers/interfaces.ts | 14 +++ cvat-ui/src/reducers/settings-reducer.ts | 43 +++++++++ 8 files changed, 335 insertions(+), 24 deletions(-) create mode 100644 cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 33802c780f87..e1614cdfb421 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -1,6 +1,7 @@ import { AnyAction } from 'redux'; import { GridColor, + ColorBy, } from 'reducers/interfaces'; export enum SettingsActionTypes { @@ -9,6 +10,46 @@ export enum SettingsActionTypes { CHANGE_GRID_SIZE = 'CHANGE_GRID_SIZE', CHANGE_GRID_COLOR = 'CHANGE_GRID_COLOR', CHANGE_GRID_OPACITY = 'CHANGE_GRID_OPACITY', + CHANGE_SHAPES_OPACITY = 'CHANGE_SHAPES_OPACITY', + CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY', + CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY', + CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS', +} + +export function changeShapesOpacity(opacity: number): AnyAction { + return { + type: SettingsActionTypes.CHANGE_SHAPES_OPACITY, + payload: { + opacity, + }, + }; +} + +export function changeSelectedShapesOpacity(selectedOpacity: number): AnyAction { + return { + type: SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY, + payload: { + selectedOpacity, + }, + }; +} + +export function changeShapesColorBy(colorBy: ColorBy): AnyAction { + return { + type: SettingsActionTypes.CHANGE_SHAPES_COLOR_BY, + payload: { + colorBy, + }, + }; +} + +export function changeShapesBlackBorders(blackBorders: boolean): AnyAction { + return { + type: SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS, + payload: { + blackBorders, + }, + }; } export function switchRotateAll(rotateAll: boolean): AnyAction { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx new file mode 100644 index 000000000000..6c86d3171c89 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx @@ -0,0 +1,90 @@ +import React from 'react'; + +import { + Checkbox, + Collapse, + Slider, + Radio, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; +import { RadioChangeEvent } from 'antd/lib/radio'; +import { SliderValue } from 'antd/lib/slider'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; + +import { ColorBy } from 'reducers/interfaces'; + +interface Props { + appearanceCollapsed: boolean; + colorBy: ColorBy; + opacity: number; + selectedOpacity: number; + blackBorders: boolean; + + collapseAppearance(): void; + changeShapesColorBy(event: RadioChangeEvent): void; + changeShapesOpacity(event: SliderValue): void; + changeSelectedShapesOpacity(event: SliderValue): void; + changeShapesBlackBorders(event: CheckboxChangeEvent): void; +} + +const AppearanceBlock = React.memo((props: Props): JSX.Element => { + const { + appearanceCollapsed, + colorBy, + opacity, + selectedOpacity, + blackBorders, + collapseAppearance, + changeShapesColorBy, + changeShapesOpacity, + changeSelectedShapesOpacity, + changeShapesBlackBorders, + } = props; + + return ( + + Appearance + } + key='appearance' + > +
+ Color by + + {ColorBy.INSTANCE} + {ColorBy.GROUP} + {ColorBy.LABEL} + + Opacity + + Selected opacity + + + Black borders + +
+
+
+ ); +}); + +export default AppearanceBlock; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 1f143baa5b68..7872877472bf 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -13,8 +13,8 @@ import { } from 'antd'; import Text from 'antd/lib/typography/Text'; -import { CheckboxChangeEvent } from 'antd/lib/checkbox'; import { RadioChangeEvent } from 'antd/lib/radio'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; import { ObjectOutsideIcon, diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index ec2b453831d0..c8330fc7e8a9 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -5,29 +5,66 @@ import { Icon, Tabs, Layout, - Collapse, } from 'antd'; import Text from 'antd/lib/typography/Text'; +import { RadioChangeEvent } from 'antd/lib/radio'; +import { SliderValue } from 'antd/lib/slider'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; + +import { ColorBy } from 'reducers/interfaces'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import LabelsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/labels-list'; +import AppearanceBlock from './appearance-block'; interface Props { sidebarCollapsed: boolean; appearanceCollapsed: boolean; + colorBy: ColorBy; + opacity: number; + selectedOpacity: number; + blackBorders: boolean; + collapseSidebar(): void; collapseAppearance(): void; + + changeShapesColorBy(event: RadioChangeEvent): void; + changeShapesOpacity(event: SliderValue): void; + changeSelectedShapesOpacity(event: SliderValue): void; + changeShapesBlackBorders(event: CheckboxChangeEvent): void; } const ObjectsSideBar = React.memo((props: Props): JSX.Element => { const { sidebarCollapsed, appearanceCollapsed, + colorBy, + opacity, + selectedOpacity, + blackBorders, collapseSidebar, collapseAppearance, + changeShapesColorBy, + changeShapesOpacity, + changeSelectedShapesOpacity, + changeShapesBlackBorders, } = props; + const appearanceProps = { + collapseAppearance, + appearanceCollapsed, + colorBy, + opacity, + selectedOpacity, + blackBorders, + + changeShapesColorBy, + changeShapesOpacity, + changeSelectedShapesOpacity, + changeShapesBlackBorders, + }; + return ( { - { !sidebarCollapsed - && ( - - Appearance - } - key='appearance' - > - - - - ) - } + { !sidebarCollapsed && } ); }); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss index c25c481cbbba..1dc3634740f3 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/styles.scss @@ -18,7 +18,7 @@ > .ant-collapse-content { background: $background-color-2; border-bottom: none; - height: 150px; + height: 230px; } } } @@ -241,4 +241,15 @@ width: 30px; height: 20px; border-radius: 5px; -} \ No newline at end of file +} + +.cvat-objects-appearance-content { + > div { + width: 100%; + + > label { + text-align: center; + width: 33%; + } + } +} diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index bd1fba14e01f..96cb4e5c3989 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -2,23 +2,47 @@ import React from 'react'; import { connect } from 'react-redux'; +import { RadioChangeEvent } from 'antd/lib/radio'; +import { SliderValue } from 'antd/lib/slider'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; + import ObjectsSidebarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; -import { CombinedState } from 'reducers/interfaces'; +import { + CombinedState, + ColorBy, +} from 'reducers/interfaces'; + import { collapseSidebar as collapseSidebarAction, collapseAppearance as collapseAppearanceAction, updateTabContentHeight as updateTabContentHeightAction, } from 'actions/annotation-actions'; +import { + changeShapesColorBy as changeShapesColorByAction, + changeShapesOpacity as changeShapesOpacityAction, + changeSelectedShapesOpacity as changeSelectedShapesOpacityAction, + changeShapesBlackBorders as changeShapesBlackBordersAction, +} from 'actions/settings-actions'; + + interface StateToProps { sidebarCollapsed: boolean; appearanceCollapsed: boolean; + colorBy: ColorBy; + opacity: number; + selectedOpacity: number; + blackBorders: boolean; } interface DispatchToProps { collapseSidebar(): void; collapseAppearance(): void; updateTabContentHeight(): void; + changeShapesColorBy(colorBy: ColorBy): void; + changeShapesOpacity(shapesOpacity: number): void; + changeSelectedShapesOpacity(selectedShapesOpacity: number): void; + changeShapesBlackBorders(blackBorders: boolean): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -27,11 +51,23 @@ function mapStateToProps(state: CombinedState): StateToProps { sidebarCollapsed, appearanceCollapsed, }, + settings: { + shapes: { + colorBy, + opacity, + selectedOpacity, + blackBorders, + }, + }, } = state; return { sidebarCollapsed, appearanceCollapsed, + colorBy, + opacity, + selectedOpacity, + blackBorders, }; } @@ -80,6 +116,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { ), ); }, + changeShapesColorBy(colorBy: ColorBy): void { + dispatch(changeShapesColorByAction(colorBy)); + }, + changeShapesOpacity(shapesOpacity: number): void { + dispatch(changeShapesOpacityAction(shapesOpacity)); + }, + changeSelectedShapesOpacity(selectedShapesOpacity: number): void { + dispatch(changeSelectedShapesOpacityAction(selectedShapesOpacity)); + }, + changeShapesBlackBorders(blackBorders: boolean): void { + dispatch(changeShapesBlackBordersAction(blackBorders)); + }, }; } @@ -90,9 +138,53 @@ class ObjectsSideBarContainer extends React.PureComponent { updateTabContentHeight(); } + private changeShapesColorBy = (event: RadioChangeEvent): void => { + const { changeShapesColorBy } = this.props; + changeShapesColorBy(event.target.value); + }; + + private changeShapesOpacity = (value: SliderValue): void => { + const { changeShapesOpacity } = this.props; + changeShapesOpacity(value as number); + }; + + private changeSelectedShapesOpacity = (value: SliderValue): void => { + const { changeSelectedShapesOpacity } = this.props; + changeSelectedShapesOpacity(value as number); + }; + + private changeShapesBlackBorders = (event: CheckboxChangeEvent): void => { + const { changeShapesBlackBorders } = this.props; + changeShapesBlackBorders(event.target.checked); + }; + public render(): JSX.Element { + const { + sidebarCollapsed, + appearanceCollapsed, + colorBy, + opacity, + selectedOpacity, + blackBorders, + collapseSidebar, + collapseAppearance, + } = this.props; + return ( - + ); } } diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index c1bcbbfae88b..f26b24680be7 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -318,6 +318,12 @@ export enum FrameSpeed { Slowest = 1, } +export enum ColorBy { + INSTANCE = 'Instance', + GROUP = 'Group', + LABEL = 'Label', +} + export interface PlayerSettingsState { frameStep: number; frameSpeed: FrameSpeed; @@ -339,7 +345,15 @@ export interface WorkspaceSettingsState { showAllInterpolationTracks: boolean; } +export interface ShapesSettingsState { + colorBy: ColorBy; + opacity: number; + selectedOpacity: number; + blackBorders: boolean; +} + export interface SettingsState { + shapes: ShapesSettingsState; workspace: WorkspaceSettingsState; player: PlayerSettingsState; } diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index d033ee0543fe..b35dd37df43d 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -5,9 +5,16 @@ import { SettingsState, GridColor, FrameSpeed, + ColorBy, } from './interfaces'; const defaultState: SettingsState = { + shapes: { + colorBy: ColorBy.INSTANCE, + opacity: 3, + selectedOpacity: 30, + blackBorders: false, + }, workspace: { autoSave: false, autoSaveInterval: 15 * 60 * 1000, @@ -76,6 +83,42 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case SettingsActionTypes.CHANGE_SHAPES_COLOR_BY: { + return { + ...state, + shapes: { + ...state.shapes, + colorBy: action.payload.colorBy, + }, + }; + } + case SettingsActionTypes.CHANGE_SHAPES_OPACITY: { + return { + ...state, + shapes: { + ...state.shapes, + opacity: action.payload.opacity, + }, + }; + } + case SettingsActionTypes.CHANGE_SELECTED_SHAPES_OPACITY: { + return { + ...state, + shapes: { + ...state.shapes, + selectedOpacity: action.payload.selectedOpacity, + }, + }; + } + case SettingsActionTypes.CHANGE_SHAPES_BLACK_BORDERS: { + return { + ...state, + shapes: { + ...state.shapes, + blackBorders: action.payload.blackBorders, + }, + }; + } default: { return state; } From e064cfa62014e8e43577e89ea40e6438f424226f Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 28 Jan 2020 19:03:58 +0300 Subject: [PATCH 06/25] Removed extra force reflow --- cvat-canvas/src/scss/canvas.scss | 30 +++++++++---------- cvat-canvas/src/typescript/canvasView.ts | 38 +++++++++++++++++------- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 529067ce2e86..75f7634c0521 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -32,33 +32,33 @@ polyline.cvat_canvas_shape { } .cvat_canvas_shape_grouping { - fill: darkmagenta; - fill-opacity: 0.5; + fill: darkmagenta !important; + fill-opacity: 0.5 !important; } polyline.cvat_canvas_shape_grouping { - stroke: darkmagenta; - stroke-opacity: 1; + stroke: darkmagenta !important; + stroke-opacity: 1 !important; } .cvat_canvas_shape_merging { - fill: blue; - fill-opacity: 0.5; + fill: blue !important; + fill-opacity: 0.5 !important; } -polyline.cvat_canvas_shape_splitting { - stroke: dodgerblue; - stroke-opacity: 1; +polyline.cvat_canvas_shape_merging { + stroke: blue !important; + stroke-opacity: 1 !important; } -.cvat_canvas_shape_splitting { - fill: dodgerblue; - fill-opacity: 0.5; +polyline.cvat_canvas_shape_splitting { + stroke: dodgerblue !important; + stroke-opacity: 1 !important; } -polyline.cvat_canvas_shape_merging { - stroke: blue; - stroke-opacity: 1; +.cvat_canvas_shape_splitting { + fill: dodgerblue !important; + fill-opacity: 0.5 !important; } .cvat_canvas_shape_drawing { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 156dd2d4bbea..b35b9b673ec4 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -376,6 +376,27 @@ export class CanvasViewImpl implements CanvasView, Listener { } private setupObjects(states: any[]): void { + const backgroundMatrix = this.background.getScreenCTM(); + const contentMatrix = (this.content.getScreenCTM() as DOMMatrix).inverse(); + + const translate = (points: number[]): number[] => { + if (backgroundMatrix && contentMatrix) { + const matrix = (contentMatrix as DOMMatrix).multiply(backgroundMatrix); + return points.reduce((result: number[], _: number, idx: number): number[] => { + if (idx % 2) { + let p = (this.background as SVGSVGElement).createSVGPoint(); + p.x = points[idx - 1]; + p.y = points[idx]; + p = p.matrixTransform(matrix); + result.push(p.x, p.y); + } + return result; + }, []); + } + + return points; + }; + this.deactivate(); const created = []; @@ -394,6 +415,7 @@ export class CanvasViewImpl implements CanvasView, Listener { const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID) .filter((id: number): boolean => !newIDs.includes(id)) .map((id: number): any => this.drawnStates[id]); + for (const state of deleted) { if (state.clientID in this.svgTexts) { this.svgTexts[state.clientID].remove(); @@ -404,8 +426,8 @@ export class CanvasViewImpl implements CanvasView, Listener { delete this.drawnStates[state.clientID]; } - this.addObjects(created); - this.updateObjects(updated); + this.addObjects(created, translate); + this.updateObjects(updated, translate); } private selectize(value: boolean, shape: SVG.Element): void { @@ -816,7 +838,7 @@ export class CanvasViewImpl implements CanvasView, Listener { }; } - private updateObjects(states: any[]): void { + private updateObjects(states: any[], translate: (points: number[]) => number[]): void { for (const state of states) { const { clientID } = state; const drawnState = this.drawnStates[clientID]; @@ -837,9 +859,7 @@ export class CanvasViewImpl implements CanvasView, Listener { if (drawnState.points .some((p: number, id: number): boolean => p !== state.points[id]) ) { - const translatedPoints: number[] = translateBetweenSVG( - this.background, this.content, state.points, - ); + const translatedPoints: number[] = translate(state.points); if (state.shapeType === 'rectangle') { const [xtl, ytl, xbr, ybr] = translatedPoints; @@ -883,15 +903,13 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - private addObjects(states: any[]): void { + private addObjects(states: any[], translate: (points: number[]) => number[]): void { for (const state of states) { if (state.objectType === 'tag') { this.addTag(state); } else { const points: number[] = (state.points as number[]); - const translatedPoints: number[] = translateBetweenSVG( - this.background, this.content, points, - ); + const translatedPoints: number[] = translate(points); // TODO: Use enums after typification cvat-core if (state.shapeType === 'rectangle') { From c07054071f098a568d2202bd26261ec0ba4a77bd Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 28 Jan 2020 19:42:01 +0300 Subject: [PATCH 07/25] Finished appearance block, fixed couple bugs --- cvat-core/src/annotations-objects.js | 13 +++- cvat-core/src/object-state.js | 12 ++-- cvat-ui/src/actions/annotation-actions.ts | 47 ++++++++------ cvat-ui/src/base.scss | 1 + .../standard-workspace/canvas-wrapper.tsx | 61 +++++++++++++++++++ .../controls-side-bar/cursor-control.tsx | 6 +- .../controls-side-bar/draw-points-control.tsx | 6 +- .../draw-polygon-control.tsx | 6 +- .../draw-polyline-control.tsx | 6 +- .../draw-rectangle-control.tsx | 6 +- .../controls-side-bar/draw-shape-popover.tsx | 6 +- .../controls-side-bar/fit-control.tsx | 6 +- .../controls-side-bar/group-control.tsx | 6 +- .../controls-side-bar/merge-control.tsx | 6 +- .../controls-side-bar/move-control.tsx | 6 +- .../controls-side-bar/resize-control.tsx | 6 +- .../controls-side-bar/rotate-control.tsx | 6 +- .../controls-side-bar/split-control.tsx | 6 +- .../objects-side-bar/appearance-block.tsx | 8 +-- .../objects-side-bar/label-item.tsx | 6 +- .../objects-side-bar/object-item.tsx | 48 +++++++++------ .../objects-side-bar/objects-list-header.tsx | 14 +++-- .../objects-side-bar/objects-list.tsx | 10 +-- .../objects-side-bar/objects-side-bar.tsx | 6 +- .../annotation-page/top-bar/left-group.tsx | 6 +- .../top-bar/player-buttons.tsx | 6 +- .../top-bar/player-navigation.tsx | 6 +- .../annotation-page/top-bar/right-group.tsx | 40 ++++++------ .../annotation-page/top-bar/top-bar.tsx | 6 +- .../src/components/settings-page/styles.scss | 4 +- .../standard-workspace/canvas-wrapper.tsx | 15 +++++ .../objects-side-bar/label-item.tsx | 22 ++++--- .../objects-side-bar/object-item.tsx | 48 +++++++++++++-- cvat-ui/src/reducers/annotation-reducer.ts | 8 ++- cvat-ui/src/reducers/interfaces.ts | 2 +- cvat-ui/src/styles.scss | 10 +++ 36 files changed, 322 insertions(+), 155 deletions(-) diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 257d6694dfcd..d652457c0a15 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -13,6 +13,7 @@ checkObjectType, } = require('./common'); const { + colors, ObjectShape, ObjectType, AttributeType, @@ -26,6 +27,8 @@ const { Label } = require('./labels'); + const defaultGroupColor = '#E0E0E0'; + // Called with the Annotation context function objectStateFactory(frame, data) { const objectState = new ObjectState(data); @@ -289,7 +292,10 @@ points: [...this.points], attributes: { ...this.attributes }, label: this.label, - group: this.group, + group: { + color: this.group ? colors[this.group % colors.length] : defaultGroupColor, + id: this.group, + }, color: this.color, hidden: this.hidden, updated: this.updated, @@ -506,7 +512,10 @@ return { ...this.getPosition(frame, prev, next), attributes: this.getAttributes(frame), - group: this.group, + group: { + color: this.group ? colors[this.group % colors.length] : defaultGroupColor, + id: this.group, + }, objectType: ObjectType.TRACK, shapeType: this.shapeType, clientID: this.clientID, diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js index 6bf7e2bc05b8..ff38205068d6 100644 --- a/cvat-core/src/object-state.js +++ b/cvat-core/src/object-state.js @@ -34,11 +34,11 @@ occluded: null, keyframe: null, - group: null, zOrder: null, lock: null, color: null, hidden: null, + group: serialized.group, keyframes: serialized.keyframes, updated: serialized.updated, @@ -62,7 +62,6 @@ this.occluded = false; this.keyframe = false; - this.group = false; this.zOrder = false; this.lock = false; this.color = false; @@ -191,16 +190,14 @@ }, group: { /** + * Object with short group info { color, id } * @name group - * @type {integer} + * @type {object} * @memberof module:API.cvat.classes.ObjectState * @instance + * @readonly */ get: () => data.group, - set: (group) => { - data.updateFlags.group = true; - data.group = group; - }, }, zOrder: { /** @@ -323,7 +320,6 @@ })); this.label = serialized.label; - this.group = serialized.group; this.zOrder = serialized.zOrder; this.outside = serialized.outside; this.keyframe = serialized.keyframe; diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 78892a6e9cf2..a36f0ed189df 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -472,23 +472,32 @@ ThunkAction, {}, {}, AnyAction> { }; } -export function changeLabelColor(label: any, color: string): AnyAction { - try { - const updatedLabel = label; - updatedLabel.color = color; - - return { - type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS, - payload: { - label: updatedLabel, - }, - }; - } catch (error) { - return { - type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED, - payload: { - error, - }, - }; - } +export function changeLabelColorAsync( + sessionInstance: any, + frameNumber: number, + label: any, + color: string, +): ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + const updatedLabel = label; + updatedLabel.color = color; + const states = await sessionInstance.annotations.get(frameNumber); + + dispatch({ + type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS, + payload: { + label: updatedLabel, + states, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED, + payload: { + error, + }, + }); + } + }; } diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 9d43c1ad0ff7..c4a83cd8dde5 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -17,5 +17,6 @@ $info-icon-color: #0074D9; $objects-bar-tabs-color: #BEBEBE; $objects-bar-icons-color: #242424; // #6E6E6E $active-object-item-background-color: #D8ECFF; +$slider-color: #1890FF; $monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 98d2aae90e99..c69405a4fa9a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -5,6 +5,7 @@ import { } from 'antd'; import { + ColorBy, GridColor, ObjectType, } from 'reducers/interfaces'; @@ -28,6 +29,10 @@ interface Props { annotations: any[]; frameData: any; frame: number; + opacity: number; + colorBy: ColorBy; + selectedOpacity: number; + blackBorders: boolean; grid: boolean; gridSize: number; gridColor: GridColor; @@ -69,6 +74,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { public componentDidUpdate(prevProps: Props): void { const { + opacity, + colorBy, + selectedOpacity, + blackBorders, grid, gridSize, gridColor, @@ -119,12 +128,28 @@ export default class CanvasWrapperComponent extends React.PureComponent { } if (prevProps.activatedStateID !== activatedStateID) { + if (prevProps.activatedStateID !== null) { + const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); + if (el) { + (el as any as SVGElement).style.fillOpacity = `${opacity / 100}`; + } + } + if (activatedStateID !== null) { canvasInstance.activate(activatedStateID); + const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); + if (el) { + (el as any as SVGElement).style.fillOpacity = `${selectedOpacity / 100}`; + } } else { canvasInstance.cancel(); } } + + if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders + || prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) { + this.updateShapesView(); + } } private async onShapeDrawn(event: any): Promise { @@ -215,6 +240,41 @@ export default class CanvasWrapperComponent extends React.PureComponent { onSplitAnnotations(jobInstance, frame, state); } + private updateShapesView(): void { + const { + annotations, + opacity, + colorBy, + blackBorders, + } = this.props; + + for (const state of annotations) { + let shapeColor = ''; + if (colorBy === ColorBy.INSTANCE) { + shapeColor = state.color; + } else if (colorBy === ColorBy.GROUP) { + shapeColor = state.group.color; + } else if (colorBy === ColorBy.LABEL) { + shapeColor = state.label.color; + } + + const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`); + if (shapeView) { + if (shapeView.tagName === 'rect' || shapeView.tagName === 'polygon') { + (shapeView as any as SVGElement).style.fillOpacity = `${opacity / 100}`; + (shapeView as any as SVGElement).style.stroke = shapeColor; + (shapeView as any as SVGElement).style.fill = shapeColor; + } else { + (shapeView as any as SVGElement).style.stroke = shapeColor; + } + + if (blackBorders) { + (shapeView as any as SVGElement).style.stroke = 'black'; + } + } + } + } + private updateCanvas(): void { const { annotations, @@ -260,6 +320,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { // Events canvasInstance.html().addEventListener('canvas.setup', (): void => { onSetupCanvas(); + this.updateShapesView(); }); canvasInstance.html().addEventListener('canvas.setup', () => { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx index dcb3c03e56fe..ae590cc44aa5 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/cursor-control.tsx @@ -22,7 +22,7 @@ interface Props { activeControl: ActiveControl; } -const CursorControl = React.memo((props: Props): JSX.Element => { +function CursorControl(props: Props): JSX.Element { const { canvasInstance, activeControl, @@ -43,6 +43,6 @@ const CursorControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default CursorControl; +export default React.memo(CursorControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx index b83e547e5d02..2d4351923e80 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-points-control.tsx @@ -15,7 +15,7 @@ interface Props { isDrawing: boolean; } -const DrawPointsControl = React.memo((props: Props): JSX.Element => { +function DrawPointsControl(props: Props): JSX.Element { const { canvasInstance, isDrawing, @@ -49,6 +49,6 @@ const DrawPointsControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default DrawPointsControl; +export default React.memo(DrawPointsControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx index 837254d5e73a..3ebebaadbef9 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polygon-control.tsx @@ -15,7 +15,7 @@ interface Props { isDrawing: boolean; } -const DrawPolygonControl = React.memo((props: Props): JSX.Element => { +function DrawPolygonControl(props: Props): JSX.Element { const { canvasInstance, isDrawing, @@ -49,6 +49,6 @@ const DrawPolygonControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default DrawPolygonControl; +export default React.memo(DrawPolygonControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx index 961a520aeab9..1959010350ec 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-polyline-control.tsx @@ -15,7 +15,7 @@ interface Props { isDrawing: boolean; } -const DrawPolylineControl = React.memo((props: Props): JSX.Element => { +function DrawPolylineControl(props: Props): JSX.Element { const { canvasInstance, isDrawing, @@ -49,6 +49,6 @@ const DrawPolylineControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default DrawPolylineControl; +export default React.memo(DrawPolylineControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx index 10e1eb1d8f00..4201ccd0315d 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-rectangle-control.tsx @@ -15,7 +15,7 @@ interface Props { isDrawing: boolean; } -const DrawRectangleControl = React.memo((props: Props): JSX.Element => { +function DrawRectangleControl(props: Props): JSX.Element { const { canvasInstance, isDrawing, @@ -49,6 +49,6 @@ const DrawRectangleControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default DrawRectangleControl; +export default React.memo(DrawRectangleControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index efad75e5072a..ba43a8c368d4 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -26,7 +26,7 @@ interface Props { onDrawShape(): void; } -const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => { +function DrawShapePopoverComponent(props: Props): JSX.Element { const { labels, shapeType, @@ -106,6 +106,6 @@ const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => {
); -}); +} -export default DrawShapePopoverComponent; +export default React.memo(DrawShapePopoverComponent); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx index 998d00820a8b..6fc8e750e85e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/fit-control.tsx @@ -17,7 +17,7 @@ interface Props { canvasInstance: Canvas; } -const FitControl = React.memo((props: Props): JSX.Element => { +function FitControl(props: Props): JSX.Element { const { canvasInstance, } = props; @@ -27,6 +27,6 @@ const FitControl = React.memo((props: Props): JSX.Element => { canvasInstance.fit()} /> ); -}); +} -export default FitControl; +export default React.memo(FitControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx index 1c2732f8f284..f6b6c7175857 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/group-control.tsx @@ -19,7 +19,7 @@ interface Props { groupObjects(enabled: boolean): void; } -const GroupControl = React.memo((props: Props): JSX.Element => { +function GroupControl(props: Props): JSX.Element { const { activeControl, canvasInstance, @@ -46,6 +46,6 @@ const GroupControl = React.memo((props: Props): JSX.Element => { ); -}); +} -export default GroupControl; +export default React.memo(GroupControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx index b4dcc0076f5d..9e43855473ca 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/merge-control.tsx @@ -19,7 +19,7 @@ interface Props { mergeObjects(enabled: boolean): void; } -const MergeControl = React.memo((props: Props): JSX.Element => { +function MergeControl(props: Props): JSX.Element { const { activeControl, canvasInstance, @@ -46,6 +46,6 @@ const MergeControl = React.memo((props: Props): JSX.Element => { ); -}); +} -export default MergeControl; +export default React.memo(MergeControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx index 89aa6542cb1c..e17659081ded 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/move-control.tsx @@ -22,7 +22,7 @@ interface Props { activeControl: ActiveControl; } -const MoveControl = React.memo((props: Props): JSX.Element => { +function MoveControl(props: Props): JSX.Element { const { canvasInstance, activeControl, @@ -46,6 +46,6 @@ const MoveControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default MoveControl; +export default React.memo(MoveControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx index 819b3c70c272..5acca3d3f1c0 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/resize-control.tsx @@ -22,7 +22,7 @@ interface Props { activeControl: ActiveControl; } -const ResizeControl = React.memo((props: Props): JSX.Element => { +function ResizeControl(props: Props): JSX.Element { const { activeControl, canvasInstance, @@ -46,6 +46,6 @@ const ResizeControl = React.memo((props: Props): JSX.Element => { /> ); -}); +} -export default ResizeControl; +export default React.memo(ResizeControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx index d0acee42b5ca..c5dca6d772a6 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx @@ -20,7 +20,7 @@ interface Props { rotateAll: boolean; } -const RotateControl = React.memo((props: Props): JSX.Element => { +function RotateControl(props: Props): JSX.Element { const { rotateAll, canvasInstance, @@ -55,6 +55,6 @@ const RotateControl = React.memo((props: Props): JSX.Element => { ); -}); +} -export default RotateControl; +export default React.memo(RotateControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx index c4725f7a77d4..6190d4dfd129 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/split-control.tsx @@ -19,7 +19,7 @@ interface Props { splitTrack(enabled: boolean): void; } -const SplitControl = React.memo((props: Props): JSX.Element => { +function SplitControl(props: Props): JSX.Element { const { activeControl, canvasInstance, @@ -46,6 +46,6 @@ const SplitControl = React.memo((props: Props): JSX.Element => { ); -}); +} -export default SplitControl; +export default React.memo(SplitControl); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx index 6c86d3171c89..530899533a04 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/appearance-block.tsx @@ -28,7 +28,7 @@ interface Props { changeShapesBlackBorders(event: CheckboxChangeEvent): void; } -const AppearanceBlock = React.memo((props: Props): JSX.Element => { +function AppearanceBlock(props: Props): JSX.Element { const { appearanceCollapsed, colorBy, @@ -77,7 +77,7 @@ const AppearanceBlock = React.memo((props: Props): JSX.Element => { /> Black borders @@ -85,6 +85,6 @@ const AppearanceBlock = React.memo((props: Props): JSX.Element => { ); -}); +} -export default AppearanceBlock; +export default React.memo(AppearanceBlock); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index 90284e39bb98..940ee66eb69e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -71,7 +71,7 @@ interface Props { changeColor(color: string): void; } -const LabelItemComponent = React.memo((props: Props): JSX.Element => { +function LabelItemComponent(props: Props): JSX.Element { const { labelName, labelColor, @@ -125,6 +125,6 @@ const LabelItemComponent = React.memo((props: Props): JSX.Element => {
); -}); +} -export default LabelItemComponent; +export default React.memo(LabelItemComponent); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 7872877472bf..5ce97092cf25 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -28,7 +28,7 @@ import { ObjectType, ShapeType, } from 'reducers/interfaces'; -interface ItemTopProps { +interface ItemTopComponentProps { clientID: number; labelID: number; labels: any[]; @@ -36,7 +36,7 @@ interface ItemTopProps { changeLabel(labelID: string): void; } -const ItemTop = React.memo((props: ItemTopProps): JSX.Element => { +function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { const { clientID, labelID, @@ -66,9 +66,11 @@ const ItemTop = React.memo((props: ItemTopProps): JSX.Element => { ); -}); +} + +const ItemTop = React.memo(ItemTopComponent); -interface ItemButtonsProps { +interface ItemButtonsComponentProps { objectType: ObjectType; occluded: boolean; outside: boolean | undefined; @@ -93,7 +95,7 @@ interface ItemButtonsProps { show(): void; } -const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => { +function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element { const { objectType, occluded, @@ -212,9 +214,11 @@ const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => { ); -}); +} -interface ItemAttributeProps { +const ItemButtons = React.memo(ItemButtonsComponent); + +interface ItemAttributeComponentProps { attrInputType: string; attrValues: string[]; attrValue: string; @@ -223,7 +227,10 @@ interface ItemAttributeProps { changeAttribute(attrID: number, value: string): void; } -function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributeProps): boolean { +function attrIsTheSame( + prevProps: ItemAttributeComponentProps, + nextProps: ItemAttributeComponentProps, +): boolean { return nextProps.attrID === prevProps.attrID && nextProps.attrValue === prevProps.attrValue && nextProps.attrName === prevProps.attrName @@ -233,7 +240,7 @@ function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributePr .every((value: boolean): boolean => value); } -const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => { +function ItemAttributeComponent(props: ItemAttributeComponentProps): JSX.Element { const { attrInputType, attrValues, @@ -355,10 +362,12 @@ const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => { ); -}, attrIsTheSame); +} +const ItemAttribute = React.memo(ItemAttributeComponent, attrIsTheSame); -interface ItemAttributesProps { + +interface ItemAttributesComponentProps { collapsed: boolean; attributes: any[]; values: Record; @@ -375,13 +384,16 @@ function attrValuesAreEqual(next: Record, prev: Record value); } -function attrAreTheSame(prevProps: ItemAttributesProps, nextProps: ItemAttributesProps): boolean { +function attrAreTheSame( + prevProps: ItemAttributesComponentProps, + nextProps: ItemAttributesComponentProps, +): boolean { return nextProps.collapsed === prevProps.collapsed && nextProps.attributes === prevProps.attributes && attrValuesAreEqual(nextProps.values, prevProps.values); } -const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => { +function ItemAttributesComponent(props: ItemAttributesComponentProps): JSX.Element { const { collapsed, attributes, @@ -426,7 +438,9 @@ const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => { ); -}, attrAreTheSame); +} + +const ItemAttributes = React.memo(ItemAttributesComponent, attrAreTheSame); interface Props { activated: boolean; @@ -488,7 +502,7 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean { && attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues); } -const ObjectItem = React.memo((props: Props): JSX.Element => { +function ObjectItemComponent(props: Props): JSX.Element { const { activated, objectType, @@ -582,6 +596,6 @@ const ObjectItem = React.memo((props: Props): JSX.Element => { } ); -}, objectItemsAreEqual); +} -export default ObjectItem; +export default React.memo(ObjectItemComponent, objectItemsAreEqual); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx index 4b43490b2c58..35b2a59a77ec 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list-header.tsx @@ -13,12 +13,12 @@ import Text from 'antd/lib/typography/Text'; import { StatesOrdering } from 'reducers/interfaces'; -interface StatesOrderingSelectorProps { +interface StatesOrderingSelectorComponentProps { statesOrdering: StatesOrdering; changeStatesOrdering(value: StatesOrdering): void; } -const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps): JSX.Element => { +function StatesOrderingSelectorComponent(props: StatesOrderingSelectorComponentProps): JSX.Element { const { statesOrdering, changeStatesOrdering, @@ -49,7 +49,9 @@ const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps): ); -}); +} + +const StatesOrderingSelector = React.memo(StatesOrderingSelectorComponent); interface Props { statesHidden: boolean; @@ -65,7 +67,7 @@ interface Props { showAllStates(): void; } -const Header = React.memo((props: Props): JSX.Element => { +function ObjectListHeader(props: Props): JSX.Element { const { statesHidden, statesLocked, @@ -116,6 +118,6 @@ const Header = React.memo((props: Props): JSX.Element => { ); -}); +} -export default Header; +export default React.memo(ObjectListHeader); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx index 8e6dda18a23a..ff3385c191af 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-list.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { StatesOrdering } from 'reducers/interfaces'; import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item'; -import Header from './objects-list-header'; +import ObjectListHeader from './objects-list-header'; interface Props { @@ -22,7 +22,7 @@ interface Props { showAllStates(): void; } -const ObjectListComponent = React.memo((props: Props): JSX.Element => { +function ObjectListComponent(props: Props): JSX.Element { const { listHeight, statesHidden, @@ -41,7 +41,7 @@ const ObjectListComponent = React.memo((props: Props): JSX.Element => { return (
-
{
); -}); +} -export default ObjectListComponent; +export default React.memo(ObjectListComponent); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index c8330fc7e8a9..51af704958cd 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -35,7 +35,7 @@ interface Props { changeShapesBlackBorders(event: CheckboxChangeEvent): void; } -const ObjectsSideBar = React.memo((props: Props): JSX.Element => { +function ObjectsSideBar(props: Props): JSX.Element { const { sidebarCollapsed, appearanceCollapsed, @@ -105,6 +105,6 @@ const ObjectsSideBar = React.memo((props: Props): JSX.Element => { { !sidebarCollapsed && } ); -}); +} -export default ObjectsSideBar; +export default React.memo(ObjectsSideBar); diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index 532664abf21c..0c1c7bd96022 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -20,7 +20,7 @@ interface Props { onSaveAnnotation(): void; } -const LeftGroup = React.memo((props: Props): JSX.Element => { +function LeftGroup(props: Props): JSX.Element { const { saving, savingStatuses, @@ -71,6 +71,6 @@ const LeftGroup = React.memo((props: Props): JSX.Element => { ); -}); +} -export default LeftGroup; +export default React.memo(LeftGroup); diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx index 1f919434a2c8..3c624c854dce 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-buttons.tsx @@ -28,7 +28,7 @@ interface Props { onLastFrame(): void; } -const PlayerButtons = React.memo((props: Props): JSX.Element => { +function PlayerButtons(props: Props): JSX.Element { const { playing, onSwitchPlay, @@ -82,6 +82,6 @@ const PlayerButtons = React.memo((props: Props): JSX.Element => { ); -}); +} -export default PlayerButtons; +export default React.memo(PlayerButtons); diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx index 32ef0c34fe74..8dcabee0bc68 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx @@ -19,7 +19,7 @@ interface Props { onInputChange(value: number | undefined): void; } -const PlayerNavigation = React.memo((props: Props): JSX.Element => { +function PlayerNavigation(props: Props): JSX.Element { const { startFrame, stopFrame, @@ -61,6 +61,6 @@ const PlayerNavigation = React.memo((props: Props): JSX.Element => { ); -}); +} -export default PlayerNavigation; +export default React.memo(PlayerNavigation); diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index 2703ecd52894..89c758a79c64 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -11,23 +11,25 @@ import { FullscreenIcon, } from '../../../icons'; -const RightGroup = React.memo((): JSX.Element => ( - -
- - Fullscreen -
-
- - Info -
-
- -
- -)); +function RightGroup(): JSX.Element { + return ( + +
+ + Fullscreen +
+
+ + Info +
+
+ +
+ + ); +} -export default RightGroup; +export default React.memo(RightGroup); diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index cc92915ecbc0..4184ac25064e 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -41,7 +41,7 @@ function propsAreEqual(curProps: Props, prevProps: Props): boolean { && curProps.savingStatuses.length === prevProps.savingStatuses.length; } -const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => { +function AnnotationTopBarComponent(props: Props): JSX.Element { const { saving, savingStatuses, @@ -94,6 +94,6 @@ const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => { ); -}, propsAreEqual); +} -export default AnnotationTopBarComponent; +export default React.memo(AnnotationTopBarComponent, propsAreEqual); diff --git a/cvat-ui/src/components/settings-page/styles.scss b/cvat-ui/src/components/settings-page/styles.scss index 3e9669652e69..5ee3a2090ee2 100644 --- a/cvat-ui/src/components/settings-page/styles.scss +++ b/cvat-ui/src/components/settings-page/styles.scss @@ -61,8 +61,8 @@ } .cvat-player-settings-step > div > span > i { - vertical-align: -1em; - transform: scale(0.3); + margin: 0px 5px; + font-size: 10px; } .cvat-player-settings-speed > div > .ant-select { diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index be6e12ee41c9..13c412c1c7e9 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -21,6 +21,7 @@ import { selectObjects, } from 'actions/annotation-actions'; import { + ColorBy, GridColor, ObjectType, CombinedState, @@ -37,6 +38,10 @@ interface StateToProps { annotations: any[]; frameData: any; frame: number; + opacity: number; + colorBy: ColorBy; + selectedOpacity: number; + blackBorders: boolean; grid: boolean; gridSize: number; gridColor: GridColor; @@ -96,6 +101,12 @@ function mapStateToProps(state: CombinedState): StateToProps { gridColor, gridOpacity, }, + shapes: { + opacity, + colorBy, + selectedOpacity, + blackBorders, + }, }, } = state; @@ -108,6 +119,10 @@ function mapStateToProps(state: CombinedState): StateToProps { activatedStateID, selectedStatesID, annotations, + opacity, + colorBy, + selectedOpacity, + blackBorders, grid, gridSize, gridColor, diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx index 32992c9e9039..b1759456659b 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/label-item.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { - changeLabelColor as changeLabelColorAction, + changeLabelColorAsync, updateAnnotationsAsync, } from 'actions/annotation-actions'; @@ -26,7 +26,7 @@ interface StateToProps { interface DispatchToProps { updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void; - changeLabelColor(label: any, color: string): void; + changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void; } function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { @@ -36,8 +36,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { states: objectStates, }, job: { - instance: jobInstance, labels, + instance: jobInstance, }, player: { frame: { @@ -48,7 +48,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { }, } = state; - const [label] = labels.filter((_label: any) => _label.id === own.labelID); + const [label] = labels + .filter((_label: any) => _label.id === own.labelID); return { label, @@ -66,8 +67,13 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void { dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states)); }, - changeLabelColor(label: any, color: string): void { - dispatch(changeLabelColorAction(label, color)); + changeLabelColor( + sessionInstance: any, + frameNumber: number, + label: any, + color: string, + ): void { + dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color)); }, }; } @@ -139,9 +145,11 @@ class LabelItemContainer extends React.PureComponent { const { changeLabelColor, label, + frameNumber, + jobInstance, } = this.props; - changeLabelColor(label, color); + changeLabelColor(jobInstance, frameNumber, label, color); }; private switchHidden(value: boolean): void { diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 8abd5e959a37..88db2942a5cf 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; import { + ActiveControl, CombinedState, + ColorBy, } from 'reducers/interfaces'; import { collapseObjectItems, @@ -24,6 +26,9 @@ interface StateToProps { jobInstance: any; frameNumber: number; activated: boolean; + colorBy: ColorBy; + ready: boolean; + activeControl: ActiveControl; } interface DispatchToProps { @@ -42,15 +47,24 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { activatedStateID, }, job: { - labels, attributes: jobAttributes, instance: jobInstance, + labels, }, player: { frame: { number: frameNumber, }, }, + canvas: { + ready, + activeControl, + }, + }, + settings: { + shapes: { + colorBy, + }, }, } = state; @@ -66,6 +80,9 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { collapsed: collapsedState, attributes: jobAttributes[states[index].label.id], labels, + ready, + activeControl, + colorBy, jobInstance, frameNumber, activated: activatedStateID === own.clientID, @@ -147,9 +164,13 @@ class ObjectItemContainer extends React.PureComponent { const { activateObject, objectState, + ready, + activeControl, } = this.props; - activateObject(objectState.clientID); + if (ready && activeControl === ActiveControl.CURSOR) { + activateObject(objectState.clientID); + } }; private lock = (): void => { @@ -260,6 +281,7 @@ class ObjectItemContainer extends React.PureComponent { attributes, frameNumber, activated, + colorBy, } = this.props; const { @@ -267,7 +289,21 @@ class ObjectItemContainer extends React.PureComponent { prev, next, last, - } = objectState.keyframes; + } = objectState.keyframes || { + first: null, // shapes don't have keyframes, so we use null + prev: null, + next: null, + last: null, + }; + + let stateColor = ''; + if (colorBy === ColorBy.INSTANCE) { + stateColor = objectState.color; + } else if (colorBy === ColorBy.GROUP) { + stateColor = objectState.group.color; + } else if (colorBy === ColorBy.LABEL) { + stateColor = objectState.label.color; + } return ( { keyframe={objectState.keyframe} attrValues={{ ...objectState.attributes }} labelID={objectState.label.id} - color={objectState.color} + color={stateColor} attributes={attributes} labels={labels} collapsed={collapsed} navigateFirstKeyframe={ - first === frameNumber + first === frameNumber || first === null ? null : this.navigateFirstKeyframe } navigatePrevKeyframe={ @@ -299,7 +335,7 @@ class ObjectItemContainer extends React.PureComponent { ? null : this.navigateNextKeyframe } navigateLastKeyframe={ - last <= frameNumber + last <= frameNumber || last === null ? null : this.navigateLastKeyframe } activate={this.activate} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index fcef79b55a82..7fd4e090b303 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -17,8 +17,8 @@ const defaultState: AnnotationState = { activeControl: ActiveControl.CURSOR, }, job: { - instance: null, labels: [], + instance: null, attributes: {}, fetching: false, }, @@ -449,6 +449,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { case AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS: { const { label, + states, } = action.payload; const { instance: job } = state.job; @@ -456,13 +457,16 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { const index = labels.indexOf(label); labels[index] = label; - return { ...state, job: { ...state.job, labels, }, + annotations: { + ...state.annotations, + states, + }, }; } case AnnotationActionTypes.ACTIVATE_OBJECT: { diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index f26b24680be7..acaab0db6efd 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -266,8 +266,8 @@ export interface AnnotationState { activeControl: ActiveControl; }; job: { - instance: any | null | undefined; labels: any[]; + instance: any | null | undefined; attributes: Record; fetching: boolean; }; diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index d3b3c0a7711d..2ce92947f0f2 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -25,6 +25,16 @@ hr { padding-top: 5px; } +.ant-slider { + > .ant-slider-track { + background-color: $slider-color; + } + + > .ant-slider-handle { + border-color: $slider-color; + } +} + #root { width: 100%; height: 100%; From 0d09751a8a2d374317425106b1ee1e1fad8c4bbe Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 12:32:57 +0300 Subject: [PATCH 08/25] Improved save() in cvat-core, changed approach to highlight shapes --- cvat-canvas/src/scss/canvas.scss | 46 +- cvat-canvas/src/typescript/canvas.ts | 4 +- cvat-canvas/src/typescript/canvasModel.ts | 6 +- cvat-canvas/src/typescript/canvasView.ts | 88 ++-- cvat-core/src/annotations-collection.js | 2 +- cvat-core/src/annotations-objects.js | 431 ++++++++---------- cvat-core/src/common.js | 4 +- .../standard-workspace/canvas-wrapper.tsx | 67 +-- cvat-ui/src/reducers/annotation-reducer.ts | 5 + 9 files changed, 317 insertions(+), 336 deletions(-) diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 75f7634c0521..1fbb27d830d2 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -3,7 +3,6 @@ } .cvat_canvas_shape { - fill-opacity: 0.03; stroke-opacity: 1; } @@ -12,6 +11,15 @@ polyline.cvat_canvas_shape { stroke-opacity: 1; } +.cvat_shape_action_opacity { + fill-opacity: 0.5; + stroke-opacity: 1; +} + +.cvat_shape_action_dasharray { + stroke-dasharray: 4 1 2 3; +} + .cvat_canvas_text { font-weight: bold; font-size: 1.2em; @@ -27,38 +35,40 @@ polyline.cvat_canvas_shape { stroke: red; } -.cvat_canvas_shape_activated { - fill-opacity: 0.3; -} - .cvat_canvas_shape_grouping { - fill: darkmagenta !important; - fill-opacity: 0.5 !important; + @extend .cvat_shape_action_dasharray; + @extend .cvat_shape_action_opacity; + fill: darkmagenta; } polyline.cvat_canvas_shape_grouping { - stroke: darkmagenta !important; - stroke-opacity: 1 !important; + @extend .cvat_shape_action_dasharray; + @extend .cvat_shape_action_opacity; + stroke: darkmagenta; } .cvat_canvas_shape_merging { - fill: blue !important; - fill-opacity: 0.5 !important; + @extend .cvat_shape_action_dasharray; + @extend .cvat_shape_action_opacity; + fill: blue; } polyline.cvat_canvas_shape_merging { - stroke: blue !important; - stroke-opacity: 1 !important; + @extend .cvat_shape_action_dasharray; + @extend .cvat_shape_action_opacity; + stroke: blue; } polyline.cvat_canvas_shape_splitting { - stroke: dodgerblue !important; - stroke-opacity: 1 !important; + @extend .cvat_shape_action_dasharray; + @extend .cvat_shape_action_opacity; + stroke: dodgerblue; } .cvat_canvas_shape_splitting { - fill: dodgerblue !important; - fill-opacity: 0.5 !important; + @extend .cvat_shape_action_dasharray; + @extend .cvat_shape_action_opacity; + fill: dodgerblue; } .cvat_canvas_shape_drawing { @@ -69,9 +79,9 @@ polyline.cvat_canvas_shape_splitting { } .cvat_canvas_zoom_selection { + @extend .cvat_shape_action_dasharray; stroke: #096dd9; fill-opacity: 0; - stroke-dasharray: 4; } .cvat_canvas_shape_occluded { diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index 3c6dfec34211..e5c4974ebd3c 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -32,7 +32,7 @@ import '../scss/canvas.scss'; interface Canvas { html(): HTMLDivElement; setup(frameData: any, objectStates: any[]): void; - activate(clientID: number, attributeID?: number): void; + activate(clientID: number | null, attributeID?: number): void; rotate(rotation: Rotation, remember?: boolean): void; focus(clientID: number, padding?: number): void; fit(): void; @@ -85,7 +85,7 @@ class CanvasImpl implements Canvas { this.model.zoomCanvas(enable); } - public activate(clientID: number, attributeID: number | null = null): void { + public activate(clientID: number | null, attributeID: number | null = null): void { this.model.activate(clientID, attributeID); } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index c390fb74cf05..c439e58902d8 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -33,7 +33,7 @@ export interface FocusData { } export interface ActiveElement { - clientID: number; + clientID: number | null; attributeID: number | null; } @@ -127,7 +127,7 @@ export interface CanvasModel { move(topOffset: number, leftOffset: number): void; setup(frameData: any, objectStates: any[]): void; - activate(clientID: number, attributeID: number | null): void; + activate(clientID: number | null, attributeID: number | null): void; rotate(rotation: Rotation, remember: boolean): void; focus(clientID: number, padding: number): void; fit(): void; @@ -316,7 +316,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { }); } - public activate(clientID: number, attributeID: number | null): void { + public activate(clientID: number | null, attributeID: number | null): void { if (this.data.mode !== Mode.IDLE) { // Exception or just return? throw Error(`Canvas is busy. Action: ${this.data.mode}`); diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index b35b9b673ec4..c2196cdcb217 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -80,10 +80,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private splitHandler: SplitHandler; private groupHandler: GroupHandler; private zoomHandler: ZoomHandler; - private activeElement: { - state: any; - attributeID: number | null; - } | null; + private activeElement: ActiveElement; private set mode(value: Mode) { this.controller.mode = value; @@ -397,8 +394,6 @@ export class CanvasViewImpl implements CanvasView, Listener { return points; }; - this.deactivate(); - const created = []; const updated = []; for (const state of states) { @@ -416,6 +411,12 @@ export class CanvasViewImpl implements CanvasView, Listener { .filter((id: number): boolean => !newIDs.includes(id)) .map((id: number): any => this.drawnStates[id]); + if (this.activeElement.clientID !== null) { + const currentActivatedStillExist = !deleted.map((state: any): number => state.clientID) + .includes(this.activeElement.clientID); + this.deactivate(currentActivatedStillExist); + } + for (const state of deleted) { if (state.clientID in this.svgTexts) { this.svgTexts[state.clientID].remove(); @@ -428,6 +429,10 @@ export class CanvasViewImpl implements CanvasView, Listener { this.addObjects(created, translate); this.updateObjects(updated, translate); + + if (this.controller.activeElement.clientID !== null) { + this.activate(this.controller.activeElement); + } } private selectize(value: boolean, shape: SVG.Element): void { @@ -437,16 +442,16 @@ export class CanvasViewImpl implements CanvasView, Listener { const pointID = Array.prototype.indexOf .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target); - if (self.activeElement) { + if (self.activeElement.clientID !== null) { + const state = self.drawnStates[self.activeElement.clientID]; if (e.ctrlKey) { - const { points } = self.activeElement.state; + const { points } = state; self.onEditDone( - self.activeElement.state, + state, points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)), ); } else if (e.shiftKey) { self.mode = Mode.EDIT; - const { state } = self.activeElement; self.deactivate(); self.editHandler.edit({ enabled: true, @@ -506,7 +511,10 @@ export class CanvasViewImpl implements CanvasView, Listener { this.svgShapes = {}; this.svgTexts = {}; this.drawnStates = {}; - this.activeElement = null; + this.activeElement = { + clientID: null, + attributeID: null, + }; this.mode = Mode.IDLE; // Create HTML elements @@ -953,10 +961,12 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - private deactivate(): void { - if (this.activeElement) { - const { state } = this.activeElement; - const shape = this.svgShapes[this.activeElement.state.clientID]; + private deactivate(silent: boolean = false): void { + if (this.activeElement.clientID !== null) { + const { clientID } = this.activeElement; + const [state] = this.controller.objects + .filter((_state: any): boolean => _state.clientID === clientID); + const shape = this.svgShapes[state.clientID]; shape.removeClass('cvat_canvas_shape_activated'); (shape as any).off('dragstart'); @@ -972,30 +982,35 @@ export class CanvasViewImpl implements CanvasView, Listener { (shape as any).off('resizedone'); (shape as any).resize(false); - this.canvas.dispatchEvent(new CustomEvent('canvas.deactivated', { - bubbles: false, - cancelable: true, - detail: { - state, - }, - })); - - // TODO: Hide text only if it is hidden by settings const text = this.svgTexts[state.clientID]; if (text) { text.remove(); delete this.svgTexts[state.clientID]; } - this.activeElement = null; + + this.activeElement = { + clientID: null, + attributeID: null, + }; + + if (!silent) { + this.canvas.dispatchEvent(new CustomEvent('canvas.deactivated', { + bubbles: false, + cancelable: true, + detail: { + state, + }, + })); + } } } private activate(activeElement: ActiveElement): void { // Check if other element have been already activated - if (this.activeElement) { + if (this.activeElement.clientID !== null) { // Check if it is the same element - if (this.activeElement.state.clientID === activeElement.clientID) { + if (this.activeElement.clientID === activeElement.clientID) { return; } @@ -1003,16 +1018,19 @@ export class CanvasViewImpl implements CanvasView, Listener { this.deactivate(); } - const state = this.controller.objects - .filter((el): boolean => el.clientID === activeElement.clientID)[0]; - this.activeElement = { - attributeID: activeElement.attributeID, - state, - }; + this.activeElement = { ...activeElement }; + const { clientID } = this.activeElement; + + if (clientID === null) { + return; + } + + const [state] = this.controller.objects + .filter((_state: any): boolean => _state.clientID === clientID); - const shape = this.svgShapes[activeElement.clientID]; + const shape = this.svgShapes[clientID]; shape.addClass('cvat_canvas_shape_activated'); - let text = this.svgTexts[activeElement.clientID]; + let text = this.svgTexts[clientID]; // Draw text if it's hidden by default if (!text) { text = this.addText(state); diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 02a208eeaf47..8c42519d1aca 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -712,7 +712,7 @@ let minimumState = null; for (const state of objectStates) { checkObjectType('object state', state, null, ObjectState); - if (state.outside) continue; + if (state.outside || state.hidden) continue; const object = this.objects[state.clientID]; if (typeof (object) === 'undefined') { diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index d652457c0a15..37ab407b5433 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -168,7 +168,7 @@ updateTimestamp(updated) { const anyChanges = updated.label || updated.attributes || updated.points || updated.outside || updated.occluded || updated.keyframe - || updated.group || updated.zOrder; + || updated.zOrder; if (anyChanges) { this.updated = Date.now(); @@ -205,6 +205,95 @@ return this.collectionZ[frame]; } + validateStateBeforeSave(frame, data) { + let fittedPoints = []; + const updated = data.updateFlags; + const labelAttributes = data.label.attributes + .reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); + + if (updated.label) { + checkObjectType('label', data.label, null, Label); + } + + if (updated.attributes) { + for (const attrID of Object.keys(data.attributes)) { + const value = data.attributes[attrID]; + if (attrID in labelAttributes) { + if (!validateAttributeValue(value, labelAttributes[attrID])) { + throw new ArgumentError( + `Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`, + ); + } + } else { + throw new ArgumentError( + `The label of the shape doesn't have the attribute with id ${attrID} and value ${value}`, + ); + } + } + } + + if (updated.points) { + checkObjectType('points', data.points, null, Array); + checkNumberOfPoints(this.shapeType, data.points); + // cut points + const { width, height } = this.frameMeta[frame]; + for (let i = 0; i < data.points.length - 1; i += 2) { + const x = data.points[i]; + const y = data.points[i + 1]; + + checkObjectType('coordinate', x, 'number', null); + checkObjectType('coordinate', y, 'number', null); + + fittedPoints.push( + Math.clamp(x, 0, width), + Math.clamp(y, 0, height), + ); + } + + if (!checkShapeArea(this.shapeType, fittedPoints)) { + fittedPoints = false; + } + } + + if (updated.occluded) { + checkObjectType('occluded', data.occluded, 'boolean', null); + } + + if (updated.outside) { + checkObjectType('outside', data.outside, 'boolean', null); + } + + if (updated.zOrder) { + checkObjectType('zOrder', data.zOrder, 'integer', null); + } + + if (updated.lock) { + checkObjectType('lock', data.lock, 'boolean', null); + } + + if (updated.color) { + checkObjectType('color', data.color, 'string', null); + if (/^#[0-9A-F]{6}$/i.test(data.color)) { + throw new ArgumentError( + `Got invalid color value: "${data.color}"`, + ); + } + } + + if (updated.hidden) { + checkObjectType('hidden', data.hidden, 'boolean', null); + } + + if (updated.keyframe) { + checkObjectType('keyframe', data.keyframe, 'boolean', null); + } + + return fittedPoints; + } + save() { throw new ScriptingError( 'Is not implemented', @@ -314,111 +403,46 @@ return objectStateFactory.call(this, frame, this.get(frame)); } - // All changes are done in this temporary object - const copy = this.get(frame); + const fittedPoints = this.validateStateBeforeSave(frame, data); const updated = data.updateFlags; + // Now when all fields are validated, we can apply them if (updated.label) { - checkObjectType('label', data.label, null, Label); - copy.label = data.label; - copy.attributes = {}; - this.appendDefaultAttributes.call(copy, copy.label); + this.label = data.label; + this.attributes = {}; + this.appendDefaultAttributes(data.label); } if (updated.attributes) { - const labelAttributes = copy.label.attributes - .reduce((accumulator, value) => { - accumulator[value.id] = value; - return accumulator; - }, {}); - for (const attrID of Object.keys(data.attributes)) { - const value = data.attributes[attrID]; - if (attrID in labelAttributes) { - if (validateAttributeValue(value, labelAttributes[attrID])) { - copy.attributes[attrID] = value; - } else { - throw new ArgumentError( - `Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`, - ); - } - } else { - throw new ArgumentError( - `Trying to save unknown attribute with id ${attrID} and value ${value}`, - ); - } + this.attributes[attrID] = data.attributes[attrID]; } } - if (updated.points) { - checkObjectType('points', data.points, null, Array); - checkNumberOfPoints(this.shapeType, data.points); - - // cut points - const { width, height } = this.frameMeta[frame]; - const cutPoints = []; - for (let i = 0; i < data.points.length - 1; i += 2) { - const x = data.points[i]; - const y = data.points[i + 1]; - - checkObjectType('coordinate', x, 'number', null); - checkObjectType('coordinate', y, 'number', null); - - cutPoints.push( - Math.clamp(x, 0, width), - Math.clamp(y, 0, height), - ); - } - - if (checkShapeArea(this.shapeType, cutPoints)) { - copy.points = cutPoints; - } + if (updated.points && fittedPoints.length) { + this.points = [...fittedPoints]; } if (updated.occluded) { - checkObjectType('occluded', data.occluded, 'boolean', null); - copy.occluded = data.occluded; - } - - if (updated.group) { - checkObjectType('group', data.group, 'integer', null); - copy.group = data.group; + this.occluded = data.occluded; } if (updated.zOrder) { - checkObjectType('zOrder', data.zOrder, 'integer', null); - copy.zOrder = data.zOrder; + this.zOrder = data.zOrder; } if (updated.lock) { - checkObjectType('lock', data.lock, 'boolean', null); - copy.lock = data.lock; + this.lock = data.lock; } if (updated.color) { - checkObjectType('color', data.color, 'string', null); - if (/^#[0-9A-F]{6}$/i.test(data.color)) { - throw new ArgumentError( - `Got invalid color value: "${data.color}"`, - ); - } - - copy.color = data.color; + this.color = data.color; } if (updated.hidden) { - checkObjectType('hidden', data.hidden, 'boolean', null); - copy.hidden = data.hidden; + this.hidden = data.hidden; } - // Commit state - for (const prop of Object.keys(copy)) { - if (prop in this) { - this[prop] = copy[prop]; - } - } - - // Reset flags and commit all changes this.updateTimestamp(updated); updated.reset(); @@ -602,181 +626,79 @@ return objectStateFactory.call(this, frame, this.get(frame)); } - // All changes are done in this temporary object - const copy = Object.assign(this.get(frame)); - copy.attributes = Object.assign(copy.attributes); - copy.points = [...copy.points]; - + const fittedPoints = this.validateStateBeforeSave(frame, data); const updated = data.updateFlags; - let positionUpdated = false; - - if (updated.label) { - checkObjectType('label', data.label, null, Label); - copy.label = data.label; - copy.attributes = {}; - - // Shape attributes will be removed later after all checks - this.appendDefaultAttributes.call(copy, copy.label); - } - - const labelAttributes = copy.label.attributes + const current = this.get(frame); + const labelAttributes = data.label.attributes .reduce((accumulator, value) => { accumulator[value.id] = value; return accumulator; }, {}); - if (updated.attributes) { - for (const attrID of Object.keys(data.attributes)) { - const value = data.attributes[attrID]; - if (attrID in labelAttributes) { - if (validateAttributeValue(value, labelAttributes[attrID])) { - copy.attributes[attrID] = value; - } else { - throw new ArgumentError( - `Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`, - ); - } - } else { - throw new ArgumentError( - `Trying to save unknown attribute with id ${attrID} and value ${value}`, - ); - } - } - } - - if (updated.points) { - checkObjectType('points', data.points, null, Array); - checkNumberOfPoints(this.shapeType, data.points); - - // cut points - const { width, height } = this.frameMeta[frame]; - const cutPoints = []; - for (let i = 0; i < data.points.length - 1; i += 2) { - const x = data.points[i]; - const y = data.points[i + 1]; - - checkObjectType('coordinate', x, 'number', null); - checkObjectType('coordinate', y, 'number', null); - - cutPoints.push( - Math.clamp(x, 0, width), - Math.clamp(y, 0, height), - ); - } - - if (checkShapeArea(this.shapeType, cutPoints)) { - copy.points = cutPoints; - positionUpdated = true; - } - } - - if (updated.occluded) { - checkObjectType('occluded', data.occluded, 'boolean', null); - copy.occluded = data.occluded; - positionUpdated = true; - } - - if (updated.outside) { - checkObjectType('outside', data.outside, 'boolean', null); - copy.outside = data.outside; - positionUpdated = true; - } - - if (updated.group) { - checkObjectType('group', data.group, 'integer', null); - copy.group = data.group; - } - - if (updated.zOrder) { - checkObjectType('zOrder', data.zOrder, 'integer', null); - copy.zOrder = data.zOrder; - positionUpdated = true; - } - - if (updated.lock) { - checkObjectType('lock', data.lock, 'boolean', null); - copy.lock = data.lock; - } - - if (updated.color) { - checkObjectType('color', data.color, 'string', null); - if (/^#[0-9A-F]{6}$/i.test(data.color)) { - throw new ArgumentError( - `Got invalid color value: "${data.color}"`, - ); - } - - copy.color = data.color; - } - - if (updated.hidden) { - checkObjectType('hidden', data.hidden, 'boolean', null); - copy.hidden = data.hidden; - } - - if (updated.keyframe) { - // Just check here - checkObjectType('keyframe', data.keyframe, 'boolean', null); - } - - // Commit all changes - for (const prop of Object.keys(copy)) { - if (prop in this) { - this[prop] = copy[prop]; + if (updated.label) { + this.label = data.label; + this.attributes = {}; + for (const shape of Object.values(this.shapes)) { + shape.attributes = {}; } + this.appendDefaultAttributes(data.label); } + let mutableAttributesUpdated = false; if (updated.attributes) { - // Mutable attributes will be updated below - for (const attrID of Object.keys(copy.attributes)) { + for (const attrID of Object.keys(data.attributes)) { if (!labelAttributes[attrID].mutable) { this.attributes[attrID] = data.attributes[attrID]; this.attributes[attrID] = data.attributes[attrID]; + } else if (data.attributes[attrID] !== current.attributes[attrID]) { + mutableAttributesUpdated = mutableAttributesUpdated + // not keyframe yet + || !(frame in this.shapes) + // keyframe, but without this attrID + || !(attrID in this.shapes[frame]) + // keyframe with attrID, but with another value + || (this.shapes[frame][attrID] !== data.attributes[attrID]); } } } - if (updated.label) { - for (const shape of Object.values(this.shapes)) { - shape.attributes = {}; - } + if (updated.lock) { + this.lock = data.lock; } - // Remove keyframe - if (updated.keyframe && !data.keyframe) { - if (frame in this.shapes) { - if (Object.keys(this.shapes).length === 1) { - throw new DataError('You cannot remove the latest keyframe of a track'); - } - - delete this.shapes[frame]; - this.updateTimestamp(updated); - updated.reset(); - } - - return objectStateFactory.call(this, frame, this.get(frame)); + if (updated.color) { + this.color = data.color; } - // Add/update keyframe - if (positionUpdated || updated.attributes || (updated.keyframe && data.keyframe)) { - data.keyframe = true; + if (updated.hidden) { + this.hidden = data.hidden; + } + if (updated.points || updated.keyframe || updated.outside + || updated.occluded || updated.zOrder || mutableAttributesUpdated) { this.shapes[frame] = { frame, - zOrder: copy.zOrder, - points: copy.points, - outside: copy.outside, - occluded: copy.occluded, + zOrder: data.zOrder, + points: updated.points && fittedPoints.length ? fittedPoints : current.points, + outside: data.outside, + occluded: data.occluded, attributes: {}, }; - if (updated.attributes) { - // Unmutable attributes were updated above - for (const attrID of Object.keys(copy.attributes)) { - if (labelAttributes[attrID].mutable) { - this.shapes[frame].attributes[attrID] = data.attributes[attrID]; - this.shapes[frame].attributes[attrID] = data.attributes[attrID]; - } + for (const attrID of Object.keys(data.attributes)) { + if (labelAttributes[attrID].mutable + && data.attributes[attrID] !== current.attributes[attrID]) { + this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + this.shapes[frame].attributes[attrID] = data.attributes[attrID]; + } + } + + if (updated.keyframe && !data.keyframe) { + if (Object.keys(this.shapes).length === 1) { + throw new DataError('You are not able to remove the latest keyframe for a track. ' + + 'Consider removing a track instead'); + } else { + delete this.shapes[frame]; } } } @@ -894,46 +816,61 @@ return objectStateFactory.call(this, frame, this.get(frame)); } - // All changes are done in this temporary object - const copy = this.get(frame); + if (this.lock && data.lock) { + return objectStateFactory.call(this, frame, this.get(frame)); + } + const updated = data.updateFlags; + // First validate all the fields if (updated.label) { checkObjectType('label', data.label, null, Label); - copy.label = data.label; - copy.attributes = {}; - this.appendDefaultAttributes.call(copy, copy.label); } if (updated.attributes) { - const labelAttributes = copy.label - .attributes.map((attr) => `${attr.id}`); + const labelAttributes = data.label.attributes + .reduce((accumulator, value) => { + accumulator[value.id] = value; + return accumulator; + }, {}); for (const attrID of Object.keys(data.attributes)) { - if (labelAttributes.includes(attrID)) { - copy.attributes[attrID] = data.attributes[attrID]; + const value = data.attributes[attrID]; + if (attrID in labelAttributes) { + if (!validateAttributeValue(value, labelAttributes[attrID])) { + throw new ArgumentError( + `Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`, + ); + } + } else { + throw new ArgumentError( + `Trying to save unknown attribute with id ${attrID} and value ${value}`, + ); } } } - if (updated.group) { - checkObjectType('group', data.group, 'integer', null); - copy.group = data.group; - } - if (updated.lock) { checkObjectType('lock', data.lock, 'boolean', null); - copy.lock = data.lock; } - // Commit state - for (const prop of Object.keys(copy)) { - if (prop in this) { - this[prop] = copy[prop]; + // Now when all fields are validated, we can apply them + if (updated.label) { + this.label = data.label; + this.attributes = {}; + this.appendDefaultAttributes(data.label); + } + + if (updated.attributes) { + for (const attrID of Object.keys(data.attributes)) { + this.attributes[attrID] = data.attributes[attrID]; } } - // Reset flags and commit all changes + if (updated.lock) { + this.lock = data.lock; + } + this.updateTimestamp(updated); updated.reset(); diff --git a/cvat-core/src/common.js b/cvat-core/src/common.js index 51819bffae14..93165db9d0d5 100644 --- a/cvat-core/src/common.js +++ b/cvat-core/src/common.js @@ -56,7 +56,7 @@ if (typeof (value) !== type) { // specific case for integers which aren't native type in JS if (type === 'integer' && Number.isInteger(value)) { - return; + return true; } throw new ArgumentError( @@ -77,6 +77,8 @@ ); } } + + return true; } module.exports = { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index c69405a4fa9a..5feeeea1c421 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -123,36 +123,28 @@ export default class CanvasWrapperComponent extends React.PureComponent { } } - if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) { - this.updateCanvas(); - } - - if (prevProps.activatedStateID !== activatedStateID) { - if (prevProps.activatedStateID !== null) { - const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); - if (el) { - (el as any as SVGElement).style.fillOpacity = `${opacity / 100}`; - } + if (prevProps.activatedStateID !== null + && prevProps.activatedStateID !== activatedStateID) { + canvasInstance.activate(null); + const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); + if (el) { + (el as any as SVGElement).setAttribute('fill-opacity', `${opacity / 100}`); } + } - if (activatedStateID !== null) { - canvasInstance.activate(activatedStateID); - const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); - if (el) { - (el as any as SVGElement).style.fillOpacity = `${selectedOpacity / 100}`; - } - } else { - canvasInstance.cancel(); - } + if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) { + this.updateCanvas(); } if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders || prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) { this.updateShapesView(); } + + this.activateOnCanvas(); } - private async onShapeDrawn(event: any): Promise { + private onShapeDrawn(event: any): void { const { jobInstance, activeLabelID, @@ -183,7 +175,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { onCreateAnnotations(jobInstance, frame, [objectState]); } - private async onShapeEdited(event: any): Promise { + private onShapeEdited(event: any): void { const { jobInstance, frame, @@ -198,7 +190,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { onUpdateAnnotations(jobInstance, frame, [state]); } - private async onObjectsMerged(event: any): Promise { + private onObjectsMerged(event: any): void { const { jobInstance, frame, @@ -212,7 +204,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { onMergeAnnotations(jobInstance, frame, states); } - private async onObjectsGroupped(event: any): Promise { + private onObjectsGroupped(event: any): void { const { jobInstance, frame, @@ -226,7 +218,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { onGroupAnnotations(jobInstance, frame, states); } - private async onTrackSplitted(event: any): Promise { + private onTrackSplitted(event: any): void { const { jobInstance, frame, @@ -240,6 +232,22 @@ export default class CanvasWrapperComponent extends React.PureComponent { onSplitAnnotations(jobInstance, frame, state); } + private activateOnCanvas(): void { + const { + activatedStateID, + canvasInstance, + selectedOpacity, + } = this.props; + + if (activatedStateID !== null) { + canvasInstance.activate(activatedStateID); + const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); + if (el) { + (el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`); + } + } + } + private updateShapesView(): void { const { annotations, @@ -261,15 +269,15 @@ export default class CanvasWrapperComponent extends React.PureComponent { const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`); if (shapeView) { if (shapeView.tagName === 'rect' || shapeView.tagName === 'polygon') { - (shapeView as any as SVGElement).style.fillOpacity = `${opacity / 100}`; - (shapeView as any as SVGElement).style.stroke = shapeColor; - (shapeView as any as SVGElement).style.fill = shapeColor; + (shapeView as any as SVGElement).setAttribute('fill-opacity', `${opacity / 100}`); + (shapeView as any as SVGElement).setAttribute('stroke', shapeColor); + (shapeView as any as SVGElement).setAttribute('fill', shapeColor); } else { - (shapeView as any as SVGElement).style.stroke = shapeColor; + (shapeView as any as SVGElement).setAttribute('stroke', shapeColor); } if (blackBorders) { - (shapeView as any as SVGElement).style.stroke = 'black'; + (shapeView as any as SVGElement).setAttribute('stroke', 'black'); } } } @@ -321,6 +329,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().addEventListener('canvas.setup', (): void => { onSetupCanvas(); this.updateShapesView(); + this.activateOnCanvas(); }); canvasInstance.html().addEventListener('canvas.setup', () => { diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 7fd4e090b303..b66d2ac6aca5 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -137,6 +137,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { states, } = action.payload; + const activatedStateID = states + .map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID) + ? state.annotations.activatedStateID : null; + return { ...state, player: { @@ -149,6 +153,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, annotations: { ...state.annotations, + activatedStateID, states, }, }; From 73f1d05626c4e74f1665a66d28ead167d6f95f85 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 17:06:58 +0300 Subject: [PATCH 09/25] Fixed exception in edit function, fixed filling for polylines and points, fixed wrong image navigation, remove and copy --- cvat-canvas/README.md | 2 +- cvat-canvas/src/scss/canvas.scss | 17 +++- cvat-canvas/src/typescript/canvasModel.ts | 23 +++-- cvat-canvas/src/typescript/canvasView.ts | 91 ++++++++----------- cvat-canvas/src/typescript/drawHandler.ts | 22 +++-- cvat-ui/src/actions/annotation-actions.ts | 45 +++++++++ .../standard-workspace/canvas-wrapper.tsx | 22 ++++- .../objects-side-bar/object-item.tsx | 67 +++++++++++++- .../objects-side-bar/styles.scss | 18 +++- .../standard-workspace/canvas-wrapper.tsx | 5 + .../objects-side-bar/object-item.tsx | 30 ++++++ cvat-ui/src/reducers/annotation-reducer.ts | 86 ++++++++++++++++++ cvat-ui/src/reducers/interfaces.ts | 1 + 13 files changed, 347 insertions(+), 82 deletions(-) diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index 1757a5dc3a8e..59f4baa6f29f 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -113,10 +113,10 @@ Standard JS events are used. - canvas.setup - canvas.activated => {state: ObjectState} - canvas.clicked => {state: ObjectState} - - canvas.deactivated - canvas.moved => {states: ObjectState[], x: number, y: number} - canvas.find => {states: ObjectState[], x: number, y: number} - canvas.drawn => {state: DrawnData} + - canvas.editstart - canvas.edited => {state: ObjectState, points: number[]} - canvas.splitted => {state: ObjectState} - canvas.groupped => {states: ObjectState[]} diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 1fbb27d830d2..57faf8cad847 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -8,7 +8,6 @@ polyline.cvat_canvas_shape { fill-opacity: 0; - stroke-opacity: 1; } .cvat_shape_action_opacity { @@ -16,6 +15,19 @@ polyline.cvat_canvas_shape { stroke-opacity: 1; } +polyline.cvat_shape_action_opacity { + fill-opacity: 0; +} + +.cvat_shape_drawing_opacity { + fill-opacity: 0.2; + stroke-opacity: 1; +} + +polyline.cvat_shape_drawing_opacity { + fill-opacity: 0; +} + .cvat_shape_action_dasharray { stroke-dasharray: 4 1 2 3; } @@ -72,8 +84,7 @@ polyline.cvat_canvas_shape_splitting { } .cvat_canvas_shape_drawing { - fill-opacity: 0.1; - stroke-opacity: 1; + @extend .cvat_shape_drawing_opacity; fill: white; stroke: black; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index c439e58902d8..cc3be054a0b2 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -291,22 +291,33 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public setup(frameData: any, objectStates: any[]): void { + if (frameData.number === this.data.imageID) { + this.data.objects = objectStates; + this.notify(UpdateReasons.OBJECTS_UPDATED); + return; + } + + this.data.imageID = frameData.number; frameData.data( (): void => { this.data.image = ''; this.notify(UpdateReasons.IMAGE_CHANGED); }, ).then((data: string): void => { + if (frameData.number !== this.data.imageID) { + // already another image + return; + } + + if (!this.data.rememberAngle) { + this.data.angle = 0; + } + this.data.imageSize = { height: (frameData.height as number), width: (frameData.width as number), }; - if (this.data.imageID !== frameData.number && !this.data.rememberAngle) { - this.data.angle = 0; - } - this.data.imageID = frameData.number; - this.data.image = data; this.notify(UpdateReasons.IMAGE_CHANGED); this.data.objects = objectStates; @@ -317,7 +328,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } public activate(clientID: number | null, attributeID: number | null): void { - if (this.data.mode !== Mode.IDLE) { + if (this.data.mode !== Mode.IDLE && clientID !== null) { // Exception or just return? throw Error(`Canvas is busy. Action: ${this.data.mode}`); } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index c2196cdcb217..1f4691f6c39f 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -44,20 +44,6 @@ export interface CanvasView { html(): HTMLDivElement; } -function darker(color: string, percentage: number): string { - const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100)); - const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100)); - const B = Math.round(parseInt(color.slice(5, 7), 16) * (1 - percentage / 100)); - - const rHex = Math.max(0, R).toString(16); - const gHex = Math.max(0, G).toString(16); - const bHex = Math.max(0, B).toString(16); - - return `#${rHex.length === 1 ? `0${rHex}` : rHex}` - + `${gHex.length === 1 ? `0${gHex}` : gHex}` - + `${bHex.length === 1 ? `0${bHex}` : bHex}`; -} - export class CanvasViewImpl implements CanvasView, Listener { private loadingAnimation: SVGSVGElement; private text: SVGSVGElement; @@ -90,7 +76,7 @@ export class CanvasViewImpl implements CanvasView, Listener { return this.controller.mode; } - private onDrawDone(data: object): void { + private onDrawDone(data: object, continueDraw?: boolean): void { if (data) { const event: CustomEvent = new CustomEvent('canvas.drawn', { bubbles: false, @@ -98,6 +84,7 @@ export class CanvasViewImpl implements CanvasView, Listener { detail: { // eslint-disable-next-line new-cap state: data, + continue: continueDraw, }, }); @@ -111,11 +98,18 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.dispatchEvent(event); } - this.controller.draw({ - enabled: false, - }); + if (continueDraw) { + this.drawHandler.draw( + this.controller.drawData, + this.geometry, + ); + } else { + this.controller.draw({ + enabled: false, + }); - this.mode = Mode.IDLE; + this.mode = Mode.IDLE; + } } private onEditDone(state: any, points: number[]): void { @@ -411,10 +405,9 @@ export class CanvasViewImpl implements CanvasView, Listener { .filter((id: number): boolean => !newIDs.includes(id)) .map((id: number): any => this.drawnStates[id]); + if (this.activeElement.clientID !== null) { - const currentActivatedStillExist = !deleted.map((state: any): number => state.clientID) - .includes(this.activeElement.clientID); - this.deactivate(currentActivatedStillExist); + this.deactivate(); } for (const state of deleted) { @@ -430,8 +423,12 @@ export class CanvasViewImpl implements CanvasView, Listener { this.addObjects(created, translate); this.updateObjects(updated, translate); + if (this.controller.activeElement.clientID !== null) { - this.activate(this.controller.activeElement); + const { clientID } = this.controller.activeElement; + if (states.map((state: any): number => state.clientID).includes(clientID)) { + this.activate(this.controller.activeElement); + } } } @@ -443,7 +440,10 @@ export class CanvasViewImpl implements CanvasView, Listener { .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target); if (self.activeElement.clientID !== null) { - const state = self.drawnStates[self.activeElement.clientID]; + const [state] = self.controller.objects + .filter((_state: any): boolean => ( + _state.clientID === self.activeElement.clientID + )); if (e.ctrlKey) { const { points } = state; self.onEditDone( @@ -451,6 +451,11 @@ export class CanvasViewImpl implements CanvasView, Listener { points.slice(0, pointID * 2).concat(points.slice(pointID * 2 + 2)), ); } else if (e.shiftKey) { + self.canvas.dispatchEvent(new CustomEvent('canvas.editstart', { + bubbles: false, + cancelable: true, + })); + self.mode = Mode.EDIT; self.deactivate(); self.editHandler.edit({ @@ -621,12 +626,6 @@ export class CanvasViewImpl implements CanvasView, Listener { ); // Setup event handlers - this.canvas.addEventListener('mouseleave', (e: MouseEvent): void => { - if (!e.ctrlKey) { - this.deactivate(); - } - }); - this.content.addEventListener('dblclick', (e: MouseEvent): void => { if (e.ctrlKey || e.shiftKey) return; self.controller.fit(); @@ -635,7 +634,6 @@ export class CanvasViewImpl implements CanvasView, Listener { this.content.addEventListener('mousedown', (event): void => { if ([1, 2].includes(event.which)) { - this.deactivate(); if ([Mode.DRAG_CANVAS, Mode.IDLE].includes(this.mode)) { self.controller.enableDrag(event.clientX, event.clientY); } else if (this.mode === Mode.ZOOM_CANVAS && event.which === 2) { @@ -734,7 +732,6 @@ export class CanvasViewImpl implements CanvasView, Listener { } else if (reason === UpdateReasons.SHAPE_ACTIVATED) { this.activate(this.controller.activeElement); } else if (reason === UpdateReasons.DRAG_CANVAS) { - this.deactivate(); if (this.mode === Mode.DRAG_CANVAS) { this.canvas.dispatchEvent(new CustomEvent('canvas.dragstart', { bubbles: false, @@ -749,7 +746,6 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.style.cursor = ''; } } else if (reason === UpdateReasons.ZOOM_CANVAS) { - this.deactivate(); if (this.mode === Mode.ZOOM_CANVAS) { this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstart', { bubbles: false, @@ -769,28 +765,24 @@ export class CanvasViewImpl implements CanvasView, Listener { const data: DrawData = this.controller.drawData; if (data.enabled) { this.mode = Mode.DRAW; - this.deactivate(); } this.drawHandler.draw(data, this.geometry); } else if (reason === UpdateReasons.MERGE) { const data: MergeData = this.controller.mergeData; if (data.enabled) { this.mode = Mode.MERGE; - this.deactivate(); } this.mergeHandler.merge(data); } else if (reason === UpdateReasons.SPLIT) { const data: SplitData = this.controller.splitData; if (data.enabled) { this.mode = Mode.SPLIT; - this.deactivate(); } this.splitHandler.split(data); } else if (reason === UpdateReasons.GROUP) { const data: GroupData = this.controller.groupData; if (data.enabled) { this.mode = Mode.GROUP; - this.deactivate(); } this.groupHandler.group(data); } else if (reason === UpdateReasons.SELECT) { @@ -802,7 +794,6 @@ export class CanvasViewImpl implements CanvasView, Listener { this.groupHandler.select(this.controller.selected); } } else if (reason === UpdateReasons.CANCEL) { - this.deactivate(); if (this.mode === Mode.DRAW) { this.drawHandler.cancel(); } else if (this.mode === Mode.MERGE) { @@ -864,8 +855,8 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - if (drawnState.points - .some((p: number, id: number): boolean => p !== state.points[id]) + if (state.points + .some((p: number, id: number): boolean => p !== drawnState.points[id]) ) { const translatedPoints: number[] = translate(state.points); @@ -888,7 +879,7 @@ export class CanvasViewImpl implements CanvasView, Listener { return `${acc}${val},`; }, '', ); - + (this.svgShapes[clientID] as any).clear(); this.svgShapes[clientID].attr('points', stringified); } } @@ -961,7 +952,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } } - private deactivate(silent: boolean = false): void { + private deactivate(): void { if (this.activeElement.clientID !== null) { const { clientID } = this.activeElement; const [state] = this.controller.objects @@ -993,16 +984,6 @@ export class CanvasViewImpl implements CanvasView, Listener { clientID: null, attributeID: null, }; - - if (!silent) { - this.canvas.dispatchEvent(new CustomEvent('canvas.deactivated', { - bubbles: false, - cancelable: true, - detail: { - state, - }, - })); - } } } @@ -1194,7 +1175,7 @@ export class CanvasViewImpl implements CanvasView, Listener { id: `cvat_canvas_shape_${state.clientID}`, fill: state.color, 'shape-rendering': 'geometricprecision', - stroke: darker(state.color, 20), + stroke: state.color, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, zOrder: state.zOrder, }).move(xtl, ytl) @@ -1218,7 +1199,7 @@ export class CanvasViewImpl implements CanvasView, Listener { id: `cvat_canvas_shape_${state.clientID}`, fill: state.color, 'shape-rendering': 'geometricprecision', - stroke: darker(state.color, 20), + stroke: state.color, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, zOrder: state.zOrder, }).addClass('cvat_canvas_shape'); @@ -1241,7 +1222,7 @@ export class CanvasViewImpl implements CanvasView, Listener { id: `cvat_canvas_shape_${state.clientID}`, fill: state.color, 'shape-rendering': 'geometricprecision', - stroke: darker(state.color, 20), + stroke: state.color, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, zOrder: state.zOrder, }).addClass('cvat_canvas_shape'); diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index 7e8e7690122d..d6d6a7315dd9 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -32,7 +32,7 @@ export interface DrawHandler { export class DrawHandlerImpl implements DrawHandler { // callback is used to notify about creating new shape - private onDrawDone: (data: object) => void; + private onDrawDone: (data: object, continueDraw?: boolean) => void; private canvas: SVG.Container; private text: SVG.Container; private background: SVGSVGElement; @@ -342,21 +342,22 @@ export class DrawHandlerImpl implements DrawHandler { private pastePolyshape(): void { this.canvas.on('click.draw', (e: MouseEvent): void => { - const targetPoints = (e.target as SVGElement) - .getAttribute('points') + const targetPoints = this.drawInstance + .attr('points') .split(/[,\s]/g) - .map((coord): number => +coord); + .map((coord: string): number => +coord); const { points } = this.getFinalPolyshapeCoordinates(targetPoints); this.release(); this.onDrawDone({ - shapeType: this.drawData.shapeType, + shapeType: this.drawData.initialState.shapeType, + objectType: this.drawData.initialState.objectType, points, occluded: this.drawData.initialState.occluded, attributes: { ...this.drawData.initialState.attributes }, label: this.drawData.initialState.label, color: this.drawData.initialState.color, - }); + }, e.ctrlKey); }); } @@ -386,17 +387,18 @@ export class DrawHandlerImpl implements DrawHandler { this.pasteShape(); this.canvas.on('click.draw', (e: MouseEvent): void => { - const bbox = (e.target as SVGRectElement).getBBox(); + const bbox = this.drawInstance.node.getBBox(); const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox); this.release(); this.onDrawDone({ - shapeType: this.drawData.shapeType, + shapeType: this.drawData.initialState.shapeType, + objectType: this.drawData.initialState.objectType, points: [xtl, ytl, xbr, ybr], occluded: this.drawData.initialState.occluded, attributes: { ...this.drawData.initialState.attributes }, label: this.drawData.initialState.label, color: this.drawData.initialState.color, - }); + }, e.ctrlKey); }); } @@ -475,7 +477,7 @@ export class DrawHandlerImpl implements DrawHandler { } public constructor( - onDrawDone: (data: object) => void, + onDrawDone: (data: object, continueDraw?: boolean) => void, canvas: SVG.Container, text: SVG.Container, background: SVGSVGElement, diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index a36f0ed189df..0b675d83498e 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -32,6 +32,8 @@ export enum AnnotationActionTypes { MERGE_OBJECTS = 'MERGE_OBJECTS', GROUP_OBJECTS = 'GROUP_OBJECTS', SPLIT_TRACK = 'SPLIT_TRACK', + COPY_SHAPE = 'COPY_SHAPE', + EDIT_SHAPE = 'EDIT_SHAPE', DRAW_SHAPE = 'DRAW_SHAPE', SHAPE_DRAWN = 'SHAPE_DRAWN', RESET_CANVAS = 'RESET_CANVAS', @@ -53,6 +55,49 @@ export enum AnnotationActionTypes { COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS', ACTIVATE_OBJECT = 'ACTIVATE_OBJECT', SELECT_OBJECTS = 'SELECT_OBJECTS', + REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS', + REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', +} + + +export function removeObjectAsync(objectState: any, force: boolean): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + await objectState.delete(force); + dispatch({ + type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS, + payload: { + objectState, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.REMOVE_OBJECT_FAILED, + payload: { + objectState, + }, + }); + } + }; +} + +export function editShape(enabled: boolean): AnyAction { + return { + type: AnnotationActionTypes.EDIT_SHAPE, + payload: { + enabled, + }, + }; +} + +export function copyShape(objectState: any): AnyAction { + return { + type: AnnotationActionTypes.COPY_SHAPE, + payload: { + objectState, + }, + }; } export function selectObjects(selectedStatesID: number[]): AnyAction { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 5feeeea1c421..e3f5e6102746 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -45,6 +45,7 @@ interface Props { onMergeObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void; + onEditShape: (enabled: boolean) => void; onShapeDrawn: () => void; onResetCanvas: () => void; onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; @@ -154,7 +155,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { onCreateAnnotations, } = this.props; - onShapeDrawn(); + if (!event.detail.continue) { + onShapeDrawn(); + } const { state } = event.detail; if (!state.objectType) { @@ -166,7 +169,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { .filter((label: any) => label.id === activeLabelID); } - if (!state.occluded) { + if (typeof (state.occluded) === 'undefined') { state.occluded = false; } @@ -179,9 +182,12 @@ export default class CanvasWrapperComponent extends React.PureComponent { const { jobInstance, frame, + onEditShape, onUpdateAnnotations, } = this.props; + onEditShape(false); + const { state, points, @@ -308,6 +314,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { onZoomCanvas, onResetCanvas, onActivateObject, + onEditShape, } = this.props; // Size @@ -326,6 +333,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.grid(gridSize, gridSize); // Events + canvasInstance.html().addEventListener('click', (e: MouseEvent): void => { + if ((e.target as HTMLElement).tagName === 'svg') { + onActivateObject(null); + } + }); + + canvasInstance.html().addEventListener('canvas.editstart', (): void => { + onActivateObject(null); + onEditShape(true); + }); + canvasInstance.html().addEventListener('canvas.setup', (): void => { onSetupCanvas(); this.updateShapesView(); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 5ce97092cf25..0373b29447ce 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -10,6 +10,10 @@ import { Collapse, Checkbox, InputNumber, + Dropdown, + Menu, + Button, + Modal, } from 'antd'; import Text from 'antd/lib/typography/Text'; @@ -28,12 +32,58 @@ import { ObjectType, ShapeType, } from 'reducers/interfaces'; +function ItemMenu( + locked: boolean, + copy: (() => void), + remove: (() => void), + propagate: (() => void), +): JSX.Element { + return ( + + + + + + + + + + + + ); +} + interface ItemTopComponentProps { clientID: number; labelID: number; labels: any[]; type: string; + locked: boolean; changeLabel(labelID: string): void; + copy(): void; + remove(): void; } function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { @@ -42,7 +92,10 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { labelID, labels, type, + locked, changeLabel, + copy, + remove, } = props; return ( @@ -62,7 +115,12 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { - + + + ); @@ -465,6 +523,8 @@ interface Props { navigateLastKeyframe: null | (() => void); activate(): void; + copy(): void; + remove(): void; setOccluded(): void; unsetOccluded(): void; setOutside(): void; @@ -526,6 +586,8 @@ function ObjectItemComponent(props: Props): JSX.Element { navigateLastKeyframe, activate, + copy, + remove, setOccluded, unsetOccluded, setOutside, @@ -559,7 +621,10 @@ function ObjectItemComponent(props: Props): JSX.Element { labelID={labelID} labels={labels} type={type} + locked={locked} changeLabel={changeLabel} + copy={copy} + remove={remove} /> li { + padding: 0px; + + > button { + padding: 5px 32px; + color: $text-color; + width: 100%; + height: 100%; + text-align: left; + } + } +} diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index 13c412c1c7e9..ff1e81a92e7e 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -12,6 +12,7 @@ import { mergeObjects, groupObjects, splitTrack, + editShape, updateAnnotationsAsync, createAnnotationsAsync, mergeAnnotationsAsync, @@ -59,6 +60,7 @@ interface DispatchToProps { onMergeObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void; + onEditShape: (enabled: boolean) => void; onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; @@ -158,6 +160,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSplitTrack(enabled: boolean): void { dispatch(splitTrack(enabled)); }, + onEditShape(enabled: boolean): void { + dispatch(editShape(enabled)); + }, onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void { dispatch(updateAnnotationsAsync(sessionInstance, frame, states)); }, diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 88db2942a5cf..f77d18ed849b 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -9,6 +9,8 @@ import { collapseObjectItems, updateAnnotationsAsync, changeFrameAsync, + removeObjectAsync, + copyShape as copyShapeAction, activateObject as activateObjectAction, } from 'actions/annotation-actions'; @@ -36,6 +38,8 @@ interface DispatchToProps { updateState(sessionInstance: any, frameNumber: number, objectState: any): void; collapseOrExpand(objectStates: any[], collapsed: boolean): void; activateObject: (activatedStateID: number | null) => void; + removeObject: (objectState: any) => void; + copyShape: (objectState: any) => void; } function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { @@ -103,6 +107,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { activateObject(activatedStateID: number | null): void { dispatch(activateObjectAction(activatedStateID)); }, + removeObject(objectState: any): void { + dispatch(removeObjectAsync(objectState, true)); + }, + copyShape(objectState: any): void { + dispatch(copyShapeAction(objectState)); + }, }; } @@ -160,6 +170,24 @@ class ObjectItemContainer extends React.PureComponent { } }; + private copy = (): void => { + const { + objectState, + copyShape, + } = this.props; + + copyShape(objectState); + }; + + private remove = (): void => { + const { + objectState, + removeObject, + } = this.props; + + removeObject(objectState); + }; + private activate = (): void => { const { activateObject, @@ -339,6 +367,8 @@ class ObjectItemContainer extends React.PureComponent { ? null : this.navigateLastKeyframe } activate={this.activate} + remove={this.remove} + copy={this.copy} setOccluded={this.setOccluded} unsetOccluded={this.unsetOccluded} setOutside={this.setOutside} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index b66d2ac6aca5..b787181b06dc 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -286,6 +286,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { return { ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, canvas: { ...state.canvas, activeControl, @@ -299,6 +303,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { return { ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, canvas: { ...state.canvas, activeControl, @@ -316,6 +324,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { return { ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, canvas: { ...state.canvas, activeControl, @@ -335,6 +347,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { return { ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, canvas: { ...state.canvas, activeControl, @@ -348,6 +364,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { return { ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, canvas: { ...state.canvas, activeControl, @@ -361,6 +381,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { return { ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + }, canvas: { ...state.canvas, activeControl, @@ -500,6 +524,68 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: { + const { + objectState, + } = action.payload; + + return { + ...state, + annotations: { + ...state.annotations, + activatedStateID: null, + states: state.annotations.states + .filter((_objectState: any) => ( + _objectState.clientID !== objectState.clientID + )), + }, + }; + } + case AnnotationActionTypes.COPY_SHAPE: { + const { + objectState, + } = action.payload; + + state.canvas.instance.cancel(); + state.canvas.instance.draw({ + enabled: true, + initialState: objectState, + }); + + let activeControl = ActiveControl.DRAW_RECTANGLE; + if (objectState.shapeType === ShapeType.POINTS) { + activeControl = ActiveControl.DRAW_POINTS; + } else if (objectState.shapeType === ShapeType.POLYGON) { + activeControl = ActiveControl.DRAW_POLYGON; + } else if (objectState.shapeType === ShapeType.POLYLINE) { + activeControl = ActiveControl.DRAW_POLYLINE; + } + + return { + ...state, + canvas: { + ...state.canvas, + activeControl, + }, + annotations: { + ...state.annotations, + activatedStateID: null, + }, + }; + } + case AnnotationActionTypes.EDIT_SHAPE: { + const { enabled } = action.payload; + const activeControl = enabled + ? ActiveControl.SPLIT : ActiveControl.CURSOR; + + return { + ...state, + canvas: { + ...state.canvas, + activeControl, + }, + }; + } case AnnotationActionTypes.RESET_CANVAS: { return { ...state, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index acaab0db6efd..2bc3d4c4c89a 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -238,6 +238,7 @@ export enum ActiveControl { MERGE = 'merge', GROUP = 'group', SPLIT = 'split', + EDIT = 'edit', } export enum ShapeType { From c707e47f0696e9cb45fd04050847fd353dbbc641 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 17:24:53 +0300 Subject: [PATCH 10/25] Added lock --- cvat-canvas/src/typescript/canvasView.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 1f4691f6c39f..c0200d9f359b 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -844,7 +844,12 @@ export class CanvasViewImpl implements CanvasView, Listener { if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) { const none = state.hidden || state.outside; - this.svgShapes[clientID].style('display', none ? 'none' : ''); + if (state.shapeType === 'points') { + this.svgShapes[clientID].remember('_selectHandler').nested + .style('display', none ? 'none' : ''); + } else { + this.svgShapes[clientID].style('display', none ? 'none' : ''); + } } if (drawnState.occluded !== state.occluded) { @@ -999,16 +1004,22 @@ export class CanvasViewImpl implements CanvasView, Listener { this.deactivate(); } - this.activeElement = { ...activeElement }; - const { clientID } = this.activeElement; - + const { clientID } = activeElement; if (clientID === null) { return; } const [state] = this.controller.objects .filter((_state: any): boolean => _state.clientID === clientID); + if (state.hidden || state.lock) { + if (state.shapeType === 'points') { + this.svgShapes[clientID].remember('_selectHandler').nested + .style('pointer-events', state.lock ? 'none' : ''); + } + return; + } + this.activeElement = { ...activeElement }; const shape = this.svgShapes[clientID]; shape.addClass('cvat_canvas_shape_activated'); let text = this.svgTexts[clientID]; From 7214d117bb5c0cd2bd9bc72a5781f8b81bf370e9 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 17:43:44 +0300 Subject: [PATCH 11/25] Some fixes with points --- cvat-canvas/src/typescript/canvasView.ts | 50 +++++++++++++++--------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index c0200d9f359b..e8b05a6617f7 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -423,7 +423,6 @@ export class CanvasViewImpl implements CanvasView, Listener { this.addObjects(created, translate); this.updateObjects(updated, translate); - if (this.controller.activeElement.clientID !== null) { const { clientID } = this.controller.activeElement; if (states.map((state: any): number => state.clientID).includes(clientID)) { @@ -886,6 +885,11 @@ export class CanvasViewImpl implements CanvasView, Listener { ); (this.svgShapes[clientID] as any).clear(); this.svgShapes[clientID].attr('points', stringified); + + if (state.shapeType === 'points') { + this.selectize(false, this.svgShapes[clientID]); + this.setupPoints(this.svgShapes[clientID] as SVG.PolyLine, state); + } } } @@ -1011,11 +1015,13 @@ export class CanvasViewImpl implements CanvasView, Listener { const [state] = this.controller.objects .filter((_state: any): boolean => _state.clientID === clientID); + + if (state.shapeType === 'points') { + this.svgShapes[clientID].remember('_selectHandler').nested + .style('pointer-events', state.lock ? 'none' : ''); + } + if (state.hidden || state.lock) { - if (state.shapeType === 'points') { - this.svgShapes[clientID].remember('_selectHandler').nested - .style('pointer-events', state.lock ? 'none' : ''); - } return; } @@ -1249,6 +1255,25 @@ export class CanvasViewImpl implements CanvasView, Listener { return polyline; } + private setupPoints(basicPolyline: SVG.PolyLine, state: any): any { + this.selectize(true, basicPolyline); + + const group = basicPolyline.remember('_selectHandler').nested + .addClass('cvat_canvas_shape').attr({ + clientID: state.clientID, + zOrder: state.zOrder, + id: `cvat_canvas_shape_${state.clientID}`, + fill: state.color, + }).style({ + 'fill-opacity': 1, + }); + + group.bbox = basicPolyline.bbox.bind(basicPolyline); + group.clone = basicPolyline.clone.bind(basicPolyline); + + return group; + } + private addPoints(points: string, state: any): SVG.PolyLine { const shape = this.adoptedContent.polyline(points).attr({ 'color-rendering': 'optimizeQuality', @@ -1260,25 +1285,12 @@ export class CanvasViewImpl implements CanvasView, Listener { opacity: 0, }); - this.selectize(true, shape); - - const group = shape.remember('_selectHandler').nested - .addClass('cvat_canvas_shape').attr({ - clientID: state.clientID, - zOrder: state.zOrder, - id: `cvat_canvas_shape_${state.clientID}`, - fill: state.color, - }).style({ - 'fill-opacity': 1, - }); + const group = this.setupPoints(shape, state); if (state.hidden || state.outside) { group.style('display', 'none'); } - group.bbox = shape.bbox.bind(shape); - group.clone = shape.clone.bind(shape); - shape.remove = (): SVG.PolyLine => { this.selectize(false, shape); shape.constructor.prototype.remove.call(shape); From a1ed8b28af0e8c17998aae4fd5415f950c954d83 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 18:17:52 +0300 Subject: [PATCH 12/25] Minor appearance fixes --- cvat-canvas/src/typescript/canvasView.ts | 8 +++----- .../standard-workspace/canvas-wrapper.tsx | 19 +++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index e8b05a6617f7..8fd1d01f92a5 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -330,11 +330,9 @@ export class CanvasViewImpl implements CanvasView, Listener { for (const key in this.svgShapes) { if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) { const object = this.svgShapes[key]; - if (object.attr('stroke-width')) { - object.attr({ - 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, - }); - } + object.attr({ + 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, + }); } } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index e3f5e6102746..8188af3fae90 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -129,7 +129,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.activate(null); const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); if (el) { - (el as any as SVGElement).setAttribute('fill-opacity', `${opacity / 100}`); + (el as any).instance.fill({ opacity: opacity / 100 }); } } @@ -272,18 +272,17 @@ export default class CanvasWrapperComponent extends React.PureComponent { shapeColor = state.label.color; } + // TODO: In this approach CVAT-UI know details of implementations CVAT-CANVAS (svg.js) const shapeView = window.document.getElementById(`cvat_canvas_shape_${state.clientID}`); if (shapeView) { - if (shapeView.tagName === 'rect' || shapeView.tagName === 'polygon') { - (shapeView as any as SVGElement).setAttribute('fill-opacity', `${opacity / 100}`); - (shapeView as any as SVGElement).setAttribute('stroke', shapeColor); - (shapeView as any as SVGElement).setAttribute('fill', shapeColor); + if (['rect', 'polygon', 'polyline'].includes(shapeView.tagName)) { + (shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 }); + (shapeView as any).instance.stroke({ color: blackBorders ? 'black' : shapeColor }); } else { - (shapeView as any as SVGElement).setAttribute('stroke', shapeColor); - } - - if (blackBorders) { - (shapeView as any as SVGElement).setAttribute('stroke', 'black'); + // group of points + for (const child of (shapeView as any).instance.children()) { + child.fill({ color: shapeColor }); + } } } } From 16d4260321f08e6d4830261414b1c9ce984e7e92 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 19:07:31 +0300 Subject: [PATCH 13/25] Fixed insert for points --- cvat-canvas/src/typescript/drawHandler.ts | 69 ++++++++++++++++++++--- cvat-canvas/src/typescript/shared.ts | 2 +- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index d6d6a7315dd9..da0798faae7e 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -46,6 +46,7 @@ export class DrawHandlerImpl implements DrawHandler { // we should use any instead of SVG.Shape because svg plugins cannot change declared interface // so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist private drawInstance: any; + private pointsGroup: SVG.G | null; private shapeSizeElement: ShapeSizeElement; private getFinalRectCoordinates(bbox: BBox): number[] { @@ -125,6 +126,11 @@ export class DrawHandlerImpl implements DrawHandler { this.canvas.off('mousemove.draw'); this.canvas.off('click.draw'); + if (this.pointsGroup) { + this.pointsGroup.remove(); + this.pointsGroup = null; + } + if (this.drawInstance) { // Draw plugin isn't activated when draw from initialState // So, we don't need to use any draw events @@ -311,7 +317,7 @@ export class DrawHandlerImpl implements DrawHandler { private drawPolygon(): void { this.drawInstance = (this.canvas as any).polygon().draw({ snapToGrid: 0.1, - }).addClass('cvat_canvas_shape_drawing').style({ + }).addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, }); @@ -321,7 +327,7 @@ export class DrawHandlerImpl implements DrawHandler { private drawPolyline(): void { this.drawInstance = (this.canvas as any).polyline().draw({ snapToGrid: 0.1, - }).addClass('cvat_canvas_shape_drawing').style({ + }).addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'fill-opacity': 0, }); @@ -332,7 +338,7 @@ export class DrawHandlerImpl implements DrawHandler { private drawPoints(): void { this.drawInstance = (this.canvas as any).polygon().draw({ snapToGrid: 0.1, - }).addClass('cvat_canvas_shape_drawing').style({ + }).addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': 0, opacity: 0, }); @@ -381,7 +387,7 @@ export class DrawHandlerImpl implements DrawHandler { private pasteBox(box: BBox): void { this.drawInstance = (this.canvas as any).rect(box.width, box.height) .move(box.x, box.y) - .addClass('cvat_canvas_shape_drawing').style({ + .addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, }); this.pasteShape(); @@ -405,7 +411,7 @@ export class DrawHandlerImpl implements DrawHandler { private pastePolygon(points: string): void { this.drawInstance = (this.canvas as any).polygon(points) - .addClass('cvat_canvas_shape_drawing').style({ + .addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, }); this.pasteShape(); @@ -414,7 +420,7 @@ export class DrawHandlerImpl implements DrawHandler { private pastePolyline(points: string): void { this.drawInstance = (this.canvas as any).polyline(points) - .addClass('cvat_canvas_shape_drawing').style({ + .addClass('cvat_canvas_shape_drawing').attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, }); this.pasteShape(); @@ -426,7 +432,42 @@ export class DrawHandlerImpl implements DrawHandler { .addClass('cvat_canvas_shape_drawing').style({ 'stroke-width': 0, }); - this.pasteShape(); + + this.pointsGroup = this.canvas.group(); + for (const point of points.split(' ')) { + const radius = consts.BASE_POINT_SIZE / this.geometry.scale; + const stroke = consts.POINTS_STROKE_WIDTH / this.geometry.scale; + const [x, y] = point.split(',').map((coord: string): number => +coord); + this.pointsGroup.circle().move(x - radius / 2, y - radius / 2) + .fill('white').stroke('black').attr({ + r: radius, + 'stroke-width': stroke, + }); + } + + this.pointsGroup.attr({ + z_order: Number.MAX_SAFE_INTEGER, + }); + + this.canvas.on('mousemove.draw', (e: MouseEvent): void => { + const [x, y] = translateToSVG( + this.canvas.node as any as SVGSVGElement, + [e.clientX, e.clientY], + ); + + const bbox = this.drawInstance.bbox(); + this.drawInstance.move(x - bbox.width / 2, y - bbox.height / 2); + const radius = consts.BASE_POINT_SIZE / this.geometry.scale; + const newPoints = this.drawInstance.attr('points').split(' '); + if (this.pointsGroup) { + this.pointsGroup.children() + .forEach((child: SVG.Element, idx: number): void => { + const [px, py] = newPoints[idx].split(','); + child.move(px - radius / 2, py - radius / 2); + }); + } + }); + this.pastePolyshape(); } @@ -490,6 +531,7 @@ export class DrawHandlerImpl implements DrawHandler { this.geometry = null; this.crosshair = null; this.drawInstance = null; + this.pointsGroup = null; this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => { if (this.crosshair) { @@ -527,16 +569,25 @@ export class DrawHandlerImpl implements DrawHandler { }); } + if (this.pointsGroup) { + for (const point of this.pointsGroup.children()) { + point.attr({ + 'stroke-width': consts.POINTS_STROKE_WIDTH / geometry.scale, + r: consts.BASE_POINT_SIZE / geometry.scale, + }); + } + } + if (this.drawInstance) { this.drawInstance.draw('transform'); - this.drawInstance.style({ + this.drawInstance.attr({ 'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale, }); const paintHandler = this.drawInstance.remember('_paintHandler'); for (const point of (paintHandler as any).set.members) { - point.style( + point.attr( 'stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`, ); diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index 226dc95bde98..d98eaaad7393 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -74,7 +74,7 @@ export function pointsToString(points: number[]): string { return `${acc},${val}`; } - return `${acc} ${val}`; + return `${acc} ${val}`.trim(); }, ''); } From 64c3d877952ef679289d74af6d467f3c86a22402 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 19:15:27 +0300 Subject: [PATCH 14/25] Fixed unit tests --- cvat-core/src/annotations-objects.js | 9 +++++---- cvat-core/tests/api/annotations.js | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 37ab407b5433..6ca75e7b8163 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -208,16 +208,17 @@ validateStateBeforeSave(frame, data) { let fittedPoints = []; const updated = data.updateFlags; + + if (updated.label) { + checkObjectType('label', data.label, null, Label); + } + const labelAttributes = data.label.attributes .reduce((accumulator, value) => { accumulator[value.id] = value; return accumulator; }, {}); - if (updated.label) { - checkObjectType('label', data.label, null, Label); - } - if (updated.attributes) { for (const attrID of Object.keys(data.attributes)) { const value = data.attributes[attrID]; diff --git a/cvat-core/tests/api/annotations.js b/cvat-core/tests/api/annotations.js index 71a70f468444..18f84f720dad 100644 --- a/cvat-core/tests/api/annotations.js +++ b/cvat-core/tests/api/annotations.js @@ -548,7 +548,7 @@ describe('Feature: group annotations', () => { expect(typeof (groupID)).toBe('number'); annotations = await task.annotations.get(0); for (const state of annotations) { - expect(state.group).toBe(groupID); + expect(state.group.id).toBe(groupID); } }); @@ -559,7 +559,7 @@ describe('Feature: group annotations', () => { expect(typeof (groupID)).toBe('number'); annotations = await job.annotations.get(0); for (const state of annotations) { - expect(state.group).toBe(groupID); + expect(state.group.id).toBe(groupID); } }); From 10874b7c47fe1524580ae8e3da957fca495b6125 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 19:23:54 +0300 Subject: [PATCH 15/25] Fixed control --- cvat-ui/src/reducers/annotation-reducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index b787181b06dc..ad8725c492d7 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -576,7 +576,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { case AnnotationActionTypes.EDIT_SHAPE: { const { enabled } = action.payload; const activeControl = enabled - ? ActiveControl.SPLIT : ActiveControl.CURSOR; + ? ActiveControl.EDIT : ActiveControl.CURSOR; return { ...state, From d27bd6731b83860eaf3b51b762b9890078c25a6f Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 29 Jan 2020 19:35:59 +0300 Subject: [PATCH 16/25] Fixed list size --- .../objects-side-bar/objects-side-bar.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index 96cb4e5c3989..dd4277cd7f2f 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -134,9 +134,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { type Props = StateToProps & DispatchToProps; class ObjectsSideBarContainer extends React.PureComponent { public componentDidMount(): void { + window.addEventListener('resize', this.alignTabHeight); + this.alignTabHeight(); + } + + public componentWillUnmount(): void { + window.removeEventListener('resize', this.alignTabHeight); + } + + private alignTabHeight = (): void => { const { updateTabContentHeight } = this.props; updateTabContentHeight(); - } + }; private changeShapesColorBy = (event: RadioChangeEvent): void => { const { changeShapesColorBy } = this.props; From c0929f8d07e119220574d74a68dec4eb22a23a79 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 30 Jan 2020 13:01:28 +0300 Subject: [PATCH 17/25] Added propagate --- cvat-core/src/annotations-objects.js | 2 +- cvat-ui/src/actions/annotation-actions.ts | 81 ++++++++++- .../objects-side-bar/object-item.tsx | 7 +- .../standard-workspace/propagate-confirm.tsx | 55 +++++++ .../standard-workspace/standard-workspace.tsx | 3 +- .../standard-workspace/styles.scss | 7 + .../objects-side-bar/object-item.tsx | 17 ++- .../standard-workspace/propagate-confirm.tsx | 134 ++++++++++++++++++ .../top-bar/statistics-modal.tsx | 0 cvat-ui/src/reducers/annotation-reducer.ts | 34 +++++ cvat-ui/src/reducers/interfaces.ts | 4 + 11 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 cvat-ui/src/components/annotation-page/standard-workspace/propagate-confirm.tsx create mode 100644 cvat-ui/src/containers/annotation-page/standard-workspace/propagate-confirm.tsx create mode 100644 cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index 6ca75e7b8163..bd72d0d084b1 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -757,7 +757,7 @@ this.removed = true; } - return true; + return this.removed; } } diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 0b675d83498e..5800d6de3edc 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -56,21 +56,92 @@ export enum AnnotationActionTypes { ACTIVATE_OBJECT = 'ACTIVATE_OBJECT', SELECT_OBJECTS = 'SELECT_OBJECTS', REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS', - REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', + REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', // todo: add message + PROPAGATE_OBJECT = 'PROPAGATE_OBJECT', + PROPAGATE_OBJECT_SUCCESS = 'PROPAGATE_OBJECT_SUCCESS', + PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED', // todo: add message + CHANGE_PROPAGATE_FRAMES = 'CHANGE_PROPAGATE_FRAMES', } -export function removeObjectAsync(objectState: any, force: boolean): -ThunkAction, {}, {}, AnyAction> { +export function propagateObjectAsync( + sessionInstance: any, + objectState: any, + from: number, + to: number, +): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - await objectState.delete(force); + const copy = { + attributes: objectState.attributes, + points: objectState.points, + occluded: objectState.occluded, + objectType: objectState.objectType !== ObjectType.TRACK + ? objectState.objectType : ObjectType.SHAPE, + shapeType: objectState.shapeType, + label: objectState.label, + frame: from, + }; + + const states = []; + for (let frame = from; frame <= to; frame++) { + copy.frame = frame; + const newState = new cvat.classes.ObjectState(copy); + states.push(newState); + } + + await sessionInstance.annotations.put(states); + dispatch({ - type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS, + type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS, payload: { objectState, }, }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.PROPAGATE_OBJECT_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function propagateObject(objectState: any | null): AnyAction { + return { + type: AnnotationActionTypes.PROPAGATE_OBJECT, + payload: { + objectState, + }, + }; +} + +export function changePropagateFrames(frames: number): AnyAction { + return { + type: AnnotationActionTypes.CHANGE_PROPAGATE_FRAMES, + payload: { + frames, + }, + }; +} + +export function removeObjectAsync(objectState: any, force: boolean): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + const removed = await objectState.delete(force); + if (removed) { + dispatch({ + type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS, + payload: { + objectState, + }, + }); + } else { + throw new Error('Could not remove the object. Is it locked?'); + } } catch (error) { dispatch({ type: AnnotationActionTypes.REMOVE_OBJECT_FAILED, diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 0373b29447ce..155530a767af 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -84,6 +84,7 @@ interface ItemTopComponentProps { changeLabel(labelID: string): void; copy(): void; remove(): void; + propagate(): void; } function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { @@ -96,6 +97,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { changeLabel, copy, remove, + propagate, } = props; return ( @@ -117,7 +119,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element { @@ -524,6 +526,7 @@ interface Props { activate(): void; copy(): void; + propagate(): void; remove(): void; setOccluded(): void; unsetOccluded(): void; @@ -587,6 +590,7 @@ function ObjectItemComponent(props: Props): JSX.Element { activate, copy, + propagate, remove, setOccluded, unsetOccluded, @@ -625,6 +629,7 @@ function ObjectItemComponent(props: Props): JSX.Element { changeLabel={changeLabel} copy={copy} remove={remove} + propagate={propagate} /> +
+ Do you want to make a copy of the object on + + { + propagateFrames > 1 + ? frames + : frame + } + up to the + + frame +
+ + ); +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx index b2656397e1eb..0951e5e9845f 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx @@ -8,7 +8,7 @@ import { import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper'; import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; - +import PropagateConfirmContainer from 'containers/annotation-page/standard-workspace/propagate-confirm'; export default function StandardWorkspaceComponent(): JSX.Element { return ( @@ -16,6 +16,7 @@ export default function StandardWorkspaceComponent(): JSX.Element { + ); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index efb38d67a24d..0d9cae20e3b6 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -104,3 +104,10 @@ } } } + +.cvat-propagate-confirm { + > .ant-input-number { + width: 70px; + margin: 0px 5px; + } +} \ No newline at end of file diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index f77d18ed849b..dd71608c089e 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -12,6 +12,7 @@ import { removeObjectAsync, copyShape as copyShapeAction, activateObject as activateObjectAction, + propagateObject as propagateObjectAction, } from 'actions/annotation-actions'; import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item'; @@ -40,6 +41,7 @@ interface DispatchToProps { activateObject: (activatedStateID: number | null) => void; removeObject: (objectState: any) => void; copyShape: (objectState: any) => void; + propagateObject: (objectState: any) => void; } function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { @@ -113,6 +115,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { copyShape(objectState: any): void { dispatch(copyShapeAction(objectState)); }, + propagateObject(objectState: any): void { + dispatch(propagateObjectAction(objectState)); + }, }; } @@ -179,6 +184,15 @@ class ObjectItemContainer extends React.PureComponent { copyShape(objectState); }; + private propagate = (): void => { + const { + objectState, + propagateObject, + } = this.props; + + propagateObject(objectState); + }; + private remove = (): void => { const { objectState, @@ -351,7 +365,7 @@ class ObjectItemContainer extends React.PureComponent { labels={labels} collapsed={collapsed} navigateFirstKeyframe={ - first === frameNumber || first === null + first >= frameNumber || first === null ? null : this.navigateFirstKeyframe } navigatePrevKeyframe={ @@ -369,6 +383,7 @@ class ObjectItemContainer extends React.PureComponent { activate={this.activate} remove={this.remove} copy={this.copy} + propagate={this.propagate} setOccluded={this.setOccluded} unsetOccluded={this.unsetOccluded} setOutside={this.setOutside} diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/propagate-confirm.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/propagate-confirm.tsx new file mode 100644 index 000000000000..951a5e5ea5ed --- /dev/null +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/propagate-confirm.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { + propagateObject as propagateObjectAction, + changePropagateFrames as changePropagateFramesAction, + propagateObjectAsync, +} from 'actions/annotation-actions'; + +import { CombinedState } from 'reducers/interfaces'; +import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; + +interface StateToProps { + objectState: any | null; + frameNumber: number; + stopFrame: number; + propagateFrames: number; + jobInstance: any; +} + +interface DispatchToProps { + cancel(): void; + propagateObject(sessionInstance: any, objectState: any, from: number, to: number): void; + changePropagateFrames(frames: number): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + propagate: { + objectState, + frames: propagateFrames, + }, + job: { + instance: { + stopFrame, + }, + instance: jobInstance, + }, + player: { + frame: { + number: frameNumber, + }, + }, + }, + } = state; + + return { + objectState, + frameNumber, + stopFrame, + propagateFrames, + jobInstance, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + propagateObject(sessionInstance: any, objectState: any, from: number, to: number): void { + dispatch(propagateObjectAsync(sessionInstance, objectState, from, to)); + }, + changePropagateFrames(frames: number): void { + dispatch(changePropagateFramesAction(frames)); + }, + cancel(): void { + dispatch(propagateObjectAction(null)); + }, + }; +} + +type Props = StateToProps & DispatchToProps; +class PropagateConfirmContainer extends React.PureComponent { + private propagateObject = (): void => { + const { + propagateObject, + objectState, + propagateFrames, + frameNumber, + stopFrame, + jobInstance, + } = this.props; + + const propagateUpToFrame = Math.min(frameNumber + propagateFrames, stopFrame); + propagateObject(jobInstance, objectState, frameNumber + 1, propagateUpToFrame); + }; + + private changePropagateFrames = (value: number | undefined): void => { + const { changePropagateFrames } = this.props; + if (typeof (value) !== 'undefined') { + changePropagateFrames(value); + } + }; + + private changeUpToFrame = (value: number | undefined): void => { + const { + stopFrame, + frameNumber, + changePropagateFrames, + } = this.props; + if (typeof (value) !== 'undefined') { + const propagateFrames = Math.max(0, Math.min(stopFrame, value)) - frameNumber; + changePropagateFrames(propagateFrames); + } + }; + + public render(): JSX.Element { + const { + frameNumber, + stopFrame, + propagateFrames, + cancel, + objectState, + } = this.props; + + const propagateUpToFrame = Math.min(frameNumber + propagateFrames, stopFrame); + + return ( + + ); + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(PropagateConfirmContainer); diff --git a/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index ad8725c492d7..d86e0a4e53c7 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -45,6 +45,10 @@ const defaultState: AnnotationState = { collapsed: {}, states: [], }, + propagate: { + objectState: null, + frames: 50, + }, colors: [], sidebarCollapsed: false, appearanceCollapsed: false, @@ -586,6 +590,36 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } + case AnnotationActionTypes.PROPAGATE_OBJECT: { + const { objectState } = action.payload; + return { + ...state, + propagate: { + ...state.propagate, + objectState, + }, + }; + } + case AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS: { + return { + ...state, + propagate: { + ...state.propagate, + objectState: null, + }, + }; + } + case AnnotationActionTypes.CHANGE_PROPAGATE_FRAMES: { + const { frames } = action.payload; + + return { + ...state, + propagate: { + ...state.propagate, + frames, + }, + }; + } case AnnotationActionTypes.RESET_CANVAS: { return { ...state, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 2bc3d4c4c89a..f4f230c22c41 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -296,6 +296,10 @@ export interface AnnotationState { statuses: string[]; }; }; + propagate: { + objectState: any | null; + frames: number; + }; colors: any[]; sidebarCollapsed: boolean; appearanceCollapsed: boolean; From 23d8f926b85bdbc6276176b4e2d75f55e3a32e28 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 30 Jan 2020 13:07:47 +0300 Subject: [PATCH 18/25] Minor fix with attr saving --- cvat-core/src/annotations-objects.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js index bd72d0d084b1..b689f372147a 100644 --- a/cvat-core/src/annotations-objects.js +++ b/cvat-core/src/annotations-objects.js @@ -677,13 +677,14 @@ if (updated.points || updated.keyframe || updated.outside || updated.occluded || updated.zOrder || mutableAttributesUpdated) { + const mutableAttributes = frame in this.shapes ? this.shapes[frame].attributes : {}; this.shapes[frame] = { frame, zOrder: data.zOrder, points: updated.points && fittedPoints.length ? fittedPoints : current.points, outside: data.outside, occluded: data.occluded, - attributes: {}, + attributes: mutableAttributes, }; for (const attrID of Object.keys(data.attributes)) { From 09df778fd1973d4d05d156bfd66edd6057d985bb Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 30 Jan 2020 16:23:51 +0300 Subject: [PATCH 19/25] Some div changed to buttons --- .../components/annotation-page/styles.scss | 42 +++++-------------- .../annotation-page/top-bar/left-group.tsx | 31 +++++++------- .../annotation-page/top-bar/right-group.tsx | 13 +++--- 3 files changed, 34 insertions(+), 52 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index 83a8c02cb7f8..6d5dc524459b 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -12,9 +12,7 @@ } .cvat-annotation-header-left-group { - height: 100%; - - > div:first-child { + > button:first-child { filter: invert(0.9); background: $background-color-1; border-radius: 0px; @@ -22,15 +20,21 @@ } } -.cvat-annotation-header-button { +.ant-btn.cvat-annotation-header-button { padding: 0px; width: 54px; height: 54px; float: left; text-align: center; user-select: none; + color: $text-color; + display: flex; + flex-direction: column; + align-items: center; + margin: 0px 3px; > span { + margin-left: 0px; font-size: 10px; } @@ -40,7 +44,7 @@ } &:hover > i { - transform: scale(0.9); + transform: scale(0.85); } &:active > i { @@ -119,35 +123,11 @@ } .cvat-annotation-header-right-group { - height: 100%; - > div { - height: 54px; float: left; - text-align: center; - margin-right: 20px; - - > span { - font-size: 10px; - } - - > i { - transform: scale(0.8); - padding: 3px; - } - - &:hover > i { - transform: scale(0.9); - } - - &:active > i { - transform: scale(0.8); - } - } - - > div:not(:nth-child(3)) > * { display: block; - line-height: 0px; + height: 54px; + margin-right: 15px; } } diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index 0c1c7bd96022..2f763bda213a 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -4,6 +4,7 @@ import { Col, Icon, Modal, + Button, Timeline, } from 'antd'; @@ -29,20 +30,20 @@ function LeftGroup(props: Props): JSX.Element { return ( -
+
-
+
-
+ +
-
+ Undo + +
+ Redo + ); } diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index 89c758a79c64..5484ce6c99b2 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -4,6 +4,7 @@ import { Col, Icon, Select, + Button, } from 'antd'; import { @@ -14,14 +15,14 @@ import { function RightGroup(): JSX.Element { return ( -
+
-
+ Fullscreen + +
+ Info +
+ From 9ba3e994b7f1d56ca831c79db4302d393f76c8ee Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 30 Jan 2020 19:24:24 +0300 Subject: [PATCH 21/25] Statistics modal, changing a job status --- cvat-ui/src/actions/annotation-actions.ts | 78 ++++++- .../annotation-page/annotation-page.tsx | 2 + .../annotation-page/statistics-modal.tsx | 0 .../components/annotation-page/styles.scss | 44 ++++ .../annotation-page/top-bar/right-group.tsx | 10 +- .../top-bar/statistics-modal.tsx | 197 ++++++++++++++++++ .../annotation-page/top-bar/top-bar.tsx | 4 +- .../model-runner-modal/model-runner-modal.tsx | 2 +- .../components/model-runner-modal/styles.scss | 4 - .../top-bar/statistics-modal.tsx | 108 ++++++++++ .../annotation-page/top-bar/top-bar.tsx | 17 ++ cvat-ui/src/reducers/annotation-reducer.ts | 74 +++++++ cvat-ui/src/reducers/interfaces.ts | 10 + cvat-ui/src/reducers/notifications-reducer.ts | 66 +++++- cvat-ui/src/styles.scss | 4 + 15 files changed, 609 insertions(+), 11 deletions(-) delete mode 100644 cvat-ui/src/components/annotation-page/statistics-modal.tsx create mode 100644 cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 5800d6de3edc..0bf029ba7cc9 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -56,13 +56,87 @@ export enum AnnotationActionTypes { ACTIVATE_OBJECT = 'ACTIVATE_OBJECT', SELECT_OBJECTS = 'SELECT_OBJECTS', REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS', - REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', // todo: add message + REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', PROPAGATE_OBJECT = 'PROPAGATE_OBJECT', PROPAGATE_OBJECT_SUCCESS = 'PROPAGATE_OBJECT_SUCCESS', - PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED', // todo: add message + PROPAGATE_OBJECT_FAILED = 'PROPAGATE_OBJECT_FAILED', CHANGE_PROPAGATE_FRAMES = 'CHANGE_PROPAGATE_FRAMES', + SWITCH_SHOWING_STATISTICS = 'SWITCH_SHOWING_STATISTICS', + COLLECT_STATISTICS = 'COLLECT_STATISTICS', + COLLECT_STATISTICS_SUCCESS = 'COLLECT_STATISTICS_SUCCESS', + COLLECT_STATISTICS_FAILED = 'COLLECT_STATISTICS_FAILED', + CHANGE_JOB_STATUS = 'CHANGE_JOB_STATUS', + CHANGE_JOB_STATUS_SUCCESS = 'CHANGE_JOB_STATUS_SUCCESS', + CHANGE_JOB_STATUS_FAILED = 'CHANGE_JOB_STATUS_FAILED', } +export function changeJobStatusAsync(jobInstance: any, status: string): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + const oldStatus = jobInstance.status; + try { + dispatch({ + type: AnnotationActionTypes.CHANGE_JOB_STATUS, + payload: {}, + }); + + // eslint-disable-next-line no-param-reassign + jobInstance.status = status; + await jobInstance.save(); + + dispatch({ + type: AnnotationActionTypes.CHANGE_JOB_STATUS_SUCCESS, + payload: {}, + }); + } catch (error) { + // eslint-disable-next-line no-param-reassign + jobInstance.status = oldStatus; + dispatch({ + type: AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function collectStatisticsAsync(sessionInstance: any): +ThunkAction, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + dispatch({ + type: AnnotationActionTypes.COLLECT_STATISTICS, + payload: {}, + }); + + const data = await sessionInstance.annotations.statistics(); + + dispatch({ + type: AnnotationActionTypes.COLLECT_STATISTICS_SUCCESS, + payload: { + data, + }, + }); + } catch (error) { + dispatch({ + type: AnnotationActionTypes.COLLECT_STATISTICS_FAILED, + payload: { + error, + }, + }); + } + }; +} + +export function showStatistics(visible: boolean): AnyAction { + return { + type: AnnotationActionTypes.SWITCH_SHOWING_STATISTICS, + payload: { + visible, + }, + }; +} export function propagateObjectAsync( sessionInstance: any, diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 842b1b0217c0..82c5bcff8353 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -8,6 +8,7 @@ import { } from 'antd'; 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'; interface Props { @@ -46,6 +47,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { + ); } diff --git a/cvat-ui/src/components/annotation-page/statistics-modal.tsx b/cvat-ui/src/components/annotation-page/statistics-modal.tsx deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index 6d5dc524459b..fc3de9677aaa 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -134,3 +134,47 @@ .cvat-workspace-selector { width: 150px; } + +.cvat-job-info-modal-window { + > div { + margin-top: 10px; + } + + > div:nth-child(1) { + > div { + > .ant-select, i { + margin-left: 10px; + } + } + } + + > div:nth-child(2) { + > div { + > span { + font-size: 20px; + } + } + } + + > div:nth-child(3) { + > div { + display: grid; + } + } + + > .cvat-job-info-bug-tracker { + > div { + display: grid; + } + } + + > .cvat-job-info-statistics { + > div { + width: 100%; + + > span { + font-size: 20px; + } + } + } +} \ No newline at end of file diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index 726e16ab2861..ae6cc54e7d36 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -12,14 +12,20 @@ import { FullscreenIcon, } from '../../../icons'; -function RightGroup(): JSX.Element { +interface Props { + showStatistics(): void; +} + +function RightGroup(props: Props): JSX.Element { + const { showStatistics } = props; + return ( - diff --git a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx new file mode 100644 index 000000000000..bd574c8d103a --- /dev/null +++ b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx @@ -0,0 +1,197 @@ +import React from 'react'; + +import { + Tooltip, + Select, + Table, + Modal, + Spin, + Icon, + Row, + Col, +} from 'antd'; + +import Text from 'antd/lib/typography/Text'; + +interface Props { + collecting: boolean; + data: any; + visible: boolean; + assignee: string | null; + startFrame: number; + stopFrame: number; + zOrder: boolean; + bugTracker: string; + jobStatus: string; + savingJobStatus: boolean; + closeStatistics(): void; + changeJobStatus(status: string): void; +} + +export default function StatisticsModalComponent(props: Props): JSX.Element { + const { + collecting, + data, + visible, + jobStatus, + assignee, + startFrame, + stopFrame, + zOrder, + bugTracker, + closeStatistics, + changeJobStatus, + savingJobStatus, + } = props; + + const baseProps = { + cancelButtonProps: { style: { display: 'none' } }, + okButtonProps: { style: { width: 100 } }, + onOk: closeStatistics, + width: 900, + visible, + closable: false, + }; + + if (collecting || !data) { + return ( + + + + ); + } + + const rows = Object.keys(data.label).map((key: string) => ({ + key, + label: key, + rectangle: `${data.label[key].rectangle.shape} / ${data.label[key].rectangle.track}`, + polygon: `${data.label[key].polygon.shape} / ${data.label[key].polygon.track}`, + polyline: `${data.label[key].polyline.shape} / ${data.label[key].polyline.track}`, + points: `${data.label[key].points.shape} / ${data.label[key].points.track}`, + tags: data.label[key].tags, + manually: data.label[key].manually, + interpolated: data.label[key].interpolated, + total: data.label[key].total, + })); + + rows.push({ + key: '___total', + label: 'Total', + rectangle: `${data.total.rectangle.shape} / ${data.total.rectangle.track}`, + polygon: `${data.total.polygon.shape} / ${data.total.polygon.track}`, + polyline: `${data.total.polyline.shape} / ${data.total.polyline.track}`, + points: `${data.total.points.shape} / ${data.total.points.track}`, + tags: data.total.tags, + manually: data.total.manually, + interpolated: data.total.interpolated, + total: data.total.total, + }); + + const makeShapesTracksTitle = (title: string): JSX.Element => ( + + {title} + + + ); + + const columns = [{ + title: Label , + dataIndex: 'label', + key: 'label', + }, { + title: makeShapesTracksTitle('Rectangle'), + dataIndex: 'rectangle', + key: 'rectangle', + }, { + title: makeShapesTracksTitle('Polygon'), + dataIndex: 'polygon', + key: 'polygon', + }, { + title: makeShapesTracksTitle('Polyline'), + dataIndex: 'polyline', + key: 'polyline', + }, { + title: makeShapesTracksTitle('Points'), + dataIndex: 'points', + key: 'points', + }, { + title: Tags , + dataIndex: 'tags', + key: 'tags', + }, { + title: Manually , + dataIndex: 'manually', + key: 'manually', + }, { + title: Interpolated , + dataIndex: 'interpolated', + key: 'interpolated', + }, { + title: Total , + dataIndex: 'total', + key: 'total', + }]; + + return ( + +
+ + + Job status + + {savingJobStatus && } + + + + + Overview + + + + + Assignee + {assignee || 'Nobody'} + + + Start frame + {startFrame} + + + Stop frame + {stopFrame} + + + Frames + {stopFrame - startFrame + 1} + + + Z-Order + {zOrder.toString()} + + + { !!bugTracker && ( + + + Bug tracker + {bugTracker} + + + )} + + + Annotations statistics + + + + + + ); +} diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index 4184ac25064e..399dc3f6ac09 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -20,6 +20,7 @@ interface Props { frameNumber: number; startFrame: number; stopFrame: number; + showStatistics(): void; onSwitchPlay(): void; onSaveAnnotation(): void; onPrevFrame(): void; @@ -49,6 +50,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element { frameNumber, startFrame, stopFrame, + showStatistics, onSwitchPlay, onSaveAnnotation, onPrevFrame, @@ -90,7 +92,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element { /> - + ); diff --git a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx index 3589e1456d85..af33b533eae8 100644 --- a/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx +++ b/cvat-ui/src/components/model-runner-modal/model-runner-modal.tsx @@ -289,7 +289,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent - + diff --git a/cvat-ui/src/components/model-runner-modal/styles.scss b/cvat-ui/src/components/model-runner-modal/styles.scss index f37f13bc4467..178551aa2361 100644 --- a/cvat-ui/src/components/model-runner-modal/styles.scss +++ b/cvat-ui/src/components/model-runner-modal/styles.scss @@ -4,10 +4,6 @@ margin-top: 10px; } -.cvat-run-model-dialog-info-icon { - color: $info-icon-color; -} - .cvat-run-model-dialog-remove-mapping-icon { color: $danger-icon-color; } diff --git a/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx index e69de29bb2d1..420e6a9a29db 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { CombinedState } from 'reducers/interfaces'; +import { + showStatistics, + changeJobStatusAsync, +} from 'actions/annotation-actions'; +import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal'; + +interface StateToProps { + visible: boolean; + collecting: boolean; + data: any; + jobInstance: any; + jobStatus: string; + savingJobStatus: boolean; +} + +interface DispatchToProps { + changeJobStatus(jobInstance: any, status: string): void; + closeStatistics(): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + statistics: { + visible, + collecting, + data, + }, + job: { + saving: savingJobStatus, + instance: { + status: jobStatus, + }, + instance: jobInstance, + }, + }, + } = state; + + return { + visible, + collecting, + data, + jobInstance, + jobStatus, + savingJobStatus, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + changeJobStatus(jobInstance: any, status: string): void { + dispatch(changeJobStatusAsync(jobInstance, status)); + }, + closeStatistics(): void { + dispatch(showStatistics(false)); + }, + }; +} + +type Props = StateToProps & DispatchToProps; + +class StatisticsModalContainer extends React.PureComponent { + private changeJobStatus = (status: string): void => { + const { + jobInstance, + changeJobStatus, + } = this.props; + + changeJobStatus(jobInstance, status); + }; + + public render(): JSX.Element { + const { + jobInstance, + visible, + collecting, + data, + closeStatistics, + jobStatus, + savingJobStatus, + } = this.props; + + return ( + + ); + } +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(StatisticsModalContainer); diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index fd039a0cd130..c063a73d88e3 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -7,6 +7,8 @@ import { changeFrameAsync, switchPlay, saveAnnotationsAsync, + collectStatisticsAsync, + showStatistics as showStatisticsAction, } from 'actions/annotation-actions'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar'; @@ -26,6 +28,7 @@ interface DispatchToProps { onChangeFrame(frame: number): void; onSwitchPlay(playing: boolean): void; onSaveAnnotation(sessionInstance: any): void; + showStatistics(sessionInstance: any): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -79,6 +82,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSaveAnnotation(sessionInstance: any): void { dispatch(saveAnnotationsAsync(sessionInstance)); }, + showStatistics(sessionInstance: any): void { + dispatch(collectStatisticsAsync(sessionInstance)); + dispatch(showStatisticsAction(true)); + }, }; } @@ -108,6 +115,15 @@ class AnnotationTopBarContainer extends React.PureComponent { } } + private showStatistics = (): void => { + const { + jobInstance, + showStatistics, + } = this.props; + + showStatistics(jobInstance); + }; + private onSwitchPlay = (): void => { const { frameNumber, @@ -288,6 +304,7 @@ class AnnotationTopBarContainer extends React.PureComponent { return ( { }, }; } + case AnnotationActionTypes.SWITCH_SHOWING_STATISTICS: { + const { visible } = action.payload; + + return { + ...state, + statistics: { + ...state.statistics, + visible, + }, + }; + } + case AnnotationActionTypes.COLLECT_STATISTICS: { + return { + ...state, + statistics: { + ...state.statistics, + collecting: true, + }, + }; + } + case AnnotationActionTypes.COLLECT_STATISTICS_SUCCESS: { + const { data } = action.payload; + return { + ...state, + statistics: { + ...state.statistics, + collecting: false, + data, + }, + }; + } + case AnnotationActionTypes.COLLECT_STATISTICS_FAILED: { + return { + ...state, + statistics: { + ...state.statistics, + collecting: false, + data: null, + }, + }; + } + case AnnotationActionTypes.CHANGE_JOB_STATUS: { + return { + ...state, + job: { + ...state.job, + saving: true, + }, + }; + } + case AnnotationActionTypes.CHANGE_JOB_STATUS_SUCCESS: { + return { + ...state, + job: { + ...state.job, + saving: false, + }, + }; + } + case AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED: { + return { + ...state, + job: { + ...state.job, + saving: false, + }, + }; + } case AnnotationActionTypes.RESET_CANVAS: { return { ...state, diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index f4f230c22c41..686686eeb5f4 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -211,6 +211,10 @@ export interface NotificationsState { merging: null | ErrorState; grouping: null | ErrorState; splitting: null | ErrorState; + removing: null | ErrorState; + propagating: null | ErrorState; + collectingStatistics: null | ErrorState; + savingJob: null | ErrorState; }; [index: string]: any; @@ -271,6 +275,7 @@ export interface AnnotationState { instance: any | null | undefined; attributes: Record; fetching: boolean; + saving: boolean; }; player: { frame: { @@ -300,6 +305,11 @@ export interface AnnotationState { objectState: any | null; frames: number; }; + statistics: { + collecting: boolean; + visible: boolean; + data: any; + }; colors: any[]; sidebarCollapsed: boolean; appearanceCollapsed: boolean; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index e02e8220c8cb..e843e1a537ac 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -59,6 +59,10 @@ const defaultState: NotificationsState = { merging: null, grouping: null, splitting: null, + removing: null, + propagating: null, + collectingStatistics: null, + savingJob: null, }, }, messages: { @@ -564,7 +568,67 @@ export default function (state = defaultState, action: AnyAction): Notifications annotation: { ...state.errors.annotation, splitting: { - message: 'Could not split a track', + message: 'Could not split the track', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AnnotationActionTypes.REMOVE_OBJECT_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + removing: { + message: 'Could not remove the object', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AnnotationActionTypes.PROPAGATE_OBJECT_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + propagating: { + message: 'Could not propagate the object', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AnnotationActionTypes.COLLECT_STATISTICS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + collectingStatistics: { + message: 'Could not collect annotations statistics', + reason: action.payload.error.toString(), + }, + }, + }, + }; + } + case AnnotationActionTypes.CHANGE_JOB_STATUS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + annotation: { + ...state.errors.annotation, + savingJob: { + message: 'Could not save the job on the server', reason: action.payload.error.toString(), }, }, diff --git a/cvat-ui/src/styles.scss b/cvat-ui/src/styles.scss index 2ce92947f0f2..222b2f2bf55b 100644 --- a/cvat-ui/src/styles.scss +++ b/cvat-ui/src/styles.scss @@ -35,6 +35,10 @@ hr { } } +.cvat-info-circle-icon { + color: $info-icon-color; +} + #root { width: 100%; height: 100%; From 35f6ca4a9290fe6e72844e163953807905b058f6 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 30 Jan 2020 19:27:50 +0300 Subject: [PATCH 22/25] Minor fix with shapes counting --- cvat-core/src/annotations-collection.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js index 8c42519d1aca..2df189f9a1a0 100644 --- a/cvat-core/src/annotations-collection.js +++ b/cvat-core/src/annotations-collection.js @@ -529,6 +529,10 @@ } for (const object of Object.values(this.objects)) { + if (object.removed) { + continue; + } + let objectType = null; if (object instanceof Shape) { objectType = 'shape'; From 5dc62f943c98f760f7ab0c9756e081d4e4a87615 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 30 Jan 2020 19:57:02 +0300 Subject: [PATCH 23/25] Couple of fixes to improve visibility --- .../standard-workspace/canvas-wrapper.tsx | 12 ++++++++++- .../components/annotation-page/styles.scss | 8 ++++++-- .../top-bar/statistics-modal.tsx | 20 ++++++++++++------- .../objects-side-bar/objects-side-bar.tsx | 10 ++++++++-- .../top-bar/statistics-modal.tsx | 2 +- 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index 8188af3fae90..696ec2cd020a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -145,6 +145,10 @@ export default class CanvasWrapperComponent extends React.PureComponent { this.activateOnCanvas(); } + public componentWillUnmount(): void { + window.removeEventListener('resize', this.fitCanvas); + } + private onShapeDrawn(event: any): void { const { jobInstance, @@ -238,6 +242,11 @@ export default class CanvasWrapperComponent extends React.PureComponent { onSplitAnnotations(jobInstance, frame, state); } + private fitCanvas = (): void => { + const { canvasInstance } = this.props; + canvasInstance.fitCanvas(); + }; + private activateOnCanvas(): void { const { activatedStateID, @@ -317,7 +326,8 @@ export default class CanvasWrapperComponent extends React.PureComponent { } = this.props; // Size - canvasInstance.fitCanvas(); + window.addEventListener('resize', this.fitCanvas); + this.fitCanvas(); // Grid const gridElement = window.document.getElementById('cvat_canvas_grid'); diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index fc3de9677aaa..3d77640a070f 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -170,11 +170,15 @@ > .cvat-job-info-statistics { > div { - width: 100%; - > span { font-size: 20px; } + + .ant-table-thead { + > tr > th { + padding: 5px 5px; + } + } } } } \ No newline at end of file diff --git a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx index bd574c8d103a..1d8301ae07d0 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/statistics-modal.tsx @@ -17,7 +17,7 @@ interface Props { collecting: boolean; data: any; visible: boolean; - assignee: string | null; + assignee: string; startFrame: number; stopFrame: number; zOrder: boolean; @@ -48,7 +48,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { cancelButtonProps: { style: { display: 'none' } }, okButtonProps: { style: { width: 100 } }, onOk: closeStatistics, - width: 900, + width: 1000, visible, closable: false, }; @@ -158,7 +158,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { Assignee - {assignee || 'Nobody'} + {assignee}Start frame @@ -181,14 +181,20 @@ export default function StatisticsModalComponent(props: Props): JSX.Element { Bug tracker - {bugTracker} + {bugTracker} )} - - + + Annotations statistics -
+
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index dd4277cd7f2f..f0c1bb15379b 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -143,8 +143,14 @@ class ObjectsSideBarContainer extends React.PureComponent { } private alignTabHeight = (): void => { - const { updateTabContentHeight } = this.props; - updateTabContentHeight(); + const { + sidebarCollapsed, + updateTabContentHeight, + } = this.props; + + if (!sidebarCollapsed) { + updateTabContentHeight(); + } }; private changeShapesColorBy = (event: RadioChangeEvent): void => { diff --git a/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx b/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx index 420e6a9a29db..0125812bdf1c 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/statistics-modal.tsx @@ -93,7 +93,7 @@ class StatisticsModalContainer extends React.PureComponent { zOrder={jobInstance.task.zOrder} startFrame={jobInstance.startFrame} stopFrame={jobInstance.stopFrame} - assignee={jobInstance.assignee} + assignee={jobInstance.assignee ? jobInstance.assignee.username : 'Nobody'} savingJobStatus={savingJobStatus} closeStatistics={closeStatistics} changeJobStatus={this.changeJobStatus} From 5cc20c4e8453e225907a055c2ce75e96ae967048 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 31 Jan 2020 15:50:47 +0300 Subject: [PATCH 24/25] Added fullscreen --- .../annotation-page/top-bar/right-group.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index ae6cc54e7d36..2add0490e09f 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -21,7 +21,19 @@ function RightGroup(props: Props): JSX.Element { return ( - From d3c843d639757080f214c8d604297e1e09a45eca Mon Sep 17 00:00:00 2001 From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com> Date: Mon, 3 Feb 2020 10:46:11 +0300 Subject: [PATCH 25/25] SVG Canvas -> HTML Canvas frame (#1113) * SVG Frame -> HTML Canvas frame --- cvat-canvas/src/typescript/canvasModel.ts | 12 ++-- cvat-canvas/src/typescript/canvasView.ts | 71 ++++++++++------------- cvat-canvas/src/typescript/drawHandler.ts | 36 ++++-------- cvat-canvas/src/typescript/editHandler.ts | 40 +++++-------- cvat-canvas/src/typescript/shared.ts | 10 ---- cvat-core/src/frames.js | 9 ++- 6 files changed, 71 insertions(+), 107 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index cc3be054a0b2..58e22952d253 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -110,7 +110,7 @@ export enum Mode { } export interface CanvasModel { - readonly image: string; + readonly image: HTMLImageElement | null; readonly objects: any[]; readonly gridSize: Size; readonly focusData: FocusData; @@ -151,7 +151,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { activeElement: ActiveElement; angle: number; canvasSize: Size; - image: string; + image: HTMLImageElement | null; imageID: number | null; imageOffset: number; imageSize: Size; @@ -183,7 +183,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { height: 0, width: 0, }, - image: '', + image: null, imageID: null, imageOffset: 0, imageSize: { @@ -300,10 +300,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.imageID = frameData.number; frameData.data( (): void => { - this.data.image = ''; + this.data.image = null; this.notify(UpdateReasons.IMAGE_CHANGED); }, - ).then((data: string): void => { + ).then((data: HTMLImageElement): void => { if (frameData.number !== this.data.imageID) { // already another image return; @@ -514,7 +514,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { )); } - public get image(): string { + public get image(): HTMLImageElement | null { return this.data.image; } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 8fd1d01f92a5..98efb21e993c 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -21,7 +21,6 @@ import consts from './consts'; import { translateToSVG, translateFromSVG, - translateBetweenSVG, pointsToArray, displayShapeSize, ShapeSizeElement, @@ -48,7 +47,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private loadingAnimation: SVGSVGElement; private text: SVGSVGElement; private adoptedText: SVG.Container; - private background: SVGSVGElement; + private background: HTMLCanvasElement; private grid: SVGSVGElement; private content: SVGSVGElement; private adoptedContent: SVG.Container; @@ -220,13 +219,14 @@ export class CanvasViewImpl implements CanvasView, Listener { private onFindObject(e: MouseEvent): void { if (e.which === 1 || e.which === 0) { - const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]); + const { offset } = this.controller.geometry; + const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]); const event: CustomEvent = new CustomEvent('canvas.find', { bubbles: false, cancelable: true, detail: { - x, - y, + x: x - offset, + y: y - offset, states: this.controller.objects, }, }); @@ -365,26 +365,9 @@ export class CanvasViewImpl implements CanvasView, Listener { } private setupObjects(states: any[]): void { - const backgroundMatrix = this.background.getScreenCTM(); - const contentMatrix = (this.content.getScreenCTM() as DOMMatrix).inverse(); - - const translate = (points: number[]): number[] => { - if (backgroundMatrix && contentMatrix) { - const matrix = (contentMatrix as DOMMatrix).multiply(backgroundMatrix); - return points.reduce((result: number[], _: number, idx: number): number[] => { - if (idx % 2) { - let p = (this.background as SVGSVGElement).createSVGPoint(); - p.x = points[idx - 1]; - p.y = points[idx]; - p = p.matrixTransform(matrix); - result.push(p.x, p.y); - } - return result; - }, []); - } - - return points; - }; + const { offset } = this.controller.geometry; + const translate = (points: number[]): number[] => points + .map((coord: number): number => coord + offset); const created = []; const updated = []; @@ -524,7 +507,8 @@ export class CanvasViewImpl implements CanvasView, Listener { .createElementNS('http://www.w3.org/2000/svg', 'svg'); this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container); - this.background = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.background = window.document.createElement('canvas'); + // window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path'); @@ -593,12 +577,10 @@ export class CanvasViewImpl implements CanvasView, Listener { this.onDrawDone.bind(this), this.adoptedContent, this.adoptedText, - this.background, ); this.editHandler = new EditHandlerImpl( this.onEditDone.bind(this), this.adoptedContent, - this.background, ); this.mergeHandler = new MergeHandlerImpl( this.onMergeDone.bind(this), @@ -646,8 +628,9 @@ export class CanvasViewImpl implements CanvasView, Listener { }); this.content.addEventListener('wheel', (event): void => { - const point = translateToSVG(self.background, [event.clientX, event.clientY]); - self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1); + const { offset } = this.controller.geometry; + const point = translateToSVG(this.content, [event.clientX, event.clientY]); + self.controller.zoom(point[0] - offset, point[1] - offset, event.deltaY > 0 ? -1 : 1); this.canvas.dispatchEvent(new CustomEvent('canvas.zoom', { bubbles: false, cancelable: true, @@ -661,13 +644,14 @@ export class CanvasViewImpl implements CanvasView, Listener { if (this.mode !== Mode.IDLE) return; if (e.ctrlKey || e.shiftKey) return; - const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]); + const { offset } = this.controller.geometry; + const [x, y] = translateToSVG(this.content, [e.clientX, e.clientY]); const event: CustomEvent = new CustomEvent('canvas.moved', { bubbles: false, cancelable: true, detail: { - x, - y, + x: x - offset, + y: y - offset, states: this.controller.objects, }, }); @@ -682,11 +666,17 @@ export class CanvasViewImpl implements CanvasView, Listener { public notify(model: CanvasModel & Master, reason: UpdateReasons): void { this.geometry = this.controller.geometry; if (reason === UpdateReasons.IMAGE_CHANGED) { - if (!model.image.length) { + const { image } = model; + if (!image) { this.loadingAnimation.classList.remove('cvat_canvas_hidden'); } else { this.loadingAnimation.classList.add('cvat_canvas_hidden'); - this.background.style.backgroundImage = `url("${model.image}")`; + const ctx = this.background.getContext('2d'); + this.background.setAttribute('width', `${image.width}px`); + this.background.setAttribute('height', `${image.height}px`); + if (ctx) { + ctx.drawImage(image, 0, 0); + } this.moveCanvas(); this.resizeCanvas(); this.transformCanvas(); @@ -1058,14 +1048,15 @@ export class CanvasViewImpl implements CanvasView, Listener { const p1 = e.detail.handler.startPoints.point; const p2 = e.detail.p; const delta = 1; + const { offset } = this.controller.geometry; if (Math.sqrt(((p1.x - p2.x) ** 2) + ((p1.y - p2.y) ** 2)) >= delta) { const points = pointsToArray( shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + `${shape.attr('x') + shape.attr('width')},` + `${shape.attr('y') + shape.attr('height')}`, - ); + ).map((x: number): number => x - offset); - this.onEditDone(state, translateBetweenSVG(this.content, this.background, points)); + this.onEditDone(state, points); } }); @@ -1105,13 +1096,15 @@ export class CanvasViewImpl implements CanvasView, Listener { this.mode = Mode.IDLE; if (resized) { + const { offset } = this.controller.geometry; + const points = pointsToArray( shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} ` + `${shape.attr('x') + shape.attr('width')},` + `${shape.attr('y') + shape.attr('height')}`, - ); + ).map((x: number): number => x - offset); - this.onEditDone(state, translateBetweenSVG(this.content, this.background, points)); + this.onEditDone(state, points); } }); diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index da0798faae7e..ed27cf7c3700 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -15,7 +15,6 @@ import { import { translateToSVG, - translateBetweenSVG, displayShapeSize, ShapeSizeElement, pointsToString, @@ -35,7 +34,6 @@ export class DrawHandlerImpl implements DrawHandler { private onDrawDone: (data: object, continueDraw?: boolean) => void; private canvas: SVG.Container; private text: SVG.Container; - private background: SVGSVGElement; private crosshair: { x: SVG.Line; y: SVG.Line; @@ -52,12 +50,10 @@ export class DrawHandlerImpl implements DrawHandler { private getFinalRectCoordinates(bbox: BBox): number[] { const frameWidth = this.geometry.image.width; const frameHeight = this.geometry.image.height; + const { offset } = this.geometry; - let [xtl, ytl, xbr, ybr] = translateBetweenSVG( - this.canvas.node as any as SVGSVGElement, - this.background, - [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height], - ); + let [xtl, ytl, xbr, ybr] = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height] + .map((coord: number): number => coord - offset); xtl = Math.min(Math.max(xtl, 0), frameWidth); xbr = Math.min(Math.max(xbr, 0), frameWidth); @@ -71,12 +67,8 @@ export class DrawHandlerImpl implements DrawHandler { points: number[]; box: Box; } { - const points = translateBetweenSVG( - this.canvas.node as any as SVGSVGElement, - this.background, - targetPoints, - ); - + const { offset } = this.geometry; + const points = targetPoints.map((coord: number): number => coord - offset); const box = { xtl: Number.MAX_SAFE_INTEGER, ytl: Number.MAX_SAFE_INTEGER, @@ -474,12 +466,10 @@ export class DrawHandlerImpl implements DrawHandler { private startDraw(): void { // TODO: Use enums after typification cvat-core if (this.drawData.initialState) { + const { offset } = this.geometry; if (this.drawData.shapeType === 'rectangle') { - const [xtl, ytl, xbr, ybr] = translateBetweenSVG( - this.background, - this.canvas.node as any as SVGSVGElement, - this.drawData.initialState.points, - ); + const [xtl, ytl, xbr, ybr] = this.drawData.initialState.points + .map((coord: number): number => coord + offset); this.pasteBox({ x: xtl, @@ -488,12 +478,8 @@ export class DrawHandlerImpl implements DrawHandler { height: ybr - ytl, }); } else { - const points = translateBetweenSVG( - this.background, - this.canvas.node as any as SVGSVGElement, - this.drawData.initialState.points, - ); - + const points = this.drawData.initialState.points + .map((coord: number): number => coord + offset); const stringifiedPoints = pointsToString(points); if (this.drawData.shapeType === 'polygon') { @@ -521,12 +507,10 @@ export class DrawHandlerImpl implements DrawHandler { onDrawDone: (data: object, continueDraw?: boolean) => void, canvas: SVG.Container, text: SVG.Container, - background: SVGSVGElement, ) { this.onDrawDone = onDrawDone; this.canvas = canvas; this.text = text; - this.background = background; this.drawData = null; this.geometry = null; this.crosshair = null; diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index bc0dc82309a4..8daf9055f16d 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -9,7 +9,6 @@ import 'svg.select.js'; import consts from './consts'; import { translateFromSVG, - translateBetweenSVG, pointsToArray, } from './shared'; import { @@ -27,7 +26,6 @@ export class EditHandlerImpl implements EditHandler { private onEditDone: (state: any, points: number[]) => void; private geometry: Geometry; private canvas: SVG.Container; - private background: SVGSVGElement; private editData: EditData; private editedShape: SVG.Shape; private editLine: SVG.PolyLine; @@ -94,21 +92,19 @@ export class EditHandlerImpl implements EditHandler { } } - private stopEdit(e: MouseEvent): void { - function selectPolygon(shape: SVG.Polygon): void { - const points = translateBetweenSVG( - this.canvas.node as any as SVGSVGElement, - this.background, - pointsToArray(shape.attr('points')), - ); - - const { state } = this.editData; - this.edit({ - enabled: false, - }); - this.onEditDone(state, points); - } + private selectPolygon(shape: SVG.Polygon): void { + const { offset } = this.geometry; + const points = pointsToArray(shape.attr('points')) + .map((coord: number): number => coord - offset); + const { state } = this.editData; + this.edit({ + enabled: false, + }); + this.onEditDone(state, points); + } + + private stopEdit(e: MouseEvent): void { if (!this.editLine) { return; } @@ -154,7 +150,7 @@ export class EditHandlerImpl implements EditHandler { } for (const clone of this.clones) { - clone.on('click', selectPolygon.bind(this, clone)); + clone.on('click', (): void => this.selectPolygon(clone)); clone.on('mouseenter', (): void => { clone.addClass('cvat_canvas_shape_splitting'); }).on('mouseleave', (): void => { @@ -170,6 +166,7 @@ export class EditHandlerImpl implements EditHandler { } let points = null; + const { offset } = this.geometry; if (this.editData.state.shapeType === 'polyline') { if (start !== this.editData.pointID) { linePoints.reverse(); @@ -181,11 +178,8 @@ export class EditHandlerImpl implements EditHandler { points = oldPoints.concat(linePoints.slice(0, -1)); } - points = translateBetweenSVG( - this.canvas.node as any as SVGSVGElement, - this.background, - pointsToArray(points.join(' ')), - ); + points = pointsToArray(points.join(' ')) + .map((coord: number): number => coord - offset); const { state } = this.editData; this.edit({ @@ -284,11 +278,9 @@ export class EditHandlerImpl implements EditHandler { public constructor( onEditDone: (state: any, points: number[]) => void, canvas: SVG.Container, - background: SVGSVGElement, ) { this.onEditDone = onEditDone; this.canvas = canvas; - this.background = background; this.editData = null; this.editedShape = null; this.editLine = null; diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index d98eaaad7393..f89ed7eb96d2 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -58,16 +58,6 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] { return output; } -// Translate point array from the first canvas coordinate system -// to another -export function translateBetweenSVG( - from: SVGSVGElement, - to: SVGSVGElement, - points: number[], -): number[] { - return translateToSVG(to, translateFromSVG(from, points)); -} - export function pointsToString(points: number[]): string { return points.reduce((acc, val, idx): string => { if (idx % 2) { diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index 1ab2eaf21a94..4141de07bfc8 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -100,9 +100,14 @@ } else if (isBrowser) { const reader = new FileReader(); reader.onload = () => { - frameCache[this.tid][this.number] = reader.result; - resolve(frameCache[this.tid][this.number]); + const image = new Image(frame.width, frame.height); + image.onload = () => { + frameCache[this.tid][this.number] = image; + resolve(frameCache[this.tid][this.number]); + }; + image.src = reader.result; }; + reader.readAsDataURL(frame); } }