|
|
|
|
@ -1,8 +1,15 @@
|
|
|
|
|
/*
|
|
|
|
|
* Copyright (C) 2018 Intel Corporation
|
|
|
|
|
* Copyright (C) 2019 Intel Corporation
|
|
|
|
|
* SPDX-License-Identifier: MIT
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as SVG from 'svg.js';
|
|
|
|
|
|
|
|
|
|
// tslint:disable-next-line: ordered-imports
|
|
|
|
|
import 'svg.draggable.js';
|
|
|
|
|
import 'svg.resize.js';
|
|
|
|
|
import 'svg.select.js';
|
|
|
|
|
|
|
|
|
|
import { CanvasController } from './canvasController';
|
|
|
|
|
import { CanvasModel, Geometry, UpdateReasons } from './canvasModel';
|
|
|
|
|
import { Listener, Master } from './master';
|
|
|
|
|
@ -11,10 +18,6 @@ export interface CanvasView {
|
|
|
|
|
html(): HTMLDivElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface HTMLAttribute {
|
|
|
|
|
[index: string]: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
|
|
|
|
|
const output = [];
|
|
|
|
|
const transformationMatrix = svg.getScreenCTM().inverse();
|
|
|
|
|
@ -29,44 +32,56 @@ function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
|
|
|
|
|
const output = [];
|
|
|
|
|
const transformationMatrix = svg.getScreenCTM();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
function darker(color: string, percentage: number) {
|
|
|
|
|
const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100));
|
|
|
|
|
const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100));
|
|
|
|
|
const B = Math.round(parseInt(color.slice(5, 7), 16) * (1 - percentage / 100));
|
|
|
|
|
|
|
|
|
|
return output;
|
|
|
|
|
const rHex = Math.max(0, R).toString(16);
|
|
|
|
|
const gHex = Math.max(0, G).toString(16);
|
|
|
|
|
const bHex = Math.max(0, B).toString(16);
|
|
|
|
|
|
|
|
|
|
return `#${rHex.length === 1 ? `0${rHex}` : rHex}`
|
|
|
|
|
+ `${gHex.length === 1 ? `0${gHex}` : gHex}`
|
|
|
|
|
+ `${bHex.length === 1 ? `0${bHex}` : bHex}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class CanvasViewImpl implements CanvasView, Listener {
|
|
|
|
|
private loadingAnimation: SVGSVGElement;
|
|
|
|
|
private text: SVGSVGElement;
|
|
|
|
|
private adoptedText: SVG.Container;
|
|
|
|
|
private background: SVGSVGElement;
|
|
|
|
|
private grid: SVGSVGElement;
|
|
|
|
|
private content: SVGSVGElement;
|
|
|
|
|
private adoptedContent: SVG.Container;
|
|
|
|
|
private rotationWrapper: HTMLDivElement;
|
|
|
|
|
private canvas: HTMLDivElement;
|
|
|
|
|
private gridPath: SVGPathElement;
|
|
|
|
|
private controller: CanvasController;
|
|
|
|
|
private svgShapes: SVG.Shape[];
|
|
|
|
|
private svgTexts: SVG.Text[];
|
|
|
|
|
private readonly BASE_STROKE_WIDTH: number;
|
|
|
|
|
private readonly BASE_POINT_SIZE: number;
|
|
|
|
|
|
|
|
|
|
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 = [];
|
|
|
|
|
|
|
|
|
|
// Create HTML elements
|
|
|
|
|
this.loadingAnimation = window.document
|
|
|
|
|
.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
|
|
|
this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
|
|
|
this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container);
|
|
|
|
|
this.background = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
|
|
|
|
|
|
|
|
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.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.canvas = window.document.createElement('div');
|
|
|
|
|
|
|
|
|
|
@ -167,9 +182,40 @@ export class CanvasViewImpl implements CanvasView, Listener {
|
|
|
|
|
|
|
|
|
|
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
|
|
|
|
|
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})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.rotationWrapper.style.transform = `rotate(${geometry.angle}deg)`;
|
|
|
|
|
|
|
|
|
|
// Transform all shapes
|
|
|
|
|
for (const element of window.document.getElementsByClassName('svg_select_points')) {
|
|
|
|
|
element.setAttribute(
|
|
|
|
|
'stroke-width',
|
|
|
|
|
`${this.BASE_STROKE_WIDTH / (3 * geometry.scale)}`,
|
|
|
|
|
);
|
|
|
|
|
element.setAttribute(
|
|
|
|
|
'r',
|
|
|
|
|
`${this.BASE_POINT_SIZE / (2 * geometry.scale)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const element of
|
|
|
|
|
window.document.getElementsByClassName('cvat_canvas_selected_point')) {
|
|
|
|
|
element.setAttribute(
|
|
|
|
|
'stroke-width',
|
|
|
|
|
`${+element.getAttribute('stroke-width') * 2}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const object of this.svgShapes) {
|
|
|
|
|
if (object.attr('stroke-width')) {
|
|
|
|
|
object.attr({
|
|
|
|
|
'stroke-width': this.BASE_STROKE_WIDTH / (geometry.scale),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resize(geometry: Geometry): void {
|
|
|
|
|
@ -198,6 +244,17 @@ export class CanvasViewImpl implements CanvasView, Listener {
|
|
|
|
|
this.content.style.transform = `scale(${geometry.scale})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupObjects(objects: any[], geometry: Geometry): void {
|
|
|
|
|
this.adoptedContent.clear();
|
|
|
|
|
const ctm = this.content.getScreenCTM()
|
|
|
|
|
.inverse().multiply(this.background.getScreenCTM());
|
|
|
|
|
|
|
|
|
|
// TODO: Compute difference
|
|
|
|
|
this.addObjects(ctm, objects, geometry);
|
|
|
|
|
// TODO: Update objects
|
|
|
|
|
// TODO: Delete objects
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { geometry } = this.controller;
|
|
|
|
|
if (reason === UpdateReasons.IMAGE) {
|
|
|
|
|
if (!model.image.length) {
|
|
|
|
|
@ -217,10 +274,162 @@ export class CanvasViewImpl implements CanvasView, Listener {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public html(): HTMLDivElement {
|
|
|
|
|
return this.canvas;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addObjects(ctm: SVGMatrix, objects: any[], geometry: Geometry) {
|
|
|
|
|
for (const object of objects) {
|
|
|
|
|
if (object.objectType === 'tag') {
|
|
|
|
|
this.addTag(object, geometry);
|
|
|
|
|
} else {
|
|
|
|
|
const points: number[] = (object.points as number[]);
|
|
|
|
|
const translatedPoints: number[] = [];
|
|
|
|
|
for (let i = 0; i <= points.length - 1; i += 2) {
|
|
|
|
|
let point: SVGPoint = this.background.createSVGPoint();
|
|
|
|
|
point.x = points[i];
|
|
|
|
|
point.y = points[i + 1];
|
|
|
|
|
point = point.matrixTransform(ctm);
|
|
|
|
|
translatedPoints.push(point.x, point.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Use enums after typification cvat-core
|
|
|
|
|
if (object.shapeType === 'rectangle') {
|
|
|
|
|
this.svgShapes.push(this.addRect(translatedPoints, object, geometry));
|
|
|
|
|
} else {
|
|
|
|
|
const stringified = translatedPoints.reduce(
|
|
|
|
|
(acc: string, val: number, idx: number): string => {
|
|
|
|
|
if (idx % 2) {
|
|
|
|
|
return `${acc}${val} `;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${acc}${val},`;
|
|
|
|
|
},
|
|
|
|
|
'' ,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: add text here if need
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.activate(geometry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private activate(geometry: Geometry) {
|
|
|
|
|
for (const shape of this.svgShapes) {
|
|
|
|
|
const self = this;
|
|
|
|
|
(shape as any).draggable().on('dragstart', () => {
|
|
|
|
|
console.log('hello');
|
|
|
|
|
}).on('dragend', () => {
|
|
|
|
|
console.log('hello');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
(shape as any).selectize({
|
|
|
|
|
deepSelect: true,
|
|
|
|
|
pointSize: this.BASE_POINT_SIZE / geometry.scale,
|
|
|
|
|
rotationPoint: false,
|
|
|
|
|
pointType(cx: number, cy: number): SVG.Circle {
|
|
|
|
|
const circle: SVG.Circle = this.nested
|
|
|
|
|
.circle(this.options.pointSize)
|
|
|
|
|
.stroke('black')
|
|
|
|
|
.fill(shape.node.getAttribute('fill'))
|
|
|
|
|
.center(cx, cy)
|
|
|
|
|
.attr({
|
|
|
|
|
'stroke-width': self.BASE_STROKE_WIDTH / (3 * geometry.scale),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
circle.node.addEventListener('mouseenter', () => {
|
|
|
|
|
circle.attr({
|
|
|
|
|
'stroke-width': circle.attr('stroke-width') * 2,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
circle.addClass('cvat_canvas_selected_point');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
circle.node.addEventListener('mouseleave', () => {
|
|
|
|
|
circle.attr({
|
|
|
|
|
'stroke-width': circle.attr('stroke-width') / 2,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
circle.removeClass('cvat_canvas_selected_point');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return circle;
|
|
|
|
|
},
|
|
|
|
|
}).resize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// add selectable
|
|
|
|
|
// add draggable
|
|
|
|
|
// add resizable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addRect(points: number[], state: any, geometry: Geometry): SVG.Rect {
|
|
|
|
|
const [xtl, ytl, xbr, ybr] = points;
|
|
|
|
|
|
|
|
|
|
return this.adoptedContent.rect().size(xbr - xtl, ybr - ytl).attr({
|
|
|
|
|
client_id: state.clientID,
|
|
|
|
|
'color-rendering': 'optimizeQuality',
|
|
|
|
|
fill: state.color,
|
|
|
|
|
'shape-rendering': 'geometricprecision',
|
|
|
|
|
stroke: darker(state.color, 50),
|
|
|
|
|
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
|
|
|
|
|
z_order: state.zOrder,
|
|
|
|
|
}).move(xtl, ytl).addClass('cvat_canvas_shape');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addPolygon(points: string, state: any, geometry: Geometry): SVG.Polygon {
|
|
|
|
|
return this.adoptedContent.polygon(points).attr({
|
|
|
|
|
client_id: state.clientID,
|
|
|
|
|
'color-rendering': 'optimizeQuality',
|
|
|
|
|
fill: state.color,
|
|
|
|
|
'shape-rendering': 'geometricprecision',
|
|
|
|
|
stroke: darker(state.color, 50),
|
|
|
|
|
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
|
|
|
|
|
z_order: state.zOrder,
|
|
|
|
|
}).addClass('cvat_canvas_shape');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addPolyline(points: string, state: any, geometry: Geometry): SVG.PolyLine {
|
|
|
|
|
return this.adoptedContent.polyline(points).attr({
|
|
|
|
|
client_id: state.clientID,
|
|
|
|
|
'color-rendering': 'optimizeQuality',
|
|
|
|
|
fill: state.color,
|
|
|
|
|
'shape-rendering': 'geometricprecision',
|
|
|
|
|
stroke: darker(state.color, 50),
|
|
|
|
|
'stroke-width': this.BASE_STROKE_WIDTH / geometry.scale,
|
|
|
|
|
z_order: state.zOrder,
|
|
|
|
|
}).addClass('cvat_canvas_shape');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addPoints(points: string, state: any, geometry: Geometry): SVG.Polygon {
|
|
|
|
|
return this.adoptedContent.polygon(points).attr({
|
|
|
|
|
client_id: 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,
|
|
|
|
|
z_order: state.zOrder,
|
|
|
|
|
}).addClass('cvat_canvas_shape');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private addTag(state: any, geometry: Geometry): void {
|
|
|
|
|
// TODO:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|