CVAT-Canvas Updates (#653)

main
Boris Sekachev 7 years ago committed by Nikita Manovich
parent 15df1daf4f
commit 43004c7969

@ -1,5 +1,5 @@
{
"python.pythonPath": "python",
"python.pythonPath": ".env/bin/python",
"eslint.enable": true,
"eslint.validate": [
"javascript",

@ -26,7 +26,7 @@ Canvas is created by using constructor:
```js
const { Canvas } = require('./canvas');
const canvas = new Canvas();
const canvas = new Canvas(ObjectStateClass);
```
- Canvas has transparent background
@ -45,6 +45,14 @@ Canvas itself handles:
All methods are sync.
```ts
interface DrawData {
enabled: boolean;
shapeType?: string;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
}
html(): HTMLDivElement;
setup(frameData: FrameData, objectStates: ObjectState): void;
activate(clientID: number, attributeID?: number): void;
@ -53,25 +61,27 @@ All methods are sync.
fit(): void;
grid(stepX: number, stepY: number): void;
draw(enabled?: boolean, shapeType?: string, numberOfPoints?: number, initialState?: any): void | ObjectState;
split(enabled?: boolean): void | ObjectState;
group(enabled?: boolean): void | ObjectState;
merge(enabled?: boolean): void | ObjectState;
draw(drawData: DrawData): void;
split(enabled?: boolean): void;
group(enabled?: boolean): void;
merge(enabled?: boolean): void;
cancel(): any;
```
### CSS Classes/IDs
- Each drawn object (tag, shape, track) has id ```cvat_canvas_object_{objectState.id}```
- All drawn objects (shapes, tracks) have an id ```cvat_canvas_object_{objectState.id}```
- 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```
- Tags has a class ```cvat_canvas_tag```
- Drawn texts have the class ```cvat_canvas_text```
- Tags have the class ```cvat_canvas_tag```
- Canvas image has ID ```cvat_canvas_image```
- Grid on the canvas has ID ```cvat_canvas_grid_pattern```
- Crosshair during a draw has class ```cvat_canvas_crosshair```
### Events
@ -86,6 +96,7 @@ Standard JS events are used.
- canvas.splitted => ObjectState
- canvas.groupped => [ObjectState]
- canvas.merged => [ObjectState]
- canvas.canceled
```
## States
@ -94,7 +105,7 @@ Standard JS events are used.
## API Reaction
| | FREE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING |
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING |
|------------|------|----------|-----------|---------|---------|---------|
| html() | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - |

@ -12,6 +12,21 @@ polyline.cvat_canvas_shape {
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 {
}
@ -25,7 +40,10 @@ polyline.cvat_canvas_shape {
}
.cvat_canvas_shape_drawing {
fill-opacity: 0.1;
stroke-opacity: 1;
fill: white;
stroke: black;
}
.svg_select_boundingRect {
@ -35,26 +53,18 @@ polyline.cvat_canvas_shape {
#cvat_canvas_wrapper {
width: 100%;
height: 80%;
border: 1px black solid;
height: 93%;
border-radius: 5px;
background-color: #B0C4DE;
background-color: white;
overflow: hidden;
position: relative;
}
#cvat_canvas_rotation_wrapper {
width: 100%;
height: 100%;
position: relative;
}
#cvat_canvas_loading_animation {
z-index: 1;
position: absolute;
width: 100%;
height: 100%;
transform-origin: top left;
}
#cvat_canvas_loading_circle {
@ -68,7 +78,6 @@ polyline.cvat_canvas_shape {
#cvat_canvas_text_content {
position: absolute;
z-index: 3;
transform-origin: center center;
pointer-events: none;
width: 100%;
height: 100%;
@ -79,15 +88,14 @@ polyline.cvat_canvas_shape {
position: absolute;
z-index: 0;
background-repeat: no-repeat;
transform-origin: top left;
width: 100%;
height: 100%;
box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.75);
}
#cvat_canvas_grid {
position: absolute;
z-index: 2;
transform-origin: top left;
pointer-events: none;
width: 100%;
height: 100%;
@ -103,7 +111,6 @@ polyline.cvat_canvas_shape {
position: absolute;
z-index: 2;
outline: 10px solid black;
transform-origin: top left;
width: 100%;
height: 100%;
}

@ -10,10 +10,11 @@
"author": "Intel",
"license": "MIT",
"dependencies": {
"svg.draggable.js": "^2.2.2",
"svg.js": "^2.7.1",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.3",
"svg.js": "2.7.1",
"svg.resize.js": "1.4.3",
"svg.select.js": "3.0.1"
},
"devDependencies": {
"@babel/cli": "^7.5.5",

@ -3,9 +3,22 @@
* SPDX-License-Identifier: MIT
*/
import { CanvasController, CanvasControllerImpl } from './canvasController';
import { CanvasModel, CanvasModelImpl, Rotation } from './canvasModel';
import { CanvasView, CanvasViewImpl } from './canvasView';
import {
CanvasModel,
CanvasModelImpl,
Rotation,
DrawData,
} from './canvasModel';
import {
CanvasController,
CanvasControllerImpl,
} from './canvasController';
import {
CanvasView,
CanvasViewImpl,
} from './canvasView';
interface Canvas {
html(): HTMLDivElement;
@ -16,10 +29,10 @@ interface Canvas {
fit(): void;
grid(stepX: number, stepY: number): void;
draw(enabled?: boolean, shapeType?: string, numberOfPoints?: number, initialState?: any): any;
split(enabled?: boolean): any;
group(enabled?: boolean): any;
merge(enabled?: boolean): any;
draw(drawData: DrawData): void;
split(enabled?: boolean): void;
group(enabled?: boolean): void;
merge(enabled?: boolean): void;
cancel(): void;
}
@ -29,8 +42,8 @@ class CanvasImpl implements Canvas {
private controller: CanvasController;
private view: CanvasView;
public constructor() {
this.model = new CanvasModelImpl();
public constructor(ObjectStateClass: any) {
this.model = new CanvasModelImpl(ObjectStateClass);
this.controller = new CanvasControllerImpl(this.model);
this.view = new CanvasViewImpl(this.model, this.controller);
}
@ -63,21 +76,20 @@ class CanvasImpl implements Canvas {
this.model.grid(stepX, stepY);
}
public draw(enabled: boolean = false, shapeType: string = '',
numberOfPoints: number = 0, initialState: any = null): any {
return this.model.draw(enabled, shapeType, numberOfPoints, initialState);
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
public split(enabled: boolean = false): any {
return this.model.split(enabled);
public split(enabled: boolean = false): void {
this.model.split(enabled);
}
public group(enabled: boolean = false): any {
return this.model.group(enabled);
public group(enabled: boolean = false): void {
this.model.group(enabled);
}
public merge(enabled: boolean = false): any {
return this.model.merge(enabled);
public merge(enabled: boolean = false): void {
this.model.merge(enabled);
}
public cancel(): void {

@ -7,15 +7,21 @@ import {
CanvasModel,
Geometry,
Position,
Size,
FocusData,
ActiveElement,
DrawData,
} from './canvasModel';
export interface CanvasController {
readonly geometry: Geometry;
readonly objects: any[];
canvasSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly objectStateClass: any;
readonly drawData: DrawData;
geometry: Geometry;
zoom(x: number, y: number, direction: number): void;
draw(drawData: DrawData): void;
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
@ -64,19 +70,35 @@ export class CanvasControllerImpl implements CanvasController {
this.isDragging = false;
}
public draw(drawData: DrawData): void {
this.model.draw(drawData);
}
public get geometry(): Geometry {
return this.model.geometry;
}
public set geometry(geometry: Geometry) {
this.model.geometry = geometry;
}
public get objects(): any[] {
return this.model.objects;
}
public set canvasSize(value: Size) {
this.model.canvasSize = value;
public get focusData(): FocusData {
return this.model.focusData;
}
public get activeElement(): ActiveElement {
return this.model.activeElement;
}
public get objectStateClass(): any {
return this.model.objectStateClass;
}
public get canvasSize(): Size {
return this.model.canvasSize;
public get drawData(): DrawData {
return this.model.drawData;
}
}

@ -21,6 +21,7 @@ export interface Position {
export interface Geometry {
image: Size;
canvas: Size;
grid: Size;
top: number;
left: number;
scale: number;
@ -28,6 +29,24 @@ export interface Geometry {
angle: number;
}
export interface FocusData {
clientID: number;
padding: number;
}
export interface ActiveElement {
clientID: number;
attributeID: number;
}
export interface DrawData {
enabled: boolean;
shapeType?: string;
numberOfPoints?: number;
initialState?: any;
crosshair?: boolean;
}
export enum FrameZoom {
MIN = 0.1,
MAX = 10,
@ -44,14 +63,21 @@ export enum UpdateReasons {
ZOOM = 'zoom',
FIT = 'fit',
MOVE = 'move',
GRID = 'grid',
FOCUS = 'focus',
ACTIVATE = 'activate',
DRAW = 'draw',
}
export interface CanvasModel extends MasterImpl {
readonly image: string;
readonly objects: any[];
readonly gridSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly objectStateClass: any;
readonly drawData: DrawData;
geometry: Geometry;
imageSize: Size;
canvasSize: Size;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;
@ -63,45 +89,69 @@ export interface CanvasModel extends MasterImpl {
fit(): void;
grid(stepX: number, stepY: number): void;
draw(enabled: boolean, shapeType: string, numberOfPoints: number, initialState: any): any;
split(enabled: boolean): any;
group(enabled: boolean): any;
merge(enabled: boolean): any;
draw(drawData: DrawData): void;
split(enabled: boolean): void;
group(enabled: boolean): void;
merge(enabled: boolean): void;
cancel(): void;
}
export class CanvasModelImpl extends MasterImpl implements CanvasModel {
private data: {
image: string;
objects: any[];
imageSize: Size;
ObjectStateClass: any;
activeElement: ActiveElement;
angle: number;
canvasSize: Size;
drawData: DrawData;
image: string;
imageOffset: number;
scale: number;
top: number;
imageSize: Size;
focusData: FocusData;
gridSize: Size;
left: number;
angle: number;
objects: any[];
rememberAngle: boolean;
scale: number;
top: number;
};
public constructor() {
public constructor(ObjectStateClass: any) {
super();
this.data = {
activeElement: {
clientID: null,
attributeID: null,
},
angle: 0,
canvasSize: {
height: 0,
width: 0,
},
drawData: {
enabled: false,
shapeType: null,
numberOfPoints: null,
initialState: null,
},
image: '',
imageOffset: 0,
imageSize: {
height: 0,
width: 0,
},
focusData: {
clientID: 0,
padding: 0,
},
gridSize: {
height: 100,
width: 100,
},
left: 0,
objects: [],
ObjectStateClass,
rememberAngle: false,
scale: 1,
top: 0,
@ -112,8 +162,22 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
this.data.scale = Math.min(Math.max(newScale, FrameZoom.MIN), FrameZoom.MAX);
this.data.left += (x * (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.top += (y * (oldScale / this.data.scale - 1)) * this.data.scale;
const { angle } = this.data;
const mutiplier = Math.sin(angle * Math.PI / 180) + Math.cos(angle * Math.PI / 180);
if ((angle / 90) % 2) {
// 90, 270, ..
this.data.top += mutiplier * ((x - this.data.imageSize.width / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.left -= mutiplier * ((y - this.data.imageSize.height / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
} else {
this.data.left += mutiplier * ((x - this.data.imageSize.width / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
this.data.top += mutiplier * ((y - this.data.imageSize.height / 2)
* (oldScale / this.data.scale - 1)) * this.data.scale;
}
this.notify(UpdateReasons.ZOOM);
}
@ -152,7 +216,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public activate(clientID: number, attributeID: number): void {
console.log(clientID, attributeID);
this.data.activeElement = {
clientID,
attributeID,
};
this.notify(UpdateReasons.ACTIVATE);
}
public rotate(rotation: Rotation, remember: boolean = false): void {
@ -168,7 +237,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
public focus(clientID: number, padding: number): void {
console.log(clientID, padding);
this.data.focusData = {
clientID,
padding,
};
this.notify(UpdateReasons.FOCUS);
}
public fit(): void {
@ -192,26 +266,38 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
FrameZoom.MAX,
);
this.data.top = (this.data.canvasSize.height
- this.data.imageSize.height * this.data.scale) / 2;
this.data.left = (this.data.canvasSize.width
- this.data.imageSize.width * this.data.scale) / 2;
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2);
this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2);
this.notify(UpdateReasons.FIT);
}
public grid(stepX: number, stepY: number): void {
console.log(stepX, stepY);
this.data.gridSize = {
height: stepY,
width: stepX,
};
this.notify(UpdateReasons.GRID);
}
public draw(enabled: boolean, shapeType: string,
numberOfPoints: number, initialState: any): any {
return {
enabled,
initialState,
numberOfPoints,
shapeType,
};
public draw(drawData: DrawData): void {
if (drawData.enabled) {
if (this.data.drawData.enabled) {
throw new Error('Drawing has been already started');
} else if (!drawData.shapeType) {
throw new Error('A shape type is not specified');
} else if (typeof (drawData.numberOfPoints) !== 'undefined') {
if (drawData.shapeType === 'polygon' && drawData.numberOfPoints < 3) {
throw new Error('A polygon consists of at least 3 points');
} else if (drawData.shapeType === 'polyline' && drawData.numberOfPoints < 2) {
throw new Error('A polyline consists of at least 2 points');
}
}
}
this.data.drawData = Object.assign({}, drawData);
this.notify(UpdateReasons.DRAW);
}
public split(enabled: boolean): any {
@ -233,14 +319,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
public get geometry(): Geometry {
return {
angle: this.data.angle,
canvas: {
height: this.data.canvasSize.height,
width: this.data.canvasSize.width,
},
image: {
height: this.data.imageSize.height,
width: this.data.imageSize.width,
},
canvas: Object.assign({}, this.data.canvasSize),
image: Object.assign({}, this.data.imageSize),
grid: Object.assign({}, this.data.gridSize),
left: this.data.left,
offset: this.data.imageOffset,
scale: this.data.scale,
@ -248,6 +329,22 @@ 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.left = geometry.left;
this.data.top = geometry.top;
this.data.imageOffset = geometry.offset;
this.data.scale = geometry.scale;
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
}
public get image(): string {
return this.data.image;
}
@ -256,36 +353,23 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return this.data.objects;
}
public set imageSize(value: Size) {
this.data.imageSize = {
height: value.height,
width: value.width,
};
public get gridSize(): Size {
return Object.assign({}, this.data.gridSize);
}
public get imageSize(): Size {
return {
height: this.data.imageSize.height,
width: this.data.imageSize.width,
};
public get focusData(): FocusData {
return Object.assign({}, this.data.focusData);
}
public set canvasSize(value: Size) {
this.data.canvasSize = {
height: value.height,
width: value.width,
};
public get activeElement(): ActiveElement {
return Object.assign({}, this.data.activeElement);
}
this.data.imageOffset = Math.floor(Math.max(
this.data.canvasSize.height / FrameZoom.MIN,
this.data.canvasSize.width / FrameZoom.MIN,
));
public get objectStateClass(): any {
return this.data.ObjectStateClass;
}
public get canvasSize(): Size {
return {
height: this.data.canvasSize.height,
width: this.data.canvasSize.width,
};
public get drawData(): DrawData {
return Object.assign({}, this.data.drawData);
}
}

@ -14,25 +14,39 @@ import 'svg.resize.js';
import 'svg.select.js';
import { CanvasController } from './canvasController';
import { CanvasModel, Geometry, UpdateReasons } from './canvasModel';
import { Listener, Master } from './master';
import { DrawHandler, DrawHandlerImpl } from './drawHandler';
import { translateToSVG, translateFromSVG } from './shared';
import consts from './consts';
import {
CanvasModel,
Geometry,
Size,
UpdateReasons,
FocusData,
FrameZoom,
ActiveElement,
DrawData,
} from './canvasModel';
export interface CanvasView {
html(): HTMLDivElement;
}
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;
[, pt.y] = points;
pt = pt.matrixTransform(transformationMatrix);
output.push(pt.x, pt.y);
}
return output;
interface ShapeDict {
[index: number]: SVG.Shape;
}
interface TextDict {
[index: number]: SVG.Text;
}
enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
}
function darker(color: string, percentage: number): string {
@ -57,21 +71,52 @@ export class CanvasViewImpl implements CanvasView, Listener {
private grid: SVGSVGElement;
private content: SVGSVGElement;
private adoptedContent: SVG.Container;
private rotationWrapper: HTMLDivElement;
private canvas: HTMLDivElement;
private gridPath: SVGPathElement;
private gridPattern: SVGPatternElement;
private controller: CanvasController;
private svgShapes: SVG.Shape[];
private svgTexts: SVG.Text[];
private readonly BASE_STROKE_WIDTH: number;
private readonly BASE_POINT_SIZE: number;
private svgShapes: ShapeDict;
private svgTexts: TextDict;
private drawHandler: DrawHandler;
private activeElement: {
state: any;
attributeID: number;
};
private mode: Mode;
private onDrawDone(data: Record<string, any>): void {
if (data) {
const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false,
cancelable: true,
detail: {
// eslint-disable-next-line new-cap
state: new this.controller.objectStateClass(data),
},
});
this.canvas.dispatchEvent(event);
} else {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
}
this.controller.draw({
enabled: false,
});
}
public constructor(model: CanvasModel & Master, controller: CanvasController) {
this.controller = controller;
this.BASE_STROKE_WIDTH = 2.5;
this.BASE_POINT_SIZE = 7;
this.svgShapes = [];
this.svgTexts = [];
this.svgShapes = {};
this.svgTexts = {};
this.activeElement = null;
this.mode = Mode.IDLE;
// Create HTML elements
this.loadingAnimation = window.document
@ -82,18 +127,17 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.gridPattern = window.document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
this.content = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedContent = (SVG.adopt((this.content as any as HTMLElement)) as SVG.Container);
this.rotationWrapper = window.document.createElement('div');
this.drawHandler = new DrawHandlerImpl(this.onDrawDone.bind(this), this.adoptedContent);
this.canvas = window.document.createElement('div');
const loadingCircle: SVGCircleElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'circle');
const gridDefs: SVGDefsElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'defs');
const gridPattern: SVGPatternElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'pattern');
const gridRect: SVGRectElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'rect');
@ -110,10 +154,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.gridPath.setAttribute('d', 'M 1000 0 L 0 0 0 1000');
this.gridPath.setAttribute('fill', 'none');
this.gridPath.setAttribute('stroke-width', '1.5');
gridPattern.setAttribute('id', 'cvat_canvas_grid_pattern');
gridPattern.setAttribute('width', '100');
gridPattern.setAttribute('height', '100');
gridPattern.setAttribute('patternUnits', 'userSpaceOnUse');
this.gridPattern.setAttribute('id', 'cvat_canvas_grid_pattern');
this.gridPattern.setAttribute('width', '100');
this.gridPattern.setAttribute('height', '100');
this.gridPattern.setAttribute('patternUnits', 'userSpaceOnUse');
gridRect.setAttribute('width', '100%');
gridRect.setAttribute('height', '100%');
gridRect.setAttribute('fill', 'url(#cvat_canvas_grid_pattern)');
@ -124,7 +168,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.setAttribute('id', 'cvat_canvas_content');
// Setup wrappers
this.rotationWrapper.setAttribute('id', 'cvat_canvas_rotation_wrapper');
this.canvas.setAttribute('id', 'cvat_canvas_wrapper');
// Unite created HTML elements together
@ -132,46 +175,52 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.grid.appendChild(gridDefs);
this.grid.appendChild(gridRect);
gridDefs.appendChild(gridPattern);
gridPattern.appendChild(this.gridPath);
gridDefs.appendChild(this.gridPattern);
this.gridPattern.appendChild(this.gridPath);
this.rotationWrapper.appendChild(this.loadingAnimation);
this.rotationWrapper.appendChild(this.text);
this.rotationWrapper.appendChild(this.background);
this.rotationWrapper.appendChild(this.grid);
this.rotationWrapper.appendChild(this.content);
this.canvas.appendChild(this.loadingAnimation);
this.canvas.appendChild(this.text);
this.canvas.appendChild(this.background);
this.canvas.appendChild(this.grid);
this.canvas.appendChild(this.content);
this.canvas.appendChild(this.rotationWrapper);
// A little hack to get size after first mounting
// http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/
const self = this;
const canvasFirstMounted = (event: AnimationEvent): void => {
if (event.animationName === 'loadingAnimation') {
self.controller.canvasSize = {
height: self.rotationWrapper.clientHeight,
width: self.rotationWrapper.clientWidth,
const { geometry } = this.controller;
geometry.canvas = {
height: self.canvas.clientHeight,
width: self.canvas.clientWidth,
};
self.rotationWrapper.removeEventListener('animationstart', canvasFirstMounted);
this.controller.geometry = geometry;
self.canvas.removeEventListener('animationstart', canvasFirstMounted);
}
};
this.canvas.addEventListener('animationstart', canvasFirstMounted);
this.content.addEventListener('dblclick', (): void => {
self.controller.fit();
});
this.content.addEventListener('mousedown', (event): void => {
self.controller.enableDrag(event.clientX, event.clientY);
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);
});
window.document.addEventListener('mouseup', (): void => {
self.controller.disableDrag();
window.document.addEventListener('mouseup', (event): void => {
if (event.which === 1 || event.which === 2) {
self.controller.disableDrag();
}
});
this.content.addEventListener('wheel', (event): void => {
@ -180,6 +229,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
event.preventDefault();
});
this.content.addEventListener('mousemove', (e): void => {
if (this.mode !== Mode.IDLE) return;
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.moved', {
bubbles: false,
cancelable: true,
detail: {
x,
y,
objects: this.controller.objects,
},
});
this.canvas.dispatchEvent(event);
});
this.content.oncontextmenu = (): boolean => false;
model.subscribe(this);
}
@ -187,20 +254,21 @@ export class CanvasViewImpl implements CanvasView, Listener {
function transform(geometry: Geometry): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
obj.style.transform = `scale(${geometry.scale})`;
obj.style.transform = `scale(${geometry.scale}) rotate(${geometry.angle}deg)`;
}
this.rotationWrapper.style.transform = `rotate(${geometry.angle}deg)`;
// Transform grid
this.gridPath.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / (2 * geometry.scale)}px`);
// Transform all shapes
// Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) {
element.setAttribute(
'stroke-width',
`${this.BASE_STROKE_WIDTH / (3 * geometry.scale)}`,
`${consts.BASE_STROKE_WIDTH / (3 * geometry.scale)}`,
);
element.setAttribute(
'r',
`${this.BASE_POINT_SIZE / (2 * geometry.scale)}`,
`${consts.BASE_POINT_SIZE / (2 * geometry.scale)}`,
);
}
@ -212,13 +280,31 @@ export class CanvasViewImpl implements CanvasView, Listener {
);
}
for (const object of this.svgShapes) {
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': this.BASE_STROKE_WIDTH / (geometry.scale),
});
// Transform all drawn shapes
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) {
const object = this.svgShapes[key];
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (geometry.scale),
});
}
}
}
// Transform all text
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)
&& Object.prototype.hasOwnProperty.call(this.svgTexts, key)) {
this.updateTextPosition(
this.svgTexts[key],
this.svgShapes[key],
);
}
}
// Transform handlers
this.drawHandler.transform(geometry);
}
function resize(geometry: Geometry): void {
@ -240,11 +326,60 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
for (const obj of [this.content, this.text]) {
obj.style.top = `${geometry.top - geometry.offset * geometry.scale}px`;
obj.style.left = `${geometry.left - geometry.offset * geometry.scale}px`;
obj.style.top = `${geometry.top - geometry.offset}px`;
obj.style.left = `${geometry.left - geometry.offset}px`;
}
// Transform handlers
this.drawHandler.transform(geometry);
}
function computeFocus(focusData: FocusData, geometry: Geometry): void {
// This computation cann't be done in the model because of lack of data
const object = this.svgShapes[focusData.clientID];
if (!object) {
return;
}
// First of all, compute and apply scale
let scale = null;
const bbox: SVG.BBox = object.node.getBBox();
if ((geometry.angle / 90) % 2) {
// 90, 270, ..
scale = Math.min(Math.max(Math.min(
geometry.canvas.width / bbox.height,
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,
), FrameZoom.MIN), FrameZoom.MAX);
}
this.content.style.transform = `scale(${geometry.scale})`;
transform.call(this, Object.assign({}, geometry, {
scale,
}));
const [x, y] = translateFromSVG(this.content, [
bbox.x + bbox.width / 2,
bbox.y + bbox.height / 2,
]);
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
];
const dragged = Object.assign({}, geometry, {
top: geometry.top + cy - y,
left: geometry.left + cx - x,
scale,
});
this.controller.geometry = dragged;
move.call(this, dragged);
}
function setupObjects(objects: any[], geometry: Geometry): void {
@ -268,17 +403,33 @@ export class CanvasViewImpl implements CanvasView, Listener {
move.call(this, geometry);
resize.call(this, geometry);
transform.call(this, geometry);
const event: Event = new Event('canvas.setup');
this.canvas.dispatchEvent(event);
}
} else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) {
move.call(this, geometry);
resize.call(this, geometry);
transform.call(this, geometry);
} else if (reason === UpdateReasons.MOVE) {
move.call(this, geometry);
} else if (reason === UpdateReasons.OBJECTS) {
setupObjects.call(this, this.controller.objects, geometry);
const event: CustomEvent = new CustomEvent('canvas.setup');
this.canvas.dispatchEvent(event);
} else if (reason === UpdateReasons.GRID) {
const size: Size = 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);
} else if (reason === UpdateReasons.ACTIVATE) {
this.activate(geometry, 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);
}
}
@ -286,12 +437,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.canvas;
}
private addObjects(ctm: SVGMatrix, objects: any[], geometry: Geometry): void {
for (const object of objects) {
if (object.objectType === 'tag') {
this.addTag(object, geometry);
private addObjects(ctm: SVGMatrix, states: any[], geometry: Geometry): void {
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state, geometry);
} else {
const points: number[] = (object.points as number[]);
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = [];
for (let i = 0; i <= points.length - 1; i += 2) {
let point: SVGPoint = this.background.createSVGPoint();
@ -302,8 +453,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
// TODO: Use enums after typification cvat-core
if (object.shapeType === 'rectangle') {
this.svgShapes.push(this.addRect(translatedPoints, object, geometry));
if (state.shapeType === 'rectangle') {
this.svgShapes[state.clientID] = this
.addRect(translatedPoints, state, geometry);
} else {
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
@ -315,34 +467,57 @@ export class CanvasViewImpl implements CanvasView, Listener {
}, '',
);
if (object.shapeType === 'polygon') {
this.svgShapes.push(this.addPolygon(stringified, object, geometry));
} else if (object.shapeType === 'polyline') {
this.svgShapes.push(this.addPolyline(stringified, object, geometry));
} else if (object.shapeType === 'points') {
this.svgShapes.push(this.addPoints(stringified, object, geometry));
if (state.shapeType === 'polygon') {
this.svgShapes[state.clientID] = this
.addPolygon(stringified, state, geometry);
} else if (state.shapeType === 'polyline') {
this.svgShapes[state.clientID] = this
.addPolyline(stringified, state, geometry);
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this
.addPoints(stringified, state, geometry);
}
}
// TODO: add text here if need
// TODO: Use enums after typification cvat-core
if (state.visibility === 'all') {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
}
}
this.activate(geometry);
}
private activate(geometry: Geometry): void {
for (const shape of this.svgShapes) {
const self = this;
(shape as any).draggable().on('dragstart', (): void => {
console.log('hello');
}).on('dragend', (): void => {
console.log('hello');
});
private deactivate(): void {
if (this.activeElement) {
const { state } = this.activeElement;
const shape = this.svgShapes[this.activeElement.state.clientID];
(shape as any).draggable(false);
if (state.shapeType !== 'points') {
this.selectize(false, shape, null);
}
(shape as any).resize(false);
// Hide text only if it is hidden by settings
const text = this.svgTexts[state.clientID];
if (text && state.visibility === 'shape') {
text.remove();
delete this.svgTexts[state.clientID];
}
this.activeElement = null;
}
}
(shape as any).selectize({
private selectize(value: boolean, shape: SVG.Element, geometry: Geometry): void {
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
pointSize: this.BASE_POINT_SIZE / geometry.scale,
pointSize: consts.BASE_POINT_SIZE / geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
@ -351,7 +526,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
.fill(shape.node.getAttribute('fill'))
.center(cx, cy)
.attr({
'stroke-width': self.BASE_STROKE_WIDTH / (3 * geometry.scale),
'stroke-width': consts.BASE_STROKE_WIDTH / (3 * geometry.scale),
});
circle.node.addEventListener('mouseenter', (): void => {
@ -372,12 +547,140 @@ export class CanvasViewImpl implements CanvasView, Listener {
return circle;
},
}).resize();
});
} else {
(shape as any).selectize(false, {
deepSelect: true,
});
}
}
private activate(geometry: Geometry, activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement) {
// Check if it is the same element
if (this.activeElement.state.clientID === activeElement.clientID) {
return;
}
// Deactivate previous element
this.deactivate();
}
const state = this.controller.objects
.filter((el): boolean => el.clientID === activeElement.clientID)[0];
this.activeElement = {
attributeID: activeElement.attributeID,
state,
};
const shape = this.svgShapes[activeElement.clientID];
let text = this.svgTexts[activeElement.clientID];
// Draw text if it's hidden by default
if (!text && state.visibility === 'shape') {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
this.updateTextPosition(
text,
shape,
);
}
const self = this;
this.content.append(shape.node);
(shape as any).draggable().on('dragstart', (): void => {
this.mode = Mode.DRAG;
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('dragend', (): void => {
this.mode = Mode.IDLE;
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
text,
shape,
);
}
});
if (state.shapeType !== 'points') {
this.selectize(true, shape, geometry);
}
(shape as any).resize().on('resizestart', (): void => {
this.mode = Mode.RESIZE;
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('resizedone', (): void => {
this.mode = Mode.IDLE;
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
text,
shape,
);
}
});
}
// Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
const [x1, y1, x2, y2]: number[] = translateFromSVG(this.content, [
box.x,
box.y,
box.x + box.width,
box.y + box.height,
]);
box = {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
// Find the best place for a text
let [clientX, clientY]: number[] = [box.x + box.width, box.y];
if (clientX + (text.node as any as SVGTextElement)
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth) {
([clientX, clientY] = [box.x, box.y]);
}
// Translate back to text SVG
const [x, y]: number[] = translateToSVG(this.text, [
clientX + consts.TEXT_MARGIN,
clientY,
]);
// Finally draw a text
text.move(x, y);
for (const tspan of (text.lines() as any).members) {
tspan.attr('x', text.attr('x'));
}
}
// add selectable
// add draggable
// add resizable
private addText(state: any): SVG.Text {
const { label, clientID, attributes } = state;
const attrNames = label.attributes.reduce((acc: any, val: any): void => {
acc[val.id] = val.name;
return acc;
}, {});
return this.adoptedText.text((block): void => {
block.tspan(`${label.name} ${clientID}`).style('text-transform', 'uppercase');
for (const attrID of Object.keys(attributes)) {
block.tspan(`${attrNames[attrID]}: ${attributes[attrID]}`).attr({
attrID,
dy: '1em',
x: 0,
});
}
}).move(0, 0).addClass('cvat_canvas_text');
}
private addRect(points: number[], state: any, geometry: Geometry): SVG.Rect {
@ -389,7 +692,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).move(xtl, ytl)
.addClass('cvat_canvas_shape');
@ -402,7 +705,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
}
@ -414,22 +717,24 @@ export class CanvasViewImpl implements CanvasView, Listener {
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
}
private addPoints(points: string, state: any, geometry: Geometry): SVG.Polygon {
return this.adoptedContent.polygon(points).attr({
private addPoints(points: string, state: any, geometry: Geometry): SVG.PolyLine {
const shape = this.adoptedContent.polyline(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
fill: state.color,
opacity: 0,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
this.selectize(true, shape, geometry);
shape.attr('fill', 'none');
return shape;
}
private addTag(state: any, geometry: Geometry): void {

@ -0,0 +1,18 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
const BASE_STROKE_WIDTH = 2;
const BASE_POINT_SIZE = 8;
const TEXT_MARGIN = 10;
const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
export default {
BASE_STROKE_WIDTH,
BASE_POINT_SIZE,
TEXT_MARGIN,
AREA_THRESHOLD,
SIZE_THRESHOLD,
};

@ -0,0 +1,349 @@
/*
* 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 {
private onDrawDone: any; // callback is used to notify about creating new shape
private canvas: SVG.Container;
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');
}
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 = 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 => {
const points = translateFromSVG(
this.canvas.node as any as SVGSVGElement,
(e.target as SVGElement)
.getAttribute('points')
.split(/[,\s]/g)
.map((coord): number => +coord),
);
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) {
this.onDrawDone = onDrawDone;
this.canvas = canvas;
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 = Object.values(this.drawInstance.memory())[0];
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

@ -0,0 +1,36 @@
/*
* 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;
}

@ -0,0 +1,172 @@
import * as SVG from 'svg.js';
/* eslint-disable */
import 'svg.draggable.js';
import 'svg.resize.js';
import 'svg.select.js';
import 'svg.draw.js';
// Update constructor
const originalDraw = SVG.Element.prototype.draw;
SVG.Element.prototype.draw = function constructor(...args: any): any {
let handler = this.remember('_paintHandler');
if (!handler) {
originalDraw.call(this, ...args);
handler = this.remember('_paintHandler');
handler.set = new SVG.Set();
} else {
originalDraw.call(this, ...args);
}
return this;
};
for (const key of Object.keys(originalDraw)) {
SVG.Element.prototype.draw[key] = originalDraw[key];
}
// Create undo for polygones and polylines
function undo(): void {
if (this.set.length()) {
this.set.members.splice(-1, 1)[0].remove();
this.el.array().value.splice(-2, 1);
this.el.plot(this.el.array());
this.el.fire('undopoint');
}
}
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
SVG.Element.prototype.draw.plugins.polyline,
{
undo: undo,
},
));
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
SVG.Element.prototype.draw.plugins.polygon,
{
undo: undo,
},
));
// Create transform for rect, polyline and polygon
function transform(): void {
this.m = this.el.node.getScreenCTM().inverse();
this.offset = { x: window.pageXOffset, y: window.pageYOffset };
}
SVG.Element.prototype.draw.extend('rect', Object.assign({},
SVG.Element.prototype.draw.plugins.rect,
{
transform: transform,
},
));
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
SVG.Element.prototype.draw.plugins.polyline,
{
transform: transform,
},
));
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
SVG.Element.prototype.draw.plugins.polygon,
{
transform: transform,
},
));
// Fix method drawCircles
function drawCircles(): void {
const array = this.el.array().valueOf();
this.set.each(function (): void {
this.remove();
});
this.set.clear();
for (let i = 0; i < array.length - 1; ++i) {
[this.p.x] = array[i];
[, this.p.y] = array[i];
const p = this.p.matrixTransform(
this.parent.node.getScreenCTM()
.inverse()
.multiply(this.el.node.getScreenCTM()),
);
this.set.add(
this.parent
.circle(5)
.stroke({
width: 1,
}).fill('#ccc')
.center(p.x, p.y),
);
}
}
SVG.Element.prototype.draw.extend('line', Object.assign({},
SVG.Element.prototype.draw.plugins.line,
{
drawCircles: drawCircles,
}
));
SVG.Element.prototype.draw.extend('polyline', Object.assign({},
SVG.Element.prototype.draw.plugins.polyline,
{
drawCircles: drawCircles,
}
));
SVG.Element.prototype.draw.extend('polygon', Object.assign({},
SVG.Element.prototype.draw.plugins.polygon,
{
drawCircles: drawCircles,
}
));
// Fix method drag
const originalDraggable = SVG.Element.prototype.draggable;
SVG.Element.prototype.draggable = function constructor(...args: any): any {
let handler = this.remember('_draggable');
if (!handler) {
originalDraggable.call(this, ...args);
handler = this.remember('_draggable');
handler.drag = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
handler.constructor.prototype.drag.call(this, e);
}
} else {
originalDraggable.call(this, ...args);
}
return this;
};
for (const key of Object.keys(originalDraggable)) {
SVG.Element.prototype.draggable[key] = originalDraggable[key];
}
// Fix method resize
const originalResize = SVG.Element.prototype.resize;
SVG.Element.prototype.resize = function constructor(...args: any): any {
let handler = this.remember('_resizeHandler');
if (!handler) {
originalResize.call(this, ...args);
handler = this.remember('_resizeHandler');
handler.update = function(e: any) {
this.m = this.el.node.getScreenCTM().inverse();
handler.constructor.prototype.update.call(this, e);
}
} else {
originalResize.call(this, ...args);
}
return this;
};
for (const key of Object.keys(originalResize)) {
SVG.Element.prototype.resize[key] = originalResize[key];
}

@ -9,11 +9,15 @@
(() => {
const ObjectState = require('./object-state');
const { checkObjectType } = require('./common');
const {
checkObjectType,
isEnum,
} = require('./common');
const {
ObjectShape,
ObjectType,
AttributeType,
VisibleState,
} = require('./enums');
const {
@ -169,6 +173,7 @@
this.frameMeta = injection.frameMeta;
this.collectionZ = injection.collectionZ;
this.visibility = VisibleState.SHAPE;
this.color = color;
this.shapeType = null;
@ -272,6 +277,7 @@
label: this.label,
group: this.group,
color: this.color,
visibility: this.visibility,
};
}
@ -373,6 +379,16 @@
copy.color = data.color;
}
if (updated.visibility) {
if (!isEnum.call(VisibleState, data.visibility)) {
throw new ArgumentError(
`Got invalid visibility value: "${data.visibility}"`,
);
}
copy.visibility = data.visibility;
}
// Reset flags and commit all changes
updated.reset();
for (const prop of Object.keys(copy)) {
@ -475,6 +491,7 @@
serverID: this.serverID,
lock: this.lock,
color: this.color,
visibility: this.visibility,
},
);
@ -643,6 +660,16 @@
copy.color = data.color;
}
if (updated.visibility) {
if (!isEnum.call(VisibleState, data.visibility)) {
throw new ArgumentError(
`Got invalid visibility value: "${data.visibility}"`,
);
}
copy.visibility = data.visibility;
}
if (updated.keyframe) {
// Just check here
checkObjectType('keyframe', data.keyframe, 'boolean', null);

@ -27,8 +27,8 @@ function build() {
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
EventType,
} = require('./enums');
const {
@ -467,8 +467,8 @@ function build() {
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
EventType,
},
/**
* Namespace is used for access to exceptions

@ -5,7 +5,7 @@
(() => {
/**
* Enum for type of server files
* Share files types
* @enum {string}
* @name ShareFileType
* @memberof module:API.cvat.enums
@ -19,7 +19,7 @@
});
/**
* Enum for a status of a task
* Task statuses
* @enum {string}
* @name TaskStatus
* @memberof module:API.cvat.enums
@ -35,7 +35,7 @@
});
/**
* Enum for a mode of a task
* Task modes
* @enum {string}
* @name TaskMode
* @memberof module:API.cvat.enums
@ -49,7 +49,7 @@
});
/**
* Enum for type of server files
* Attribute types
* @enum {string}
* @name AttributeType
* @memberof module:API.cvat.enums
@ -69,7 +69,7 @@
});
/**
* Enum for type of an object
* Object types
* @enum {string}
* @name ObjectType
* @memberof module:API.cvat.enums
@ -85,7 +85,7 @@
});
/**
* Enum for type of server files
* Object shapes
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
@ -103,7 +103,23 @@
});
/**
* Enum for type of server files
* Object visibility states
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} ALL 'all'
* @property {string} SHAPE 'shape'
* @property {string} NONE 'none'
* @readonly
*/
const VisibleState = Object.freeze({
ALL: 'all',
SHAPE: 'shape',
NONE: 'none',
});
/**
* Event types
* @enum {number}
* @name LogType
* @memberof module:API.cvat.enums
@ -166,18 +182,6 @@
rotateImage: 26,
};
/**
* Enum for type of server files
* @enum {number}
* @name EventType
* @memberof module:API.cvat.enums
* @property {number} frameDownloaded 0
* @readonly
*/
const EventType = Object.freeze({
frameDownloaded: 0,
});
module.exports = {
ShareFileType,
TaskStatus,
@ -185,7 +189,7 @@
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
EventType,
};
})();

@ -39,6 +39,7 @@
zOrder: null,
lock: null,
color: null,
visibility: null,
clientID: serialized.clientID,
serverID: serialized.serverID,
@ -64,6 +65,7 @@
this.zOrder = false;
this.lock = false;
this.color = false;
this.visibility = false;
},
writable: false,
});
@ -149,6 +151,19 @@
data.color = color;
},
},
visibility: {
/**
* @name visibility
* @type {module:API.cvat.enums.VisibleState}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.visibility,
set: (visibility) => {
data.updateFlags.visibility = true;
data.visibility = visibility;
},
},
points: {
/**
* @name points
@ -285,6 +300,7 @@
this.occluded = serialized.occluded;
this.color = serialized.color;
this.lock = serialized.lock;
this.visibility = serialized.visibility;
// It can be undefined in a constructor and it can be defined later
if (typeof (serialized.points) !== 'undefined') {

Loading…
Cancel
Save