React UI: Batch of fixes (#1211)

* Disabled tracks for polyshapes in UI

* RectDrawingMethod enum pushed to cvat-canvas, fixed some code issues

* Optional arguments

* Draw a text for locked shapes, some fixes with not keyframe shapes

* Fixed zooming & batch grouping

* Reset zoom for tasks with images

* Fixed putting shapes out of canvas

* Fixed grid opacity, little refactoring of componentDidUpdate in canvas-wrapper component

* Fixed corner cases for drawing

* Fixed putting shapes out of canvas

* Improved drawing

* Removed extra event handler
main
Boris Sekachev 6 years ago committed by GitHub
parent 5659a0a0e1
commit 5dc52f94a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -37,10 +37,15 @@ Canvas itself handles:
CLOCKWISE90,
}
enum RectDrawingMethod {
CLASSIC = 'By 2 points',
EXTREME_POINTS = 'By 4 points'
}
interface DrawData {
enabled: boolean;
shapeType?: string;
rectDrawingMethod?: string;
rectDrawingMethod?: RectDrawingMethod;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
@ -147,6 +152,7 @@ Standard JS events are used.
enabled: true,
shapeType: 'rectangle',
crosshair: true,
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
});
```

@ -10,6 +10,7 @@ import {
GroupData,
CanvasModel,
CanvasModelImpl,
RectDrawingMethod,
} from './canvasModel';
import {
@ -141,4 +142,5 @@ export {
CanvasImpl as Canvas,
Rotation,
CanvasVersion,
RectDrawingMethod,
};

@ -4,7 +4,6 @@
import { MasterImpl } from './master';
export interface Size {
width: number;
height: number;
@ -36,10 +35,15 @@ export interface ActiveElement {
attributeID: number | null;
}
export enum RectDrawingMethod {
CLASSIC = 'By 2 points',
EXTREME_POINTS = 'By 4 points'
}
export interface DrawData {
enabled: boolean;
shapeType?: string;
rectDrawingMethod?: string;
rectDrawingMethod?: RectDrawingMethod;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;

@ -106,11 +106,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.geometry,
);
} else {
this.mode = Mode.IDLE;
this.controller.draw({
enabled: false,
});
this.mode = Mode.IDLE;
}
}
@ -551,7 +550,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.grid.setAttribute('version', '2');
this.gridPath.setAttribute('d', 'M 1000 0 L 0 0 0 1000');
this.gridPath.setAttribute('fill', 'none');
this.gridPath.setAttribute('stroke-width', '1.5');
this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH}`);
this.gridPath.setAttribute('opacity', 'inherit');
this.gridPattern.setAttribute('id', 'cvat_canvas_grid_pattern');
this.gridPattern.setAttribute('width', '100');
this.gridPattern.setAttribute('height', '100');
@ -626,7 +626,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.addEventListener('mousedown', (event): void => {
if ([1, 2].includes(event.which)) {
self.controller.enableDrag(event.clientX, event.clientY);
if (![Mode.ZOOM_CANVAS, Mode.GROUP].includes(this.mode) || event.which === 2) {
self.controller.enableDrag(event.clientX, event.clientY);
}
event.preventDefault();
}
});
@ -760,13 +762,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
} else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData;
if (data.enabled) {
if (data.enabled && this.mode === Mode.IDLE) {
this.canvas.style.cursor = 'crosshair';
this.mode = Mode.DRAW;
this.drawHandler.draw(data, this.geometry);
} else {
this.canvas.style.cursor = '';
if (this.mode !== Mode.IDLE) {
this.drawHandler.draw(data, this.geometry);
}
}
this.drawHandler.draw(data, this.geometry);
} else if (reason === UpdateReasons.MERGE) {
const data: MergeData = this.controller.mergeData;
if (data.enabled) {
@ -1060,24 +1065,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
if (!state) {
return;
}
if (state.shapeType === 'points') {
if (state && state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('pointer-events', state.lock ? 'none' : '');
}
if (state.hidden || state.lock) {
if (!state || state.hidden || state.outside) {
return;
}
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];
shape.addClass('cvat_canvas_shape_activated');
let text = this.svgTexts[clientID];
// Draw text if it's hidden by default
if (!text) {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
@ -1087,7 +1086,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
);
}
const self = this;
if (state.lock) {
return;
}
shape.addClass('cvat_canvas_shape_activated');
if (state.shapeType === 'points') {
this.content.append(this.svgShapes[clientID]
.remember('_selectHandler').nested.node);
@ -1103,7 +1106,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}).on('dragend', (e: CustomEvent): void => {
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
this.updateTextPosition(
text,
shape,
);
@ -1122,6 +1125,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
+ `${shape.attr('y') + shape.attr('height')}`,
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points;
this.onEditDone(state, points);
}
});
@ -1153,7 +1157,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
this.updateTextPosition(
text,
shape,
);
@ -1170,6 +1174,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
+ `${shape.attr('y') + shape.attr('height')}`,
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points;
this.onEditDone(state, points);
}
});

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
const BASE_STROKE_WIDTH = 1.75;
const BASE_GRID_WIDTH = 1;
const BASE_GRID_WIDTH = 2;
const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9;

@ -10,6 +10,7 @@ import './svg.patch';
import {
DrawData,
Geometry,
RectDrawingMethod,
} from './canvasModel';
import {
@ -43,6 +44,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 initialized: boolean;
private pointsGroup: SVG.G | null;
private shapeSizeElement: ShapeSizeElement;
@ -113,6 +115,12 @@ export class DrawHandlerImpl implements DrawHandler {
}
private release(): void {
if (!this.initialized) {
// prevents recursive calls
return;
}
this.initialized = false;
this.canvas.off('mousedown.draw');
this.canvas.off('mouseup.draw');
this.canvas.off('mousemove.draw');
@ -123,18 +131,21 @@ export class DrawHandlerImpl implements DrawHandler {
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
if (!this.drawData.initialState) {
this.drawInstance.off('drawdone');
this.drawInstance.off('drawstop');
this.drawInstance.draw('stop');
// Draw plugin in some cases isn't activated
// For example when draw from initialState
// Or when no drawn points, but we call cancel() drawing
// We check if it is activated with remember function
if (this.drawInstance.remember('_paintHandler')) {
if (this.drawData.shapeType !== 'rectangle') {
// Check for unsaved drawn shapes
this.drawInstance.draw('done');
}
this.drawInstance.remove();
this.drawInstance = null;
// Clear drawing
this.drawInstance.draw('stop');
}
this.drawInstance.off();
this.drawInstance.remove();
this.drawInstance = null;
if (this.shapeSizeElement) {
this.shapeSizeElement.rm();
@ -152,45 +163,19 @@ export class DrawHandlerImpl implements DrawHandler {
}
}
private closeDrawing(): void {
if (this.drawInstance) {
// Draw plugin isn't activated when draw from initialState
// So, we don't need to use any draw events
if (!this.drawData.initialState) {
const { drawInstance } = this;
this.drawInstance = null;
if (this.drawData.shapeType === 'rectangle'
&& this.drawData.rectDrawingMethod !== 'by_four_points') {
drawInstance.draw('cancel');
} else {
drawInstance.draw('done');
}
this.drawInstance = drawInstance;
this.release();
} else {
this.release();
this.onDrawDone(null);
}
// here is a cycle
// onDrawDone => controller => model => view => closeDrawing
// one call of closeDrawing is unuseful, but it's okey
}
}
private drawBox(): void {
this.drawInstance = this.canvas.rect();
this.drawInstance.on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType,
points: [xtl, ytl, xbr, ybr],
});
} else {
this.onDrawDone(null);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
@ -206,10 +191,10 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': 0,
opacity: 0,
}).on('drawstart', () => {
}).on('drawstart', (): void => {
// init numberOfPoints as one on drawstart
numberOfPoints = 1;
}).on('drawpoint', (e: CustomEvent) => {
}).on('drawpoint', (e: CustomEvent): void => {
// increase numberOfPoints by one on drawpoint
numberOfPoints += 1;
@ -217,23 +202,20 @@ export class DrawHandlerImpl implements DrawHandler {
if (numberOfPoints === 4) {
const bbox = (e.target as SVGPolylineElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
const { shapeType } = this.drawData;
this.cancel();
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType,
points: [xtl, ytl, xbr, ybr],
});
} else {
this.onDrawDone(null);
}
}
}).on('undopoint', () => {
}).on('undopoint', (): void => {
if (numberOfPoints > 0) {
numberOfPoints -= 1;
}
}).off('drawdone').on('drawdone', () => {
// close drawing mode without drawing rect
this.onDrawDone(null);
});
this.drawPolyshape();
@ -308,34 +290,31 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const {
points,
box,
} = this.getFinalPolyshapeCoordinates(targetPoints);
const { points, box } = this.getFinalPolyshapeCoordinates(targetPoints);
const { shapeType } = this.drawData;
this.cancel();
if (this.drawData.shapeType === 'polygon'
if (shapeType === 'polygon'
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType,
points,
});
} else if (this.drawData.shapeType === 'polyline'
} else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
&& points.length >= 2 * 2) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType,
points,
});
} else if (this.drawData.shapeType === 'points'
} else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
shapeType: this.drawData.shapeType,
shapeType,
points,
});
} else {
this.onDrawDone(null);
}
});
}
@ -578,7 +557,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.setupPasteEvents();
} else {
if (this.drawData.shapeType === 'rectangle') {
if (this.drawData.rectDrawingMethod === 'by_four_points') {
if (this.drawData.rectDrawingMethod === RectDrawingMethod.EXTREME_POINTS) {
// draw box by extreme clicking
this.drawBoxBy4Points();
} else {
@ -596,6 +575,8 @@ export class DrawHandlerImpl implements DrawHandler {
}
this.setupDrawEvents();
}
this.initialized = true;
}
public constructor(
@ -606,6 +587,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.text = text;
this.initialized = false;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
@ -686,7 +668,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.initDrawing();
this.startDraw();
} else {
this.closeDrawing();
this.cancel();
this.drawData = drawData;
}
}
@ -694,8 +676,5 @@ export class DrawHandlerImpl implements DrawHandler {
public cancel(): void {
this.release();
this.onDrawDone(null);
// here is a cycle
// onDrawDone => controller => model => view => closeDrawing
// one call of closeDrawing is unuseful, but it's okey
}
}

@ -20,6 +20,7 @@ import {
} from 'reducers/interfaces';
import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store';
const cvat = getCore();
@ -807,7 +808,7 @@ export function drawShape(
labelID: number,
objectType: ObjectType,
points?: number,
rectDrawingMethod?: string,
rectDrawingMethod?: RectDrawingMethod,
): AnyAction {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (shapeType === ShapeType.POLYGON) {

@ -100,10 +100,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
colorBy,
selectedOpacity,
blackBorders,
grid,
gridSize,
gridColor,
gridOpacity,
frameData,
annotations,
canvasInstance,
@ -122,34 +118,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (prevProps.grid !== grid) {
const gridElement = window.document.getElementById('cvat_canvas_grid');
if (gridElement) {
gridElement.style.display = grid ? 'block' : 'none';
}
}
if (prevProps.gridSize !== gridSize) {
canvasInstance.grid(gridSize, gridSize);
}
if (prevProps.gridColor !== gridColor) {
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
}
}
if (prevProps.gridOpacity !== gridOpacity) {
const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern');
if (gridPattern) {
gridPattern.style.opacity = `${gridOpacity / 100}`;
}
}
if (prevProps.activatedStateID !== null
&& prevProps.activatedStateID !== activatedStateID) {
canvasInstance.activate(null);
}
if (activatedStateID) {
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity: opacity / 100 });
@ -160,17 +134,17 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateCanvas();
}
if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders
|| prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) {
this.updateShapesView();
}
if (prevProps.frame !== frameData.number && resetZoom) {
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
}, { once: true });
}
if (prevProps.opacity !== opacity || prevProps.blackBorders !== blackBorders
|| prevProps.selectedOpacity !== selectedOpacity || prevProps.colorBy !== colorBy) {
this.updateShapesView();
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}

@ -16,16 +16,14 @@ import {
import { RadioChangeEvent } from 'antd/lib/radio';
import Text from 'antd/lib/typography/Text';
import {
ShapeType,
RectDrawingMethod,
} from 'reducers/interfaces';
import { RectDrawingMethod } from 'cvat-canvas';
import { ShapeType } from 'reducers/interfaces';
interface Props {
shapeType: ShapeType;
rectDrawingMethod: RectDrawingMethod;
labels: any[];
minimumPoints: number;
rectDrawingMethod?: RectDrawingMethod;
numberOfPoints?: number;
selectedLabeID: number;
onChangeLabel(value: string): void;
@ -42,6 +40,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
minimumPoints,
selectedLabeID,
numberOfPoints,
rectDrawingMethod,
onDrawTrack,
onDrawShape,
onChangeLabel,
@ -92,17 +91,17 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
<Col>
<Radio.Group
style={{ display: 'flex' }}
defaultValue={RectDrawingMethod.BY_TWO_POINTS}
value={rectDrawingMethod}
onChange={onChangeRectDrawingMethod}
>
<Radio
value={RectDrawingMethod.BY_TWO_POINTS}
value={RectDrawingMethod.CLASSIC}
style={{ width: 'auto' }}
>
By 2 Points
</Radio>
<Radio
value={RectDrawingMethod.BY_FOUR_POINTS}
value={RectDrawingMethod.EXTREME_POINTS}
style={{ width: 'auto' }}
>
By 4 Points
@ -139,6 +138,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
<Col span={12}>
<Button
onClick={onDrawTrack}
disabled={shapeType !== ShapeType.RECTANGLE}
>
Track
</Button>

@ -10,13 +10,12 @@ import {
CombinedState,
ShapeType,
ObjectType,
RectDrawingMethod,
} from 'reducers/interfaces';
import {
drawShape,
} from 'actions/annotation-actions';
import { Canvas } from 'cvat-canvas';
import { Canvas, RectDrawingMethod } from 'cvat-canvas';
import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface OwnProps {
@ -29,7 +28,7 @@ interface DispatchToProps {
labelID: number,
objectType: ObjectType,
points?: number,
rectDrawingMethod?: string,
rectDrawingMethod?: RectDrawingMethod,
): void;
}
@ -46,7 +45,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
labelID: number,
objectType: ObjectType,
points?: number,
rectDrawingMethod?: string,
rectDrawingMethod?: RectDrawingMethod,
): void {
dispatch(drawShape(shapeType, labelID, objectType, points, rectDrawingMethod));
},
@ -75,7 +74,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
type Props = StateToProps & DispatchToProps;
interface State {
rectDrawingMethod?: string;
rectDrawingMethod?: RectDrawingMethod;
numberOfPoints?: number;
selectedLabelID: number;
}
@ -85,13 +84,15 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const { shapeType } = props;
const defaultLabelID = props.labels[0].id;
const defaultRectDrawingMethod = RectDrawingMethod.BY_TWO_POINTS;
const defaultRectDrawingMethod = RectDrawingMethod.CLASSIC;
this.state = {
selectedLabelID: defaultLabelID,
rectDrawingMethod: shapeType === ShapeType.RECTANGLE
? defaultRectDrawingMethod : undefined,
};
const { shapeType } = props;
if (shapeType === ShapeType.POLYGON) {
this.minimumPoints = 3;
}
@ -101,9 +102,6 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
if (shapeType === ShapeType.POINTS) {
this.minimumPoints = 1;
}
if (shapeType === ShapeType.RECTANGLE) {
this.state.rectDrawingMethod = defaultRectDrawingMethod;
}
}
private onDraw(objectType: ObjectType): void {
@ -129,7 +127,7 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
});
onDrawStart(shapeType, selectedLabelID,
objectType, numberOfPoints);
objectType, numberOfPoints, rectDrawingMethod);
}
private onChangeRectDrawingMethod = (event: RadioChangeEvent): void => {
@ -166,6 +164,7 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element {
const {
rectDrawingMethod,
selectedLabelID,
numberOfPoints,
} = this.state;
@ -182,6 +181,7 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
minimumPoints={this.minimumPoints}
selectedLabeID={selectedLabelID}
numberOfPoints={numberOfPoints}
rectDrawingMethod={rectDrawingMethod}
onChangeLabel={this.onChangeLabel}
onChangePoints={this.onChangePoints}
onChangeRectDrawingMethod={this.onChangeRectDrawingMethod}

@ -6,10 +6,12 @@ import {
Canvas,
Rotation,
CanvasVersion,
RectDrawingMethod,
} from '../../cvat-canvas/src/typescript/canvas';
export {
Canvas,
Rotation,
CanvasVersion,
RectDrawingMethod,
};

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
import { Canvas } from 'cvat-canvas';
import { Canvas, RectDrawingMethod } from 'cvat-canvas';
export type StringObject = {
[index: string]: string;
@ -251,11 +251,6 @@ export enum ActiveControl {
EDIT = 'edit',
}
export enum RectDrawingMethod {
BY_TWO_POINTS = 'by_two_points',
BY_FOUR_POINTS = 'by_four_points'
}
export enum ShapeType {
RECTANGLE = 'rectangle',
POLYGON = 'polygon',

@ -4,6 +4,7 @@
import { AnyAction } from 'redux';
import { SettingsActionTypes } from 'actions/settings-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import {
SettingsState,
@ -33,7 +34,7 @@ const defaultState: SettingsState = {
grid: false,
gridSize: 100,
gridColor: GridColor.White,
gridOpacity: 0,
gridOpacity: 100,
brightnessLevel: 100,
contrastLevel: 100,
saturationLevel: 100,
@ -213,6 +214,17 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case AnnotationActionTypes.GET_JOB_SUCCESS: {
const { job } = action.payload;
return {
...state,
player: {
...state.player,
resetZoom: job && job.task.mode === 'annotation',
},
};
}
default: {
return state;
}

Loading…
Cancel
Save