CVAT-Canvas Updates (#675)
* Removed extra files * Merge(), group(), split() and cancel() * More visual effects * More strict state checks * Display shape size during draw/resizemain
parent
4fd966dacc
commit
03eaf59d98
@ -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
@ -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.');
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue