CVAT-Canvas Updates (#675)

* Removed extra files
* Merge(), group(), split() and cancel()
* More visual effects
* More strict state checks
* Display shape size during draw/resize
main
Boris Sekachev 7 years ago committed by Nikita Manovich
parent 4fd966dacc
commit 03eaf59d98

@ -39,6 +39,7 @@ module.exports = {
'no-console': 0, // this rule deprecates console.log, console.warn etc. because 'it is not good in production code'
'lines-between-class-members': 0,
'import/prefer-default-export': 0, // works incorrect with interfaces
'newline-per-chained-call': 0, // makes code uglier
},
'settings': {
'import/resolver': {

@ -58,6 +58,16 @@ Canvas itself handles:
enabled: boolean;
}
interface DrawnData {
shapeType: string;
points: number[];
objectType?: string;
occluded?: boolean;
attributes?: [index: number]: string;
label?: Label;
color?: string;
}
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void;
@ -71,6 +81,7 @@ Canvas itself handles:
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
cancel(): void;
}
@ -78,12 +89,13 @@ Canvas itself handles:
### API CSS
- All drawn objects (shapes, tracks) have an id ```cvat_canvas_object_{objectState.id}```
- All drawn objects (shapes, tracks) have an id ```cvat_canvas_shape_{objectState.clientID}```
- Drawn shapes and tracks have classes ```cvat_canvas_shape```,
```cvat_canvas_shape_activated```,
```cvat_canvas_shape_grouping```,
```cvat_canvas_shape_merging```,
```cvat_canvas_shape_drawing```
```cvat_canvas_shape_drawing```,
```cvat_canvas_shape_occluded```
- Drawn texts have the class ```cvat_canvas_text```
- Tags have the class ```cvat_canvas_tag```
- Canvas image has ID ```cvat_canvas_image```
@ -98,10 +110,11 @@ Standard JS events are used.
- canvas.activated => ObjectState
- canvas.deactivated
- canvas.moved => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: ObjectState}
- canvas.edited => {state: ObjectState}
- canvas.splitted => {state: ObjectState, frame: number}
- canvas.groupped => {states: ObjectState[], reset: boolean}
- canvas.find => {states: ObjectState[], x: number, y: number}
- canvas.drawn => {state: DrawnData}
- canvas.edited => {state: ObjectState, points: number[]}
- canvas.splitted => {state: ObjectState}
- canvas.groupped => {states: ObjectState[]}
- canvas.merged => {states: ObjectState[]}
- canvas.canceled
```
@ -109,7 +122,7 @@ Standard JS events are used.
### WEB
```js
// Create an instance of a canvas
const canvas = new window.canvas.Canvas(window.cvat.classes.ObjectState);
const canvas = new window.canvas.Canvas();
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
@ -149,7 +162,7 @@ Than you can use it in TypeScript:
```ts
import * as CANVAS from 'cvat-canvas.node';
// Create an instance of a canvas
const canvas = new CANVAS.Canvas(null);
const canvas = new CANVAS.Canvas();
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());

@ -25,12 +25,14 @@
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"dts-bundle-webpack": "^1.0.2",
"eslint": "^6.1.0",
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-config-typescript-recommended": "^1.4.17",
"eslint-plugin-import": "^2.18.2",
"nodemon": "^1.19.1",
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",

@ -1,122 +0,0 @@
.cvat_canvas_hidden {
display: none;
}
.cvat_canvas_shape {
fill-opacity: 0.1;
stroke-opacity: 1;
}
polyline.cvat_canvas_shape {
fill-opacity: 0;
stroke-opacity: 1;
}
.cvat_canvas_text {
font-weight: bold;
font-size: 1.2em;
fill: white;
cursor: default;
font-family: Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif;
text-shadow: 0px 0px 4px black;
user-select: none;
pointer-events: none;
}
.cvat_canvas_crosshair {
stroke: red;
}
.cvat_canvas_shape_activated {
}
.cvat_canvas_shape_grouping {
}
.cvat_canvas_shape_merging {
}
.cvat_canvas_shape_drawing {
fill-opacity: 0.1;
stroke-opacity: 1;
fill: white;
stroke: black;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;
}
#cvat_canvas_wrapper {
width: 100%;
height: 93%;
border-radius: 5px;
background-color: white;
overflow: hidden;
position: relative;
}
#cvat_canvas_loading_animation {
z-index: 1;
position: absolute;
width: 100%;
height: 100%;
}
#cvat_canvas_loading_circle {
fill-opacity: 0;
stroke: #09c;
stroke-width: 3px;
stroke-dasharray: 50;
animation: loadingAnimation 1s linear infinite;
}
#cvat_canvas_text_content {
position: absolute;
z-index: 3;
pointer-events: none;
width: 100%;
height: 100%;
pointer-events: none;
}
#cvat_canvas_background {
position: absolute;
z-index: 0;
background-repeat: no-repeat;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}
#cvat_canvas_grid {
position: absolute;
z-index: 2;
pointer-events: none;
width: 100%;
height: 100%;
pointer-events: none;
}
#cvat_canvas_grid_pattern {
opacity: 1;
stroke: white;
}
#cvat_canvas_content {
position: absolute;
z-index: 2;
outline: 10px solid black;
width: 100%;
height: 100%;
}
@keyframes loadingAnimation {
0% {stroke-dashoffset: 1; stroke: #09c;}
50% {stroke-dashoffset: 100; stroke: #f44;}
100% {stroke-dashoffset: 300; stroke: #09c;}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -3,7 +3,7 @@
}
.cvat_canvas_shape {
fill-opacity: 0.1;
fill-opacity: 0.05;
stroke-opacity: 1;
}
@ -28,15 +28,37 @@ polyline.cvat_canvas_shape {
}
.cvat_canvas_shape_activated {
fill-opacity: 0.3;
}
.cvat_canvas_shape_grouping {
fill: darkmagenta;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_grouping {
stroke: darkmagenta;
stroke-opacity: 1;
}
.cvat_canvas_shape_merging {
fill: blue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_splitting {
stroke: dodgerblue;
stroke-opacity: 1;
}
.cvat_canvas_shape_splitting {
fill: dodgerblue;
fill-opacity: 0.5;
}
polyline.cvat_canvas_shape_merging {
stroke: blue;
stroke-opacity: 1;
}
.cvat_canvas_shape_drawing {
@ -46,6 +68,10 @@ polyline.cvat_canvas_shape {
stroke: black;
}
.cvat_canvas_shape_occluded {
stroke-dasharray: 5;
}
.svg_select_boundingRect {
opacity: 0;
pointer-events: none;

@ -1,366 +0,0 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import consts from './consts';
import 'svg.draw.js';
import './svg.patch';
import {
DrawData,
Geometry,
} from './canvasModel';
import {
translateToSVG,
translateFromSVG,
} from './shared';
export interface DrawHandler {
draw(drawData: DrawData, geometry: Geometry): void;
}
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object) => void;
private canvas: SVG.Container;
private background: SVGSVGElement;
private crosshair: {
x: SVG.Line;
y: SVG.Line;
};
private drawData: DrawData;
private geometry: Geometry;
private drawInstance: any;
private addCrosshair(): void {
this.crosshair = {
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
};
}
private removeCrosshair(): void {
this.crosshair.x.remove();
this.crosshair.y.remove();
this.crosshair = null;
}
private initDrawing(): void {
if (this.drawData.crosshair) {
this.addCrosshair();
}
}
private closeDrawing(): void {
if (this.crosshair) {
this.removeCrosshair();
}
if (this.drawInstance) {
if (this.drawData.shapeType === 'rectangle') {
this.drawInstance.draw('cancel');
} else {
this.drawInstance.draw('done');
}
// We should check again because state can be changed in 'cancel' and 'done'
if (this.drawInstance) {
this.drawInstance.remove();
this.drawInstance = null;
}
}
}
private drawBox(): void {
this.drawInstance = this.canvas.rect();
this.drawInstance.draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
}).on('drawstop', (e: Event): void => {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const bbox = (e.target as SVGRectElement).getBBox();
let [xtl, ytl, xbr, ybr] = translateFromSVG(
this.canvas.node as any as SVGSVGElement,
[bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height],
);
([xtl, ytl, xbr, ybr] = translateToSVG(
this.background,
[xtl, ytl, xbr, ybr],
));
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
points: [xtl, ytl, xbr, ybr],
});
} else {
this.onDrawDone(null);
}
});
}
private drawPolyshape(): void {
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
this.drawInstance.draw('done');
}
}.bind(this);
const sizeIncrement = function sizeIncrement(): void {
size++;
};
if (this.drawData.numberOfPoints) {
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', sizeIncrement);
}
// Add ability to cancel the latest drawn point
const handleUndo = function handleUndo(e: MouseEvent): void {
if (e.which === 3) {
e.stopPropagation();
e.preventDefault();
this.drawInstance.draw('undo');
}
}.bind(this);
this.canvas.node.addEventListener('mousedown', handleUndo);
// Add ability to draw shapes by sliding
// We need to remember last drawn point
// to implementation of slide drawing
const lastDrawnPoint: {
x: number;
y: number;
} = {
x: null,
y: null,
};
const handleSlide = function handleSlide(e: MouseEvent): void {
// TODO: Use enumeration after typification cvat-core
if (e.shiftKey && ['polygon', 'polyline'].includes(this.drawData.shapeType)) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.drawInstance.draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) {
this.drawInstance.draw('point', e);
}
}
}
}.bind(this);
this.canvas.node.addEventListener('mousemove', handleSlide);
// We need scale just drawn points
const self = this;
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
self.transform(self.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
});
this.drawInstance.on('drawstop', (): void => {
self.canvas.node.removeEventListener('mousedown', handleUndo);
self.canvas.node.removeEventListener('mousemove', handleSlide);
});
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
let points = translateFromSVG(
this.canvas.node as any as SVGSVGElement,
(e.target as SVGElement)
.getAttribute('points')
.split(/[,\s]/g)
.map((coord): number => +coord),
);
points = translateToSVG(
this.background,
points,
);
const bbox = {
xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER,
xbr: Number.MAX_SAFE_INTEGER,
ybr: Number.MAX_SAFE_INTEGER,
};
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
for (let i = 0; i < points.length - 1; i += 2) {
points[i] = Math.min(Math.max(points[i], 0), frameWidth);
points[i + 1] = Math.min(Math.max(points[i + 1], 0), frameHeight);
bbox.xtl = Math.min(bbox.xtl, points[i]);
bbox.ytl = Math.min(bbox.ytl, points[i + 1]);
bbox.xbr = Math.max(bbox.xbr, points[i]);
bbox.ybr = Math.max(bbox.ybr, points[i + 1]);
}
if (this.drawData.shapeType === 'polygon'
&& ((bbox.xbr - bbox.xtl) * (bbox.ybr - bbox.ytl) >= consts.AREA_THRESHOLD)) {
this.onDrawDone({
points,
});
} else if (this.drawData.shapeType === 'polyline'
&& ((bbox.xbr - bbox.xtl) >= consts.SIZE_THRESHOLD
|| (bbox.ybr - bbox.ytl) >= consts.SIZE_THRESHOLD)) {
this.onDrawDone({
points,
});
} else if (this.drawData.shapeType === 'points') {
this.onDrawDone({
points,
});
} else {
this.onDrawDone(null);
}
});
}
private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.drawPolyshape();
}
private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0,
});
this.drawPolyshape();
}
private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
opacity: 0,
});
this.drawPolyshape();
}
private startDraw(): void {
// TODO: Use enums after typification cvat-core
if (this.drawData.shapeType === 'rectangle') {
this.drawBox();
} else if (this.drawData.shapeType === 'polygon') {
this.drawPolygon();
} else if (this.drawData.shapeType === 'polyline') {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
}
}
public constructor(onDrawDone: any, canvas: SVG.Container, background: SVGSVGElement) {
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.background = background;
this.drawData = null;
this.geometry = null;
this.crosshair = null;
this.drawInstance = null;
this.canvas.node.addEventListener('mousemove', (e): void => {
if (this.crosshair) {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.crosshair.x.attr({
y1: y,
y2: y,
});
this.crosshair.y.attr({
x1: x,
x2: x,
});
}
});
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.crosshair) {
this.crosshair.x.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.y.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
}
if (this.drawInstance) {
this.drawInstance.draw('transform');
this.drawInstance.style({
'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(
'stroke-width',
`${consts.BASE_STROKE_WIDTH / (3 * geometry.scale)}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / (2 * geometry.scale)}`,
);
}
}
}
public draw(drawData: DrawData, geometry: Geometry): void {
this.geometry = geometry;
if (drawData.enabled) {
this.drawData = drawData;
this.initDrawing();
this.startDraw();
} else {
this.closeDrawing();
this.drawData = drawData;
}
}
}
// TODO: handle initial state

@ -1,19 +0,0 @@
import { GroupData } from './canvasModel';
export interface GroupHandler {
group(groupData: GroupData): void;
}
export class GroupHandlerImpl implements GroupHandler {
// callback is used to notify about grouping end
private onGroupDone: (objects: any[], reset: boolean) => void;
public constructor(onGroupDone: any) {
this.onGroupDone = onGroupDone;
}
/* eslint-disable-next-line */
public group(groupData: GroupData): void {
throw new Error('Method not implemented.');
}
}

@ -1,19 +0,0 @@
import { MergeData } from './canvasModel';
export interface MergeHandler {
merge(mergeData: MergeData): void;
}
export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end
private onMergeDone: (objects: any[]) => void;
public constructor(onMergeDone: any) {
this.onMergeDone = onMergeDone;
}
/* eslint-disable-next-line */
public merge(mergeData: MergeData): void {
throw new Error('Method not implemented.');
}
}

@ -1,36 +0,0 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
// Translate point array from the client coordinate system
// to a coordinate system of a canvas
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = [];
const transformationMatrix = svg.getScreenCTM();
let pt = svg.createSVGPoint();
for (let i = 0; i < points.length - 1; i += 2) {
pt.x = points[i];
pt.y = points[i + 1];
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
return output;
}
// Translate point array from a coordinate system of a canvas
// to the client coordinate system
export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = [];
const transformationMatrix = svg.getScreenCTM().inverse();
let pt = svg.createSVGPoint();
for (let i = 0; i < points.length; i += 2) {
pt.x = points[i];
pt.y = points[i + 1];
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
return output;
}

@ -1,19 +0,0 @@
import { SplitData } from './canvasModel';
export interface SplitHandler {
split(splitData: SplitData): void;
}
export class SplitHandlerImpl implements SplitHandler {
// callback is used to notify about splitting end
private onSplitDone: (object: any) => void;
public constructor(onSplitDone: any) {
this.onSplitDone = onSplitDone;
}
/* eslint-disable-next-line */
public split(splitData: SplitData): void {
throw new Error('Method not implemented.');
}
}

@ -28,6 +28,9 @@ import {
} from './canvasView';
import '../css/canvas.css';
interface Canvas {
html(): HTMLDivElement;
setup(frameData: any, objectStates: any[]): void;
@ -41,6 +44,7 @@ interface Canvas {
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
cancel(): void;
}
@ -50,8 +54,8 @@ class CanvasImpl implements Canvas {
private controller: CanvasController;
private view: CanvasView;
public constructor(ObjectStateClass: any) {
this.model = new CanvasModelImpl(ObjectStateClass);
public constructor() {
this.model = new CanvasModelImpl();
this.controller = new CanvasControllerImpl(this.model);
this.view = new CanvasViewImpl(this.model, this.controller);
}
@ -100,6 +104,10 @@ class CanvasImpl implements Canvas {
this.model.merge(mergeData);
}
public select(objectState: any): void {
this.model.select(objectState);
}
public cancel(): void {
this.model.cancel();
}

@ -10,18 +10,29 @@ import {
FocusData,
ActiveElement,
DrawData,
MergeData,
SplitData,
GroupData,
Mode,
} from './canvasModel';
export interface CanvasController {
readonly objects: any[];
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly objectStateClass: any;
readonly drawData: DrawData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly selected: any;
mode: Mode;
geometry: Geometry;
zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void;
merge(mergeData: MergeData): void;
split(splitData: SplitData): void;
group(groupData: GroupData): void;
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
@ -74,6 +85,18 @@ export class CanvasControllerImpl implements CanvasController {
this.model.draw(drawData);
}
public merge(mergeData: MergeData): void {
this.model.merge(mergeData);
}
public split(splitData: SplitData): void {
this.model.split(splitData);
}
public group(groupData: GroupData): void {
this.model.group(groupData);
}
public get geometry(): Geometry {
return this.model.geometry;
}
@ -94,11 +117,31 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.activeElement;
}
public get objectStateClass(): any {
return this.model.objectStateClass;
}
public get drawData(): DrawData {
return this.model.drawData;
}
public get mergeData(): MergeData {
return this.model.mergeData;
}
public get splitData(): SplitData {
return this.model.splitData;
}
public get groupData(): GroupData {
return this.model.groupData;
}
public get selected(): any {
return this.model.selected;
}
public set mode(value: Mode) {
this.model.mode = value;
}
public get mode(): Mode {
return this.model.mode;
}
}

@ -8,6 +8,7 @@
import { MasterImpl } from './master';
export interface Size {
width: number;
height: number;
@ -47,9 +48,14 @@ export interface DrawData {
crosshair?: boolean;
}
export interface EditData {
enabled: boolean;
state: any;
pointID: number;
}
export interface GroupData {
enabled: boolean;
resetGroup: boolean;
}
export interface MergeData {
@ -83,6 +89,19 @@ export enum UpdateReasons {
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
SELECT = 'select',
CANCEL = 'cancel',
}
export enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
}
export interface CanvasModel {
@ -91,9 +110,13 @@ export interface CanvasModel {
readonly gridSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly objectStateClass: any;
readonly drawData: DrawData;
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly selected: any;
geometry: Geometry;
mode: Mode;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;
@ -109,13 +132,13 @@ export interface CanvasModel {
group(groupData: GroupData): void;
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
cancel(): void;
}
export class CanvasModelImpl extends MasterImpl implements CanvasModel {
private data: {
ObjectStateClass: any;
activeElement: ActiveElement;
angle: number;
canvasSize: Size;
@ -133,9 +156,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
mergeData: MergeData;
groupData: GroupData;
splitData: SplitData;
selected: any;
mode: Mode;
};
public constructor(ObjectStateClass: any) {
public constructor() {
super();
this.data = {
@ -164,7 +189,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
},
left: 0,
objects: [],
ObjectStateClass,
rememberAngle: false,
scale: 1,
top: 0,
@ -179,11 +203,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
},
groupData: {
enabled: false,
resetGroup: false,
},
splitData: {
enabled: false,
},
selected: null,
mode: null,
};
}
@ -243,6 +268,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public activate(clientID: number, attributeID: number): void {
if (this.data.mode !== Mode.IDLE) {
// Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
this.data.activeElement = {
clientID,
attributeID,
@ -309,10 +339,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public draw(drawData: DrawData): void {
if (![Mode.IDLE, Mode.DRAW].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (drawData.enabled) {
if (this.data.drawData.enabled) {
throw new Error('Drawing has been already started');
} else if (!drawData.shapeType) {
} else if (!drawData.shapeType && !drawData.initialState) {
throw new Error('A shape type is not specified');
} else if (typeof (drawData.numberOfPoints) !== 'undefined') {
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
@ -323,11 +357,18 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
}
this.data.drawData = Object.assign({}, drawData);
this.data.drawData = { ...drawData };
if (this.data.drawData.initialState) {
this.data.drawData.shapeType = this.data.drawData.initialState.shapeType;
}
this.notify(UpdateReasons.DRAW);
}
public split(splitData: SplitData): void {
if (![Mode.IDLE, Mode.SPLIT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.splitData.enabled && splitData.enabled) {
return;
}
@ -336,11 +377,15 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return;
}
this.data.splitData = splitData;
this.data.splitData = { ...splitData };
this.notify(UpdateReasons.SPLIT);
}
public group(groupData: GroupData): void {
if (![Mode.IDLE, Mode.GROUP].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.groupData.enabled && groupData.enabled) {
return;
}
@ -349,11 +394,15 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return;
}
this.data.groupData = groupData;
this.data.groupData = { ...groupData };
this.notify(UpdateReasons.GROUP);
}
public merge(mergeData: MergeData): void {
if (![Mode.IDLE, Mode.MERGE].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (this.data.mergeData.enabled && mergeData.enabled) {
return;
}
@ -362,20 +411,26 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return;
}
this.data.mergeData = mergeData;
this.data.mergeData = { ...mergeData };
this.notify(UpdateReasons.MERGE);
}
public select(objectState: any): void {
this.data.selected = objectState;
this.notify(UpdateReasons.SELECT);
this.data.selected = null;
}
public cancel(): void {
console.log('hello');
this.notify(UpdateReasons.CANCEL);
}
public get geometry(): Geometry {
return {
angle: this.data.angle,
canvas: Object.assign({}, this.data.canvasSize),
image: Object.assign({}, this.data.imageSize),
grid: Object.assign({}, this.data.gridSize),
canvas: { ...this.data.canvasSize },
image: { ...this.data.imageSize },
grid: { ...this.data.gridSize },
left: this.data.left,
offset: this.data.imageOffset,
scale: this.data.scale,
@ -385,9 +440,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
public set geometry(geometry: Geometry) {
this.data.angle = geometry.angle;
this.data.canvasSize = Object.assign({}, geometry.canvas);
this.data.imageSize = Object.assign({}, geometry.image);
this.data.gridSize = Object.assign({}, geometry.grid);
this.data.canvasSize = { ...geometry.canvas };
this.data.imageSize = { ...geometry.image };
this.data.gridSize = { ...geometry.grid };
this.data.left = geometry.left;
this.data.top = geometry.top;
this.data.imageOffset = geometry.offset;
@ -408,22 +463,42 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public get gridSize(): Size {
return Object.assign({}, this.data.gridSize);
return { ...this.data.gridSize };
}
public get focusData(): FocusData {
return Object.assign({}, this.data.focusData);
return { ...this.data.focusData };
}
public get activeElement(): ActiveElement {
return Object.assign({}, this.data.activeElement);
return { ...this.data.activeElement };
}
public get objectStateClass(): any {
return this.data.ObjectStateClass;
public get drawData(): DrawData {
return { ...this.data.drawData };
}
public get drawData(): DrawData {
return Object.assign({}, this.data.drawData);
public get mergeData(): MergeData {
return { ...this.data.mergeData };
}
public get splitData(): SplitData {
return { ...this.data.splitData };
}
public get groupData(): GroupData {
return { ...this.data.groupData };
}
public get selected(): any {
return this.data.selected;
}
public set mode(value: Mode) {
this.data.mode = value;
}
public get mode(): Mode {
return this.data.mode;
}
}

@ -5,7 +5,6 @@
import * as SVG from 'svg.js';
// tslint:disable-next-line: ordered-imports
import 'svg.draggable.js';
import 'svg.resize.js';
import 'svg.select.js';
@ -13,27 +12,38 @@ import 'svg.select.js';
import { CanvasController } from './canvasController';
import { Listener, Master } from './master';
import { DrawHandler, DrawHandlerImpl } from './drawHandler';
import { EditHandler, EditHandlerImpl } from './editHandler';
import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { translateToSVG, translateFromSVG } from './shared';
import consts from './consts';
import {
translateToSVG,
translateFromSVG,
translateBetweenSVG,
pointsToArray,
displayShapeSize,
ShapeSizeElement,
} from './shared';
import {
CanvasModel,
Geometry,
Size,
UpdateReasons,
FocusData,
FrameZoom,
ActiveElement,
DrawData,
MergeData,
SplitData,
GroupData,
Mode,
Size,
} from './canvasModel';
export interface CanvasView {
html(): HTMLDivElement;
}
interface ShapeDict {
[index: number]: SVG.Shape;
}
@ -42,55 +52,6 @@ interface TextDict {
[index: number]: SVG.Text;
}
enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
}
function selectize(value: boolean, shape: SVG.Element, geometry: Geometry): void {
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
pointSize: consts.BASE_POINT_SIZE / geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
.stroke('black')
.fill(shape.node.getAttribute('fill'))
.center(cx, cy)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (3 * geometry.scale),
});
circle.node.addEventListener('mouseenter', (): void => {
circle.attr({
'stroke-width': circle.attr('stroke-width') * 2,
});
circle.addClass('cvat_canvas_selected_point');
});
circle.node.addEventListener('mouseleave', (): void => {
circle.attr({
'stroke-width': circle.attr('stroke-width') / 2,
});
circle.removeClass('cvat_canvas_selected_point');
});
return circle;
},
});
} else {
(shape as any).selectize(false, {
deepSelect: true,
});
}
}
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));
@ -119,7 +80,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
private controller: CanvasController;
private svgShapes: ShapeDict;
private svgTexts: TextDict;
private geometry: Geometry;
private drawHandler: DrawHandler;
private editHandler: EditHandler;
private mergeHandler: MergeHandler;
private splitHandler: SplitHandler;
private groupHandler: GroupHandler;
@ -128,7 +91,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
attributeID: number;
};
private mode: Mode;
private set mode(value: Mode) {
this.controller.mode = value;
}
private get mode(): Mode {
return this.controller.mode;
}
private onDrawDone(data: object): void {
if (data) {
@ -137,7 +106,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
cancelable: true,
detail: {
// eslint-disable-next-line new-cap
state: new this.controller.objectStateClass(data),
state: data,
},
});
@ -154,6 +123,32 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.controller.draw({
enabled: false,
});
this.mode = Mode.IDLE;
}
private onEditDone(state: any, points: number[]): void {
if (state && points) {
const event: CustomEvent = new CustomEvent('canvas.edited', {
bubbles: false,
cancelable: true,
detail: {
state,
points,
},
});
this.canvas.dispatchEvent(event);
} else {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
}
this.mode = Mode.IDLE;
}
private onMergeDone(objects: any[]): void {
@ -175,6 +170,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event);
}
this.controller.merge({
enabled: false,
});
this.mode = Mode.IDLE;
}
private onSplitDone(object: any): void {
@ -184,6 +185,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
cancelable: true,
detail: {
state: object,
frame: object.frame,
},
});
@ -196,16 +198,21 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event);
}
this.controller.split({
enabled: false,
});
this.mode = Mode.IDLE;
}
private onGroupDone(objects: any[], reset: boolean): void {
private onGroupDone(objects: any[]): void {
if (objects) {
const event: CustomEvent = new CustomEvent('canvas.groupped', {
bubbles: false,
cancelable: true,
detail: {
states: objects,
reset,
},
});
@ -218,6 +225,101 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.dispatchEvent(event);
}
this.controller.group({
enabled: false,
});
this.mode = Mode.IDLE;
}
private onFindObject(e: MouseEvent): void {
if (e.which === 1 || e.which === 0) {
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.find', {
bubbles: false,
cancelable: true,
detail: {
x,
y,
states: this.controller.objects,
},
});
this.canvas.dispatchEvent(event);
e.preventDefault();
}
}
private selectize(value: boolean, shape: SVG.Element): void {
const self = this;
function dblClickHandler(e: MouseEvent): void {
const pointID = Array.prototype.indexOf
.call((e.target as HTMLElement).parentElement.children, e.target);
if (self.activeElement) {
if (e.ctrlKey) {
const { points } = self.activeElement.state;
self.onEditDone(
self.activeElement.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,
state,
pointID,
});
}
}
}
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
pointSize: 2 * consts.BASE_POINT_SIZE / self.geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
.stroke('black')
.fill(shape.node.getAttribute('fill') || 'inherit')
.center(cx, cy)
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('dblclick', dblClickHandler);
circle.addClass('cvat_canvas_selected_point');
});
circle.node.addEventListener('mouseleave', (): void => {
circle.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.removeEventListener('dblclick', dblClickHandler);
circle.removeClass('cvat_canvas_selected_point');
});
return circle;
},
});
} else {
(shape as any).selectize(false, {
deepSelect: true,
});
}
}
public constructor(model: CanvasModel & Master, controller: CanvasController) {
@ -306,6 +408,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
};
this.controller.geometry = geometry;
this.geometry = geometry;
self.canvas.removeEventListener('animationstart', canvasFirstMounted);
}
};
@ -316,32 +419,45 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.drawHandler = new DrawHandlerImpl(
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),
this.onFindObject.bind(this),
this.adoptedContent,
);
this.splitHandler = new SplitHandlerImpl(
this.onSplitDone.bind(this),
this.onFindObject.bind(this),
this.adoptedContent,
);
this.groupHandler = new GroupHandlerImpl(
this.onGroupDone.bind(this),
(): any[] => this.controller.objects,
this.onFindObject.bind(this),
this.adoptedContent,
);
// Setup event handlers
this.content.addEventListener('dblclick', (): void => {
this.content.addEventListener('dblclick', (e: MouseEvent): void => {
if (e.ctrlKey || e.shiftKey) return;
self.controller.fit();
e.preventDefault();
});
this.content.addEventListener('mousedown', (event): void => {
if ((event.which === 1 && this.mode === Mode.IDLE) || (event.which === 2)) {
self.controller.enableDrag(event.clientX, event.clientY);
}
});
this.content.addEventListener('mousemove', (event): void => {
self.controller.drag(event.clientX, event.clientY);
event.preventDefault();
}
});
window.document.addEventListener('mouseup', (event): void => {
@ -357,7 +473,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
this.content.addEventListener('mousemove', (e): void => {
self.controller.drag(e.clientX, e.clientY);
if (this.mode !== Mode.IDLE) return;
if (e.ctrlKey || e.shiftKey) return;
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.moved', {
@ -378,24 +497,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
function transform(geometry: Geometry): void {
function transform(): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
obj.style.transform = `scale(${geometry.scale}) rotate(${geometry.angle}deg)`;
obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`;
}
// Transform grid
this.gridPath.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / (2 * geometry.scale)}px`);
this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / (this.geometry.scale)}px`);
// Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) {
element.setAttribute(
'stroke-width',
`${consts.BASE_STROKE_WIDTH / (3 * geometry.scale)}`,
`${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`,
);
element.setAttribute(
'r',
`${consts.BASE_POINT_SIZE / (2 * geometry.scale)}`,
`${consts.BASE_POINT_SIZE / this.geometry.scale}`,
);
}
@ -413,7 +532,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
const object = this.svgShapes[key];
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (geometry.scale),
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
}
@ -431,37 +550,39 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
// Transform handlers
this.drawHandler.transform(geometry);
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
}
function resize(geometry: Geometry): void {
function resize(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.width = `${geometry.image.width}px`;
obj.style.height = `${geometry.image.height}px`;
obj.style.width = `${this.geometry.image.width}px`;
obj.style.height = `${this.geometry.image.height}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.width = `${geometry.image.width + geometry.offset * 2}px`;
obj.style.height = `${geometry.image.height + geometry.offset * 2}px`;
obj.style.width = `${this.geometry.image.width + this.geometry.offset * 2}px`;
obj.style.height = `${this.geometry.image.height + this.geometry.offset * 2}px`;
}
}
function move(geometry: Geometry): void {
function move(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.top = `${geometry.top}px`;
obj.style.left = `${geometry.left}px`;
obj.style.top = `${this.geometry.top}px`;
obj.style.left = `${this.geometry.left}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.top = `${geometry.top - geometry.offset}px`;
obj.style.left = `${geometry.left - geometry.offset}px`;
obj.style.top = `${this.geometry.top - this.geometry.offset}px`;
obj.style.left = `${this.geometry.left - this.geometry.offset}px`;
}
// Transform handlers
this.drawHandler.transform(geometry);
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
}
function computeFocus(focusData: FocusData, geometry: Geometry): void {
function computeFocus(focusData: FocusData): void {
// This computation cann't be done in the model because of lack of data
const object = this.svgShapes[focusData.clientID];
if (!object) {
@ -471,23 +592,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
// First of all, compute and apply scale
let scale = null;
const bbox: SVG.BBox = object.node.getBBox();
if ((geometry.angle / 90) % 2) {
const bbox: SVG.BBox = object.bbox();
if ((this.geometry.angle / 90) % 2) {
// 90, 270, ..
scale = Math.min(Math.max(Math.min(
geometry.canvas.width / bbox.height,
geometry.canvas.height / bbox.width,
this.geometry.canvas.width / bbox.height,
this.geometry.canvas.height / bbox.width,
), FrameZoom.MIN), FrameZoom.MAX);
} else {
scale = Math.min(Math.max(Math.min(
geometry.canvas.width / bbox.width,
geometry.canvas.height / bbox.height,
this.geometry.canvas.width / bbox.width,
this.geometry.canvas.height / bbox.height,
), FrameZoom.MIN), FrameZoom.MAX);
}
transform.call(this, Object.assign({}, geometry, {
scale,
}));
this.geometry = { ...this.geometry, scale };
transform.call(this);
const [x, y] = translateFromSVG(this.content, [
bbox.x + bbox.width / 2,
@ -499,17 +619,19 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
];
const dragged = Object.assign({}, geometry, {
top: geometry.top + cy - y,
left: geometry.left + cx - x,
const dragged = {
...this.geometry,
top: this.geometry.top + cy - y,
left: this.geometry.left + cx - x,
scale,
});
};
this.controller.geometry = dragged;
move.call(this, dragged);
this.geometry = dragged;
move.call(this);
}
function setupObjects(objects: any[], geometry: Geometry): void {
function setupObjects(objects: any[]): void {
const ctm = this.content.getScreenCTM()
.inverse().multiply(this.background.getScreenCTM());
@ -529,51 +651,87 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.svgTexts = {};
this.svgShapes = {};
this.addObjects(ctm, objects, geometry);
this.addObjects(ctm, objects);
// TODO: Update objects
// TODO: Delete objects
this.mergeHandler.updateObjects(objects);
this.groupHandler.updateObjects(objects);
this.splitHandler.updateObjects(objects);
}
const { geometry } = this.controller;
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE) {
if (!model.image.length) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else {
this.loadingAnimation.classList.add('cvat_canvas_hidden');
this.background.style.backgroundImage = `url("${model.image}")`;
move.call(this, geometry);
resize.call(this, geometry);
transform.call(this, geometry);
move.call(this);
resize.call(this);
transform.call(this);
}
} else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) {
move.call(this, geometry);
transform.call(this, geometry);
move.call(this);
transform.call(this);
} else if (reason === UpdateReasons.MOVE) {
move.call(this, geometry);
move.call(this);
} else if (reason === UpdateReasons.OBJECTS) {
setupObjects.call(this, this.controller.objects, geometry);
setupObjects.call(this, this.controller.objects);
const event: CustomEvent = new CustomEvent('canvas.setup');
this.canvas.dispatchEvent(event);
} else if (reason === UpdateReasons.GRID) {
const size: Size = geometry.grid;
const size: Size = this.geometry.grid;
this.gridPattern.setAttribute('width', `${size.width}`);
this.gridPattern.setAttribute('height', `${size.height}`);
} else if (reason === UpdateReasons.FOCUS) {
computeFocus.call(this, this.controller.focusData, geometry);
computeFocus.call(this, this.controller.focusData);
} else if (reason === UpdateReasons.ACTIVATE) {
this.activate(geometry, this.controller.activeElement);
this.activate(this.controller.activeElement);
} else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData;
if (data.enabled) {
this.mode = Mode.DRAW;
this.deactivate();
} else {
this.mode = Mode.IDLE;
}
this.drawHandler.draw(data, geometry);
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) {
if (this.mode === Mode.MERGE) {
this.mergeHandler.select(this.controller.selected);
} else if (this.mode === Mode.SPLIT) {
this.splitHandler.select(this.controller.selected);
} else if (this.mode === Mode.GROUP) {
this.groupHandler.select(this.controller.selected);
}
} else if (reason === UpdateReasons.CANCEL) {
if (this.mode === Mode.DRAW) {
this.drawHandler.cancel();
} else if (this.mode === Mode.MERGE) {
this.mergeHandler.cancel();
} else if (this.mode === Mode.SPLIT) {
this.splitHandler.cancel();
} else if (this.mode === Mode.GROUP) {
this.groupHandler.cancel();
} else if (this.mode === Mode.EDIT) {
this.editHandler.cancel();
}
}
}
@ -581,10 +739,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.canvas;
}
private addObjects(ctm: SVGMatrix, states: any[], geometry: Geometry): void {
private addObjects(ctm: SVGMatrix, states: any[]): void {
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state, geometry);
this.addTag(state);
} else {
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = [];
@ -599,7 +757,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
this.svgShapes[state.clientID] = this
.addRect(translatedPoints, state, geometry);
.addRect(translatedPoints, state);
} else {
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
@ -613,13 +771,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (state.shapeType === 'polygon') {
this.svgShapes[state.clientID] = this
.addPolygon(stringified, state, geometry);
.addPolygon(stringified, state);
} else if (state.shapeType === 'polyline') {
this.svgShapes[state.clientID] = this
.addPolyline(stringified, state, geometry);
.addPolyline(stringified, state);
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this
.addPoints(stringified, state, geometry);
.addPoints(stringified, state);
}
}
@ -639,10 +797,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (this.activeElement) {
const { state } = this.activeElement;
const shape = this.svgShapes[this.activeElement.state.clientID];
shape.removeClass('cvat_canvas_shape_activated');
(shape as any).draggable(false);
if (state.shapeType !== 'points') {
selectize(false, shape, null);
this.selectize(false, shape);
}
(shape as any).resize(false);
@ -657,7 +817,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private activate(geometry: Geometry, activeElement: ActiveElement): void {
private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement) {
// Check if it is the same element
@ -677,6 +837,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
};
const shape = this.svgShapes[activeElement.clientID];
shape.addClass('cvat_canvas_shape_activated');
let text = this.svgTexts[activeElement.clientID];
// Draw text if it's hidden by default
if (!text && state.visibility === 'shape') {
@ -695,8 +856,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('dragend', (): void => {
this.mode = Mode.IDLE;
}).on('dragend', (e: CustomEvent): void => {
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
@ -704,19 +864,48 @@ export class CanvasViewImpl implements CanvasView, Listener {
shape,
);
}
this.mode = Mode.IDLE;
const p1 = e.detail.handler.startPoints.point;
const p2 = e.detail.p;
const delta = 1;
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')}`,
);
this.onEditDone(state, translateBetweenSVG(this.content, this.background, points));
}
});
if (state.shapeType !== 'points') {
selectize(true, shape, geometry);
this.selectize(true, shape);
}
let shapeSizeElement: ShapeSizeElement = null;
let resized = false;
(shape as any).resize().on('resizestart', (): void => {
this.mode = Mode.RESIZE;
if (state.shapeType === 'rectangle') {
shapeSizeElement = displayShapeSize(this.adoptedContent, this.adoptedText);
}
resized = false;
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('resizing', (): void => {
resized = true;
if (shapeSizeElement) {
shapeSizeElement.update(shape);
}
}).on('resizedone', (): void => {
this.mode = Mode.IDLE;
if (shapeSizeElement) {
shapeSizeElement.rm();
}
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
@ -724,6 +913,18 @@ export class CanvasViewImpl implements CanvasView, Listener {
shape,
);
}
this.mode = Mode.IDLE;
if (resized) {
const points = pointsToArray(
shape.attr('points') || `${shape.attr('x')},${shape.attr('y')} `
+ `${shape.attr('x') + shape.attr('width')},`
+ `${shape.attr('y') + shape.attr('height')}`,
);
this.onEditDone(state, translateBetweenSVG(this.content, this.background, points));
}
});
}
@ -756,7 +957,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
// Translate back to text SVG
const [x, y]: number[] = translateToSVG(this.text, [
clientX + consts.TEXT_MARGIN,
clientY,
clientY + consts.TEXT_MARGIN,
]);
// Finally draw a text
@ -785,66 +986,101 @@ export class CanvasViewImpl implements CanvasView, Listener {
}).move(0, 0).addClass('cvat_canvas_text');
}
private addRect(points: number[], state: any, geometry: Geometry): SVG.Rect {
private addRect(points: number[], state: any): SVG.Rect {
const [xtl, ytl, xbr, ybr] = points;
return this.adoptedContent.rect().size(xbr - xtl, ybr - ytl).attr({
const rect = this.adoptedContent.rect().size(xbr - xtl, ybr - ytl).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
}).move(xtl, ytl)
.addClass('cvat_canvas_shape');
if (state.occluded) {
rect.addClass('cvat_canvas_shape_occluded');
}
return rect;
}
private addPolygon(points: string, state: any, geometry: Geometry): SVG.Polygon {
return this.adoptedContent.polygon(points).attr({
private addPolygon(points: string, state: any): SVG.Polygon {
const polygon = this.adoptedContent.polygon(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
if (state.occluded) {
polygon.addClass('cvat_canvas_shape_occluded');
}
return polygon;
}
private addPolyline(points: string, state: any, geometry: Geometry): SVG.PolyLine {
return this.adoptedContent.polyline(points).attr({
private addPolyline(points: string, state: any): SVG.PolyLine {
const polyline = this.adoptedContent.polyline(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
if (state.occluded) {
polyline.addClass('cvat_canvas_shape_occluded');
}
return polyline;
}
private addPoints(points: string, state: any, geometry: Geometry): SVG.PolyLine {
private addPoints(points: string, state: any): SVG.PolyLine {
const shape = this.adoptedContent.polyline(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
fill: state.color,
'pointer-events': 'none',
'shape-rendering': 'geometricprecision',
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
'stroke-width': 0,
fill: state.color, // to right fill property when call SVG.Shape::clone()
}).style({
opacity: 0,
});
this.selectize(true, shape);
selectize(true, shape, geometry);
shape.remove = function remove(): void {
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,
});
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);
}.bind(this);
shape.attr('fill', 'none');
return shape;
};
return shape;
}
/* eslint-disable-next-line */
private addTag(state: any, geometry: Geometry): void {
console.log(state, geometry);
private addTag(state: any): void {
console.log(state);
}
}

@ -4,15 +4,21 @@
*/
const BASE_STROKE_WIDTH = 2;
const BASE_POINT_SIZE = 8;
const BASE_GRID_WIDTH = 1;
const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
const POINTS_STROKE_WIDTH = 1.5;
const POINTS_SELECTED_STROKE_WIDTH = 4;
export default {
BASE_STROKE_WIDTH,
BASE_GRID_WIDTH,
BASE_POINT_SIZE,
TEXT_MARGIN,
AREA_THRESHOLD,
SIZE_THRESHOLD,
POINTS_STROKE_WIDTH,
POINTS_SELECTED_STROKE_WIDTH,
};

@ -0,0 +1,568 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import consts from './consts';
import 'svg.draw.js';
import './svg.patch';
import {
DrawData,
Geometry,
} from './canvasModel';
import {
translateToSVG,
translateBetweenSVG,
displayShapeSize,
ShapeSizeElement,
pointsToString,
pointsToArray,
BBox,
Box,
} from './shared';
export interface DrawHandler {
draw(drawData: DrawData, geometry: Geometry): void;
cancel(): void;
}
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object) => void;
private canvas: SVG.Container;
private text: SVG.Container;
private background: SVGSVGElement;
private crosshair: {
x: SVG.Line;
y: SVG.Line;
};
private drawData: DrawData;
private geometry: Geometry;
// 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 shapeSizeElement: ShapeSizeElement;
private getFinalRectCoordinates(bbox: BBox): number[] {
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
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],
);
xtl = Math.min(Math.max(xtl, 0), frameWidth);
xbr = Math.min(Math.max(xbr, 0), frameWidth);
ytl = Math.min(Math.max(ytl, 0), frameHeight);
ybr = Math.min(Math.max(ybr, 0), frameHeight);
return [xtl, ytl, xbr, ybr];
}
private getFinalPolyshapeCoordinates(targetPoints: number[]): {
points: number[];
box: Box;
} {
const points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
targetPoints,
);
const box = {
xtl: Number.MAX_SAFE_INTEGER,
ytl: Number.MAX_SAFE_INTEGER,
xbr: Number.MAX_SAFE_INTEGER,
ybr: Number.MAX_SAFE_INTEGER,
};
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
for (let i = 0; i < points.length - 1; i += 2) {
points[i] = Math.min(Math.max(points[i], 0), frameWidth);
points[i + 1] = Math.min(Math.max(points[i + 1], 0), frameHeight);
box.xtl = Math.min(box.xtl, points[i]);
box.ytl = Math.min(box.ytl, points[i + 1]);
box.xbr = Math.max(box.xbr, points[i]);
box.ybr = Math.max(box.ybr, points[i + 1]);
}
return {
points,
box,
};
}
private addCrosshair(): void {
this.crosshair = {
x: this.canvas.line(0, 0, this.canvas.node.clientWidth, 0).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
y: this.canvas.line(0, 0, 0, this.canvas.node.clientHeight).attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * this.geometry.scale),
zOrder: Number.MAX_SAFE_INTEGER,
}).addClass('cvat_canvas_crosshair'),
};
}
private removeCrosshair(): void {
this.crosshair.x.remove();
this.crosshair.y.remove();
this.crosshair = null;
}
private release(): void {
this.canvas.off('mousedown.draw');
this.canvas.off('mousemove.draw');
this.canvas.off('click.draw');
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');
}
this.drawInstance.remove();
this.drawInstance = null;
}
if (this.shapeSizeElement) {
this.shapeSizeElement.rm();
this.shapeSizeElement = null;
}
if (this.crosshair) {
this.removeCrosshair();
}
}
private initDrawing(): void {
if (this.drawData.crosshair) {
this.addCrosshair();
}
}
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') {
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.draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
z_order: Number.MAX_SAFE_INTEGER,
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
}).on('drawstop', (e: Event): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
if ((xbr - xtl) * (ybr - ytl) >= consts.AREA_THRESHOLD) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points: [xtl, ytl, xbr, ybr],
});
} else {
this.onDrawDone(null);
}
});
}
private drawPolyshape(): void {
this.drawInstance.attr({
z_order: Number.MAX_SAFE_INTEGER,
});
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
this.drawInstance.draw('done');
}
}.bind(this);
const sizeIncrement = function sizeIncrement(): void {
size++;
};
if (this.drawData.numberOfPoints) {
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', sizeIncrement);
}
// Add ability to cancel the latest drawn point
const handleUndo = function handleUndo(e: MouseEvent): void {
if (e.which === 3) {
e.stopPropagation();
e.preventDefault();
this.drawInstance.draw('undo');
}
}.bind(this);
this.canvas.on('mousedown.draw', handleUndo);
// Add ability to draw shapes by sliding
// We need to remember last drawn point
// to implementation of slide drawing
const lastDrawnPoint: {
x: number;
y: number;
} = {
x: null,
y: null,
};
const handleSlide = function handleSlide(e: MouseEvent): void {
// TODO: Use enumeration after typification cvat-core
if (e.shiftKey && ['polygon', 'polyline'].includes(this.drawData.shapeType)) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.drawInstance.draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) {
this.drawInstance.draw('point', e);
}
}
}
}.bind(this);
this.canvas.on('mousemove.draw', handleSlide);
// We need scale just drawn points
const self = this;
this.drawInstance.on('drawstart drawpoint', (e: CustomEvent): void => {
self.transform(self.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
});
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const {
points,
box,
} = this.getFinalPolyshapeCoordinates(targetPoints);
if (this.drawData.shapeType === 'polygon'
&& ((box.xbr - box.xtl) * (box.ybr - box.ytl) >= consts.AREA_THRESHOLD)
&& points.length >= 3 * 2) {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
});
} else if (this.drawData.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,
points,
});
} else if (this.drawData.shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
});
} else {
this.onDrawDone(null);
}
});
}
private drawPolygon(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.drawPolyshape();
}
private drawPolyline(): void {
this.drawInstance = (this.canvas as any).polyline().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': 0,
});
this.drawPolyshape();
}
private drawPoints(): void {
this.drawInstance = (this.canvas as any).polygon().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
opacity: 0,
});
this.drawPolyshape();
}
private pastePolyshape(): void {
this.canvas.on('click.draw', (e: MouseEvent): void => {
const targetPoints = (e.target as SVGElement)
.getAttribute('points')
.split(/[,\s]/g)
.map((coord): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
this.onDrawDone({
shapeType: this.drawData.shapeType,
points,
occluded: this.drawData.initialState.occluded,
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
});
});
}
// Common settings for rectangle and polyshapes
private pasteShape(): void {
this.drawInstance.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);
});
}
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({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.canvas.on('click.draw', (e: MouseEvent): void => {
const bbox = (e.target as SVGRectElement).getBBox();
const [xtl, ytl, xbr, ybr] = this.getFinalRectCoordinates(bbox);
this.release();
this.onDrawDone({
shapeType: this.drawData.shapeType,
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,
});
});
}
private pastePolygon(points: string): void {
this.drawInstance = (this.canvas as any).polygon(points)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.pastePolyshape();
}
private pastePolyline(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.pasteShape();
this.pastePolyshape();
}
private pastePoints(points: string): void {
this.drawInstance = (this.canvas as any).polyline(points)
.addClass('cvat_canvas_shape_drawing').style({
'stroke-width': 0,
});
this.pasteShape();
this.pastePolyshape();
}
private startDraw(): void {
// TODO: Use enums after typification cvat-core
if (this.drawData.initialState) {
if (this.drawData.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translateBetweenSVG(
this.background,
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
this.pasteBox({
x: xtl,
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
});
} else {
const points = translateBetweenSVG(
this.background,
this.canvas.node as any as SVGSVGElement,
this.drawData.initialState.points,
);
const stringifiedPoints = pointsToString(points);
if (this.drawData.shapeType === 'polygon') {
this.pastePolygon(stringifiedPoints);
} else if (this.drawData.shapeType === 'polyline') {
this.pastePolyline(stringifiedPoints);
} else if (this.drawData.shapeType === 'points') {
this.pastePoints(stringifiedPoints);
}
}
} else if (this.drawData.shapeType === 'rectangle') {
this.drawBox();
// Draw instance was initialized after drawBox();
this.shapeSizeElement = displayShapeSize(this.canvas, this.text);
} else if (this.drawData.shapeType === 'polygon') {
this.drawPolygon();
} else if (this.drawData.shapeType === 'polyline') {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
}
}
public constructor(
onDrawDone: (data: object) => 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;
this.drawInstance = null;
this.canvas.on('mousemove.crosshair', (e: MouseEvent): void => {
if (this.crosshair) {
const [x, y] = translateToSVG(
this.canvas.node as any as SVGSVGElement,
[e.clientX, e.clientY],
);
this.crosshair.x.attr({
y1: y,
y2: y,
});
this.crosshair.y.attr({
x1: x,
x2: x,
});
}
});
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.shapeSizeElement && this.drawInstance && this.drawData.shapeType === 'rectangle') {
this.shapeSizeElement.update(this.drawInstance);
}
if (this.crosshair) {
this.crosshair.x.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
this.crosshair.y.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * geometry.scale),
});
}
if (this.drawInstance) {
this.drawInstance.draw('transform');
this.drawInstance.style({
'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(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
}
}
}
public draw(drawData: DrawData, geometry: Geometry): void {
this.geometry = geometry;
if (drawData.enabled) {
this.drawData = drawData;
this.initDrawing();
this.startDraw();
} else {
this.closeDrawing();
this.drawData = drawData;
}
}
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
}
}

