React UI: Automatic bordering for polygons and polylines during drawing/editing (#1394)

* Fixed: cannot read property 'set' of undefined

* Fixed UI failing: save during drag/resize

* Fixed multiple saving (shortcut sticking)

* Undo/redo fixed

* Allowed one interpolated point

* Fixed API reaction when repository synchronization is failed

* Updated changelog

* Auto bordering feature

* Some fixes, added shortcuts

* Fixed draw when start with one of supporting point
main
Boris Sekachev 6 years ago committed by GitHub
parent a237c66474
commit 4bcd9596d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,9 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Option to display shape text always
- Dedicated message with clarifications when share is unmounted (https://github.com/opencv/cvat/pull/1373)
- Ability to create one tracked point (https://github.com/opencv/cvat/pull/1383)
- Ability to draw/edit polygons and polylines with automatic bordering feature (https://github.com/opencv/cvat/pull/1394)
- Tutorial: instructions for CVAT over HTTPS
- Added deep extreme cut (semi-automatic segmentation) to the new UI (https://github.com/opencv/cvat/pull/1398)
### Changed
- Increase preview size of a task till 256, 256 on the server
- Minor style updates

@ -153,6 +153,7 @@ Standard JS events are used.
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.resizeshape => {id: number}
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
```
### WEB
@ -196,7 +197,7 @@ Standard JS events are used.
| dragCanvas() | + | - | - | - | - | - | + | - | - | + |
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - |
| cancel() | - | + | + | + | + | + | + | + | + | + |
| configure() | + | - | - | - | - | - | - | - | - | - |
| configure() | + | + | + | + | + | + | + | + | + | + |
| bitmap() | + | + | + | + | + | + | + | + | + | + |
| setZLayer() | + | + | + | + | + | + | + | + | + | + |

@ -103,6 +103,24 @@ polyline.cvat_canvas_shape_splitting {
stroke-dasharray: 5;
}
.cvat_canvas_autoborder_point {
opacity: 0.55;
}
.cvat_canvas_autoborder_point:hover {
opacity: 1;
fill: red;
}
.cvat_canvas_autoborder_point:active {
opacity: 0.55;
fill: red;
}
.cvat_canvas_autoborder_point_direction {
fill: blueviolet;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;

@ -0,0 +1,301 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import * as SVG from 'svg.js';
import consts from './consts';
import { Geometry } from './canvasModel';
interface TransformedShape {
points: string;
color: string;
}
export interface AutoborderHandler {
autoborder(enabled: boolean, currentShape?: SVG.Shape, ignoreCurrent?: boolean): void;
transform(geometry: Geometry): void;
updateObjects(): void;
}
export class AutoborderHandlerImpl implements AutoborderHandler {
private currentShape: SVG.Shape | null;
private ignoreCurrent: boolean;
private frameContent: SVGSVGElement;
private enabled: boolean;
private scale: number;
private groups: SVGGElement[];
private auxiliaryGroupID: number | null;
private auxiliaryClicks: number[];
private listeners: Record<number, Record<number, {
click: (event: MouseEvent) => void;
dblclick: (event: MouseEvent) => void;
}>>;
public constructor(frameContent: SVGSVGElement) {
this.frameContent = frameContent;
this.ignoreCurrent = false;
this.currentShape = null;
this.enabled = false;
this.scale = 1;
this.groups = [];
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
}
private removeMarkers(): void {
this.groups.forEach((group: SVGGElement): void => {
const groupID = group.dataset.groupId;
Array.from(group.children)
.forEach((circle: SVGCircleElement, pointID: number): void => {
circle.removeEventListener('click', this.listeners[+groupID][pointID].click);
circle.removeEventListener('dblclick', this.listeners[+groupID][pointID].click);
circle.remove();
});
group.remove();
});
this.groups = [];
this.auxiliaryGroupID = null;
this.auxiliaryClicks = [];
this.listeners = {};
}
private release(): void {
this.removeMarkers();
this.enabled = false;
this.currentShape = null;
}
private addPointToCurrentShape(x: number, y: number): void {
const array: number[][] = (this.currentShape as any).array().valueOf();
array.pop();
// need to append twice (specific of the library)
array.push([x, y]);
array.push([x, y]);
const paintHandler = this.currentShape.remember('_paintHandler');
paintHandler.drawCircles();
paintHandler.set.members.forEach((el: SVG.Circle): void => {
el.attr('stroke-width', 1 / this.scale).attr('r', 2.5 / this.scale);
});
(this.currentShape as any).plot(array);
}
private resetAuxiliaryShape(): void {
if (this.auxiliaryGroupID !== null) {
while (this.auxiliaryClicks.length > 0) {
const resetID = this.auxiliaryClicks.pop();
this.groups[this.auxiliaryGroupID]
.children[resetID].classList.remove('cvat_canvas_autoborder_point_direction');
}
}
this.auxiliaryClicks = [];
this.auxiliaryGroupID = null;
}
// convert each shape to group of clicable points
// save all groups
private drawMarkers(transformedShapes: TransformedShape[]): void {
const svgNamespace = 'http://www.w3.org/2000/svg';
this.groups = transformedShapes
.map((shape: TransformedShape, groupID: number): SVGGElement => {
const group = document.createElementNS(svgNamespace, 'g');
group.setAttribute('data-group-id', `${groupID}`);
this.listeners[groupID] = this.listeners[groupID] || {};
const circles = shape.points.split(/\s/).map((
point: string, pointID: number, points: string[],
): SVGCircleElement => {
const [x, y] = point.split(',');
const circle = document.createElementNS(svgNamespace, 'circle');
circle.classList.add('cvat_canvas_autoborder_point');
circle.setAttribute('fill', shape.color);
circle.setAttribute('stroke', 'black');
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
circle.setAttribute('cx', x);
circle.setAttribute('cy', y);
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
const click = (event: MouseEvent): void => {
event.stopPropagation();
// another shape was clicked
if (this.auxiliaryGroupID !== null
&& this.auxiliaryGroupID !== groupID
) {
this.resetAuxiliaryShape();
}
this.auxiliaryGroupID = groupID;
// up clicked group for convenience
this.frameContent.appendChild(group);
if (this.auxiliaryClicks[1] === pointID) {
// the second point was clicked twice
this.addPointToCurrentShape(+x, +y);
this.resetAuxiliaryShape();
return;
}
// the first point can not be clicked twice
// just ignore such a click if it is
if (this.auxiliaryClicks[0] !== pointID) {
this.auxiliaryClicks.push(pointID);
} else {
return;
}
// it is the first click
if (this.auxiliaryClicks.length === 1) {
const handler = this.currentShape.remember('_paintHandler');
// draw and remove initial point just to initialize data structures
if (!handler || !handler.startPoint) {
(this.currentShape as any).draw('point', event);
(this.currentShape as any).draw('undo');
}
this.addPointToCurrentShape(+x, +y);
// is is the second click
} else if (this.auxiliaryClicks.length === 2) {
circle.classList.add('cvat_canvas_autoborder_point_direction');
// it is the third click
} else {
// sign defines bypass direction
const landmarks = this.auxiliaryClicks;
const sign = Math.sign(landmarks[2] - landmarks[0])
* Math.sign(landmarks[1] - landmarks[0])
* Math.sign(landmarks[2] - landmarks[1]);
// go via a polygon and get vertexes
// the first vertex has been already drawn
const way = [];
for (let i = landmarks[0] + sign; ; i += sign) {
if (i < 0) {
i = points.length - 1;
} else if (i === points.length) {
i = 0;
}
way.push(points[i]);
if (i === this.auxiliaryClicks[this.auxiliaryClicks.length - 1]) {
// put the last element twice
// specific of svg.draw.js
// way.push(points[i]);
break;
}
}
// remove the latest cursor position from drawing array
for (const wayPoint of way) {
const [_x, _y] = wayPoint.split(',')
.map((coordinate: string): number => +coordinate);
this.addPointToCurrentShape(_x, _y);
}
this.resetAuxiliaryShape();
}
};
const dblclick = (event: MouseEvent): void => {
event.stopPropagation();
};
this.listeners[groupID][pointID] = {
click,
dblclick,
};
circle.addEventListener('mousedown', this.listeners[groupID][pointID].click);
circle.addEventListener('dblclick', this.listeners[groupID][pointID].click);
return circle;
});
group.append(...circles);
return group;
});
this.frameContent.append(...this.groups);
}
public updateObjects(): void {
if (!this.enabled) return;
this.removeMarkers();
const currentClientID = this.currentShape.node.dataset.originClientId;
const shapes = Array.from(this.frameContent.getElementsByClassName('cvat_canvas_shape'));
const transformedShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
const color = shape.getAttribute('fill');
const clientID = shape.getAttribute('clientID');
if (color === null || clientID === null) return null;
if (+clientID === +currentClientID) {
return null;
}
let points = '';
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
points = shape.getAttribute('points');
} else if (shape.tagName === 'rect') {
const x = +shape.getAttribute('x');
const y = +shape.getAttribute('y');
const width = +shape.getAttribute('width');
const height = +shape.getAttribute('height');
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(x) || Number.isNaN(x)) {
return null;
}
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
} else if (shape.tagName === 'g') {
const polylineID = shape.dataset.polylineId;
const polyline = this.frameContent.getElementById(polylineID);
if (polyline && polyline.getAttribute('points')) {
points = polyline.getAttribute('points');
} else {
return null;
}
}
return {
color,
points: points.trim(),
};
}).filter((state: TransformedShape | null): boolean => state !== null);
this.drawMarkers(transformedShapes);
}
public autoborder(
enabled: boolean,
currentShape?: SVG.Shape,
ignoreCurrent: boolean = false,
): void {
if (enabled && !this.enabled && currentShape) {
this.enabled = true;
this.currentShape = currentShape;
this.ignoreCurrent = ignoreCurrent;
this.updateObjects();
} else {
this.release();
}
}
public transform(geometry: Geometry): void {
this.scale = geometry.scale;
this.groups.forEach((group: SVGGElement): void => {
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
circle.setAttribute('r', `${consts.BASE_POINT_SIZE / this.scale}`);
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
});
});
}
}

@ -47,6 +47,7 @@ export enum RectDrawingMethod {
}
export interface Configuration {
autoborders?: boolean;
displayAllText?: boolean;
undefinedAttrValue?: string;
}
@ -206,6 +207,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
},
configuration: {
displayAllText: false,
autoborders: false,
undefinedAttrValue: '',
},
imageBitmap: false,
@ -519,14 +521,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public configure(configuration: Configuration): void {
if (this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (typeof (configuration.displayAllText) !== 'undefined') {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof (configuration.autoborders) !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders;
}
if (typeof (configuration.undefinedAttrValue) !== 'undefined') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}

@ -16,6 +16,7 @@ import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler';
import { AutoborderHandler, AutoborderHandlerImpl } from './autoborderHandler';
import consts from './consts';
import {
translateToSVG,
@ -23,6 +24,7 @@ import {
pointsToArray,
displayShapeSize,
ShapeSizeElement,
DrawnState,
} from './shared';
import {
CanvasModel,
@ -58,7 +60,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private controller: CanvasController;
private svgShapes: Record<number, SVG.Shape>;
private svgTexts: Record<number, SVG.Text>;
private drawnStates: Record<number, any>;
private drawnStates: Record<number, DrawnState>;
private geometry: Geometry;
private drawHandler: DrawHandler;
private editHandler: EditHandler;
@ -66,6 +68,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
private splitHandler: SplitHandler;
private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler;
private autoborderHandler: AutoborderHandler;
private activeElement: ActiveElement;
private configuration: Configuration;
@ -358,6 +361,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
this.autoborderHandler.transform(this.geometry);
}
private resizeCanvas(): void {
@ -421,6 +425,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(this.controller.activeElement);
}
}
this.autoborderHandler.updateObjects();
}
}
@ -461,7 +467,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
e.preventDefault();
}
function contextmenuHandler(e: MouseEvent): void {
function contextMenuHandler(e: MouseEvent): void {
const pointID = Array.prototype.indexOf
.call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
if (self.activeElement.clientID !== null) {
@ -469,7 +475,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
self.canvas.dispatchEvent(new CustomEvent('point.contextmenu', {
self.canvas.dispatchEvent(new CustomEvent('canvas.contextmenu', {
bubbles: false,
cancelable: true,
detail: {
@ -504,7 +510,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.on('dblclick', dblClickHandler);
circle.on('contextmenu', contextmenuHandler);
circle.on('contextmenu', contextMenuHandler);
circle.addClass('cvat_canvas_selected_point');
});
@ -514,7 +520,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
circle.off('dblclick', dblClickHandler);
circle.off('contextmenu', contextmenuHandler);
circle.off('contextmenu', contextMenuHandler);
circle.removeClass('cvat_canvas_selected_point');
});
@ -622,14 +628,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
const self = this;
// Setup API handlers
this.autoborderHandler = new AutoborderHandlerImpl(
this.content,
);
this.drawHandler = new DrawHandlerImpl(
this.onDrawDone.bind(this),
this.adoptedContent,
this.adoptedText,
this.autoborderHandler,
);
this.editHandler = new EditHandlerImpl(
this.onEditDone.bind(this),
this.adoptedContent,
this.autoborderHandler,
);
this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this),
@ -714,8 +725,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.CONFIG_UPDATED) {
this.configuration = model.configuration;
this.setupObjects([]);
this.setupObjects(model.objects);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
// todo: setup text, add if doesn't exist and enabled
// remove if exist and not enabled
// this.setupObjects([]);
// this.setupObjects(model.objects);
} else if (reason === UpdateReasons.BITMAP) {
const { imageBitmap } = model;
if (imageBitmap) {
@ -1053,7 +1069,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
for (const attrID of Object.keys(state.attributes)) {
if (state.attributes[attrID] !== drawnState.attributes[attrID]) {
if (state.attributes[attrID] !== drawnState.attributes[+attrID]) {
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[];
@ -1541,6 +1557,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
id: `cvat_canvas_shape_${state.clientID}`,
'data-polyline-id': basicPolyline.attr('id'),
'data-z-order': state.zOrder,
});

@ -7,11 +7,7 @@ import consts from './consts';
import 'svg.draw.js';
import './svg.patch';
import {
DrawData,
Geometry,
RectDrawingMethod,
} from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
import {
translateToSVG,
@ -23,7 +19,15 @@ import {
Box,
} from './shared';
import {
DrawData,
Geometry,
RectDrawingMethod,
Configuration,
} from './canvasModel';
export interface DrawHandler {
configurate(configuration: Configuration): void;
draw(drawData: DrawData, geometry: Geometry): void;
transform(geometry: Geometry): void;
cancel(): void;
@ -45,6 +49,8 @@ export class DrawHandlerImpl implements DrawHandler {
};
private drawData: DrawData;
private geometry: Geometry;
private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean;
// 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
@ -127,6 +133,7 @@ export class DrawHandlerImpl implements DrawHandler {
return;
}
this.autoborderHandler.autoborder(false);
this.initialized = false;
this.canvas.off('mousedown.draw');
this.canvas.off('mouseup.draw');
@ -334,6 +341,9 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
}
}
private drawPolyline(): void {
@ -344,6 +354,9 @@ export class DrawHandlerImpl implements DrawHandler {
});
this.drawPolyshape();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
}
}
private drawPoints(): void {
@ -599,7 +612,10 @@ export class DrawHandlerImpl implements DrawHandler {
onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void,
canvas: SVG.Container,
text: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone;
this.canvas = canvas;
@ -629,6 +645,19 @@ export class DrawHandlerImpl implements DrawHandler {
});
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.drawInstance, false);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
}
public transform(geometry: Geometry): void {
this.geometry = geometry;

@ -6,29 +6,27 @@ import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import {
translateFromSVG,
pointsToArray,
} from './shared';
import {
EditData,
Geometry,
} from './canvasModel';
import { translateFromSVG, pointsToArray } from './shared';
import { EditData, Geometry, Configuration } from './canvasModel';
import { AutoborderHandler } from './autoborderHandler';
export interface EditHandler {
edit(editData: EditData): void;
transform(geometry: Geometry): void;
configurate(configuration: Configuration): void;
cancel(): void;
}
export class EditHandlerImpl implements EditHandler {
private onEditDone: (state: any, points: number[]) => void;
private autoborderHandler: AutoborderHandler;
private geometry: Geometry;
private canvas: SVG.Container;
private editData: EditData;
private editedShape: SVG.Shape;
private editLine: SVG.PolyLine;
private clones: SVG.Polygon[];
private autobordersEnabled: boolean;
private startEdit(): void {
// get started coordinates
@ -77,6 +75,8 @@ export class EditHandlerImpl implements EditHandler {
(this.editLine as any).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
}).attr({
'data-origin-client-id': this.editData.state.clientID,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
@ -89,6 +89,9 @@ export class EditHandlerImpl implements EditHandler {
}
this.setupEditEvents();
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
}
}
private setupEditEvents(): void {
@ -273,6 +276,7 @@ export class EditHandlerImpl implements EditHandler {
this.canvas.off('mousedown.edit');
this.canvas.off('mouseup.edit');
this.canvas.off('mousemove.edit');
this.autoborderHandler.autoborder(false);
if (this.editedShape) {
this.setupPoints(false);
@ -314,7 +318,10 @@ export class EditHandlerImpl implements EditHandler {
public constructor(
onEditDone: (state: any, points: number[]) => void,
canvas: SVG.Container,
autoborderHandler: AutoborderHandler,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
this.onEditDone = onEditDone;
this.canvas = canvas;
this.editData = null;
@ -343,6 +350,19 @@ export class EditHandlerImpl implements EditHandler {
this.onEditDone(null, null);
}
public configurate(configuration: Configuration): void {
if (typeof (configuration.autoborders) === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.editLine) {
if (this.autobordersEnabled) {
this.autoborderHandler.autoborder(true, this.editLine, true);
} else {
this.autoborderHandler.autoborder(false);
}
}
}
}
public transform(geometry: Geometry): void {
this.geometry = geometry;

@ -25,6 +25,21 @@ export interface BBox {
y: number;
}
export interface DrawnState {
clientID: number;
outside?: boolean;
occluded?: boolean;
hidden?: boolean;
lock: boolean;
shapeType: string;
points?: number[];
attributes: Record<number, string>;
zOrder?: number;
pinned?: boolean;
updated: number;
frame: number;
}
// Translate point array from the canvas coordinate system
// to the coordinate system of a client
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {

@ -28,6 +28,7 @@ export enum SettingsActionTypes {
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_AUTOMATIC_BORDERING = 'SWITCH_AUTOMATIC_BORDERING',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS',
}
@ -220,3 +221,12 @@ export function switchShowingObjectsTextAlways(showObjectsTextAlways: boolean):
},
};
}
export function switchAutomaticBordering(automaticBordering: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_AUTOMATIC_BORDERING,
payload: {
automaticBordering,
},
};
}

@ -62,7 +62,9 @@ interface Props {
aamZoomMargin: number;
showObjectsTextAlways: boolean;
workspace: Workspace;
automaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>;
switchableAutomaticBordering: boolean;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
@ -89,11 +91,13 @@ interface Props {
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
public componentDidMount(): void {
const {
automaticBordering,
showObjectsTextAlways,
canvasInstance,
curZLayer,
@ -106,6 +110,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
wrapper.appendChild(canvasInstance.html());
canvasInstance.configure({
autoborders: automaticBordering,
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways,
});
@ -139,12 +144,16 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
workspace,
frameFetching,
showObjectsTextAlways,
automaticBordering,
} = this.props;
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways) {
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways
|| prevProps.automaticBordering !== automaticBordering
) {
canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways,
autoborders: automaticBordering,
});
}
@ -262,7 +271,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().removeEventListener('point.contextmenu', this.onCanvasPointContextMenu);
canvasInstance.html().removeEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
window.removeEventListener('resize', this.fitCanvas);
}
@ -683,7 +692,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
canvasInstance.html().addEventListener('point.contextmenu', this.onCanvasPointContextMenu);
canvasInstance.html().addEventListener('canvas.contextmenu', this.onCanvasPointContextMenu);
}
public render(): JSX.Element {
@ -696,16 +705,19 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel,
contrastLevel,
saturationLevel,
keyMap,
grid,
gridColor,
gridOpacity,
switchableAutomaticBordering,
automaticBordering,
onChangeBrightnessLevel,
onChangeSaturationLevel,
onChangeContrastLevel,
onChangeGridColor,
onChangeGridOpacity,
onSwitchGrid,
keyMap,
onSwitchAutomaticBordering,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -724,8 +736,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
INCREASE_GRID_OPACITY: keyMap.INCREASE_GRID_OPACITY,
DECREASE_GRID_OPACITY: keyMap.DECREASE_GRID_OPACITY,
CHANGE_GRID_COLOR: keyMap.CHANGE_GRID_COLOR,
SWITCH_AUTOMATIC_BORDERING: keyMap.SWITCH_AUTOMATIC_BORDERING,
};
const step = 10;
const handlers = {
INCREASE_BRIGHTNESS: (event: KeyboardEvent | undefined) => {
@ -800,6 +814,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const color = colors[indexOf >= colors.length ? 0 : indexOf];
onChangeGridColor(color);
},
SWITCH_AUTOMATIC_BORDERING: (event: KeyboardEvent | undefined) => {
if (switchableAutomaticBordering) {
preventDefault(event);
onSwitchAutomaticBordering(!automaticBordering);
}
},
};
return (

@ -23,10 +23,14 @@
.cvat-player-settings-grid,
.cvat-workspace-settings-auto-save,
.cvat-workspace-settings-autoborders,
.cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-text-always-checkbox,
.cvat-workspace-settings-show-interpolated-checkbox {
margin-bottom: 10px;
.cvat-workspace-settings-show-interpolated {
margin-bottom: 25px;
> div:first-child {
margin-bottom: 10px;
}
}
.cvat-player-settings-grid-size,
@ -36,8 +40,6 @@
.cvat-player-settings-speed,
.cvat-player-settings-reset-zoom,
.cvat-player-settings-rotate-all,
.cvat-workspace-settings-show-text-always,
.cvat-workspace-settings-show-interpolated,
.cvat-workspace-settings-aam-zoom-margin,
.cvat-workspace-settings-auto-save-interval {
margin-bottom: 25px;

@ -17,11 +17,13 @@ interface Props {
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
showObjectsTextAlways: boolean;
automaticBordering: boolean;
onSwitchAutoSave(enabled: boolean): void;
onChangeAutoSaveInterval(interval: number): void;
onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void;
onSwitchShowingObjectsTextAlways(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
@ -31,11 +33,13 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
aamZoomMargin,
showAllInterpolationTracks,
showObjectsTextAlways,
automaticBordering,
onSwitchAutoSave,
onChangeAutoSaveInterval,
onChangeAAMZoomMargin,
onSwitchShowingInterpolatedTracks,
onSwitchShowingObjectsTextAlways,
onSwitchAutomaticBordering,
} = props;
const minAutoSaveInterval = 5;
@ -82,7 +86,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Col>
</Row>
<Row className='cvat-workspace-settings-show-interpolated'>
<Col className='cvat-workspace-settings-show-interpolated-checkbox'>
<Col>
<Checkbox
className='cvat-text-color'
checked={showAllInterpolationTracks}
@ -98,7 +102,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
</Col>
</Row>
<Row className='cvat-workspace-settings-show-text-always'>
<Col className='cvat-workspace-settings-show-text-always-checkbox'>
<Col>
<Checkbox
className='cvat-text-color'
checked={showObjectsTextAlways}
@ -113,6 +117,22 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
<Text type='secondary'> Show text for an object on the canvas not only when the object is activated </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-autoborders'>
<Col>
<Checkbox
className='cvat-text-color'
checked={automaticBordering}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchAutomaticBordering(event.target.checked);
}}
>
Automatic bordering
</Checkbox>
</Col>
<Col>
<Text type='secondary'> Enable automatic bordering for polygons and polylines during drawing/editing </Text>
</Col>
</Row>
<Row className='cvat-workspace-settings-aam-zoom-margin'>
<Col>
<Text className='cvat-text-color'> Attribute annotation mode (AAM) zoom margin </Text>

@ -34,6 +34,7 @@ import {
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
switchAutomaticBordering,
} from 'actions/settings-actions';
import {
ColorBy,
@ -42,6 +43,7 @@ import {
CombinedState,
ContextMenuType,
Workspace,
ActiveControl,
} from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
@ -79,8 +81,10 @@ interface StateToProps {
minZLayer: number;
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean;
contextVisible: boolean;
contextType: ContextMenuType;
switchableAutomaticBordering: boolean;
keyMap: Record<string, ExtendedKeyMapOptions>;
}
@ -111,12 +115,14 @@ interface DispatchToProps {
onChangeGridOpacity(opacity: number): void;
onChangeGridColor(color: GridColor): void;
onSwitchGrid(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
canvas: {
activeControl,
contextMenu: {
visible: contextVisible,
type: contextType,
@ -166,6 +172,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
workspace: {
aamZoomMargin,
showObjectsTextAlways,
automaticBordering,
},
shapes: {
opacity,
@ -212,10 +219,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
curZLayer,
minZLayer,
maxZLayer,
automaticBordering,
contextVisible,
contextType,
workspace,
keyMap,
switchableAutomaticBordering: activeControl === ActiveControl.DRAW_POLYGON
|| activeControl === ActiveControl.DRAW_POLYLINE
|| activeControl === ActiveControl.EDIT,
};
}
@ -301,6 +312,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchGrid(enabled: boolean): void {
dispatch(switchGrid(enabled));
},
onSwitchAutomaticBordering(enabled: boolean): void {
dispatch(switchAutomaticBordering(enabled));
},
};
}

@ -11,6 +11,7 @@ import {
changeAAMZoomMargin,
switchShowingInterpolatedTracks,
switchShowingObjectsTextAlways,
switchAutomaticBordering,
} from 'actions/settings-actions';
import { CombinedState } from 'reducers/interfaces';
@ -23,6 +24,7 @@ interface StateToProps {
aamZoomMargin: number;
showAllInterpolationTracks: boolean;
showObjectsTextAlways: boolean;
automaticBordering: boolean;
}
interface DispatchToProps {
@ -31,6 +33,7 @@ interface DispatchToProps {
onChangeAAMZoomMargin(margin: number): void;
onSwitchShowingInterpolatedTracks(enabled: boolean): void;
onSwitchShowingObjectsTextAlways(enabled: boolean): void;
onSwitchAutomaticBordering(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -41,6 +44,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
aamZoomMargin,
showAllInterpolationTracks,
showObjectsTextAlways,
automaticBordering,
} = workspace;
return {
@ -49,6 +53,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
aamZoomMargin,
showAllInterpolationTracks,
showObjectsTextAlways,
automaticBordering,
};
}
@ -69,6 +74,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchShowingObjectsTextAlways(enabled: boolean): void {
dispatch(switchShowingObjectsTextAlways(enabled));
},
onSwitchAutomaticBordering(enabled: boolean): void {
dispatch(switchAutomaticBordering(enabled));
},
};
}

@ -429,6 +429,7 @@ export interface WorkspaceSettingsState {
autoSave: boolean;
autoSaveInterval: number; // in ms
aamZoomMargin: number;
automaticBordering: boolean;
showObjectsTextAlways: boolean;
showAllInterpolationTracks: boolean;
}

@ -28,6 +28,7 @@ const defaultState: SettingsState = {
autoSave: false,
autoSaveInterval: 15 * 60 * 1000,
aamZoomMargin: 100,
automaticBordering: false,
showObjectsTextAlways: false,
showAllInterpolationTracks: false,
},
@ -237,6 +238,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.SWITCH_AUTOMATIC_BORDERING: {
return {
...state,
workspace: {
...state.workspace,
automaticBordering: action.payload.automaticBordering,
},
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AnnotationActionTypes.GET_JOB_SUCCESS: {
const { job } = action.payload;

@ -314,6 +314,12 @@ const defaultKeyMap = {
sequences: ['`', '~'],
action: 'keydown',
},
SWITCH_AUTOMATIC_BORDERING: {
name: 'Switch automatic bordering',
description: 'Switch automatic bordering for polygons and polylines during drawing/editing',
sequences: ['Control'],
action: 'keydown',
},
} as any as Record<string, ExtendedKeyMapOptions>;

Loading…
Cancel
Save