React UI: cuboids (#1451)

main
Dmitry Kalinin 6 years ago committed by GitHub
parent 227ab05e73
commit bf6f550561
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

5
.gitignore vendored

@ -23,4 +23,9 @@ __pycache__
# Ignore development npm files
node_modules
# Ignore npm logs file
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Re-Identification algorithm to merging bounding boxes automatically to the new UI (<https://github.com/opencv/cvat/pull/1406>)
- Methods ``import`` and ``export`` to import/export raw annotations for Job and Task in ``cvat-core`` (<https://github.com/opencv/cvat/pull/1406>)
- Versioning of client packages (``cvat-core``, ``cvat-canvas``, ``cvat-ui``). Initial versions are set to 1.0.0 (<https://github.com/opencv/cvat/pull/1448>)
- Cuboids feature was migrated from old UI to new one. (<https://github.com/opencv/cvat/pull/1451>)
### Changed
-

@ -134,7 +134,8 @@ polyline.cvat_canvas_shape_splitting {
cursor: nwse-resize;
}
.svg_select_points_l:hover, .svg_select_points_r:hover {
.svg_select_points_l:hover, .svg_select_points_r:hover,
.svg_select_points_ew:hover {
cursor: ew-resize;
}

@ -50,6 +50,7 @@ export interface Configuration {
autoborders?: boolean;
displayAllText?: boolean;
undefinedAttrValue?: string;
showProjections?: boolean;
}
export interface DrawData {
@ -527,6 +528,9 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.displayAllText = configuration.displayAllText;
}
if (typeof (configuration.showProjections) !== 'undefined') {
this.data.configuration.showProjections = configuration.showProjections;
}
if (typeof (configuration.autoborders) !== 'undefined') {
this.data.configuration.autoborders = configuration.autoborders;
}

@ -437,6 +437,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
.filter((_state: any): boolean => (
_state.clientID === self.activeElement.clientID
));
if (['cuboid', 'rectangle'].includes(state.shapeType)) {
e.preventDefault();
return;
}
if (e.ctrlKey) {
const { points } = state;
self.onEditDone(
@ -721,7 +725,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.CONFIG_UPDATED) {
const { activeElement } = this;
this.deactivate();
this.configuration = model.configuration;
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
@ -939,26 +946,63 @@ export class CanvasViewImpl implements CanvasView, Listener {
for (const state of states) {
if (state.hidden || state.outside) continue;
ctx.fillStyle = 'white';
if (['rectangle', 'polygon'].includes(state.shapeType)) {
const points = state.shapeType === 'rectangle' ? [
state.points[0], // xtl
state.points[1], // ytl
state.points[2], // xbr
state.points[1], // ytl
state.points[2], // xbr
state.points[3], // ybr
state.points[0], // xtl
state.points[3], // ybr
] : state.points;
if (['rectangle', 'polygon', 'cuboid'].includes(state.shapeType)) {
let points = [];
if (state.shapeType === 'rectangle') {
points = [
state.points[0], // xtl
state.points[1], // ytl
state.points[2], // xbr
state.points[1], // ytl
state.points[2], // xbr
state.points[3], // ybr
state.points[0], // xtl
state.points[3], // ybr
];
} else if (state.shapeType === 'cuboid') {
points = [
state.points[0],
state.points[1],
state.points[4],
state.points[5],
state.points[8],
state.points[9],
state.points[12],
state.points[13],
];
} else {
points = [...state.points];
}
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
for (let i = 0; i < points.length; i += 2) {
ctx.lineTo(points[i], points[i + 1]);
}
ctx.closePath();
ctx.fill();
}
ctx.fill();
if (state.shapeType === 'cuboid') {
for (let i = 0; i < 5; i++) {
const points = [
state.points[(0 + i * 4) % 16],
state.points[(1 + i * 4) % 16],
state.points[(2 + i * 4) % 16],
state.points[(3 + i * 4) % 16],
state.points[(6 + i * 4) % 16],
state.points[(7 + i * 4) % 16],
state.points[(4 + i * 4) % 16],
state.points[(5 + i * 4) % 16],
];
ctx.beginPath();
ctx.moveTo(points[0], points[1]);
for (let j = 0; j < points.length; j += 2) {
ctx.lineTo(points[j], points[j + 1]);
}
ctx.closePath();
ctx.fill();
}
}
}
}
}
@ -1055,7 +1099,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
return `${acc}${val},`;
}, '',
);
(shape as any).clear();
if (state.shapeType !== 'cuboid') {
(shape as any).clear();
}
shape.attr('points', stringified);
if (state.shapeType === 'points' && !isInvisible) {
@ -1116,6 +1162,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this
.addPoints(stringified, state);
} else if (state.shapeType === 'cuboid') {
this.svgShapes[state.clientID] = this
.addCuboid(stringified, state);
}
}
@ -1202,6 +1251,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.selectize(false, shape);
}
if (drawnState.shapeType === 'cuboid') {
(shape as any).attr('projections', false);
}
(shape as any).off('resizestart');
(shape as any).off('resizing');
(shape as any).off('resizedone');
@ -1281,6 +1334,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.append(shape.node);
}
const { showProjections } = this.configuration;
if (state.shapeType === 'cuboid' && showProjections) {
(shape as any).attr('projections', true);
}
if (!state.pinned) {
shape.addClass('cvat_canvas_shape_draggable');
(shape as any).draggable().on('dragstart', (): void => {
@ -1548,6 +1606,30 @@ export class CanvasViewImpl implements CanvasView, Listener {
return polyline;
}
private addCuboid(points: string, state: any): any {
const cube = (this.adoptedContent as any).cube(points)
.fill(state.color).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape');
if (state.occluded) {
cube.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
cube.style('display', 'none');
}
return cube;
}
private setupPoints(basicPolyline: SVG.PolyLine, state: any): any {
this.selectize(true, basicPolyline);

@ -10,6 +10,7 @@ const AREA_THRESHOLD = 9;
const SIZE_THRESHOLD = 3;
const POINTS_STROKE_WIDTH = 1.5;
const POINTS_SELECTED_STROKE_WIDTH = 4;
const MIN_EDGE_LENGTH = 3;
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
export default {
@ -21,5 +22,6 @@ export default {
SIZE_THRESHOLD,
POINTS_STROKE_WIDTH,
POINTS_SELECTED_STROKE_WIDTH,
MIN_EDGE_LENGTH,
UNDEFINED_ATTRIBUTE_VALUE,
};

@ -0,0 +1,494 @@
/* eslint-disable func-names */
/* eslint-disable no-underscore-dangle */
/* eslint-disable curly */
/*
* Copyright (C) 2020 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
import consts from './consts';
export interface Point {
x: number;
y: number;
}
export enum Orientation {
LEFT = 'left',
RIGHT = 'right',
}
function line(p1: Point, p2: Point): number[] {
const a = p1.y - p2.y;
const b = p2.x - p1.x;
const c = b * p1.y + a * p1.x;
return [a, b, c];
}
function intersection(
p1: Point, p2: Point, p3: Point, p4: Point,
): Point | null {
const L1 = line(p1, p2);
const L2 = line(p3, p4);
const D = L1[0] * L2[1] - L1[1] * L2[0];
const Dx = L1[2] * L2[1] - L1[1] * L2[2];
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
let x = null;
let y = null;
if (D !== 0) {
x = Dx / D;
y = Dy / D;
return { x, y };
}
return null;
}
export class Equation {
private a: number;
private b: number;
private c: number;
public constructor(p1: Point, p2: Point) {
this.a = p1.y - p2.y;
this.b = p2.x - p1.x;
this.c = this.b * p1.y + this.a * p1.x;
}
// get the line equation in actual coordinates
public getY(x: number): number {
return (this.c - this.a * x) / this.b;
}
}
export class Figure {
private indices: number[];
private allPoints: Point[];
public constructor(indices: number[], points: Point[]) {
this.indices = indices;
this.allPoints = points;
}
public get points(): Point[] {
const points = [];
for (const index of this.indices) {
points.push(this.allPoints[index]);
}
return points;
}
// sets the point for a given edge, points must be given in
// array form in the same ordering as the getter
// if you only need to update a subset of the points,
// simply put null for the points you want to keep
public set points(newPoints) {
const oldPoints = this.allPoints;
for (let i = 0; i < newPoints.length; i += 1) {
if (newPoints[i] !== null) {
oldPoints[this.indices[i]] = { x: newPoints[i].x, y: newPoints[i].y };
}
}
}
}
export class Edge extends Figure {
public getEquation(): Equation {
return new Equation(this.points[0], this.points[1]);
}
}
export class CuboidModel {
public points: Point[];
private fr: Edge;
private fl: Edge;
private dr: Edge;
private dl: Edge;
private ft: Edge;
private rt: Edge;
private lt: Edge;
private dt: Edge;
private fb: Edge;
private rb: Edge;
private lb: Edge;
private db: Edge;
public edgeList: Edge[];
private front: Figure;
private right: Figure;
private dorsal: Figure;
private left: Figure;
private top: Figure;
private bot: Figure;
public facesList: Figure[];
public vpl: Point | null;
public vpr: Point | null;
public orientation: Orientation;
public constructor(points?: Point[]) {
this.points = points;
this.initEdges();
this.initFaces();
this.updateVanishingPoints(false);
this.buildBackEdge(false);
this.updatePoints();
this.updateOrientation();
}
public getPoints(): Point[] {
return this.points;
}
public setPoints(points: (Point | null)[]): void {
points.forEach((point: Point | null, i: number): void => {
if (point !== null) {
this.points[i].x = point.x;
this.points[i].y = point.y;
}
});
}
public updateOrientation(): void {
if (this.dl.points[0].x > this.fl.points[0].x) {
this.orientation = Orientation.LEFT;
} else {
this.orientation = Orientation.RIGHT;
}
}
public updatePoints(): void {
// making sure that the edges are vertical
this.fr.points[0].x = this.fr.points[1].x;
this.fl.points[0].x = this.fl.points[1].x;
this.dr.points[0].x = this.dr.points[1].x;
this.dl.points[0].x = this.dl.points[1].x;
}
public computeSideEdgeConstraints(edge: any): any {
const midLength = this.fr.points[1].y - this.fr.points[0].y - 1;
const minY = edge.points[1].y - midLength;
const maxY = edge.points[0].y + midLength;
const y1 = edge.points[0].y;
const y2 = edge.points[1].y;
const miny1 = y2 - midLength;
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
const maxy2 = y1 + midLength;
return {
constraint: {
minY,
maxY,
},
y1Range: {
max: maxy1,
min: miny1,
},
y2Range: {
max: maxy2,
min: miny2,
},
};
}
// boolean value parameter controls which edges should be used to recalculate vanishing points
private updateVanishingPoints(buildright: boolean): void {
let leftEdge = [];
let rightEdge = [];
let midEdge = [];
if (buildright) {
leftEdge = this.fr.points;
rightEdge = this.dl.points;
midEdge = this.fl.points;
} else {
leftEdge = this.fl.points;
rightEdge = this.dr.points;
midEdge = this.fr.points;
}
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
this.vpr = intersection(rightEdge[0], midEdge[0], rightEdge[1], midEdge[1]);
if (this.vpl === null) {
// shift the edge slightly to avoid edge case
leftEdge[0].y -= 0.001;
leftEdge[0].x += 0.001;
leftEdge[1].x += 0.001;
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
}
if (this.vpr === null) {
// shift the edge slightly to avoid edge case
rightEdge[0].y -= 0.001;
rightEdge[0].x -= 0.001;
rightEdge[1].x -= 0.001;
this.vpr = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
}
}
private initEdges(): void {
this.fl = new Edge([0, 1], this.points);
this.fr = new Edge([2, 3], this.points);
this.dr = new Edge([4, 5], this.points);
this.dl = new Edge([6, 7], this.points);
this.ft = new Edge([0, 2], this.points);
this.lt = new Edge([0, 6], this.points);
this.rt = new Edge([2, 4], this.points);
this.dt = new Edge([6, 4], this.points);
this.fb = new Edge([1, 3], this.points);
this.lb = new Edge([1, 7], this.points);
this.rb = new Edge([3, 5], this.points);
this.db = new Edge([7, 5], this.points);
this.edgeList = [this.fl, this.fr, this.dl, this.dr, this.ft, this.lt,
this.rt, this.dt, this.fb, this.lb, this.rb, this.db];
}
private initFaces(): void {
this.front = new Figure([0, 1, 3, 2], this.points);
this.right = new Figure([2, 3, 5, 4], this.points);
this.dorsal = new Figure([4, 5, 7, 6], this.points);
this.left = new Figure([6, 7, 1, 0], this.points);
this.top = new Figure([0, 2, 4, 6], this.points);
this.bot = new Figure([1, 3, 5, 7], this.points);
this.facesList = [this.front, this.right, this.dorsal, this.left];
}
private buildBackEdge(buildright: boolean): void {
this.updateVanishingPoints(buildright);
let leftPoints = [];
let rightPoints = [];
let topIndex = 0;
let botIndex = 0;
if (buildright) {
leftPoints = this.dl.points;
rightPoints = this.fr.points;
topIndex = 4;
botIndex = 5;
} else {
leftPoints = this.dr.points;
rightPoints = this.fl.points;
topIndex = 6;
botIndex = 7;
}
const vpLeft = this.vpl;
const vpRight = this.vpr;
let p1 = intersection(vpLeft, leftPoints[0], vpRight, rightPoints[0]);
let p2 = intersection(vpLeft, leftPoints[1], vpRight, rightPoints[1]);
if (p1 === null) {
p1 = { x: p2.x, y: vpLeft.y };
} else if (p2 === null) {
p2 = { x: p1.x, y: vpLeft.y };
}
this.points[topIndex] = { x: p1.x, y: p1.y };
this.points[botIndex] = { x: p2.x, y: p2.y };
// Making sure that the vertical edges stay vertical
this.updatePoints();
}
}
function sortPointsClockwise(points: any[]): any[] {
points.sort((a, b): number => a.y - b.y);
// Get center y
const cy = (points[0].y + points[points.length - 1].y) / 2;
// Sort from right to left
points.sort((a, b): number => b.x - a.x);
// Get center x
const cx = (points[0].x + points[points.length - 1].x) / 2;
// Center point
const center = {
x: cx,
y: cy,
};
// Starting angle used to reference other angles
let startAng: number | undefined;
points.forEach((point): void => {
let ang = Math.atan2(point.y - center.y, point.x - center.x);
if (!startAng) {
startAng = ang;
// ensure that all points are clockwise of the start point
} else if (ang < startAng) {
ang += Math.PI * 2;
}
// eslint-disable-next-line no-param-reassign
point.angle = ang; // add the angle to the point
});
// first sort clockwise
points.sort((a, b): number => a.angle - b.angle);
return points.reverse();
}
function setupCuboidPoints(points: Point[]): any[] {
let left;
let right;
let left2;
let right2;
let p1;
let p2;
let p3;
let p4;
const height = Math.abs(points[0].x - points[1].x)
< Math.abs(points[1].x - points[2].x)
? Math.abs(points[1].y - points[0].y)
: Math.abs(points[1].y - points[2].y);
// seperate into left and right point
// we pick the first and third point because we know assume they will be on
// opposite corners
if (points[0].x < points[2].x) {
[left,, right] = points;
} else {
[right,, left] = points;
}
// get other 2 points using the given height
if (left.y < right.y) {
left2 = { x: left.x, y: left.y + height };
right2 = { x: right.x, y: right.y - height };
} else {
left2 = { x: left.x, y: left.y - height };
right2 = { x: right.x, y: right.y + height };
}
// get the vector for the last point relative to the previous point
const vec = {
x: points[3].x - points[2].x,
y: points[3].y - points[2].y,
};
if (left.y < left2.y) {
p1 = left;
p2 = left2;
} else {
p1 = left2;
p2 = left;
}
if (right.y < right2.y) {
p3 = right;
p4 = right2;
} else {
p3 = right2;
p4 = right;
}
const p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
const p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
const p7 = { x: p1.x + vec.x, y: p1.y + vec.y + 0.1 };
const p8 = { x: p2.x + vec.x, y: p2.y + vec.y - 0.1 };
p1.y += 0.1;
return [p1, p2, p3, p4, p5, p6, p7, p8];
}
export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
const points: Point[] = [];
for (let i = 0; i < 4; i++) {
const [x, y] = flattenedPoints.slice(i * 2, i * 2 + 2);
points.push({ x, y });
}
const unsortedPlanePoints = points.slice(0, 3);
function rotate(array: any[], times: number): void{
let t = times;
while (t--) {
const temp = array.shift();
array.push(temp);
}
}
const plane2 = {
p1: points[0],
p2: points[0],
p3: points[0],
p4: points[0],
};
// completing the plane
const vector = {
x: points[2].x - points[1].x,
y: points[2].y - points[1].y,
};
// sorting the first plane
unsortedPlanePoints.push({
x: points[0].x + vector.x,
y: points[0].y + vector.y,
});
const sortedPlanePoints = sortPointsClockwise(unsortedPlanePoints);
let leftIndex = 0;
for (let i = 0; i < 4; i++) {
leftIndex = sortedPlanePoints[i].x < sortedPlanePoints[leftIndex].x ? i : leftIndex;
}
rotate(sortedPlanePoints, leftIndex);
const plane1 = {
p1: sortedPlanePoints[0],
p2: sortedPlanePoints[1],
p3: sortedPlanePoints[2],
p4: sortedPlanePoints[3],
};
const vec = {
x: points[3].x - points[2].x,
y: points[3].y - points[2].y,
};
// determine the orientation
const angle = Math.atan2(vec.y, vec.x);
// making the other plane
plane2.p1 = { x: plane1.p1.x + vec.x, y: plane1.p1.y + vec.y };
plane2.p2 = { x: plane1.p2.x + vec.x, y: plane1.p2.y + vec.y };
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
let cuboidPoints;
// right
if (Math.abs(angle) < Math.PI / 2 - 0.1) {
cuboidPoints = setupCuboidPoints(points);
// left
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
cuboidPoints = setupCuboidPoints(points);
// down
} else if (angle > 0) {
cuboidPoints = [
plane1.p1, plane2.p1, plane1.p2, plane2.p2,
plane1.p3, plane2.p3, plane1.p4, plane2.p4,
];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
// up
} else {
cuboidPoints = [
plane2.p1, plane1.p1, plane2.p2, plane1.p2,
plane2.p3, plane1.p3, plane2.p4, plane1.p4,
];
cuboidPoints[0].y += 0.1;
cuboidPoints[4].y += 0.1;
}
return cuboidPoints.reduce((arr: number[], point: any): number[] => {
arr.push(point.x);
arr.push(point.y);
return arr;
}, []);
}

@ -24,6 +24,8 @@ import {
Configuration,
} from './canvasModel';
import { cuboidFrom4Points } from './cuboid';
export interface DrawHandler {
configurate(configuration: Configuration): void;
draw(drawData: DrawData, geometry: Geometry): void;
@ -105,6 +107,84 @@ export class DrawHandlerImpl implements DrawHandler {
};
}
private getFinalCuboidCoordinates(targetPoints: number[]): {
points: number[];
box: Box;
} {
const { offset } = this.geometry;
let points = targetPoints;
const box = {
xtl: 0,
ytl: 0,
xbr: Number.MAX_SAFE_INTEGER,
ybr: Number.MAX_SAFE_INTEGER,
};
const frameWidth = this.geometry.image.width;
const frameHeight = this.geometry.image.height;
const cuboidOffsets = [];
const minCuboidOffset = {
d: Number.MAX_SAFE_INTEGER,
dx: 0,
dy: 0,
};
for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i);
if (x >= offset && x <= offset + frameWidth
&& y >= offset && y <= offset + frameHeight) continue;
let xOffset = 0;
let yOffset = 0;
if (x < offset) {
xOffset = offset - x;
} else if (x > offset + frameWidth) {
xOffset = offset + frameWidth - x;
}
if (y < offset) {
yOffset = offset - y;
} else if (y > offset + frameHeight) {
yOffset = offset + frameHeight - y;
}
cuboidOffsets.push([xOffset, yOffset]);
}
if (cuboidOffsets.length === points.length / 2) {
cuboidOffsets.forEach((offsetCoords: number[]): void => {
if (Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2))
< minCuboidOffset.d) {
minCuboidOffset.d = Math.sqrt((offsetCoords[0] ** 2) + (offsetCoords[1] ** 2));
[minCuboidOffset.dx, minCuboidOffset.dy] = offsetCoords;
}
});
points = points.map((coord: number, i: number): number => {
const finalCoord = coord + (i % 2 === 0 ? minCuboidOffset.dx : minCuboidOffset.dy);
if (i % 2 === 0) {
box.xtl = Math.max(box.xtl, finalCoord);
box.xbr = Math.min(box.xbr, finalCoord);
} else {
box.ytl = Math.max(box.ytl, finalCoord);
box.ybr = Math.min(box.ybr, finalCoord);
}
return finalCoord;
});
}
return {
points: points.map((coord: number): number => coord - offset),
box,
};
}
private addCrosshair(): void {
const { x, y } = this.cursorPosition;
this.crosshair = {
@ -236,18 +316,17 @@ export class DrawHandlerImpl implements DrawHandler {
}
private drawPolyshape(): void {
let size = this.drawData.numberOfPoints;
const sizeDecrement = function sizeDecrement(): void {
if (!--size) {
let size = this.drawData.shapeType === 'cuboid' ? 4 : this.drawData.numberOfPoints;
const sizeDecrement = (): void => {
if (--size === 0) {
this.drawInstance.draw('done');
}
}.bind(this);
};
if (this.drawData.numberOfPoints) {
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', (): number => size++);
}
this.drawInstance.on('drawstart', sizeDecrement);
this.drawInstance.on('drawpoint', sizeDecrement);
this.drawInstance.on('undopoint', (): number => size++);
// Add ability to cancel the latest drawn point
this.canvas.on('mousedown.draw', (e: MouseEvent): void => {
@ -299,8 +378,9 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawInstance.on('drawdone', (e: CustomEvent): void => {
const targetPoints = pointsToArray((e.target as SVGElement).getAttribute('points'));
const { points, box } = this.getFinalPolyshapeCoordinates(targetPoints);
const { shapeType } = this.drawData;
const { points, box } = shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
this.release();
if (this.canceled) return;
@ -325,6 +405,13 @@ export class DrawHandlerImpl implements DrawHandler {
shapeType,
points,
}, Date.now() - this.startTimestamp);
// TODO: think about correct constraign for cuboids
} else if (shapeType === 'cuboid'
&& points.length === 4 * 2) {
this.onDrawDone({
shapeType,
points: cuboidFrom4Points(points),
}, Date.now() - this.startTimestamp);
}
});
}
@ -364,6 +451,14 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyshape();
}
private drawCuboid(): void {
this.drawInstance = (this.canvas as any).polyline()
.addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
this.drawPolyshape();
}
private pastePolyshape(): void {
this.drawInstance.on('done', (e: CustomEvent): void => {
const targetPoints = this.drawInstance
@ -371,7 +466,9 @@ export class DrawHandlerImpl implements DrawHandler {
.split(/[,\s]/g)
.map((coord: string): number => +coord);
const { points } = this.getFinalPolyshapeCoordinates(targetPoints);
const { points } = this.drawData.initialState.shapeType === 'cuboid' ? this.getFinalCuboidCoordinates(targetPoints)
: this.getFinalPolyshapeCoordinates(targetPoints);
if (!e.detail.originalEvent.ctrlKey) {
this.release();
}
@ -450,6 +547,15 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePolyshape();
}
private pasteCuboid(points: string): void {
this.drawInstance = (this.canvas as any).cube(points).addClass('cvat_canvas_shape_drawing').attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
});
this.pasteShape();
this.pastePolyshape();
}
private pastePoints(initialPoints: string): void {
function moveShape(
shape: SVG.PolyLine,
@ -550,6 +656,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.pastePolyline(stringifiedPoints);
} else if (this.drawData.shapeType === 'points') {
this.pastePoints(stringifiedPoints);
} else if (this.drawData.shapeType === 'cuboid') {
this.pasteCuboid(stringifiedPoints);
}
}
this.setupPasteEvents();
@ -570,6 +678,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.drawPolyline();
} else if (this.drawData.shapeType === 'points') {
this.drawPoints();
} else if (this.drawData.shapeType === 'cuboid') {
this.drawCuboid();
}
this.setupDrawEvents();
}

@ -25,6 +25,10 @@ export interface BBox {
y: number;
}
interface Point {
x: number;
y: number;
}
export interface DrawnState {
clientID: number;
outside?: boolean;
@ -115,3 +119,26 @@ export function displayShapeSize(
return shapeSize;
}
export function convertToArray(points: Point[]): number[][] {
const arr: number[][] = [];
points.forEach((point: Point): void => {
arr.push([point.x, point.y]);
});
return arr;
}
export function parsePoints(stringified: string): Point[] {
return stringified.trim().split(/\s/).map((point: string): Point => {
const [x, y] = point.split(',').map((coord: string): number => +coord);
return { x, y };
});
}
export function stringifyPoints(points: Point[]): string {
return points.map((point: Point): string => `${point.x},${point.y}`).join(' ');
}
export function clamp(x: number, min: number, max: number): number {
return Math.min(Math.max(x, min), max);
}

@ -9,6 +9,16 @@ import 'svg.resize.js';
import 'svg.select.js';
import 'svg.draw.js';
import consts from './consts';
import {
Point,
Equation,
CuboidModel,
Orientation,
Edge,
} from './cuboid';
import { parsePoints, stringifyPoints, clamp } from './shared';
// Update constructor
const originalDraw = SVG.Element.prototype.draw;
SVG.Element.prototype.draw = function constructor(...args: any): any {
@ -181,3 +191,862 @@ SVG.Element.prototype.resize = function constructor(...args: any): any {
for (const key of Object.keys(originalResize)) {
SVG.Element.prototype.resize[key] = originalResize[key];
}
enum EdgeIndex {
FL = 1,
FR = 2,
DR = 3,
DL = 4,
}
function getEdgeIndex(cuboidPoint: number): EdgeIndex {
switch (cuboidPoint) {
case 0:
case 1:
return EdgeIndex.FL;
case 2:
case 3:
return EdgeIndex.FR;
case 4:
case 5:
return EdgeIndex.DR;
default:
return EdgeIndex.DL;
}
}
function getTopDown(edgeIndex: EdgeIndex): number[] {
switch (edgeIndex) {
case EdgeIndex.FL:
return [0, 1];
case EdgeIndex.FR:
return [2, 3];
case EdgeIndex.DR:
return [4, 5];
default:
return [6, 7];
}
}
(SVG as any).Cube = SVG.invent({
create: 'g',
inherit: SVG.G,
extend: {
constructorMethod(points: string) {
this.cuboidModel = new CuboidModel(parsePoints(points));
this.setupFaces();
this.setupEdges();
this.setupProjections();
this.hideProjections();
this._attr('points', points);
return this;
},
setupFaces() {
this.bot = this.polygon(this.cuboidModel.bot.points);
this.top = this.polygon(this.cuboidModel.top.points);
this.right = this.polygon(this.cuboidModel.right.points);
this.dorsal = this.polygon(this.cuboidModel.dorsal.points);
this.left = this.polygon(this.cuboidModel.left.points);
this.face = this.polygon(this.cuboidModel.front.points);
},
setupProjections() {
this.ftProj = this.line(this.updateProjectionLine(this.cuboidModel.ft.getEquation(),
this.cuboidModel.ft.points[0], this.cuboidModel.vpl));
this.fbProj = this.line(this.updateProjectionLine(this.cuboidModel.fb.getEquation(),
this.cuboidModel.ft.points[0], this.cuboidModel.vpl));
this.rtProj = this.line(this.updateProjectionLine(this.cuboidModel.rt.getEquation(),
this.cuboidModel.rt.points[1], this.cuboidModel.vpr));
this.rbProj = this.line(this.updateProjectionLine(this.cuboidModel.rb.getEquation(),
this.cuboidModel.rb.points[1], this.cuboidModel.vpr));
this.ftProj.stroke({ color: '#C0C0C0' });
this.fbProj.stroke({ color: '#C0C0C0' });
this.rtProj.stroke({ color: '#C0C0C0' });
this.rbProj.stroke({ color: '#C0C0C0' });
},
setupEdges() {
this.frontLeftEdge = this.line(this.cuboidModel.fl.points);
this.frontRightEdge = this.line(this.cuboidModel.fr.points);
this.dorsalRightEdge = this.line(this.cuboidModel.dr.points);
this.dorsalLeftEdge = this.line(this.cuboidModel.dl.points);
this.frontTopEdge = this.line(this.cuboidModel.ft.points);
this.rightTopEdge = this.line(this.cuboidModel.rt.points);
this.frontBotEdge = this.line(this.cuboidModel.fb.points);
this.rightBotEdge = this.line(this.cuboidModel.rb.points);
},
setupGrabPoints(circleType) {
const viewModel = this.cuboidModel;
const circle = typeof circleType === 'function' ? circleType : this.circle;
this.flCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_l');
this.frCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_r');
this.ftCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_t');
this.fbCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_b');
this.drCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_ew');
this.dlCenter = circle(0, 0).addClass('svg_select_points').addClass('svg_select_points_ew');
const grabPoints = this.getGrabPoints();
const edges = this.getEdges();
for (let i = 0; i < grabPoints.length; i += 1) {
const edge = edges[i];
const cx = (edge.attr('x2') + edge.attr('x1')) / 2;
const cy = (edge.attr('y2') + edge.attr('y1')) / 2;
grabPoints[i].center(cx, cy);
}
if (viewModel.orientation === Orientation.LEFT) {
this.dlCenter.hide();
} else {
this.drCenter.hide();
}
},
showProjections() {
if (this.projectionLineEnable) {
this.ftProj.show();
this.fbProj.show();
this.rtProj.show();
this.rbProj.show();
}
},
hideProjections() {
this.ftProj.hide();
this.fbProj.hide();
this.rtProj.hide();
this.rbProj.hide();
},
getEdges() {
const arr = [];
arr.push(this.frontLeftEdge);
arr.push(this.frontRightEdge);
arr.push(this.dorsalRightEdge);
arr.push(this.frontTopEdge);
arr.push(this.frontBotEdge);
arr.push(this.dorsalLeftEdge);
arr.push(this.rightTopEdge);
arr.push(this.rightBotEdge);
return arr;
},
getGrabPoints() {
const arr = [];
arr.push(this.flCenter);
arr.push(this.frCenter);
arr.push(this.drCenter);
arr.push(this.ftCenter);
arr.push(this.fbCenter);
arr.push(this.dlCenter);
return arr;
},
updateProjectionLine(equation: Equation, source: Point, direction: Point) {
const x1 = source.x;
const y1 = equation.getY(x1);
const x2 = direction.x;
const y2 = equation.getY(x2);
return [[x1, y1], [x2, y2]];
},
selectize(value: boolean, options: object) {
this.face.selectize(value, options);
if (this.cuboidModel.orientation === Orientation.LEFT) {
this.dorsalLeftEdge.selectize(false, options);
this.dorsalRightEdge.selectize(value, options);
} else {
this.dorsalRightEdge.selectize(false, options);
this.dorsalLeftEdge.selectize(value, options);
}
if (value === false) {
this.getGrabPoints().forEach((point) => {point && point.remove()});
} else {
this.setupGrabPoints(this.face.remember('_selectHandler').drawPoint.bind(
{nested: this, options: this.face.remember('_selectHandler').options}
));
// setup proper classes for selection points for proper cursor
Array.from(this.face.remember('_selectHandler').nested.node.children)
.forEach((point: SVG.Circle, i: number) => {
point.classList.add(`svg_select_points_${['lt', 'lb', 'rb', 'rt'][i]}`)
});
if (this.cuboidModel.orientation === Orientation.LEFT) {
Array.from(this.dorsalRightEdge.remember('_selectHandler').nested.node.children)
.forEach((point: SVG.Circle, i: number) => {
point.classList.add(`svg_select_points_${['t', 'b'][i]}`);
point.ondblclick = this.resetPerspective.bind(this);
});
} else {
Array.from(this.dorsalLeftEdge.remember('_selectHandler').nested.node.children)
.forEach((point: SVG.Circle, i: number) => {
point.classList.add(`svg_select_points_${['t', 'b'][i]}`);
point.ondblclick = this.resetPerspective.bind(this);
});
}
}
return this;
},
resize(value?: string | object) {
this.face.resize(value);
if (value === 'stop') {
this.dorsalRightEdge.resize(value);
this.dorsalLeftEdge.resize(value);
this.face.off('resizing').off('resizedone').off('resizestart');
this.dorsalRightEdge.off('resizing').off('resizedone').off('resizestart');
this.dorsalLeftEdge.off('resizing').off('resizedone').off('resizestart');
this.getGrabPoints().forEach((point: SVG.Element) => {
if (point) {
point.off('dragstart');
point.off('dragmove');
point.off('dragend');
}
})
return;
}
function getResizedPointIndex(event: CustomEvent): number {
const { target } = event.detail.event.detail.event;
const { parentElement } = target;
return Array
.from(parentElement.children)
.indexOf(target);
}
let resizedCubePoint: null | number = null;
const accumulatedOffset: Point = {
x: 0,
y: 0,
};
this.face.on('resizestart', (event: CustomEvent) => {
accumulatedOffset.x = 0;
accumulatedOffset.y = 0;
const resizedFacePoint = getResizedPointIndex(event);
resizedCubePoint = [0, 1].includes(resizedFacePoint) ? resizedFacePoint
: 5 - resizedFacePoint; // 2,3 -> 3,2
this.fire(new CustomEvent('resizestart', event));
}).on('resizing', (event: CustomEvent) => {
let { dx, dy } = event.detail;
let dxPortion = dx - accumulatedOffset.x;
let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.x += dxPortion;
accumulatedOffset.y += dyPortion;
const edge = getEdgeIndex(resizedCubePoint);
const [edgeTopIndex, edgeBottomIndex] = getTopDown(edge);
let cuboidPoints = this.cuboidModel.getPoints();
let x1 = cuboidPoints[edgeTopIndex].x + dxPortion;
let x2 = cuboidPoints[edgeBottomIndex].x + dxPortion;
if (edge === EdgeIndex.FL
&& (cuboidPoints[2].x - (cuboidPoints[0].x + dxPortion) < consts.MIN_EDGE_LENGTH)
) {
x1 = cuboidPoints[edgeTopIndex].x;
x2 = cuboidPoints[edgeBottomIndex].x;
} else if (edge === EdgeIndex.FR
&& (cuboidPoints[2].x + dxPortion - cuboidPoints[0].x < consts.MIN_EDGE_LENGTH)
) {
x1 = cuboidPoints[edgeTopIndex].x;
x2 = cuboidPoints[edgeBottomIndex].x;
}
const y1 = this.cuboidModel.ft.getEquation().getY(x1);
const y2 = this.cuboidModel.fb.getEquation().getY(x2);
const topPoint = { x: x1, y: y1 };
const botPoint = { x: x2, y: y2 };
if (edge === 1) {
this.cuboidModel.fl.points = [topPoint, botPoint];
} else {
this.cuboidModel.fr.points = [topPoint, botPoint];
}
this.updateViewAndVM(edge === EdgeIndex.FR);
cuboidPoints = this.cuboidModel.getPoints();
const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
if (midPointDown.y - midPointUp.y > consts.MIN_EDGE_LENGTH) {
const topPoints = this.computeHeightFace(midPointUp, edge);
const bottomPoints = this.computeHeightFace(midPointDown, edge);
this.cuboidModel.top.points = topPoints;
this.cuboidModel.bot.points = bottomPoints;
this.updateViewAndVM(false);
}
this.face.plot(this.cuboidModel.front.points);
this.fire(new CustomEvent('resizing', event));
}).on('resizedone', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
function computeSideEdgeConstraints(edge: Edge, fr: Edge) {
const midLength = fr.points[1].y - fr.points[0].y - 1;
const minY = edge.points[1].y - midLength;
const maxY = edge.points[0].y + midLength;
const y1 = edge.points[0].y;
const y2 = edge.points[1].y;
const miny1 = y2 - midLength;
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
const maxy2 = y1 + midLength;
return {
constraint: {
minY,
maxY,
},
y1Range: {
max: maxy1,
min: miny1,
},
y2Range: {
max: maxy2,
min: miny2,
},
};
}
function setupDorsalEdge(edge: SVG.Line, orientation: Orientation) {
edge.on('resizestart', (event: CustomEvent) => {
accumulatedOffset.x = 0;
accumulatedOffset.y = 0;
resizedCubePoint = getResizedPointIndex(event) + (orientation === Orientation.LEFT ? 4 : 6);
this.fire(new CustomEvent('resizestart', event));
}).on('resizing', (event: CustomEvent) => {
let { dy } = event.detail;
let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.y += dyPortion;
const edge = getEdgeIndex(resizedCubePoint);
const [edgeTopIndex, edgeBottomIndex] = getTopDown(edge);
let cuboidPoints = this.cuboidModel.getPoints();
if (!event.detail.event.shiftKey) {
cuboidPoints = this.cuboidModel.getPoints();
const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
if (midPointDown.y - midPointUp.y > consts.MIN_EDGE_LENGTH) {
const topPoints = this.computeHeightFace(midPointUp, edge);
const bottomPoints = this.computeHeightFace(midPointDown, edge);
this.cuboidModel.top.points = topPoints;
this.cuboidModel.bot.points = bottomPoints;
}
} else {
const midPointUp = { ...cuboidPoints[edgeTopIndex] };
const midPointDown = { ...cuboidPoints[edgeBottomIndex] };
(edgeTopIndex === resizedCubePoint ? midPointUp : midPointDown).y += dyPortion;
const dorselEdge = (orientation === Orientation.LEFT ? this.cuboidModel.dr : this.cuboidModel.dl);
const constraints = computeSideEdgeConstraints(dorselEdge, this.cuboidModel.fr);
midPointUp.y = clamp(midPointUp.y, constraints.y1Range.min, constraints.y1Range.max);
midPointDown.y = clamp(midPointDown.y, constraints.y2Range.min, constraints.y2Range.max);
dorselEdge.points = [midPointUp, midPointDown];
this.updateViewAndVM(edge === EdgeIndex.DL);
}
this.updateViewAndVM(false);
this.face.plot(this.cuboidModel.front.points);
this.fire(new CustomEvent('resizing', event));
}).on('resizedone', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
}
if (this.cuboidModel.orientation === Orientation.LEFT) {
this.dorsalRightEdge.resize(value);
setupDorsalEdge.call(this, this.dorsalRightEdge, this.cuboidModel.orientation);
} else {
this.dorsalLeftEdge.resize(value);
setupDorsalEdge.call(this, this.dorsalLeftEdge, this.cuboidModel.orientation);
}
function horizontalEdgeControl(updatingFace, midX, midY) {
const leftPoints = this.updatedEdge(
this.cuboidModel.fl.points[0],
{x: midX, y: midY},
this.cuboidModel.vpl,
);
const rightPoints = this.updatedEdge(
this.cuboidModel.dr.points[0],
{x: midX, y: midY},
this.cuboidModel.vpr,
);
updatingFace.points = [leftPoints, {x: midX, y: midY}, rightPoints, null];
}
this.drCenter.draggable((x: number) => {
let xStatus;
if (this.drCenter.cx() < this.cuboidModel.fr.points[0].x) {
xStatus = x < this.cuboidModel.fr.points[0].x - consts.MIN_EDGE_LENGTH
&& x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH;
} else {
xStatus = x > this.cuboidModel.fr.points[0].x + consts.MIN_EDGE_LENGTH
&& x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH;
}
return { x: xStatus, y: this.drCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.dorsalRightEdge.center(this.drCenter.cx(), this.drCenter.cy());
const x = this.dorsalRightEdge.attr('x1');
const y1 = this.cuboidModel.rt.getEquation().getY(x);
const y2 = this.cuboidModel.rb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.dr.points = [topPoint, botPoint];
this.updateViewAndVM();
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.dlCenter.draggable((x: number) => {
let xStatus;
if (this.dlCenter.cx() < this.cuboidModel.fl.points[0].x) {
xStatus = x < this.cuboidModel.fl.points[0].x - consts.MIN_EDGE_LENGTH
&& x > this.cuboidModel.vpr.x + consts.MIN_EDGE_LENGTH;
} else {
xStatus = x > this.cuboidModel.fl.points[0].x + consts.MIN_EDGE_LENGTH
&& x < this.cuboidModel.vpr.x - consts.MIN_EDGE_LENGTH;
}
return { x: xStatus, y: this.dlCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.dorsalLeftEdge.center(this.dlCenter.cx(), this.dlCenter.cy());
const x = this.dorsalLeftEdge.attr('x1');
const y1 = this.cuboidModel.lt.getEquation().getY(x);
const y2 = this.cuboidModel.lb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.dl.points = [topPoint, botPoint];
this.updateViewAndVM(true);
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});;
this.flCenter.draggable((x: number) => {
const vpX = this.flCenter.cx() - this.cuboidModel.vpl.x > 0 ? this.cuboidModel.vpl.x : 0;
return { x: x < this.cuboidModel.fr.points[0].x && x > vpX + consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontLeftEdge.center(this.flCenter.cx(), this.flCenter.cy());
const x = this.frontLeftEdge.attr('x1');
const y1 = this.cuboidModel.ft.getEquation().getY(x);
const y2 = this.cuboidModel.fb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.fl.points = [topPoint, botPoint];
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.frCenter.draggable((x: number) => {
return { x: x > this.cuboidModel.fl.points[0].x, y: this.frCenter.attr('y1') };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontRightEdge.center(this.frCenter.cx(), this.frCenter.cy());
const x = this.frontRightEdge.attr('x1');
const y1 = this.cuboidModel.ft.getEquation().getY(x);
const y2 = this.cuboidModel.fb.getEquation().getY(x);
const topPoint = { x, y: y1 };
const botPoint = { x, y: y2 };
this.cuboidModel.fr.points = [topPoint, botPoint];
this.updateViewAndVM(true);
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.ftCenter.draggable((x: number, y: number) => {
return { x: x === this.ftCenter.cx(), y: y < this.fbCenter.cy() - consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontTopEdge.center(this.ftCenter.cx(), this.ftCenter.cy());
horizontalEdgeControl.call(this, this.cuboidModel.top, this.frontTopEdge.attr('x2'), this.frontTopEdge.attr('y2'));
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
this.fbCenter.draggable((x: number, y: number) => {
return { x: x === this.fbCenter.cx(), y: y > this.ftCenter.cy() + consts.MIN_EDGE_LENGTH };
}).on('dragstart', ((event: CustomEvent) => {
this.fire(new CustomEvent('resizestart', event));
})).on('dragmove', (event: CustomEvent) => {
this.frontBotEdge.center(this.fbCenter.cx(), this.fbCenter.cy());
horizontalEdgeControl.call(this, this.cuboidModel.bot, this.frontBotEdge.attr('x2'), this.frontBotEdge.attr('y2'));
this.updateViewAndVM();
this.fire(new CustomEvent('resizing', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('resizedone', event));
});
return this;
},
draggable(value: any, constraint: any) {
const { cuboidModel } = this;
const faces = [this.face, this.right, this.dorsal, this.left]
const accumulatedOffset: Point = {
x: 0,
y: 0,
};
if (value === false) {
faces.forEach((face: any) => {
face.draggable(false);
face.off('dragstart');
face.off('dragmove');
face.off('dragend');
})
return
}
this.face.draggable().on('dragstart', (event: CustomEvent) => {
accumulatedOffset.x = 0;
accumulatedOffset.y = 0;
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
const dx = event.detail.p.x - event.detail.handler.startPoints.point.x;
const dy = event.detail.p.y - event.detail.handler.startPoints.point.y;
let dxPortion = dx - accumulatedOffset.x;
let dyPortion = dy - accumulatedOffset.y;
accumulatedOffset.x += dxPortion;
accumulatedOffset.y += dyPortion;
this.dmove(dxPortion, dyPortion);
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
})
this.left.draggable((x: number, y: number) => ({
x: x < Math.min(cuboidModel.dr.points[0].x,
cuboidModel.fr.points[0].x) - consts.MIN_EDGE_LENGTH, y
})).on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
this.cuboidModel.left.points = parsePoints(this.left.attr('points'));
this.updateViewAndVM();
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
this.dorsal.draggable().on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
this.cuboidModel.dorsal.points = parsePoints(this.dorsal.attr('points'));
this.updateViewAndVM();
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
this.right.draggable((x: number, y: number) => ({
x: x > Math.min(cuboidModel.dl.points[0].x,
cuboidModel.fl.points[0].x) + consts.MIN_EDGE_LENGTH, y
})).on('dragstart', (event: CustomEvent) => {
this.fire(new CustomEvent('dragstart', event));
}).on('dragmove', (event: CustomEvent) => {
this.cuboidModel.right.points = parsePoints(this.right.attr('points'));
this.updateViewAndVM(true);
this.fire(new CustomEvent('dragmove', event));
}).on('dragend', (event: CustomEvent) => {
this.fire(new CustomEvent('dragend', event));
});
return this;
},
_attr: SVG.Element.prototype.attr,
attr(a: any, v: any, n: any) {
if ((a === 'fill' || a === 'stroke' || a === 'face-stroke')
&& v !== undefined) {
this._attr(a, v, n);
this.paintOrientationLines();
} else if (a === 'points' && typeof v === 'string') {
const points = parsePoints(v);
this.cuboidModel.setPoints(points);
this.updateViewAndVM();
} else if (a === 'projections') {
this._attr(a, v, n);
if (v === true) {
this.ftProj.show();
this.fbProj.show();
this.rtProj.show();
this.rbProj.show();
} else {
this.ftProj.hide();
this.fbProj.hide();
this.rtProj.hide();
this.rbProj.hide();
}
} else if (a === 'stroke-width' && typeof v === "number") {
this._attr(a, v, n);
this.updateThickness();
} else if (a === 'data-z-order' && typeof v !== 'undefined') {
this._attr(a, v, n);
[this.face, this.left, this.dorsal, this.right, ...this.getEdges(), ...this.getGrabPoints()]
.forEach((el) => {if (el) el.attr(a, v, n)})
} else {
return this._attr(a ,v, n);
}
return this;
},
updateThickness() {
const edges = [this.frontLeftEdge, this.frontRightEdge, this.frontTopEdge, this.frontBotEdge]
const width = this.attr('stroke-width');
edges.forEach((edge: SVG.Element) => {
edge.attr('stroke-width', width * (this.strokeOffset || 1.75));
});
this.on('mouseover', () => {
edges.forEach((edge: SVG.Element) => {
this.strokeOffset = 2.5;
edge.attr('stroke-width', width * this.strokeOffset);
})
}).on('mouseout', () => {
edges.forEach((edge: SVG.Element) => {
this.strokeOffset = 1.75;
edge.attr('stroke-width', width * this.strokeOffset);
})
});
},
paintOrientationLines() {
const fillColor = this.attr('fill');
const strokeColor = this.attr('stroke');
const selectedColor = this.attr('face-stroke') || '#b0bec5';
this.frontTopEdge.stroke({ color: selectedColor });
this.frontLeftEdge.stroke({ color: selectedColor });
this.frontBotEdge.stroke({ color: selectedColor });
this.frontRightEdge.stroke({ color: selectedColor });
this.rightTopEdge.stroke({ color: strokeColor });
this.rightBotEdge.stroke({ color: strokeColor });
this.dorsalRightEdge.stroke({ color: strokeColor });
this.dorsalLeftEdge.stroke({ color: strokeColor });
this.bot.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.top.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.face.stroke({ color: strokeColor, width: 0 })
.fill({ color: fillColor });
this.right.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.dorsal.stroke({ color: strokeColor })
.fill({ color: fillColor });
this.left.stroke({ color: strokeColor })
.fill({ color: fillColor });
},
dmove(dx: number, dy: number) {
this.cuboidModel.points.forEach((point: Point) => {
point.x += dx;
point.y += dy;
});
this.updateViewAndVM();
},
x(x?: number) {
if (typeof x === 'number') {
const { x: xInitial } = this.bbox();
this.dmove(x - xInitial, 0);
return this;
} else {
return this.bbox().x;
}
},
y(y?: number) {
if (typeof y === 'number') {
const { y: yInitial } = this.bbox();
this.dmove(0, y - yInitial);
return this;
} else {
return this.bbox().y;
}
},
resetPerspective(){
if (this.cuboidModel.orientation === Orientation.LEFT) {
const edgePoints = this.cuboidModel.dl.points;
const constraints = this.cuboidModel.computeSideEdgeConstraints(this.cuboidModel.dl);
edgePoints[0].y = constraints.y1Range.min;
this.cuboidModel.dl.points = [edgePoints[0],edgePoints[1]];
this.updateViewAndVM(true);
} else {
const edgePoints = this.cuboidModel.dr.points;
const constraints = this.cuboidModel.computeSideEdgeConstraints(this.cuboidModel.dr);
edgePoints[0].y = constraints.y1Range.min;
this.cuboidModel.dr.points = [edgePoints[0],edgePoints[1]];
this.updateViewAndVM();
}
},
updateViewAndVM(build: boolean) {
this.cuboidModel.updateOrientation();
this.cuboidModel.buildBackEdge(build);
this.updateView();
// to correct getting of points in resizedone, dragdone
this._attr('points', this.cuboidModel
.getPoints()
.reduce((acc: string, point: Point): string => `${acc} ${point.x},${point.y}`, '').trim());
},
computeHeightFace(point: Point, index: number) {
switch (index) {
// fl
case 1: {
const p2 = this.updatedEdge(this.cuboidModel.fr.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.dr.points[0], p2, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.dl.points[0], point, this.cuboidModel.vpr);
return [point, p2, p3, p4];
}
// fr
case 2: {
const p1 = this.updatedEdge(this.cuboidModel.fl.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.dr.points[0], point, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.dl.points[0], p3, this.cuboidModel.vpr);
return [p1, point, p3, p4];
}
// dr
case 3: {
const p2 = this.updatedEdge(this.cuboidModel.dl.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.fr.points[0], point, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.fl.points[0], p2, this.cuboidModel.vpr);
return [p4, p3, point, p2];
}
// dl
case 4: {
const p2 = this.updatedEdge(this.cuboidModel.dr.points[0], point, this.cuboidModel.vpl);
const p3 = this.updatedEdge(this.cuboidModel.fl.points[0], point, this.cuboidModel.vpr);
const p4 = this.updatedEdge(this.cuboidModel.fr.points[0], p2, this.cuboidModel.vpr);
return [p3, p4, p2, point];
}
default: {
return [null, null, null, null];
}
}
},
updatedEdge(target: Point, base: Point, pivot: Point) {
const targetX = target.x;
const line = new Equation(pivot, base);
const newY = line.getY(targetX);
return { x: targetX, y: newY };
},
updateView() {
this.updateFaces();
this.updateEdges();
this.updateProjections();
this.updateGrabPoints();
},
updateFaces() {
const viewModel = this.cuboidModel;
this.bot.plot(viewModel.bot.points);
this.top.plot(viewModel.top.points);
this.right.plot(viewModel.right.points);
this.dorsal.plot(viewModel.dorsal.points);
this.left.plot(viewModel.left.points);
this.face.plot(viewModel.front.points);
},
updateEdges() {
const viewModel = this.cuboidModel;
this.frontLeftEdge.plot(viewModel.fl.points);
this.frontRightEdge.plot(viewModel.fr.points);
this.dorsalRightEdge.plot(viewModel.dr.points);
this.dorsalLeftEdge.plot(viewModel.dl.points);
this.frontTopEdge.plot(viewModel.ft.points);
this.rightTopEdge.plot(viewModel.rt.points);
this.frontBotEdge.plot(viewModel.fb.points);
this.rightBotEdge.plot(viewModel.rb.points);
},
updateProjections() {
const viewModel = this.cuboidModel;
this.ftProj.plot(this.updateProjectionLine(viewModel.ft.getEquation(),
viewModel.ft.points[0], viewModel.vpl));
this.fbProj.plot(this.updateProjectionLine(viewModel.fb.getEquation(),
viewModel.ft.points[0], viewModel.vpl));
this.rtProj.plot(this.updateProjectionLine(viewModel.rt.getEquation(),
viewModel.rt.points[1], viewModel.vpr));
this.rbProj.plot(this.updateProjectionLine(viewModel.rb.getEquation(),
viewModel.rt.points[1], viewModel.vpr));
},
updateGrabPoints() {
const centers = this.getGrabPoints();
const edges = this.getEdges();
for (let i = 0; i < centers.length; i += 1) {
const edge = edges[i];
if (centers[i]) centers[i].center(edge.cx(), edge.cy());
}
},
},
construct: {
cube(points: string) {
return this.put(new (SVG as any).Cube()).constructorMethod(points);
},
},
});

@ -13,10 +13,12 @@
PolygonShape,
PolylineShape,
PointsShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
CuboidTrack,
Track,
Shape,
Tag,
@ -58,6 +60,9 @@
case 'points':
shapeModel = new PointsShape(shapeData, clientID, color, injection);
break;
case 'cuboid':
shapeModel = new CuboidShape(shapeData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of shape "${type}"`,
@ -87,6 +92,9 @@
case 'points':
trackModel = new PointsTrack(trackData, clientID, color, injection);
break;
case 'cuboid':
trackModel = new CuboidTrack(trackData, clientID, color, injection);
break;
default:
throw new DataError(
`An unexpected type of track "${type}"`,
@ -150,7 +158,6 @@
}
for (const shape of data.shapes) {
if (shape.type === 'cuboid') continue;
const clientID = ++this.count;
const shapeModel = shapeFactory(shape, clientID, this.injection);
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
@ -592,6 +599,10 @@
shape: 0,
track: 0,
},
cuboid: {
shape: 0,
track: 0,
},
tags: 0,
manually: 0,
interpolated: 0,

@ -69,6 +69,12 @@
`Points must have at least 1 points, but got ${points.length / 2}`,
);
}
} else if (shapeType === ObjectShape.CUBOID) {
if (points.length / 2 !== 8) {
throw new DataError(
`Points must have exact 8 points, but got ${points.length / 2}`,
);
}
} else {
throw new ArgumentError(
`Unknown value of shapeType has been recieved ${shapeType}`,
@ -109,6 +115,35 @@
return area >= MIN_SHAPE_AREA;
}
function fitPoints(shapeType, points, maxX, maxY) {
const fittedPoints = [];
for (let i = 0; i < points.length - 1; i += 2) {
const x = points[i];
const y = points[i + 1];
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
fittedPoints.push(
Math.clamp(x, 0, maxX),
Math.clamp(y, 0, maxY),
);
}
return shapeType === ObjectShape.CUBOID ? points : fittedPoints;
}
function checkOutside(points, width, height) {
let inside = false;
for (let i = 0; i < points.length - 1; i += 2) {
const [x, y] = points.slice(i);
inside = inside || (x >= 0 && x <= width && y >= 0 && y <= height);
}
return !inside;
}
function validateAttributeValue(value, attr) {
const { values } = attr;
const type = attr.inputType;
@ -296,20 +331,9 @@
checkNumberOfPoints(this.shapeType, data.points);
// cut points
const { width, height } = this.frameMeta[frame];
for (let i = 0; i < data.points.length - 1; i += 2) {
const x = data.points[i];
const y = data.points[i + 1];
fittedPoints = fitPoints(this.shapeType, data.points, width, height);
checkObjectType('coordinate', x, 'number', null);
checkObjectType('coordinate', y, 'number', null);
fittedPoints.push(
Math.clamp(x, 0, width),
Math.clamp(y, 0, height),
);
}
if (!checkShapeArea(this.shapeType, fittedPoints)) {
if ((!checkShapeArea(this.shapeType, fittedPoints)) || checkOutside(fittedPoints, width, height)) {
fittedPoints = [];
}
}
@ -1378,6 +1402,127 @@
}
}
class CuboidShape extends PolyShape {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
checkNumberOfPoints(this.shapeType, this.points);
}
static makeHull(geoPoints) {
// Returns the convex hull, assuming that each points[i] <= points[i + 1].
function makeHullPresorted(points) {
if (points.length <= 1) return points.slice();
// Andrew's monotone chain algorithm. Positive y coordinates correspond to 'up'
// as per the mathematical convention, instead of 'down' as per the computer
// graphics convention. This doesn't affect the correctness of the result.
const upperHull = [];
for (let i = 0; i < points.length; i += 1) {
const p = points[`${i}`];
while (upperHull.length >= 2) {
const q = upperHull[upperHull.length - 1];
const r = upperHull[upperHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) upperHull.pop();
else break;
}
upperHull.push(p);
}
upperHull.pop();
const lowerHull = [];
for (let i = points.length - 1; i >= 0; i -= 1) {
const p = points[`${i}`];
while (lowerHull.length >= 2) {
const q = lowerHull[lowerHull.length - 1];
const r = lowerHull[lowerHull.length - 2];
if ((q.x - r.x) * (p.y - r.y) >= (q.y - r.y) * (p.x - r.x)) lowerHull.pop();
else break;
}
lowerHull.push(p);
}
lowerHull.pop();
if (upperHull.length
=== 1 && lowerHull.length
=== 1 && upperHull[0].x
=== lowerHull[0].x && upperHull[0].y
=== lowerHull[0].y) return upperHull;
return upperHull.concat(lowerHull);
}
function POINT_COMPARATOR(a, b) {
if (a.x < b.x) return -1;
if (a.x > b.x) return +1;
if (a.y < b.y) return -1;
if (a.y > b.y) return +1;
return 0;
}
const newPoints = geoPoints.slice();
newPoints.sort(POINT_COMPARATOR);
return makeHullPresorted(newPoints);
}
static contain(points, x, y) {
function isLeft(P0, P1, P2) {
return ((P1.x - P0.x) * (P2.y - P0.y) - (P2.x - P0.x) * (P1.y - P0.y));
}
points = CuboidShape.makeHull(points);
let wn = 0;
for (let i = 0; i < points.length; i += 1) {
const p1 = points[`${i}`];
const p2 = points[i + 1] || points[0];
if (p1.y <= y) {
if (p2.y > y) {
if (isLeft(p1, p2, { x, y }) > 0) {
wn += 1;
}
}
} else if (p2.y < y) {
if (isLeft(p1, p2, { x, y }) < 0) {
wn -= 1;
}
}
}
return wn !== 0;
}
static distance(actualPoints, x, y) {
const points = [];
for (let i = 0; i < 16; i += 2) {
points.push({ x: actualPoints[i], y: actualPoints[i + 1] });
}
if (!CuboidShape.contain(points, x, y)) return null;
let minDistance = Number.MAX_SAFE_INTEGER;
for (let i = 0; i < points.length; i += 1) {
const p1 = points[`${i}`];
const p2 = points[i + 1] || points[0];
// perpendicular from point to straight length
const distance = (Math.abs((p2.y - p1.y) * x
- (p2.x - p1.x) * y + p2.x * p1.y - p2.y * p1.x))
/ Math.sqrt(Math.pow(p2.y - p1.y, 2) + Math.pow(p2.x - p1.x, 2));
// check if perpendicular belongs to the straight segment
const a = Math.pow(p1.x - x, 2) + Math.pow(p1.y - y, 2);
const b = Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2);
const c = Math.pow(p2.x - x, 2) + Math.pow(p2.y - y, 2);
if (distance < minDistance && (a + b - c) >= 0 && (c + b - a) >= 0) {
minDistance = distance;
}
}
return minDistance;
}
}
class RectangleTrack extends Track {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
@ -1389,20 +1534,15 @@
}
interpolatePosition(leftPosition, rightPosition, offset) {
const positionOffset = [
rightPosition.points[0] - leftPosition.points[0],
rightPosition.points[1] - leftPosition.points[1],
rightPosition.points[2] - leftPosition.points[2],
rightPosition.points[3] - leftPosition.points[3],
];
return { // xtl, ytl, xbr, ybr
points: [
leftPosition.points[0] + positionOffset[0] * offset,
leftPosition.points[1] + positionOffset[1] * offset,
leftPosition.points[2] + positionOffset[2] * offset,
leftPosition.points[3] + positionOffset[3] * offset,
],
const positionOffset = leftPosition.points.map((point, index) => (
rightPosition.points[index] - point
))
return {
points: leftPosition.points.map((point ,index) => (
point + positionOffset[index] * offset
)),
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
@ -1827,20 +1967,35 @@
}
}
class CuboidTrack extends PolyTrack {
constructor(data, clientID, color, injection) {
super(data, clientID, color, injection);
this.shapeType = ObjectShape.CUBOID;
this.pinned = false;
for (const shape of Object.values(this.shapes)) {
checkNumberOfPoints(this.shapeType, shape.points);
}
}
}
RectangleTrack.distance = RectangleShape.distance;
PolygonTrack.distance = PolygonShape.distance;
PolylineTrack.distance = PolylineShape.distance;
PointsTrack.distance = PointsShape.distance;
CuboidTrack.distance = CuboidShape.distance;
CuboidTrack.interpolatePosition = RectangleTrack.interpolatePosition;
module.exports = {
RectangleShape,
PolygonShape,
PolylineShape,
PointsShape,
CuboidShape,
RectangleTrack,
PolygonTrack,
PolylineTrack,
PointsTrack,
CuboidTrack,
Track,
Shape,
Tag,

@ -93,6 +93,7 @@
* @property {string} POLYGON 'polygon'
* @property {string} POLYLINE 'polyline'
* @property {string} POINTS 'points'
* @property {string} CUBOID 'cuboid'
* @readonly
*/
const ObjectShape = Object.freeze({
@ -100,6 +101,7 @@
POLYGON: 'polygon',
POLYLINE: 'polyline',
POINTS: 'points',
CUBOID: 'cuboid',
});
/**

@ -34,6 +34,10 @@
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,
@ -69,6 +73,10 @@
* tracks: 19,
* shapes: 20,
* },
* cuboids: {
* tracks: 21,
* shapes: 22,
* },
* tags: 66,
* manually: 186,
* interpolated: 500,

@ -25,7 +25,7 @@ describe('Feature: get annotations', () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const annotations = await task.annotations.get(0);
expect(Array.isArray(annotations)).toBeTruthy();
expect(annotations).toHaveLength(11);
expect(annotations).toHaveLength(12);
for (const state of annotations) {
expect(state).toBeInstanceOf(window.cvat.classes.ObjectState);
}
@ -692,7 +692,7 @@ describe('Feature: get statistics', () => {
await task.annotations.clear(true);
const statistics = await task.annotations.statistics();
expect(statistics).toBeInstanceOf(window.cvat.classes.Statistics);
expect(statistics.total.total).toBe(29);
expect(statistics.total.total).toBe(30);
});
test('get statistics from a job', async () => {
@ -719,6 +719,9 @@ describe('Feature: select object', () => {
result = await task.annotations.select(annotations, 613, 811);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(94);
result = await task.annotations.select(annotations, 600, 900);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.CUBOID);
expect(result.state.points.length).toBe(16);
});
test('select object in a job', async () => {
@ -733,6 +736,9 @@ describe('Feature: select object', () => {
result = await job.annotations.select(annotations, 1490, 237);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.POLYGON);
expect(result.state.points.length).toBe(94);
result = await job.annotations.select(annotations, 600, 900);
expect(result.state.shapeType).toBe(window.cvat.enums.ObjectShape.CUBOID);
expect(result.state.points.length).toBe(16);
});
test('trying to select from not object states', async () => {

@ -2514,6 +2514,35 @@ const taskAnnotationsDummyData = {
"label_id": 2,
"group": 0,
"attributes": []
}, {
"type": "cuboid",
"occluded": false,
"z_order":12,
"points": [
37.037109375,
834.1583663313359,
37.037109375,
1005.6748046875,
500.1052119006872,
850.3421313142153,
500.1052119006872,
1021.9585696703798,
600.6842465753452,
763.1514501284273,
600.6842465753452,
934.6678884845915,
137.82724152601259,
747.0278858154179,
137.82724152601259,
918.4444406426646,
],
"id": 137,
"frame": 0,
"label_id": 1,
"group": 0,
"attributes": [
]
}],
"tracks":[]
}

@ -98,12 +98,14 @@ async function jobInfoGenerator(job: any): Promise<Record<string, number>> {
'track count': total.rectangle.shape + total.rectangle.track
+ total.polygon.shape + total.polygon.track
+ total.polyline.shape + total.polyline.track
+ total.points.shape + total.points.track,
+ total.points.shape + total.points.track
+ total.cuboid.shape + total.cuboid.track,
'object count': total.total,
'box count': total.rectangle.shape + total.rectangle.track,
'polygon count': total.polygon.shape + total.polygon.track,
'polyline count': total.polyline.shape + total.polyline.track,
'points count': total.points.shape + total.points.track,
'cuboids count': total.cuboid.shape + total.cuboid.track,
'tag count': total.tags,
};
}
@ -1059,6 +1061,8 @@ export function rememberObject(
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
} else if (shapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
return {
@ -1386,6 +1390,8 @@ export function pasteShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction>
activeControl = ActiveControl.DRAW_POLYGON;
} else if (initialState.shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (initialState.shapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
dispatch({
@ -1447,6 +1453,8 @@ export function repeatDrawShapeAsync(): ThunkAction<Promise<void>, {}, {}, AnyAc
activeControl = ActiveControl.DRAW_POLYGON;
} else if (activeShapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (activeShapeType === ShapeType.CUBOID) {
activeControl = ActiveControl.DRAW_CUBOID;
}
dispatch({

@ -18,6 +18,7 @@ export enum SettingsActionTypes {
CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY',
CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY',
CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS',
CHANGE_SHAPES_SHOW_PROJECTIONS = 'CHANGE_SHAPES_SHOW_PROJECTIONS',
CHANGE_SHOW_UNLABELED_REGIONS = 'CHANGE_SHOW_UNLABELED_REGIONS',
CHANGE_FRAME_STEP = 'CHANGE_FRAME_STEP',
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
@ -78,6 +79,15 @@ export function changeShowBitmap(showBitmap: boolean): AnyAction {
};
}
export function changeShowProjections(showProjections: boolean): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SHAPES_SHOW_PROJECTIONS,
payload: {
showProjections,
},
};
}
export function switchRotateAll(rotateAll: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_ROTATE_ALL,

@ -0,0 +1 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 13L2.34921 12.2407L2 12.5401V13H3ZM3 33H2V34H3V33ZM30 33V34H30.3699L30.6508 33.7593L30 33ZM37 27L37.6508 27.7593L38 27.4599V27H37ZM37 7H38V6H37V7ZM10 7V6H9.63008L9.34921 6.24074L10 7ZM2 13V33H4V13H2ZM3 34H30V32H3V34ZM30.6508 33.7593L37.6508 27.7593L36.3492 26.2407L29.3492 32.2407L30.6508 33.7593ZM38 27V7H36V27H38ZM36.3492 6.24074L29.3492 12.2407L30.6508 13.7593L37.6508 7.75926L36.3492 6.24074ZM30 12H3V14H30V12ZM31 33V13H29V33H31ZM3.65079 13.7593L10.6508 7.75926L9.34921 6.24074L2.34921 12.2407L3.65079 13.7593ZM10 8H37V6H10V8Z" fill="black"/></svg>

After

Width:  |  Height:  |  Size: 660 B

@ -44,6 +44,7 @@ interface Props {
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
@ -145,15 +146,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
frameFetching,
showObjectsTextAlways,
automaticBordering,
showProjections,
} = this.props;
if (prevProps.showObjectsTextAlways !== showObjectsTextAlways
|| prevProps.automaticBordering !== automaticBordering
|| prevProps.showProjections !== showProjections
) {
canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
displayAllText: showObjectsTextAlways,
autoborders: automaticBordering,
showProjections,
});
}
@ -540,7 +544,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} = this.props;
const [state] = annotations.filter((el: any) => (el.clientID === activatedStateID));
if (state.shapeType !== ShapeType.RECTANGLE) {
if (![ShapeType.CUBOID, ShapeType.RECTANGLE].includes(state.shapeType)) {
onUpdateContextMenu(activatedStateID !== null, e.detail.mouseEvent.clientX,
e.detail.mouseEvent.clientY, ContextMenuType.CANVAS_SHAPE_POINT, e.detail.pointID);
}

@ -18,6 +18,7 @@ import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control';
import DrawPointsControl from './draw-points-control';
import DrawCuboidControl from './draw-cuboid-control';
import SetupTagControl from './setup-tag-control';
import MergeControl from './merge-control';
import GroupControl from './group-control';
@ -80,7 +81,8 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
SWITCH_DRAW_MODE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
const drawing = [ActiveControl.DRAW_POINTS, ActiveControl.DRAW_POLYGON,
ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE].includes(activeControl);
ActiveControl.DRAW_POLYLINE, ActiveControl.DRAW_RECTANGLE,
ActiveControl.DRAW_CUBOID].includes(activeControl);
if (!drawing) {
canvasInstance.cancel();
@ -177,7 +179,10 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/>
<DrawCuboidControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_CUBOID}
/>
<SetupTagControl
canvasInstance={canvasInstance}
isDrawing={false}

@ -0,0 +1,57 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Popover from 'antd/lib/popover';
import Icon from 'antd/lib/icon';
import { Canvas } from 'cvat-canvas';
import { ShapeType } from 'reducers/interfaces';
import { CubeIcon } from 'icons';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
isDrawing: boolean;
}
function DrawPolygonControl(props: Props): JSX.Element {
const {
canvasInstance,
isDrawing,
} = props;
const dynamcPopoverPros = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
{...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={(
<DrawShapePopoverContainer shapeType={ShapeType.CUBOID} />
)}
>
<Icon
{...dynamicIconProps}
component={CubeIcon}
/>
</Popover>
);
}
export default React.memo(DrawPolygonControl);

@ -48,7 +48,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
} = props;
const trackDisabled = shapeType === ShapeType.POLYGON || shapeType === ShapeType.POLYLINE
|| (shapeType === ShapeType.POINTS && numberOfPoints !== 1);
|| shapeType === ShapeType.CUBOID || (shapeType === ShapeType.POINTS && numberOfPoints !== 1);
return (
<div className='cvat-draw-shape-popover-content'>
@ -85,7 +85,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
shapeType === ShapeType.POLYGON && <DEXTRPlugin />
}
{
shapeType === ShapeType.RECTANGLE ? (
shapeType === ShapeType.RECTANGLE && (
<>
<Row>
<Col>
@ -115,7 +115,10 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
</Col>
</Row>
</>
) : (
)
}
{
shapeType !== ShapeType.RECTANGLE && shapeType !== ShapeType.CUBOID && (
<Row type='flex' justify='space-around' align='middle'>
<Col span={14}>
<Text className='cvat-text-color'> Number of points: </Text>

@ -18,6 +18,7 @@ interface Props {
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
collapseAppearance(): void;
changeShapesColorBy(event: RadioChangeEvent): void;
@ -25,6 +26,7 @@ interface Props {
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
changeShowBitmap(event: CheckboxChangeEvent): void;
changeShowProjections(event: CheckboxChangeEvent): void;
}
function AppearanceBlock(props: Props): JSX.Element {
@ -35,12 +37,14 @@ function AppearanceBlock(props: Props): JSX.Element {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
collapseAppearance,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
changeShowBitmap,
changeShowProjections,
} = props;
return (
@ -88,6 +92,12 @@ function AppearanceBlock(props: Props): JSX.Element {
>
Show bitmap
</Checkbox>
<Checkbox
onChange={changeShowProjections}
checked={showProjections}
>
Show projections
</Checkbox>
</div>
</Collapse.Panel>
</Collapse>

@ -25,6 +25,7 @@ interface Props {
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
collapseSidebar(): void;
collapseAppearance(): void;
@ -34,6 +35,7 @@ interface Props {
changeSelectedShapesOpacity(event: SliderValue): void;
changeShapesBlackBorders(event: CheckboxChangeEvent): void;
changeShowBitmap(event: CheckboxChangeEvent): void;
changeShowProjections(event: CheckboxChangeEvent): void;
}
function ObjectsSideBar(props: Props): JSX.Element {
@ -45,6 +47,7 @@ function ObjectsSideBar(props: Props): JSX.Element {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
collapseSidebar,
collapseAppearance,
changeShapesColorBy,
@ -52,6 +55,7 @@ function ObjectsSideBar(props: Props): JSX.Element {
changeSelectedShapesOpacity,
changeShapesBlackBorders,
changeShowBitmap,
changeShowProjections,
} = props;
const appearanceProps = {
@ -62,12 +66,14 @@ function ObjectsSideBar(props: Props): JSX.Element {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
changeShapesColorBy,
changeShapesOpacity,
changeSelectedShapesOpacity,
changeShapesBlackBorders,
changeShowBitmap,
changeShowProjections,
};
return (

@ -65,6 +65,7 @@ interface StateToProps {
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
grid: boolean;
gridSize: number;
gridColor: GridColor;
@ -180,6 +181,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
},
},
shortcuts: {
@ -204,6 +206,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
grid,
gridSize,
gridColor,

@ -121,7 +121,7 @@ class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
rectDrawingMethod,
numberOfPoints,
shapeType,
crosshair: shapeType === ShapeType.RECTANGLE,
crosshair: [ShapeType.RECTANGLE, ShapeType.CUBOID].includes(shapeType),
});
onDrawStart(shapeType, selectedLabelID,

@ -28,6 +28,7 @@ import {
changeSelectedShapesOpacity as changeSelectedShapesOpacityAction,
changeShapesBlackBorders as changeShapesBlackBordersAction,
changeShowBitmap as changeShowUnlabeledRegionsAction,
changeShowProjections as changeShowProjectionsAction,
} from 'actions/settings-actions';
@ -39,6 +40,7 @@ interface StateToProps {
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
}
interface DispatchToProps {
@ -50,6 +52,7 @@ interface DispatchToProps {
changeSelectedShapesOpacity(selectedShapesOpacity: number): void;
changeShapesBlackBorders(blackBorders: boolean): void;
changeShowBitmap(showBitmap: boolean): void;
changeShowProjections(showProjections: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -65,6 +68,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
},
},
} = state;
@ -77,6 +81,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
};
}
@ -140,6 +145,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
changeShowBitmap(showBitmap: boolean) {
dispatch(changeShowUnlabeledRegionsAction(showBitmap));
},
changeShowProjections(showProjections: boolean) {
dispatch(changeShowProjectionsAction(showProjections));
},
};
}
@ -190,6 +198,11 @@ class ObjectsSideBarContainer extends React.PureComponent<Props> {
changeShowBitmap(event.target.checked);
};
private changeShowProjections = (event: CheckboxChangeEvent): void => {
const { changeShowProjections } = this.props;
changeShowProjections(event.target.checked);
};
public render(): JSX.Element {
const {
sidebarCollapsed,
@ -199,6 +212,7 @@ class ObjectsSideBarContainer extends React.PureComponent<Props> {
selectedOpacity,
blackBorders,
showBitmap,
showProjections,
collapseSidebar,
collapseAppearance,
} = this.props;
@ -212,6 +226,7 @@ class ObjectsSideBarContainer extends React.PureComponent<Props> {
selectedOpacity={selectedOpacity}
blackBorders={blackBorders}
showBitmap={showBitmap}
showProjections={showProjections}
collapseSidebar={collapseSidebar}
collapseAppearance={collapseAppearance}
changeShapesColorBy={this.changeShapesColorBy}
@ -219,6 +234,7 @@ class ObjectsSideBarContainer extends React.PureComponent<Props> {
changeSelectedShapesOpacity={this.changeSelectedShapesOpacity}
changeShapesBlackBorders={this.changeShapesBlackBorders}
changeShowBitmap={this.changeShowBitmap}
changeShowProjections={this.changeShowProjections}
/>
);
}

@ -39,6 +39,7 @@ import SVGObjectOutsideIcon from './assets/object-outside-icon.svg';
import SVGObjectInsideIcon from './assets/object-inside-icon.svg';
import SVGBackgroundIcon from './assets/background-icon.svg';
import SVGForegroundIcon from './assets/foreground-icon.svg';
import SVGCubeIcon from './assets/cube-icon.svg';
export const CVATLogo = React.memo(
(): JSX.Element => <SVGCVATLogo />,
@ -145,3 +146,6 @@ export const BackgroundIcon = React.memo(
export const ForegroundIcon = React.memo(
(): JSX.Element => <SVGForegroundIcon />,
);
export const CubeIcon = React.memo(
(): JSX.Element => <SVGCubeIcon />,
);

@ -262,6 +262,7 @@ export enum ActiveControl {
DRAW_POLYGON = 'draw_polygon',
DRAW_POLYLINE = 'draw_polyline',
DRAW_POINTS = 'draw_points',
DRAW_CUBOID = 'draw_cuboid',
MERGE = 'merge',
GROUP = 'group',
SPLIT = 'split',
@ -273,6 +274,7 @@ export enum ShapeType {
POLYGON = 'polygon',
POLYLINE = 'polyline',
POINTS = 'points',
CUBOID = 'cuboid',
}
export enum ObjectType {
@ -441,6 +443,7 @@ export interface ShapesSettingsState {
selectedOpacity: number;
blackBorders: boolean;
showBitmap: boolean;
showProjections: boolean;
}
export interface SettingsState {

@ -23,6 +23,7 @@ const defaultState: SettingsState = {
selectedOpacity: 30,
blackBorders: false,
showBitmap: false,
showProjections: false,
},
workspace: {
autoSave: false,
@ -130,6 +131,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.CHANGE_SHAPES_SHOW_PROJECTIONS: {
return {
...state,
shapes: {
...state.shapes,
showProjections: action.payload.showProjections,
},
};
}
case SettingsActionTypes.CHANGE_SHOW_UNLABELED_REGIONS: {
return {
...state,

Loading…
Cancel
Save