@ -0,0 +1,343 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import 'svg.select.js';
import consts from './consts';
import {
translateFromSVG,
translateBetweenSVG,
pointsToArray,
} from './shared';
import {
EditData,
Geometry,
} from './canvasModel';
export interface EditHandler {
edit(editData: EditData): void;
transform(geometry: Geometry): void;
cancel(): void;
}
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;
private clones: SVG.Polygon[];
private startEdit(): void {
// get started coordinates
const [clientX, clientY] = translateFromSVG(
this.canvas.node as any as SVGSVGElement,
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
);
// Add ability to edit shapes by sliding
// We need to remember last drawn point
// to implementation of slide drawing
const lastDrawnPoint: {
x: number;
y: number;
} = {
x: null,
y: null,
};
const handleSlide = function handleSlide(e: MouseEvent): void {
if (e.shiftKey) {
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
this.editLine.draw('point', e);
} else {
const deltaTreshold = 15;
const delta = Math.sqrt(
((e.clientX - lastDrawnPoint.x) ** 2)
+ ((e.clientY - lastDrawnPoint.y) ** 2),
);
if (delta > deltaTreshold) {
this.editLine.draw('point', e);
}
}
}
}.bind(this);
this.canvas.on('mousemove.draw', handleSlide);
this.editLine = (this.canvas as any).polyline().draw({
snapToGrid: 0.1,
}).addClass('cvat_canvas_shape_drawing').style({
'pointer-events': 'none',
'fill-opacity': 0,
}).on('drawstart drawpoint', (e: CustomEvent): void => {
this.transform(this.geometry);
lastDrawnPoint.x = e.detail.event.clientX;
lastDrawnPoint.y = e.detail.event.clientY;
});
if (this.editData.state.shapeType === 'points') {
this.editLine.style('stroke-width', 0);
} else {
// generate mouse event
const dummyEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
clientX,
clientY,
});
(this.editLine as any).draw('point', dummyEvent);
}
}
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);
}
if (!this.editLine) {
return;
}
// Get stop point and all points
const stopPointID = Array.prototype.indexOf
.call((e.target as HTMLElement).parentElement.children, e.target);
const oldPoints = this.editedShape.attr('points').trim().split(' ');
const linePoints = this.editLine.attr('points').trim().split(' ');
if (this.editLine.attr('points') === '0,0') {
this.cancel();
return;
}
// Compute new point array
const [start, stop] = [this.editData.pointID, stopPointID]
.sort((a, b): number => +a - +b);
if (this.editData.state.shapeType === 'polygon') {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
const firstPart = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
linePoints.reverse();
const secondPart = oldPoints.slice(start + 1, stop)
.concat(linePoints);
if (firstPart.length < 3 || secondPart.length < 3) {
this.cancel();
return;
}
for (const points of [firstPart, secondPart]) {
this.clones.push(this.canvas.polygon(points.join(' '))
.attr('fill', this.editedShape.attr('fill'))
.style('fill-opacity', '0.5')
.addClass('cvat_canvas_shape'));
}
for (const clone of this.clones) {
clone.on('click', selectPolygon.bind(this, clone));
clone.on('mouseenter', (): void => {
clone.addClass('cvat_canvas_shape_splitting');
}).on('mouseleave', (): void => {
clone.removeClass('cvat_canvas_shape_splitting');
});
}
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
return;
}
let points = null;
if (this.editData.state.shapeType === 'polyline') {
if (start !== this.editData.pointID) {
linePoints.reverse();
}
points = oldPoints.slice(0, start)
.concat(linePoints)
.concat(oldPoints.slice(stop + 1));
} else {
points = oldPoints.concat(linePoints.slice(0, -1));
}
points = translateBetweenSVG(
this.canvas.node as any as SVGSVGElement,
this.background,
pointsToArray(points.join(' ')),
);
const { state } = this.editData;
this.edit({
enabled: false,
});
this.onEditDone(state, points);
}
private setupPoints(enabled: boolean): void {
const self = this;
const stopEdit = self.stopEdit.bind(self);
if (enabled) {
(this.editedShape as any).selectize(true, {
deepSelect: true,
pointSize: 2 * consts.BASE_POINT_SIZE / self.geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
.stroke('black')
.fill(self.editedShape.attr('fill') || 'inherit')
.center(cx, cy)
.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('mouseenter', (): void => {
circle.attr({
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / self.geometry.scale,
});
circle.node.addEventListener('click', stopEdit);
circle.addClass('cvat_canvas_selected_point');
});
circle.node.addEventListener('mouseleave', (): void => {
circle.attr({
'stroke-width': consts.POINTS_STROKE_WIDTH / self.geometry.scale,
});
circle.node.removeEventListener('click', stopEdit);
circle.removeClass('cvat_canvas_selected_point');
});
return circle;
},
});
} else {
(this.editedShape as any).selectize(false, {
deepSelect: true,
});
}
}
private release(): void {
this.canvas.off('mousemove.draw');
if (this.editedShape) {
this.setupPoints(false);
this.editedShape.remove();
this.editedShape = null;
}
if (this.editLine) {
(this.editLine as any).draw('stop');
this.editLine.remove();
this.editLine = null;
}
if (this.clones.length) {
for (const clone of this.clones) {
clone.remove();
}
this.clones = [];
}
}
private initEditing(): void {
this.editedShape = this.canvas
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`)
.first().clone();
this.setupPoints(true);
this.startEdit();
// draw points for this with selected and start editing till another point is clicked
// click one of two parts to remove (in case of polygon only)
// else we can start draw polyline
// after we have got shape and points, we are waiting for second point pressed on this shape
}
private closeEditing(): void {
this.release();
}
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;
this.geometry = null;
this.clones = [];
}
public edit(editData: any): void {
if (editData.enabled) {
if (editData.state.shapeType !== 'rectangle') {
this.editData = editData;
this.initEditing();
} else {
this.cancel();
}
} else {
this.closeEditing();
this.editData = editData;
}
}
public cancel(): void {
this.release();
this.onEditDone(null, null);
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.editLine) {
(this.editLine as any).draw('transform');
if (this.editData.state.shapeType !== 'points') {
this.editLine.style({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
const paintHandler = this.editLine.remember('_paintHandler');
for (const point of (paintHandler as any).set.members) {
point.style(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / geometry.scale}`,
);
point.attr(
'r',
`${consts.BASE_POINT_SIZE / geometry.scale}`,
);
}
}
}
}

