diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b78c7e..6c18209e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [VGGFace2](https://github.com/ox-vgg/vgg_face2) format support () - [Backup/Restore guide](cvat/apps/documentation/backup_guide.md) () - Label deletion from tasks and projects () +- CVAT-3D: Implemented initial cuboid placement in 3D View and select cuboid in Top, Side and Front views + () - [Market-1501](https://www.aitribune.com/dataset/2018051063) format support () - Annotations filters UI using react-awesome-query-builder (https://github.com/openvinotoolkit/cvat/issues/1418) diff --git a/cvat-canvas3d/package-lock.json b/cvat-canvas3d/package-lock.json index 44e7d258..940ee883 100644 --- a/cvat-canvas3d/package-lock.json +++ b/cvat-canvas3d/package-lock.json @@ -1184,6 +1184,11 @@ "integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==", "dev": true }, + "@types/three": { + "version": "0.125.3", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz", + "integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA==" + }, "@typescript-eslint/eslint-plugin": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz", diff --git a/cvat-canvas3d/package.json b/cvat-canvas3d/package.json index f9c3f5d2..f6fd7126 100644 --- a/cvat-canvas3d/package.json +++ b/cvat-canvas3d/package.json @@ -38,7 +38,8 @@ "webpack-dev-server": "^3.11.0" }, "dependencies": { - "three": "^0.125.0", - "camera-controls": "^1.25.3" + "@types/three": "^0.125.3", + "camera-controls": "^1.25.3", + "three": "^0.125.0" } } diff --git a/cvat-canvas3d/src/typescript/canvas3d.ts b/cvat-canvas3d/src/typescript/canvas3d.ts index d08ec0ad..139321fe 100644 --- a/cvat-canvas3d/src/typescript/canvas3d.ts +++ b/cvat-canvas3d/src/typescript/canvas3d.ts @@ -4,8 +4,12 @@ import pjson from '../../package.json'; import { Canvas3dController, Canvas3dControllerImpl } from './canvas3dController'; -import { Canvas3dModel, Canvas3dModelImpl, Mode } from './canvas3dModel'; -import { Canvas3dView, Canvas3dViewImpl, ViewsDOM } from './canvas3dView'; +import { + Canvas3dModel, Canvas3dModelImpl, Mode, DrawData, ViewType, MouseInteraction, +} from './canvas3dModel'; +import { + Canvas3dView, Canvas3dViewImpl, ViewsDOM, CAMERA_ACTION, +} from './canvas3dView'; import { Master } from './master'; const Canvas3dVersion = pjson.version; @@ -17,6 +21,9 @@ interface Canvas3d { mode(): Mode; render(): void; keyControls(keys: KeyboardEvent): void; + mouseControls(type: string, event: MouseEvent): void; + draw(drawData: DrawData): void; + cancel(): void; } class Canvas3dImpl implements Canvas3d { @@ -38,10 +45,18 @@ class Canvas3dImpl implements Canvas3d { this.view.keyControls(keys); } + public mouseControls(type: MouseInteraction, event: MouseEvent): void { + this.view.mouseControls(type, event); + } + public render(): void { this.view.render(); } + public draw(drawData: DrawData): void { + this.model.draw(drawData); + } + public setup(frameData: any): void { this.model.setup(frameData); } @@ -53,6 +68,12 @@ class Canvas3dImpl implements Canvas3d { public isAbleToChangeFrame(): boolean { return this.model.isAbleToChangeFrame(); } + + public cancel(): void { + this.model.cancel(); + } } -export { Canvas3dImpl as Canvas3d, Canvas3dVersion }; +export { + Canvas3dImpl as Canvas3d, Canvas3dVersion, ViewType, MouseInteraction, CAMERA_ACTION, +}; diff --git a/cvat-canvas3d/src/typescript/canvas3dController.ts b/cvat-canvas3d/src/typescript/canvas3dController.ts index 30c1a848..5320fc03 100644 --- a/cvat-canvas3d/src/typescript/canvas3dController.ts +++ b/cvat-canvas3d/src/typescript/canvas3dController.ts @@ -2,9 +2,10 @@ // // SPDX-License-Identifier: MIT -import { Canvas3dModel, Mode } from './canvas3dModel'; +import { Canvas3dModel, Mode, DrawData } from './canvas3dModel'; export interface Canvas3dController { + readonly drawData: DrawData; mode: Mode; } @@ -22,4 +23,8 @@ export class Canvas3dControllerImpl implements Canvas3dController { public get mode(): Mode { return this.model.mode; } + + public get drawData(): DrawData { + return this.model.data.drawData; + } } diff --git a/cvat-canvas3d/src/typescript/canvas3dModel.ts b/cvat-canvas3d/src/typescript/canvas3dModel.ts index 6c02c923..0b0aef59 100644 --- a/cvat-canvas3d/src/typescript/canvas3dModel.ts +++ b/cvat-canvas3d/src/typescript/canvas3dModel.ts @@ -26,6 +26,19 @@ export enum FrameZoom { MAX = 10, } +export enum ViewType { + PERSPECTIVE = 'perspective', + TOP = 'top', + SIDE = 'side', + FRONT = 'front', +} + +export enum MouseInteraction { + CLICK = 'click', + DOUBLE_CLICK = 'dblclick', + HOVER = 'hover', +} + export enum UpdateReasons { IMAGE_CHANGED = 'image_changed', OBJECTS_UPDATED = 'objects_updated', @@ -61,6 +74,8 @@ export interface Canvas3dModel { data: Canvas3dDataModel; setup(frameData: any): void; isAbleToChangeFrame(): boolean; + draw(drawData: DrawData): void; + cancel(): void; } export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel { @@ -133,4 +148,18 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel { return !isUnable; } + + public draw(drawData: DrawData): void { + if (drawData.enabled && this.data.drawData.enabled) { + throw new Error('Drawing has been already started'); + } + this.data.drawData.enabled = drawData.enabled; + this.data.mode = Mode.DRAW; + + this.notify(UpdateReasons.DRAW); + } + + public cancel(): void { + this.notify(UpdateReasons.CANCEL); + } } diff --git a/cvat-canvas3d/src/typescript/canvas3dView.ts b/cvat-canvas3d/src/typescript/canvas3dView.ts index 450457fc..866e7cc2 100644 --- a/cvat-canvas3d/src/typescript/canvas3dView.ts +++ b/cvat-canvas3d/src/typescript/canvas3dView.ts @@ -4,20 +4,23 @@ import * as THREE from 'three'; import { PCDLoader } from 'three/examples/jsm/loaders/PCDLoader'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import CameraControls from 'camera-controls'; import { Canvas3dController } from './canvas3dController'; import { Listener, Master } from './master'; import CONST from './consts'; -import { Canvas3dModel, UpdateReasons, Mode } from './canvas3dModel'; +import { + Canvas3dModel, UpdateReasons, Mode, DrawData, ViewType, MouseInteraction, +} from './canvas3dModel'; +import { CuboidModel } from './cuboid'; export interface Canvas3dView { html(): ViewsDOM; render(): void; keyControls(keys: KeyboardEvent): void; + mouseControls(type: MouseInteraction, event: MouseEvent): void; } -enum CAMERA_ACTION { +export enum CAMERA_ACTION { ZOOM_IN = 'KeyI', MOVE_UP = 'KeyU', MOVE_DOWN = 'KeyO', @@ -30,6 +33,11 @@ enum CAMERA_ACTION { ROTATE_LEFT = 'ArrowLeft', } +export interface RayCast { + renderer: THREE.Raycaster; + mouseVector: THREE.Vector2; +} + export interface Views { perspective: RenderView; top: RenderView; @@ -37,11 +45,19 @@ export interface Views { front: RenderView; } +export interface CubeObject { + perspective: THREE.Mesh; + top: THREE.Mesh; + side: THREE.Mesh; + front: THREE.Mesh; +} + export interface RenderView { renderer: THREE.WebGLRenderer; scene: THREE.Scene; camera?: THREE.PerspectiveCamera | THREE.OrthographicCamera; - controls?: CameraControls | OrbitControls; + controls?: CameraControls; + rayCaster?: RayCast; } export interface ViewsDOM { @@ -56,6 +72,9 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { private views: Views; private clock: THREE.Clock; private speed: number; + private cube: CuboidModel; + private highlighted: boolean; + private selected: CubeObject; private set mode(value: Mode) { this.controller.mode = value; @@ -69,10 +88,18 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { this.controller = controller; this.clock = new THREE.Clock(); this.speed = CONST.MOVEMENT_FACTOR; + this.cube = new CuboidModel(); + this.highlighted = false; + this.selected = this.cube; + this.views = { perspective: { renderer: new THREE.WebGLRenderer({ antialias: true }), scene: new THREE.Scene(), + rayCaster: { + renderer: new THREE.Raycaster(), + mouseVector: new THREE.Vector2(), + }, }, top: { renderer: new THREE.WebGLRenderer({ antialias: true }), @@ -139,13 +166,13 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { this.views.front.camera.up.set(0, 0, 1); this.views.front.camera.lookAt(0, 0, 0); - Object.keys(this.views).forEach((view: string) => { + Object.keys(this.views).forEach((view: string): void => { const viewType = this.views[view as keyof Views]; viewType.renderer.setSize(width, height); - if (view !== 'perspective') { - viewType.controls = new OrbitControls(viewType.camera, viewType.renderer.domElement); - viewType.controls.enableRotate = false; - viewType.controls.enablePan = false; + if (view !== ViewType.PERSPECTIVE) { + viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement); + viewType.controls.mouseButtons.left = CameraControls.ACTION.NONE; + viewType.controls.mouseButtons.right = CameraControls.ACTION.NONE; } else { viewType.controls = new CameraControls(viewType.camera, viewType.renderer.domElement); } @@ -165,94 +192,235 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { URL.revokeObjectURL(objectURL); const event: CustomEvent = new CustomEvent('canvas.setup'); this.views.perspective.renderer.domElement.dispatchEvent(event); + } else if (reason === UpdateReasons.DRAW) { + const data: DrawData = this.controller.drawData; + if (data.enabled && this.mode === Mode.IDLE) { + this.mode = Mode.DRAW; + this.cube = new CuboidModel(); + } else if (this.mode !== Mode.IDLE) { + this.cube = new CuboidModel(); + } + } else if (reason === UpdateReasons.CANCEL) { + if (this.mode === Mode.DRAW) { + this.controller.drawData.enabled = false; + Object.keys(this.views).forEach((view: string): void => { + this.views[view as keyof Views].scene.children[0].remove(this.cube[view as keyof Views]); + }); + } + this.mode = Mode.IDLE; + const event: CustomEvent = new CustomEvent('canvas.canceled'); + this.views.perspective.renderer.domElement.dispatchEvent(event); } } private clearScene(): void { - for (let i = this.views.perspective.scene.children.length - 1; i >= 0; i--) { - this.views.perspective.scene.remove(this.views.perspective.scene.children[i]); - this.views.top.scene.remove(this.views.top.scene.children[i]); - this.views.side.scene.remove(this.views.side.scene.children[i]); - this.views.front.scene.remove(this.views.front.scene.children[i]); - } + Object.keys(this.views).forEach((view: string): void => { + this.views[view as keyof Views].scene.children = []; + }); } private addScene(points: any): void { // eslint-disable-next-line no-param-reassign - points.material.size = 0.03; + points.material.size = 0.08; // eslint-disable-next-line no-param-reassign points.material.color = new THREE.Color(0x0000ff); + const sphereCenter = points.geometry.boundingSphere.center; + const { radius } = points.geometry.boundingSphere; + const xRange = -radius / 2 < this.views.perspective.camera.position.x - sphereCenter.x + && radius / 2 > this.views.perspective.camera.position.x - sphereCenter.x; + const yRange = -radius / 2 < this.views.perspective.camera.position.y - sphereCenter.y + && radius / 2 > this.views.perspective.camera.position.y - sphereCenter.y; + const zRange = -radius / 2 < this.views.perspective.camera.position.z - sphereCenter.z + && radius / 2 > this.views.perspective.camera.position.z - sphereCenter.z; + let newX = 0; + let newY = 0; + let newZ = 0; + if (!xRange) { + newX = sphereCenter.x; + } + if (!yRange) { + newY = sphereCenter.y; + } + if (!zRange) { + newZ = sphereCenter.z; + } + if (newX || newY || newZ) { + this.positionAllViews(newX, newY, newZ); + } this.views.perspective.scene.add(points); this.views.top.scene.add(points.clone()); this.views.side.scene.add(points.clone()); this.views.front.scene.add(points.clone()); } - private static resizeRendererToDisplaySize(view: RenderView): void { - const canvas = view.renderer.domElement; + private positionAllViews(x: number, y: number, z: number): void { + this.views.perspective.controls.setLookAt(x - 8, y - 8, z + 3, x, y, z, false); + this.views.top.controls.setLookAt(x, y, z + 8, x, y, z, false); + this.views.side.controls.setLookAt(x, y + 8, z, x, y, z, false); + this.views.front.controls.setLookAt(x + 8, y, z, x, y, z, false); + } + + private static resizeRendererToDisplaySize(viewName: string, view: RenderView): void { + const { camera, renderer } = view; + const canvas = renderer.domElement; const width = canvas.parentElement.clientWidth; const height = canvas.parentElement.clientHeight; const needResize = canvas.clientWidth !== width || canvas.clientHeight !== height; if (needResize) { - if (!(view.camera instanceof THREE.OrthographicCamera)) { - // eslint-disable-next-line no-param-reassign - view.camera.aspect = width / height; + if (camera instanceof THREE.PerspectiveCamera) { + camera.aspect = width / height; + } else { + const topViewFactor = 0; // viewName === ViewType.TOP ? 2 : 0; + const viewSize = CONST.ZOOM_FACTOR; + const aspectRatio = width / height; + if (!(camera instanceof THREE.PerspectiveCamera)) { + camera.left = (-aspectRatio * viewSize) / 2 - topViewFactor; + camera.right = (aspectRatio * viewSize) / 2 + topViewFactor; + camera.top = viewSize / 2 + topViewFactor; + camera.bottom = -viewSize / 2 - topViewFactor; + } + camera.near = -10; + camera.far = 10; } - view.camera.updateProjectionMatrix(); view.renderer.setSize(width, height); + view.camera.updateProjectionMatrix(); } } + private renderRayCaster = (viewType: RenderView): void => { + viewType.rayCaster.renderer.setFromCamera(viewType.rayCaster.mouseVector, viewType.camera); + if (this.mode === Mode.DRAW) { + const intersects = this.views.perspective.rayCaster.renderer.intersectObjects( + this.views.perspective.scene.children, + false, + ); + if (intersects.length > 0) { + this.views.perspective.scene.children[0].add(this.cube.perspective); + const newPoints = intersects[0].point; + this.cube.perspective.position.copy(newPoints); + } + } else if (this.mode === Mode.IDLE) { + const intersects = this.views.perspective.rayCaster.renderer.intersectObjects( + this.views.perspective.scene.children[0].children, + false, + ); + if (intersects.length !== 0) { + this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => { + if (this.selected.perspective !== sceneItem) { + // eslint-disable-next-line no-param-reassign + sceneItem.material.color = new THREE.Color(0xff0000); + } + }); + const selectedObject = intersects[0].object as THREE.Mesh; + if (this.selected.perspective !== selectedObject) { + selectedObject.material.color = new THREE.Color(0xffff00); + this.highlighted = true; + } + } else { + if (this.highlighted) { + this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => { + if (this.selected.perspective !== sceneItem) { + // eslint-disable-next-line no-param-reassign + sceneItem.material.color = new THREE.Color(0xff0000); + } + }); + } + this.highlighted = false; + } + } + }; + public render(): void { - Object.keys(this.views).forEach((view: string) => { + Object.keys(this.views).forEach((view: string): void => { const viewType = this.views[view as keyof Views]; - Canvas3dViewImpl.resizeRendererToDisplaySize(viewType); + Canvas3dViewImpl.resizeRendererToDisplaySize(view, viewType); viewType.controls.update(this.clock.getDelta()); viewType.renderer.render(viewType.scene, viewType.camera); + if (view === ViewType.PERSPECTIVE && viewType.scene.children.length !== 0) { + this.renderRayCaster(viewType); + } }); } public keyControls(key: any): void { - if (!(this.views.perspective.controls instanceof OrbitControls)) { - const { controls } = this.views.perspective; + const { controls } = this.views.perspective; + switch (key.code) { + case CAMERA_ACTION.ROTATE_RIGHT: + controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true); + break; + case CAMERA_ACTION.ROTATE_LEFT: + controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true); + break; + case CAMERA_ACTION.TILT_UP: + controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true); + break; + case CAMERA_ACTION.TILT_DOWN: + controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true); + break; + default: + break; + } + if (key.altKey === true) { switch (key.code) { - case CAMERA_ACTION.ROTATE_RIGHT: - controls.rotate(0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true); + case CAMERA_ACTION.ZOOM_IN: + controls.dolly(CONST.DOLLY_FACTOR, true); + break; + case CAMERA_ACTION.ZOOM_OUT: + controls.dolly(-CONST.DOLLY_FACTOR, true); + break; + case CAMERA_ACTION.MOVE_LEFT: + controls.truck(-0.01 * this.speed, 0, true); break; - case CAMERA_ACTION.ROTATE_LEFT: - controls.rotate(-0.1 * THREE.MathUtils.DEG2RAD * this.speed, 0, true); + case CAMERA_ACTION.MOVE_RIGHT: + controls.truck(0.01 * this.speed, 0, true); break; - case CAMERA_ACTION.TILT_UP: - controls.rotate(0, -0.05 * THREE.MathUtils.DEG2RAD * this.speed, true); + case CAMERA_ACTION.MOVE_DOWN: + controls.truck(0, -0.01 * this.speed, true); break; - case CAMERA_ACTION.TILT_DOWN: - controls.rotate(0, 0.05 * THREE.MathUtils.DEG2RAD * this.speed, true); + case CAMERA_ACTION.MOVE_UP: + controls.truck(0, 0.01 * this.speed, true); break; default: break; } - if (key.altKey === true) { - switch (key.code) { - case CAMERA_ACTION.ZOOM_IN: - controls.dolly(CONST.DOLLY_FACTOR, true); - break; - case CAMERA_ACTION.ZOOM_OUT: - controls.dolly(-CONST.DOLLY_FACTOR, true); - break; - case CAMERA_ACTION.MOVE_LEFT: - controls.truck(-0.01 * this.speed, 0, true); - break; - case CAMERA_ACTION.MOVE_RIGHT: - controls.truck(0.01 * this.speed, 0, true); - break; - case CAMERA_ACTION.MOVE_DOWN: - controls.truck(0, -0.01 * this.speed, true); - break; - case CAMERA_ACTION.MOVE_UP: - controls.truck(0, 0.01 * this.speed, true); - break; - default: - break; + } + } + + public mouseControls(type: MouseInteraction, event: MouseEvent): void { + event.preventDefault(); + if (type === MouseInteraction.DOUBLE_CLICK && this.mode === Mode.DRAW) { + this.controller.drawData.enabled = false; + this.mode = Mode.IDLE; + const cancelEvent: CustomEvent = new CustomEvent('canvas.canceled'); + this.views.perspective.renderer.domElement.dispatchEvent(cancelEvent); + } else { + const canvas = this.views.perspective.renderer.domElement; + const rect = canvas.getBoundingClientRect(); + const { mouseVector } = this.views.perspective.rayCaster; + mouseVector.x = ((event.clientX - (canvas.offsetLeft + rect.left)) / canvas.clientWidth) * 2 - 1; + mouseVector.y = -((event.clientY - (canvas.offsetTop + rect.top)) / canvas.clientHeight) * 2 + 1; + + if (type === MouseInteraction.CLICK && this.mode === Mode.IDLE) { + const intersects = this.views.perspective.rayCaster.renderer.intersectObjects( + this.views.perspective.scene.children[0].children, + false, + ); + if (intersects.length !== 0) { + this.views.perspective.scene.children[0].children.forEach((sceneItem: THREE.Mesh): void => { + // eslint-disable-next-line no-param-reassign + sceneItem.material.color = new THREE.Color(0xff0000); + }); + const selectedObject = intersects[0].object; + selectedObject.material.color = new THREE.Color(0x00ffff); + Object.keys(this.views).forEach((view: string): void => { + if (view !== ViewType.PERSPECTIVE) { + this.views[view as keyof Views].scene.children[0].children = [selectedObject.clone()]; + this.views[view as keyof Views].controls.fitToBox(selectedObject, false); + this.views[view as keyof Views].controls.zoom(view === ViewType.TOP ? -5 : -5, false); + } + this.views[view as keyof Views].scene.background = new THREE.Color(0x000000); + }); + this.selected.perspective = selectedObject as THREE.Mesh; } } } diff --git a/cvat-canvas3d/src/typescript/cuboid.ts b/cvat-canvas3d/src/typescript/cuboid.ts new file mode 100644 index 00000000..47a37e33 --- /dev/null +++ b/cvat-canvas3d/src/typescript/cuboid.ts @@ -0,0 +1,20 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT +import * as THREE from 'three'; + +export class CuboidModel { + public perspective: THREE.Mesh; + public top: THREE.Mesh; + public side: THREE.Mesh; + public front: THREE.Mesh; + + public constructor() { + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true }); + this.perspective = new THREE.Mesh(geometry, material); + this.top = new THREE.Mesh(geometry, material); + this.side = new THREE.Mesh(geometry, material); + this.front = new THREE.Mesh(geometry, material); + } +} diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 46df6f0d..c18ad23c 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1318,6 +1318,14 @@ "redux": "^4.0.0" } }, + "@types/react-resizable": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.2.tgz", + "integrity": "sha512-6c6L94+VOksr9838LDrlYeucic2+0qkGnwolGE77YJztYHCWSucQV0e9+Qyl+uHpJTBRS95A5JESBg5NgCAC3A==", + "requires": { + "@types/react": "*" + } + }, "@types/react-router": { "version": "5.1.12", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.12.tgz", @@ -12915,10 +12923,16 @@ "cvat-canvas3d": { "version": "file:../cvat-canvas3d", "requires": { + "@types/three": "^0.125.3", "camera-controls": "^1.25.3", "three": "^0.125.0" }, "dependencies": { + "@types/three": { + "version": "0.125.3", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz", + "integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA==" + }, "camera-controls": { "version": "1.25.3", "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-1.25.3.tgz", @@ -28941,6 +28955,15 @@ "scheduler": "^0.19.1" } }, + "react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "react-is": { "version": "16.11.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.11.0.tgz", @@ -28986,6 +29009,15 @@ } } }, + "react-resizable": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-1.11.0.tgz", + "integrity": "sha512-VoGz2ddxUFvildS8r8/29UZJeyiM3QJnlmRZSuXm+FpTqq/eIrMPc796Y9XQLg291n2hFZJtIoP1xC3hSTw/jg==", + "requires": { + "prop-types": "15.x", + "react-draggable": "^4.0.3" + } + }, "react-router": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index c9a2cea7..010cb5bc 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -78,6 +78,8 @@ "react-cookie": "^4.0.3", "react-dom": "^16.14.0", "react-redux": "^7.2.2", + "react-resizable": "^1.11.0", + "@types/react-resizable": "^1.7.2", "react-router": "^5.1.0", "react-router-dom": "^5.1.0", "react-share": "^3.0.1", diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx index a354ef7e..654cbb13 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx @@ -2,14 +2,20 @@ // // SPDX-License-Identifier: MIT -import React, { ReactElement, useEffect, useRef } from 'react'; +import React, { + ReactElement, SyntheticEvent, useEffect, useReducer, useRef, +} from 'react'; import Layout from 'antd/lib/layout/layout'; import { - ArrowUpOutlined, ArrowRightOutlined, ArrowLeftOutlined, ArrowDownOutlined, + ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined, } from '@ant-design/icons'; +import { ResizableBox } from 'react-resizable'; import { Workspace } from 'reducers/interfaces'; -import { Canvas3d } from 'cvat-canvas3d-wrapper'; +import { + CAMERA_ACTION, Canvas3d, MouseInteraction, ViewType, +} from 'cvat-canvas3d-wrapper'; import ContextImage from '../standard3D-workspace/context-image/context-image'; +import CVATTooltip from '../../common/cvat-tooltip'; interface Props { canvasInstance: Canvas3d; @@ -22,18 +28,96 @@ interface Props { annotations: any[]; onSetupCanvas: () => void; getContextImage(): void; + onResetCanvas(): void; workspace: Workspace; animateID: any; automaticBordering: boolean; showObjectsTextAlways: boolean; } +interface ViewSize { + fullHeight: number; + vertical: number; + top: number; + side: number; + front: number; +} + +function viewSizeReducer( + state: ViewSize, + action: { type: ViewType | 'set'; e?: SyntheticEvent; data?: ViewSize }, +): ViewSize { + const event = (action.e as unknown) as MouseEvent; + const canvas3dContainer = document.getElementById('canvas3d-container'); + if (canvas3dContainer) { + switch (action.type) { + case ViewType.TOP: { + const width = event.clientX - canvas3dContainer.getBoundingClientRect().left; + const topWidth = state.top; + if (topWidth < width) { + const top = state.top + (width - topWidth); + const side = state.side - (width - topWidth); + return { + ...state, + top, + side, + }; + } + const top = state.top - (topWidth - width); + const side = state.side + (topWidth - width); + return { + ...state, + top, + side, + }; + } + case ViewType.SIDE: { + const width = event.clientX - canvas3dContainer.getBoundingClientRect().left; + const topSideWidth = state.top + state.side; + if (topSideWidth < width) { + const side = state.side + (width - topSideWidth); + const front = state.front - (width - topSideWidth); + return { + ...state, + side, + front, + }; + } + const side = state.side - (topSideWidth - width); + const front = state.front + (topSideWidth - width); + return { + ...state, + side, + front, + }; + } + case ViewType.PERSPECTIVE: + return { + ...state, + vertical: event.clientY - canvas3dContainer.getBoundingClientRect().top, + }; + case 'set': + return action.data as ViewSize; + default: + throw new Error(); + } + } + return state; +} + const CanvasWrapperComponent = (props: Props): ReactElement => { const animateId = useRef(0); - const perspectiveView = useRef(null); - const topView = useRef(null); - const sideView = useRef(null); - const frontView = useRef(null); + const [viewSize, setViewSize] = useReducer(viewSizeReducer, { + fullHeight: 0, + vertical: 0, + top: 0, + side: 0, + front: 0, + }); + const perspectiveView = useRef(null); + const topView = useRef(null); + const sideView = useRef(null); + const frontView = useRef(null); const { frameData, contextImageHide, getContextImage, loaded, data, annotations, curZLayer, @@ -59,12 +143,36 @@ const CanvasWrapperComponent = (props: Props): ReactElement => { } }; + const onMouseClick = (event: MouseEvent): void => { + const { canvasInstance } = props; + canvasInstance.mouseControls(MouseInteraction.CLICK, event); + }; + + const onMouseDoubleClick = (event: MouseEvent): void => { + const { canvasInstance } = props; + canvasInstance.mouseControls(MouseInteraction.DOUBLE_CLICK, event); + }; + + const onMouseHover = (event: MouseEvent): void => { + const { canvasInstance } = props; + canvasInstance.mouseControls(MouseInteraction.HOVER, event); + }; + + const onCanvasCancel = (): void => { + const { onResetCanvas } = props; + onResetCanvas(); + }; + const initialSetup = (): void => { const { canvasInstance } = props; const canvasInstanceDOM = canvasInstance.html(); // Events canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup); + canvasInstanceDOM.perspective.addEventListener('mousemove', onMouseHover); + canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel); + canvasInstanceDOM.perspective.addEventListener(MouseInteraction.DOUBLE_CLICK, onMouseDoubleClick); + canvasInstanceDOM.perspective.addEventListener(MouseInteraction.CLICK, onMouseClick); }; const keyControls = (key: KeyboardEvent): void => { @@ -91,6 +199,20 @@ const CanvasWrapperComponent = (props: Props): ReactElement => { topView.current.appendChild(canvasInstanceDOM.top); sideView.current.appendChild(canvasInstanceDOM.side); frontView.current.appendChild(canvasInstanceDOM.front); + const canvas3dContainer = document.getElementById('canvas3d-container'); + if (canvas3dContainer) { + const width = canvas3dContainer.clientWidth / 3; + setViewSize({ + type: 'set', + data: { + fullHeight: canvas3dContainer.clientHeight, + vertical: canvas3dContainer.clientHeight / 2, + top: width, + side: width, + front: width, + }, + }); + } } document.addEventListener('keydown', keyControls); @@ -101,59 +223,128 @@ const CanvasWrapperComponent = (props: Props): ReactElement => { return () => { canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup); + canvasInstanceDOM.perspective.removeEventListener('mousemove', onMouseHover); + canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel); + canvasInstanceDOM.perspective.removeEventListener(MouseInteraction.DOUBLE_CLICK, onMouseDoubleClick); + canvasInstanceDOM.perspective.removeEventListener(MouseInteraction.CLICK, onMouseClick); document.removeEventListener('keydown', keyControls); cancelAnimationFrame(animateId.current); }; - }); + }, []); useEffect(() => { updateCanvas(); }, [frameData, annotations, curZLayer]); - const renderArrowGroup = (): ReactElement => ( + const screenKeyControl = (code: CAMERA_ACTION): void => { + const { canvasInstance } = props; + canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey: true })); + }; + + const ArrowGroup = (): ReactElement => ( - + + +
- - - + + + + + + + + +
); - const renderControlGroup = (): ReactElement => ( + const ControlGroup = (): ReactElement => ( - - - + + + + + + + + +
- - - + + + + + + + + +
); return ( - + { loaded={loaded} data={data} /> - -
-
- {renderArrowGroup()} - {renderControlGroup()} -
-
-
-
TOP VIEW
-
-
-
-
SIDE VIEW
-
+ } + onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })} + > +
+
+ +
-
-
FRONT VIEW
+ +
+ } + onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.TOP, e })} + > +
+
TOP
+
+
+ + } + onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.SIDE, e })} + > +
+
SIDE
+
+
+ +
+
FRONT
-
); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index b85f5d6a..f2851862 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -8,14 +8,16 @@ import Button from 'antd/lib/button'; import InputNumber from 'antd/lib/input-number'; import Radio, { RadioChangeEvent } from 'antd/lib/radio'; import Text from 'antd/lib/typography/Text'; +import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; +import { Canvas3d } from 'cvat-canvas3d-wrapper'; -import { RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; import { ShapeType } from 'reducers/interfaces'; import { clamp } from 'utils/math'; import LabelSelector from 'components/label-selector/label-selector'; import CVATTooltip from 'components/common/cvat-tooltip'; interface Props { + canvasInstance: Canvas | Canvas3d; shapeType: ShapeType; labels: any[]; minimumPoints: number; @@ -48,8 +50,11 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { onChangePoints, onChangeRectDrawingMethod, onChangeCuboidDrawingMethod, + canvasInstance, } = props; + const is2D = canvasInstance instanceof Canvas; + return (
@@ -72,7 +77,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { /> - {shapeType === ShapeType.RECTANGLE && ( + {is2D && shapeType === ShapeType.RECTANGLE && ( <> @@ -97,7 +102,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { )} - {shapeType === ShapeType.CUBOID && ( + {is2D && shapeType === ShapeType.CUBOID && ( <> @@ -122,7 +127,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { )} - {shapeType !== ShapeType.RECTANGLE && shapeType !== ShapeType.CUBOID && ( + {is2D && shapeType !== ShapeType.RECTANGLE && shapeType !== ShapeType.CUBOID && ( Number of points: @@ -150,11 +155,13 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { - - - - - + {is2D && ( + + + + + + )}
); diff --git a/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar.tsx index 519ed5a2..304f8ebb 100644 --- a/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar.tsx @@ -4,15 +4,11 @@ import React from 'react'; import Layout from 'antd/lib/layout'; - import { ActiveControl } from 'reducers/interfaces'; import { Canvas3d as Canvas } from 'cvat-canvas3d-wrapper'; - import CursorControl from './cursor-control'; import MoveControl from './move-control'; - import DrawCuboidControl from './draw-cuboid-control'; - import PhotoContextControl from './photo-context'; interface Props { @@ -31,7 +27,6 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { return ( - @@ -27,6 +27,7 @@ function CursorControl(props: Props): JSX.Element { 'cvat-cursor-control', activeControl === ActiveControl.CURSOR ? 'cvat-active-canvas-control ' : '', ].join(' ')} + onClick={activeControl !== ActiveControl.CURSOR ? (): void => canvasInstance.cancel() : undefined} /> ); diff --git a/cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss index 540d0925..ad321301 100644 --- a/cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss @@ -14,7 +14,6 @@ background: $border-color-3; top: $grid-unit-size; right: $grid-unit-size; - max-height: $grid-unit-size * 16; z-index: 100; border-radius: $grid-unit-size; border: 1px solid $border-color-3; @@ -173,7 +172,7 @@ } .cvat-canvas3d-perspective { - height: 50%; + height: 100%; width: 100%; position: relative; padding: $grid-unit-size/2; @@ -219,7 +218,7 @@ } .cvat-canvas3d-orthographic-view { - width: 33.333%; + width: 100%; height: 100%; padding-top: $grid-unit-size/2; padding-bottom: $grid-unit-size/2; @@ -227,10 +226,10 @@ .cvat-canvas3d-topview { padding-left: $grid-unit-size/2; + padding-right: $grid-unit-size/2; } .cvat-canvas3d-sideview { - padding-left: $grid-unit-size/2; padding-right: $grid-unit-size/2; } @@ -263,3 +262,44 @@ text-align: center; vertical-align: middle; } + +.cvat-resizable { + position: relative; +} + +.cvat-resizable-handle-horizontal { + position: absolute; + margin-left: auto; + width: 100%; + margin-right: auto; + right: 0; + bottom: 0; + left: 0; + background-color: grey; + height: $grid-unit-size/2; + cursor: ns-resize; +} + +.cvat-resizable-handle-vertical-side { + position: absolute; + width: $grid-unit-size/2; + margin-right: auto; + top: $grid-unit-size * 4.5; + right: 0; + bottom: 0; + background-color: grey; + height: 100%; + cursor: ew-resize; +} + +.cvat-resizable-handle-vertical-top { + position: absolute; + width: $grid-unit-size/2; + margin-right: auto; + top: $grid-unit-size * 4.5; + right: 0; + bottom: 0; + background-color: grey; + height: 100%; + cursor: ew-resize; +} diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx index e71ada8e..f65bd054 100644 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx +++ b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper3D'; -import { confirmCanvasReady, getContextImage } from 'actions/annotation-actions'; +import { confirmCanvasReady, getContextImage, resetCanvas } from 'actions/annotation-actions'; import { CombinedState } from 'reducers/interfaces'; @@ -25,6 +25,7 @@ interface StateToProps { interface DispatchToProps { onSetupCanvas(): void; getContextImage(): void; + onResetCanvas(): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -63,6 +64,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { getContextImage(): void { dispatch(getContextImage()); }, + onResetCanvas(): void { + dispatch(resetCanvas()); + }, }; } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 151be978..f88c752d 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx @@ -156,10 +156,13 @@ class DrawShapePopoverContainer extends React.PureComponent { rectDrawingMethod, cuboidDrawingMethod, selectedLabelID, numberOfPoints, } = this.state; - const { normalizedKeyMap, labels, shapeType } = this.props; + const { + normalizedKeyMap, labels, shapeType, canvasInstance, + } = this.props; return ( 1 else None - file_list = [f for f in self._zip_source.namelist() if get_mime(f) == 'image'] + file_list = [f for f in self._zip_source.namelist() if files_to_ignore(f) and get_mime(f) == 'image'] super().__init__(file_list, step, start, stop) def __del__(self): @@ -675,7 +681,7 @@ class ValidateDimension: def validate_pointcloud(self, *args): root, actual_path, files = args pointcloud_files = self.process_files(root, actual_path, files) - related_path = root.split("pointcloud")[0] + related_path = root.rsplit("/pointcloud", 1)[0] related_images_path = os.path.join(related_path, "related_images") if os.path.isdir(related_images_path): @@ -685,7 +691,7 @@ class ValidateDimension: for k in pointcloud_files: for path in paths: - if k == path.split("_")[0]: + if k == path.rsplit("_", 1)[0]: file_path = os.path.abspath(os.path.join(related_images_path, path)) files = [file for file in os.listdir(file_path) if os.path.isfile(os.path.join(file_path, file))] @@ -704,7 +710,7 @@ class ValidateDimension: current_directory_name = os.path.split(root) if len(pcd_files.keys()) == 1: - pcd_name = list(pcd_files.keys())[0].split(".")[0] + pcd_name = list(pcd_files.keys())[0].rsplit(".", 1)[0] if current_directory_name[1] == pcd_name: for related_image in self.image_files.values(): if root == os.path.split(related_image)[0]: @@ -718,6 +724,8 @@ class ValidateDimension: return actual_path = self.path for root, _, files in os.walk(actual_path): + if not files_to_ignore(root): + continue if root.endswith("data"): if os.path.split(os.path.split(root)[0])[1] == "velodyne_points":