@ -0,0 +1,208 @@
import * as SVG from 'svg.js';
import { GroupData } from './canvasModel';
import {
translateToSVG,
} from './shared';
export interface GroupHandler {
group(groupData: GroupData): void;
select(state: any): void;
cancel(): void;
}
export class GroupHandlerImpl implements GroupHandler {
// callback is used to notify about grouping end
private onGroupDone: (objects: any[]) => void;
private getStates: () => any[];
private onFindObject: (event: MouseEvent) => void;
private onSelectStart: (event: MouseEvent) => void;
private onSelectUpdate: (event: MouseEvent) => void;
private onSelectStop: (event: MouseEvent) => void;
private selectionRect: SVG.Rect;
private startSelectionPoint: {
x: number;
y: number;
};
private canvas: SVG.Container;
private initialized: boolean;
private states: any[];
private highlightedShapes: Record<number, SVG.Shape>;
private getSelectionBox(event: MouseEvent): {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
} {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const stopSelectionPoint = {
x: point[0],
y: point[1],
};
return {
xtl: Math.min(this.startSelectionPoint.x, stopSelectionPoint.x),
ytl: Math.min(this.startSelectionPoint.y, stopSelectionPoint.y),
xbr: Math.max(this.startSelectionPoint.x, stopSelectionPoint.x),
ybr: Math.max(this.startSelectionPoint.y, stopSelectionPoint.y),
};
}
private release(): void {
this.canvas.node.removeEventListener('click', this.onFindObject);
this.canvas.node.removeEventListener('mousedown', this.onSelectStart);
this.canvas.node.removeEventListener('mousemove', this.onSelectUpdate);
this.canvas.node.removeEventListener('mouseup', this.onSelectStop);
this.canvas.node.removeEventListener('mouseleave', this.onSelectStop);
for (const state of this.states) {
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
this.states = [];
this.highlightedShapes = {};
this.initialized = false;
this.selectionRect = null;
this.startSelectionPoint = {
x: null,
y: null,
};
}
private initGrouping(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
this.canvas.node.addEventListener('mousedown', this.onSelectStart);
this.canvas.node.addEventListener('mousemove', this.onSelectUpdate);
this.canvas.node.addEventListener('mouseup', this.onSelectStop);
this.canvas.node.addEventListener('mouseleave', this.onSelectStop);
this.initialized = true;
}
private closeGrouping(): void {
if (this.initialized) {
const { states } = this;
this.release();
if (states.length) {
this.onGroupDone(states);
} else {
this.onGroupDone(null);
}
}
}
public constructor(
onGroupDone: (objects: any[]) => void,
getStates: () => any[],
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onGroupDone = onGroupDone;
this.getStates = getStates;
this.onFindObject = onFindObject;
this.canvas = canvas;
this.states = [];
this.highlightedShapes = {};
this.selectionRect = null;
this.startSelectionPoint = {
x: null,
y: null,
};
this.onSelectStart = function (event: MouseEvent): void {
if (!this.selectionRect) {
const point = translateToSVG(this.canvas.node, [event.clientX, event.clientY]);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
}.bind(this);
this.onSelectUpdate = function (event: MouseEvent): void {
// called on mousemove
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.attr({
x: box.xtl,
y: box.ytl,
width: box.xbr - box.xtl,
height: box.ybr - box.ytl,
});
}
}.bind(this);
this.onSelectStop = function (event: MouseEvent): void {
// called on mouseup, mouseleave
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
if (objectState) {
this.states.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
}
}
}
}.bind(this);
}
/* eslint-disable-next-line */
public group(groupData: GroupData): void {
if (groupData.enabled) {
this.initGrouping();
} else {
this.closeGrouping();
}
}
public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID);
const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1);
if (shape) {
delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
} else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape) {
this.states.push(objectState);
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_grouping');
}
}
}
public cancel(): void {
this.release();
this.onGroupDone(null);
}
}

@ -0,0 +1,133 @@
import * as SVG from 'svg.js';
import { MergeData } from './canvasModel';
export interface MergeHandler {
merge(mergeData: MergeData): void;
select(state: any): void;
cancel(): void;
}
export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end
private onMergeDone: (objects: any[]) => void;
private onFindObject: (event: MouseEvent) => void;
private canvas: SVG.Container;
private initialized: boolean;
private states: any[]; // are being merged
private highlightedShapes: Record<number, SVG.Shape>;
private constraints: {
labelID: number;
shapeType: string;
};
private addConstraints(): void {
const shape = this.states[0];
this.constraints = {
labelID: shape.label.id,
shapeType: shape.shapeType,
};
}
private removeConstraints(): void {
this.constraints = null;
}
private checkConstraints(state: any): boolean {
return !this.constraints || (state.label.id === this.constraints.labelID
&& state.shapeType === this.constraints.shapeType);
}
private release(): void {
this.removeConstraints();
this.canvas.node.removeEventListener('click', this.onFindObject);
for (const state of this.states) {
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_merging');
}
this.states = [];
this.highlightedShapes = {};
this.initialized = false;
}
private initMerging(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
this.initialized = true;
}
private closeMerging(): void {
if (this.initialized) {
const { states } = this;
this.release();
if (states.length > 1) {
this.onMergeDone(states);
} else {
this.onMergeDone(null);
// here is a cycle
// onMergeDone => controller => model => view => closeMerging
// one call of closeMerging is unuseful, but it's okey
}
}
}
public constructor(
onMergeDone: (objects: any[]) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject;
this.canvas = canvas;
this.states = [];
this.highlightedShapes = {};
this.constraints = null;
this.initialized = false;
}
public merge(mergeData: MergeData): void {
if (mergeData.enabled) {
this.initMerging();
} else {
this.closeMerging();
}
}
public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID);
const stateFrames = this.states.map((state): number => state.frame);
const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1);
if (shape) {
delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_merging');
}
if (!this.states.length) {
this.removeConstraints();
}
} else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape && this.checkConstraints(objectState)
&& !stateFrames.includes(objectState.frame)) {
this.states.push(objectState);
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging');
if (this.states.length === 1) {
this.addConstraints();
}
}
}
}
public cancel(): void {
this.release();
this.onMergeDone(null);
// here is a cycle
// onMergeDone => controller => model => view => closeMerging
// one call of closeMerging is unuseful, but it's okey
}
}

@ -0,0 +1,113 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
import consts from './consts';
export interface ShapeSizeElement {
sizeElement: any;
update(shape: SVG.Shape): void;
rm(): void;
}
export interface Box {
xtl: number;
ytl: number;
xbr: number;
ybr: number;
}
export interface BBox {
width: number;
height: number;
x: number;
y: number;
}
// Translate point array from the client coordinate system
// to a coordinate system of a canvas
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = [];
const transformationMatrix = svg.getScreenCTM();
let pt = svg.createSVGPoint();
for (let i = 0; i < points.length - 1; i += 2) {
pt.x = points[i];
pt.y = points[i + 1];
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
return output;
}
// Translate point array from a coordinate system of a canvas
// to the client coordinate system
export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = [];
const transformationMatrix = svg.getScreenCTM().inverse();
let pt = svg.createSVGPoint();
for (let i = 0; i < points.length; i += 2) {
pt.x = points[i];
pt.y = points[i + 1];
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
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) {
return `${acc},${val}`;
}
return `${acc} ${val}`;
}, '');
}
export function pointsToArray(points: string): number[] {
return points.trim().split(/[,\s]+/g)
.map((coord: string): number => +coord);
}
export function displayShapeSize(
shapesContainer: SVG.Container,
textContainer: SVG.Container,
): ShapeSizeElement {
const shapeSize: ShapeSizeElement = {
sizeElement: textContainer.text('').font({
weight: 'bolder',
}).fill('white').addClass('cvat_canvas_text'),
update(shape: SVG.Shape): void{
const bbox = shape.bbox();
const text = `${bbox.width.toFixed(1)}x${bbox.height.toFixed(1)}`;
const [x, y]: number[] = translateToSVG(
textContainer.node as any as SVGSVGElement,
translateFromSVG((shapesContainer.node as any as SVGSVGElement), [bbox.x, bbox.y]),
);
this.sizeElement.clear().plain(text)
.move(x + consts.TEXT_MARGIN, y + consts.TEXT_MARGIN);
},
rm(): void {
if (this.sizeElement) {
this.sizeElement.remove();
this.sizeElement = null;
}
},
};
return shapeSize;
}

@ -0,0 +1,98 @@
import * as SVG from 'svg.js';
import { SplitData } from './canvasModel';
export interface SplitHandler {
split(splitData: SplitData): void;
select(state: any): void;
cancel(): void;
}
export class SplitHandlerImpl implements SplitHandler {
// callback is used to notify about splitting end
private onSplitDone: (object: any) => void;
private onFindObject: (event: MouseEvent) => void;
private canvas: SVG.Container;
private highlightedShape: SVG.Shape;
private initialized: boolean;
private splitDone: boolean;
private resetShape(): void {
if (this.highlightedShape) {
this.highlightedShape.removeClass('cvat_canvas_shape_splitting');
this.highlightedShape.off('click.split');
this.highlightedShape = null;
}
}
private release(): void {
if (this.initialized) {
this.resetShape();
this.canvas.node.removeEventListener('mousemove', this.onFindObject);
this.initialized = false;
}
}
private initSplitting(): void {
this.canvas.node.addEventListener('mousemove', this.onFindObject);
this.initialized = true;
this.splitDone = false;
}
private closeSplitting(): void {
// Split done is true if an object was splitted
// Split also can be called with { enabled: false } without splitting an object
if (!this.splitDone) {
this.onSplitDone(null);
}
this.release();
}
public constructor(
onSplitDone: (object: any) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onSplitDone = onSplitDone;
this.onFindObject = onFindObject;
this.canvas = canvas;
this.highlightedShape = null;
this.initialized = false;
this.splitDone = false;
}
public split(splitData: SplitData): void {
if (splitData.enabled) {
this.initSplitting();
} else {
this.closeSplitting();
}
}
public select(state: any): void {
if (state.objectType === 'track') {
const shape = this.canvas.select(`#cvat_canvas_shape_${state.clientID}`).first();
if (shape && shape !== this.highlightedShape) {
this.resetShape();
this.highlightedShape = shape;
this.highlightedShape.addClass('cvat_canvas_shape_splitting');
this.canvas.node.append(this.highlightedShape.node);
this.highlightedShape.on('click.split', (): void => {
this.splitDone = true;
this.onSplitDone(state);
}, {
once: true,
});
}
} else {
this.resetShape();
}
}
public cancel(): void {
this.release();
this.onSplitDone(null);
// here is a cycle
// onSplitDone => controller => model => view => closeSplitting
// one call of closeMerging is unuseful, but it's okey
}
}

@ -138,7 +138,7 @@ SVG.Element.prototype.draggable = function constructor(...args: any): any {
handler = this.remember('_draggable');
handler.drag = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
handler.constructor.prototype.drag.call(this, e);
return handler.constructor.prototype.drag.call(this, e);
}
} else {
originalDraggable.call(this, ...args);
@ -159,7 +159,7 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
handler = this.remember('_resizeHandler');
handler.update = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
handler.constructor.prototype.update.call(this, e);
return handler.constructor.prototype.update.call(this, e);
}
} else {
originalResize.call(this, ...args);

@ -14,6 +14,6 @@
}
},
"include": [
"src/*.ts"
"src/typescript/*.ts"
]
}

@ -6,7 +6,7 @@ const nodeConfig = {
target: 'node',
mode: 'production',
devtool: 'source-map',
entry: './src/canvas.ts',
entry: './src/typescript/canvas.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.node.js',
@ -29,6 +29,9 @@ const nodeConfig = {
sourceType: 'unambiguous',
},
},
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}],
},
plugins: [
@ -44,7 +47,7 @@ const webConfig = {
target: 'web',
mode: 'production',
devtool: 'source-map',
entry: './src/canvas.ts',
entry: './src/typescript/canvas.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-canvas.js',
@ -73,6 +76,9 @@ const webConfig = {
sourceType: 'unambiguous',
},
},
}, {
test: /\.css$/,
use: ['style-loader', 'css-loader']
}],
},
plugins: [

@ -20,7 +20,9 @@
* @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState;
* Necessary fields: objectType, shapeType
* (don't have setters)
* Necessary fields for objects which haven't been added to collection yet: frame
* (doesn't have setters)
* Optional fields: points, group, zOrder, outside, occluded,
* attributes, lock, label, mode, color, keyframe, clientID, serverID
* These fields can be set later via setters
@ -378,7 +380,7 @@
}
}
// Default implementation saves element in collection
// Updates element in collection which contains it
ObjectState.prototype.save.implementation = async function () {
if (this.hidden && this.hidden.save) {
return this.hidden.save();
@ -387,7 +389,7 @@
return this;
};
// Default implementation do nothing
// Delete element from a collection which contains it
ObjectState.prototype.delete.implementation = async function (force) {
if (this.hidden && this.hidden.delete) {
return this.hidden.delete(force);

Loading…
Cancel
Save