diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c8b4d6b8833..bf3c5a1775fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 () - \[SDK\] Class to represent a project as a PyTorch dataset () +- Grid view and multiple context images supported () - Support for custom file to job splits in tasks (server API & SDK only) () - \[SDK\] A PyTorch adapter setting to disable cache updates diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index b97d416139f0..b42a186919c6 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.16.1", + "version": "2.16.2", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 556ba4a337de..a6723d59b632 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -290,21 +290,6 @@ g.cvat_canvas_shape_occluded { position: relative; } -#cvat_canvas_loading_animation { - z-index: 1; - position: absolute; - width: 100%; - height: 100%; -} - -#cvat_canvas_loading_circle { - fill-opacity: 0; - stroke: #09c; - stroke-width: 3px; - stroke-dasharray: 50; - animation: loadingAnimation 1s linear infinite; -} - #cvat_canvas_text_content { text-rendering: optimizeSpeed; position: absolute; diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index b7591f02277b..5f271f88b4d6 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -527,7 +527,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (typeof exception !== 'number' || exception === this.data.imageID) { this.notify(UpdateReasons.DATA_FAILED); } - throw exception; }); } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 1eeafbf1ee59..e9f057f79baf 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -66,7 +66,6 @@ export interface CanvasView { } export class CanvasViewImpl implements CanvasView, Listener { - private loadingAnimation: SVGSVGElement; private text: SVGSVGElement; private adoptedText: SVG.Container; private background: HTMLCanvasElement; @@ -1082,7 +1081,6 @@ export class CanvasViewImpl implements CanvasView, Listener { }; // Create HTML elements - this.loadingAnimation = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); this.adoptedText = SVG.adopt((this.text as any) as HTMLElement) as SVG.Container; this.background = window.document.createElement('canvas'); @@ -1101,8 +1099,6 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas = window.document.createElement('div'); - const loadingCircle: SVGCircleElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - const gridDefs: SVGDefsElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'defs'); const gridRect: SVGRectElement = window.document.createElementNS('http://www.w3.org/2000/svg', 'rect'); @@ -1129,13 +1125,6 @@ export class CanvasViewImpl implements CanvasView, Listener { patternUnits: 'userSpaceOnUse', }); - // Setup loading animation - this.loadingAnimation.setAttribute('id', 'cvat_canvas_loading_animation'); - loadingCircle.setAttribute('id', 'cvat_canvas_loading_circle'); - loadingCircle.setAttribute('r', '30'); - loadingCircle.setAttribute('cx', '50%'); - loadingCircle.setAttribute('cy', '50%'); - // Setup grid this.grid.setAttribute('id', 'cvat_canvas_grid'); this.grid.setAttribute('version', '2'); @@ -1166,14 +1155,12 @@ export class CanvasViewImpl implements CanvasView, Listener { this.canvas.setAttribute('id', 'cvat_canvas_wrapper'); // Unite created HTML elements together - this.loadingAnimation.appendChild(loadingCircle); this.grid.appendChild(gridDefs); this.grid.appendChild(gridRect); gridDefs.appendChild(this.gridPattern); this.gridPattern.appendChild(this.gridPath); - this.canvas.appendChild(this.loadingAnimation); this.canvas.appendChild(this.text); this.canvas.appendChild(this.background); this.canvas.appendChild(this.masksContent); @@ -1412,10 +1399,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } } else if (reason === UpdateReasons.IMAGE_CHANGED) { const { image } = model; - if (!image) { - this.loadingAnimation.classList.remove('cvat_canvas_hidden'); - } else { - this.loadingAnimation.classList.add('cvat_canvas_hidden'); + if (image) { const ctx = this.background.getContext('2d'); this.background.setAttribute('width', `${image.renderWidth}px`); this.background.setAttribute('height', `${image.renderHeight}px`); diff --git a/cvat-canvas3d/package.json b/cvat-canvas3d/package.json index f8c1ea524157..920ef664436b 100644 --- a/cvat-canvas3d/package.json +++ b/cvat-canvas3d/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas3d", - "version": "0.0.6", + "version": "0.0.7", "description": "Part of Computer Vision Annotation Tool which presents its canvas3D library", "main": "src/canvas3d.ts", "scripts": { diff --git a/cvat-canvas3d/src/typescript/canvas3dModel.ts b/cvat-canvas3d/src/typescript/canvas3dModel.ts index c87feb6bf1ef..48acec10ae4b 100644 --- a/cvat-canvas3d/src/typescript/canvas3dModel.ts +++ b/cvat-canvas3d/src/typescript/canvas3dModel.ts @@ -238,7 +238,10 @@ export class Canvas3dModelImpl extends MasterImpl implements Canvas3dModel { }) .catch((exception: any): void => { this.data.isFrameUpdating = false; - throw exception; + // don't notify when the frame is no longer needed + if (typeof exception !== 'number' || exception === this.data.imageID) { + throw exception; + } }); } diff --git a/cvat-canvas3d/src/typescript/canvas3dView.ts b/cvat-canvas3d/src/typescript/canvas3dView.ts index 8310bc7bd94a..c1afd80497e7 100644 --- a/cvat-canvas3d/src/typescript/canvas3dView.ts +++ b/cvat-canvas3d/src/typescript/canvas3dView.ts @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -107,6 +107,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { private cube: CuboidModel; private isPerspectiveBeingDragged: boolean; private activatedElementID: number | null; + private isCtrlDown: boolean; private drawnObjects: Record { + Object.defineProperty(view, 'scene', { + value: scene, + enumerable: false, + configurable: false, + writable: false, + }); + }); + canvasPerspectiveView.addEventListener('contextmenu', (e: MouseEvent): void => { if (this.model.data.activeElement.clientID !== null) { this.dispatchEvent( @@ -330,6 +346,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { canvasPerspectiveView.addEventListener('mousemove', (event: MouseEvent): void => { event.preventDefault(); + this.isCtrlDown = event.ctrlKey; if (this.mode === Mode.DRAG_CANVAS) return; const canvas = this.views.perspective.renderer.domElement; const rect = canvas.getBoundingClientRect(); @@ -539,7 +556,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { } private setDefaultZoom(): void { - if (this.model.data.activeElement === null) { + if (this.model.data.activeElement.clientID === null) { Object.keys(this.views).forEach((view: string): void => { const viewType = this.views[view as keyof Views]; if (view !== ViewType.PERSPECTIVE) { @@ -554,7 +571,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { canvasTop.offsetWidth / (bboxtop.max.x - bboxtop.min.x), canvasTop.offsetHeight / (bboxtop.max.y - bboxtop.min.y), ) * 0.4; - this.views.top.camera.zoom = x1 / 100; + this.views.top.camera.zoom = x1 / 50; this.views.top.camera.updateProjectionMatrix(); this.views.top.camera.updateMatrix(); this.updateHelperPointsSize(ViewType.TOP); @@ -565,7 +582,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { canvasFront.offsetWidth / (bboxfront.max.y - bboxfront.min.y), canvasFront.offsetHeight / (bboxfront.max.z - bboxfront.min.z), ) * 0.4; - this.views.front.camera.zoom = x2 / 100; + this.views.front.camera.zoom = x2 / 50; this.views.front.camera.updateProjectionMatrix(); this.views.front.camera.updateMatrix(); this.updateHelperPointsSize(ViewType.FRONT); @@ -576,7 +593,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { canvasSide.offsetWidth / (bboxside.max.x - bboxside.min.x), canvasSide.offsetHeight / (bboxside.max.z - bboxside.min.z), ) * 0.4; - this.views.side.camera.zoom = x3 / 100; + this.views.side.camera.zoom = x3 / 50; this.views.side.camera.updateProjectionMatrix(); this.views.side.camera.updateMatrix(); this.updateHelperPointsSize(ViewType.SIDE); @@ -842,7 +859,8 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { this.activatedElementID = +clientID; this.rotatePlane(null, null); this.detachCamera(null); - this.setDefaultZoom(); + [ViewType.TOP, ViewType.SIDE, ViewType.FRONT] + .forEach((type) => this.updateHelperPointsSize(type)); } } @@ -1030,6 +1048,9 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { } else if (reason === UpdateReasons.SHAPE_ACTIVATED) { this.deactivateObject(); this.activateObject(); + if (this.activatedElementID) { + this.setDefaultZoom(); + } } else if (reason === UpdateReasons.DRAW) { const data: DrawData = this.controller.drawData; if (Number.isInteger(data.redraw)) { @@ -1385,7 +1406,7 @@ export class Canvas3dViewImpl implements Canvas3dView, Listener { const { x, y, z } = intersection.point; object.position.set(x, y, z); } - } else if (this.mode === Mode.IDLE && !this.isPerspectiveBeingDragged) { + } else if (this.mode === Mode.IDLE && !this.isPerspectiveBeingDragged && !this.isCtrlDown) { const { renderer } = this.views.perspective.rayCaster; const intersects = renderer.intersectObjects(this.getAllVisibleCuboids(), false); if (intersects.length !== 0) { diff --git a/cvat-core/package.json b/cvat-core/package.json index 0448cd2db207..99ad94c19b38 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "7.5.0", + "version": "8.0.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index f97b6d5cb40d..1dc3ef24d35a 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -3,14 +3,29 @@ // // SPDX-License-Identifier: MIT -import * as cvatData from 'cvat-data'; import { isBrowser, isNode } from 'browser-or-node'; -import PluginRegistry from './plugins'; -import serverProxy from './server-proxy'; -import { Exception, ArgumentError, DataError } from './exceptions'; -// This is the frames storage -const frameDataCache = {}; +import * as cvatData from 'cvat-data'; +import { DimensionType } from 'enums'; +import PluginRegistry from './plugins'; +import serverProxy, { FramesMetaData } from './server-proxy'; +import { + Exception, ArgumentError, DataError, ServerError, +} from './exceptions'; + +// frame storage by job id +const frameDataCache: Record = {}; export class FrameData { constructor({ @@ -23,7 +38,7 @@ export class FrameData { stopFrame, decodeForward, deleted, - has_related_context: hasRelatedContext, + related_files: relatedFiles, }) { Object.defineProperties( this, @@ -48,8 +63,8 @@ export class FrameData { value: frameNumber, writable: false, }, - hasRelatedContext: { - value: hasRelatedContext, + relatedFiles: { + value: relatedFiles, writable: false, }, startFrame: { @@ -300,7 +315,7 @@ FrameData.prototype.data.implementation = async function (onServerRequest) { }); }; -function getFrameMeta(jobID, frame) { +function getFrameMeta(jobID, frame): FramesMetaData['frames'][0] { const { meta, mode, startFrame } = frameDataCache[jobID]; let size = null; if (mode === 'interpolation') { @@ -314,6 +329,7 @@ function getFrameMeta(jobID, frame) { } else { throw new DataError(`Invalid mode is specified ${mode}`); } + return size; } @@ -329,16 +345,46 @@ class FrameBuffer { this._jobID = jobID; } - isContextImageAvailable(frame) { - return frame in this._contextImage; + addContextImage(frame, data): void { + const promise = new Promise((resolve, reject) => { + data.then((resolvedData) => { + const meta = getFrameMeta(this._jobID, frame); + return cvatData + .decodeZip(resolvedData, 0, meta.related_files, cvatData.DimensionType.DIMENSION_2D); + }).then((decodedData) => { + this._contextImage[frame] = decodedData; + resolve(); + }).catch((error: Error) => { + if (error instanceof ServerError && (error as any).code === 404) { + this._contextImage[frame] = {}; + resolve(); + } else { + reject(error); + } + }); + }); + + this._contextImage[frame] = promise; } - getContextImage(frame) { - return this._contextImage[frame] || null; + isContextImageAvailable(frame): boolean { + return frame in this._contextImage; } - addContextImage(frame, data) { - this._contextImage[frame] = data; + getContextImage(frame): Promise { + return new Promise((resolve) => { + if (frame in this._contextImage) { + if (this._contextImage[frame] instanceof Promise) { + this._contextImage[frame].then(() => { + resolve(this.getContextImage(frame)); + }); + } else { + resolve({ ...this._contextImage[frame] }); + } + } else { + resolve([]); + } + }); } getFreeBufferSize() { @@ -477,7 +523,7 @@ class FrameBuffer { } } - async require(frameNumber, jobID, fillBuffer, frameStep) { + async require(frameNumber: number, jobID: number, fillBuffer: boolean, frameStep: number): FrameData { for (const frame in this._buffer) { if (+frame < frameNumber || +frame >= frameNumber + this._size * frameStep) { delete this._buffer[frame]; @@ -554,11 +600,7 @@ async function getImageContext(jobID, frame) { // eslint-disable-next-line no-undef resolve(global.Buffer.from(result, 'binary').toString('base64')); } else if (isBrowser) { - const reader = new FileReader(); - reader.onload = () => { - resolve(reader.result); - }; - reader.readAsDataURL(result); + resolve(result); } }) .catch((error) => { @@ -572,7 +614,7 @@ export async function getContextImage(jobID, frame) { return frameDataCache[jobID].frameBuffer.getContextImage(frame); } const response = getImageContext(jobID, frame); - frameDataCache[jobID].frameBuffer.addContextImage(frame, response); + await frameDataCache[jobID].frameBuffer.addContextImage(frame, response); return frameDataCache[jobID].frameBuffer.getContextImage(frame); } @@ -600,16 +642,16 @@ export async function getPreview(taskID = null, jobID = null) { } export async function getFrame( - jobID, - chunkSize, - chunkType, - mode, - frame, - startFrame, - stopFrame, - isPlaying, - step, - dimension, + jobID: number, + chunkSize: number, + chunkType: 'video' | 'imageset', + mode: 'interpolation' | 'annotation', // todo: obsolete, need to remove + frame: number, + startFrame: number, + stopFrame: number, + isPlaying: boolean, + step: number, + dimension: DimensionType, ) { if (!(jobID in frameDataCache)) { const blockType = chunkType === 'video' ? cvatData.BlockType.MP4VIDEO : cvatData.BlockType.ARCHIVE; @@ -648,8 +690,9 @@ export async function getFrame( activeChunkRequest: null, nextChunkRequest: null, }; + + // relevant only for video chunks const frameMeta = getFrameMeta(jobID, frame); - // actual only for video chunks frameDataCache[jobID].provider.setRenderSize(frameMeta.width, frameMeta.height); } diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 65fa03b7be42..38e6dbd47a27 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -1384,7 +1384,7 @@ async function getImageContext(jid, frame) { number: frame, }, proxy: config.proxy, - responseType: 'blob', + responseType: 'arraybuffer', }); } catch (errorData) { throw generateError(errorData); @@ -1423,7 +1423,23 @@ async function getData(tid, jid, chunk) { return response; } -async function getMeta(session, jid) { +export interface FramesMetaData { + chunk_size: number; + deleted_frames: number[]; + frame_filter: string; + frames: { + width: number; + height: number; + name: string; + related_files: number; + }[]; + image_quality: number; + size: number; + start_frame: number; + stop_frame: number; +} + +async function getMeta(session, jid): Promise { const { backendAPI } = config; let response = null; diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index ef8b15d66213..0bc7642d9533 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -2963,22 +2963,22 @@ const frameMetaDummyData = { width: 2560, height: 1703, name: '1598296101_1033667.jpg', - has_related_context: false + related_files: 0 }, { width: 1600, height: 1200, name: '30fdce7f27b9c7b1d50108d7c16d23ef.jpg', - has_related_context: false + related_files: 0 }, { width: 2880, height: 1800, name: '567362-ily-comedy-drama-1finding-3.jpg', - has_related_context: false + related_files: 0 }, { width: 1920, height: 1080, name: '730443-under-the-sea-wallpapers-1920x1080-windows-10.jpg', - has_related_context: false + related_files: 0 }], deleted_frames: [] }, diff --git a/cvat-core/webpack.config.js b/cvat-core/webpack.config.js index 24098cf87608..cad51dbb44a1 100644 --- a/cvat-core/webpack.config.js +++ b/cvat-core/webpack.config.js @@ -55,7 +55,7 @@ const webConfig = { output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].min.js', - library: 'cvat', + library: 'cvat-core.js', libraryTarget: 'window', }, resolve: { diff --git a/cvat-data/package.json b/cvat-data/package.json index 2158e87ad650..63c1be63d625 100644 --- a/cvat-data/package.json +++ b/cvat-data/package.json @@ -1,6 +1,6 @@ { "name": "cvat-data", - "version": "1.0.2", + "version": "1.1.0", "description": "", "main": "src/ts/cvat-data.ts", "scripts": { diff --git a/cvat-data/src/ts/cvat-data.ts b/cvat-data/src/ts/cvat-data.ts index 1484fff07f4a..b60d0a82384d 100644 --- a/cvat-data/src/ts/cvat-data.ts +++ b/cvat-data/src/ts/cvat-data.ts @@ -1,139 +1,235 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { Mutex } from 'async-mutex'; -// eslint-disable-next-line max-classes-per-file import { MP4Reader, Bytestream } from './3rdparty/mp4'; import ZipDecoder from './unzip_imgs.worker'; import H264Decoder from './3rdparty/Decoder.worker'; -export const BlockType = Object.freeze({ - MP4VIDEO: 'mp4video', - ARCHIVE: 'archive', -}); +export enum BlockType { + MP4VIDEO = 'mp4video', + ARCHIVE = 'archive', +} + +export enum DimensionType { + DIMENSION_3D = '3d', + DIMENSION_2D = '2d', +} + +export function decodeZip( + block: any, start: number, end: number, dimension: any, +): Promise> { + return new Promise((resolve, reject) => { + decodeZip.mutex.acquire().then((release) => { + const worker = new ZipDecoder(); + const result: Record = {}; + let decoded = 0; + + worker.onerror = (e: ErrorEvent) => { + release(); + worker.terminate(); + reject(new Error(`Archive can not be decoded. ${e.message}`)); + }; + + worker.onmessage = async (event) => { + const { error, fileName } = event.data; + if (error) { + worker.onerror(new ErrorEvent('error', { message: error.toString() })); + } + + const { data } = event.data; + result[fileName.split('.')[0]] = data; + decoded++; + + if (decoded === end) { + release(); + worker.terminate(); + resolve(result); + } + }; + + worker.postMessage({ + block, + start, + end, + dimension, + dimension2D: DimensionType.DIMENSION_2D, + }); + }); + }); +} -export const DimensionType = Object.freeze({ - DIM_3D: '3d', - DIM_2D: '2d', -}); +decodeZip.mutex = new Mutex(); + +interface BlockToDecode { + start: number; + end: number; + block: ArrayBuffer; + resolveCallback: (frame: number) => void; + rejectCallback: (e: ErrorEvent) => void; +} export class FrameProvider { + private blocksRanges: string[]; + private blockSize: number; + private blockType: BlockType; + + /* + ImageBitmap when decode zip chunks + ImageData when decode video chunks + Blob when 3D dimension + null when not decoded yet + */ + private frames: Record; + private requestedBlockToDecode: null | BlockToDecode; + private blocksAreBeingDecoded: Record; + private promisedFrames: Record void; + reject: () => void; + }>; + private currentDecodingThreads: number; + private currentFrame: number; + private mutex: Mutex; + + private dimension: DimensionType; + private workerThreadsLimit: number; + private cachedEncodedBlocksLimit: number; + private cachedDecodedBlocksLimit: number; + + // used for video chunks to resize after decoding + private renderWidth: number; + private renderHeight: number; + constructor( - blockType, - blockSize, - cachedBlockCount, + blockType: BlockType, + blockSize: number, + cachedBlockCount: number, decodedBlocksCacheSize = 5, maxWorkerThreadCount = 2, - dimension = DimensionType.DIM_2D, + dimension: DimensionType = DimensionType.DIMENSION_2D, ) { - this._frames = {}; - this._cachedBlockCount = Math.max(1, cachedBlockCount); // number of stored blocks - this._decodedBlocksCacheSize = decodedBlocksCacheSize; - this._blocksRanges = []; + this.mutex = new Mutex(); + this.blocksRanges = []; + this.frames = {}; + this.promisedFrames = {}; + this.currentDecodingThreads = 0; + this.currentFrame = -1; + + this.cachedEncodedBlocksLimit = Math.max(1, cachedBlockCount); // number of stored blocks + this.cachedDecodedBlocksLimit = decodedBlocksCacheSize; + this.workerThreadsLimit = maxWorkerThreadCount; + this.dimension = dimension; + + this.renderWidth = 1920; + this.renderHeight = 1080; + this.blockSize = blockSize; + this.blockType = blockType; + + // todo: sort out with logic of blocks this._blocks = {}; - this._running = false; - this._blockSize = blockSize; - this._blockType = blockType; - this._currFrame = -1; - this._requestedBlockDecode = null; - this._width = null; - this._height = null; - this._decodingBlocks = {}; - this._decodeThreadCount = 0; - this._timerId = setTimeout(this._worker.bind(this), 100); - this._mutex = new Mutex(); - this._promisedFrames = {}; - this._maxWorkerThreadCount = maxWorkerThreadCount; - this._dimension = dimension; + this.requestedBlockToDecode = null; + this.blocksAreBeingDecoded = {}; + + setTimeout(this._checkDecodeRequests.bind(this), 100); } - async _worker() { - if (this._requestedBlockDecode !== null && this._decodeThreadCount < this._maxWorkerThreadCount) { - await this.startDecode(); + _checkDecodeRequests(): void { + if (this.requestedBlockToDecode !== null && this.currentDecodingThreads < this.workerThreadsLimit) { + this.startDecode().then(() => { + setTimeout(this._checkDecodeRequests.bind(this), 100); + }); + } else { + setTimeout(this._checkDecodeRequests.bind(this), 100); } - this._timerId = setTimeout(this._worker.bind(this), 100); } - isChunkCached(start, end) { - return `${start}:${end}` in this._blocksRanges; + isChunkCached(start: number, end: number): boolean { + // todo: always returns false because this.blocksRanges is Array, not dictionary + // but if try to correct other errors happens, need to debug.. + return `${start}:${end}` in this.blocksRanges; } /* This method removes extra data from a cache when memory overflow */ - async _cleanup() { - if (this._blocksRanges.length > this._cachedBlockCount) { - const shifted = this._blocksRanges.shift(); // get the oldest block + async _cleanup(): Promise { + if (this.blocksRanges.length > this.cachedEncodedBlocksLimit) { + const shifted = this.blocksRanges.shift(); // get the oldest block const [start, end] = shifted.split(':').map((el) => +el); - delete this._blocks[start / this._blockSize]; + delete this._blocks[Math.floor(start / this.blockSize)]; for (let i = start; i <= end; i++) { - delete this._frames[i]; + delete this.frames[i]; } } // delete frames whose are not in areas of current frame - const distance = Math.floor(this._decodedBlocksCacheSize / 2); - for (let i = 0; i < this._blocksRanges.length; i++) { - const [start, end] = this._blocksRanges[i].split(':').map((el) => +el); + const distance = Math.floor(this.cachedDecodedBlocksLimit / 2); + for (let i = 0; i < this.blocksRanges.length; i++) { + const [start, end] = this.blocksRanges[i].split(':').map((el) => +el); if ( - end < this._currFrame - distance * this._blockSize || - start > this._currFrame + distance * this._blockSize + end < this.currentFrame - distance * this.blockSize || + start > this.currentFrame + distance * this.blockSize ) { for (let j = start; j <= end; j++) { - delete this._frames[j]; + delete this.frames[j]; } } } } - async requestDecodeBlock(block, start, end, resolveCallback, rejectCallback) { - const release = await this._mutex.acquire(); + async requestDecodeBlock( + block: ArrayBuffer, + start: number, + end: number, + resolveCallback: () => void, + rejectCallback: () => void, + ): Promise { + const release = await this.mutex.acquire(); try { - if (this._requestedBlockDecode !== null) { - if (start === this._requestedBlockDecode.start && end === this._requestedBlockDecode.end) { - this._requestedBlockDecode.resolveCallback = resolveCallback; - this._requestedBlockDecode.rejectCallback = rejectCallback; - } else if (this._requestedBlockDecode.rejectCallback) { - this._requestedBlockDecode.rejectCallback(); + if (this.requestedBlockToDecode !== null) { + if (start === this.requestedBlockToDecode.start && end === this.requestedBlockToDecode.end) { + // only rewrite callbacks if the same block was requested again + this.requestedBlockToDecode.resolveCallback = resolveCallback; + this.requestedBlockToDecode.rejectCallback = rejectCallback; + + // todo: should we reject the previous request here? + } else if (this.requestedBlockToDecode.rejectCallback) { + // if another block requested, the previous request should be rejected + this.requestedBlockToDecode.rejectCallback(); } } - if (!(`${start}:${end}` in this._decodingBlocks)) { - this._requestedBlockDecode = { - block: block || this._blocks[Math.floor(start / this._blockSize)], + + if (!(`${start}:${end}` in this.blocksAreBeingDecoded)) { + this.requestedBlockToDecode = { + block: block || this._blocks[Math.floor(start / this.blockSize)], start, end, resolveCallback, rejectCallback, }; } else { - this._decodingBlocks[`${start}:${end}`].rejectCallback = rejectCallback; - this._decodingBlocks[`${start}:${end}`].resolveCallback = resolveCallback; + this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback = rejectCallback; + this.blocksAreBeingDecoded[`${start}:${end}`].resolveCallback = resolveCallback; } } finally { release(); } } - isRequestExist() { - return this._requestedBlockDecode !== null; - } - - setRenderSize(width, height) { - this._width = width; - this._height = height; + setRenderSize(width: number, height: number): void { + this.renderWidth = width; + this.renderHeight = height; } - /* Method returns frame from collection. Else method returns 0 */ - async frame(frameNumber) { - this._currFrame = frameNumber; + /* Method returns frame from collection. Else method returns null */ + async frame(frameNumber: number): Promise { + this.currentFrame = frameNumber; return new Promise((resolve, reject) => { - if (frameNumber in this._frames) { - if (this._frames[frameNumber] !== null) { - resolve(this._frames[frameNumber]); + if (frameNumber in this.frames) { + if (this.frames[frameNumber] !== null) { + resolve(this.frames[frameNumber]); } else { - this._promisedFrames[frameNumber] = { - resolve, - reject, - }; + this.promisedFrames[frameNumber] = { resolve, reject }; } } else { resolve(null); @@ -141,30 +237,24 @@ export class FrameProvider { }); } - isNextChunkExists(frameNumber) { - const nextChunkNum = Math.floor(frameNumber / this._blockSize) + 1; - if (this._blocks[nextChunkNum] === 'loading') { - return true; - } - + isNextChunkExists(frameNumber: number): boolean { + const nextChunkNum = Math.floor(frameNumber / this.blockSize) + 1; return nextChunkNum in this._blocks; } - /* - Method start asynchronic decode a block of data - - @param block - is a data from a server as is (ts file or archive) - @param start {number} - is the first frame of a block - @param end {number} - is the last frame of a block + 1 - @param callback - callback) - - */ - - setReadyToLoading(chunkNumber) { + setReadyToLoading(chunkNumber: number): void { this._blocks[chunkNumber] = 'loading'; } - static cropImage(imageBuffer, imageWidth, imageHeight, xOffset, yOffset, width, height) { + static cropImage( + imageBuffer: ArrayBuffer, + imageWidth: number, + imageHeight: number, + xOffset: number, + yOffset: number, + width: number, + height: number, + ): ImageData { if (xOffset === 0 && width === imageWidth && yOffset === 0 && height === imageHeight) { return new ImageData(new Uint8ClampedArray(imageBuffer), width, height); } @@ -189,22 +279,26 @@ export class FrameProvider { return new ImageData(rgbaInt8Clamped, width, height); } - async startDecode() { - const release = await this._mutex.acquire(); + async startDecode(): Promise { + const release = await this.mutex.acquire(); try { - const height = this._height; - const width = this._width; - const { start, end, block } = this._requestedBlockDecode; - - this._blocksRanges.push(`${start}:${end}`); - this._decodingBlocks[`${start}:${end}`] = this._requestedBlockDecode; - this._requestedBlockDecode = null; - this._blocks[Math.floor((start + 1) / this._blockSize)] = block; + const height = this.renderHeight; + const width = this.renderWidth; + const { start, end, block } = this.requestedBlockToDecode; + + this.blocksRanges.push(`${start}:${end}`); + this.blocksAreBeingDecoded[`${start}:${end}`] = this.requestedBlockToDecode; + this.requestedBlockToDecode = null; + this._blocks[Math.floor((start + 1) / this.blockSize)] = block; + for (let i = start; i <= end; i++) { - this._frames[i] = null; + this.frames[i] = null; } + this._cleanup(); - if (this._blockType === BlockType.MP4VIDEO) { + this.currentDecodingThreads++; + + if (this.blockType === BlockType.MP4VIDEO) { const worker = new H264Decoder(); let index = start; @@ -214,8 +308,8 @@ export class FrameProvider { return; } - const scaleFactor = Math.ceil(this._height / e.data.height); - this._frames[index] = FrameProvider.cropImage( + const scaleFactor = Math.ceil(height / e.data.height); + this.frames[index] = FrameProvider.cropImage( e.data.buf, e.data.width, e.data.height, @@ -225,37 +319,44 @@ export class FrameProvider { Math.floor(height / scaleFactor), ); - if (this._decodingBlocks[`${start}:${end}`].resolveCallback) { - this._decodingBlocks[`${start}:${end}`].resolveCallback(index); + const { resolveCallback } = this.blocksAreBeingDecoded[`${start}:${end}`]; + if (resolveCallback) { + resolveCallback(index); } - if (index in this._promisedFrames) { - this._promisedFrames[index].resolve(this._frames[index]); - delete this._promisedFrames[index]; + if (index in this.promisedFrames) { + const { resolve } = this.promisedFrames[index]; + delete this.promisedFrames[index]; + resolve(this.frames[index]); } + if (index === end) { - this._decodeThreadCount--; - delete this._decodingBlocks[`${start}:${end}`]; worker.terminate(); + this.currentDecodingThreads--; + delete this.blocksAreBeingDecoded[`${start}:${end}`]; } + index++; }; - worker.onerror = (e) => { + worker.onerror = (e: ErrorEvent) => { worker.terminate(); - this._decodeThreadCount--; + this.currentDecodingThreads--; for (let i = index; i <= end; i++) { - if (i in this._promisedFrames) { - this._promisedFrames[i].reject(); - delete this._promisedFrames[i]; + // reject all the following frames + if (i in this.promisedFrames) { + const { reject } = this.promisedFrames[i]; + delete this.promisedFrames[i]; + reject(); } } - if (this._decodingBlocks[`${start}:${end}`].rejectCallback) { - this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e)); + if (this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback) { + this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback(e); } - delete this._decodingBlocks[`${start}:${end}`]; + + delete this.blocksAreBeingDecoded[`${start}:${end}`]; }; worker.postMessage({ @@ -284,92 +385,69 @@ export class FrameProvider { worker.postMessage({ buf: nal, offset: 0, length: nal.length }); }); } - this._decodeThreadCount++; } else { const worker = new ZipDecoder(); let index = start; - worker.onerror = (e) => { - for (let i = start; i <= end; i++) { - if (i in this._promisedFrames) { - this._promisedFrames[i].reject(); - delete this._promisedFrames[i]; - } - } - if (this._decodingBlocks[`${start}:${end}`].rejectCallback) { - this._decodingBlocks[`${start}:${end}`].rejectCallback(Error(e)); - } - this._decodeThreadCount--; - worker.terminate(); - }; - worker.onmessage = async (event) => { - if (this._dimension === DimensionType.DIM_2D && event.data.isRaw) { - // safary doesn't support createImageBitmap - // there is a way to polyfill it with using document.createElement - // but document.createElement doesn't work in worker - // so, we get raw data and decode it here, no other way - - const createImageBitmap = async function (blob) { - return new Promise((resolve) => { - const img = document.createElement('img'); - img.addEventListener('load', function loadListener() { - resolve(this); - }); - img.src = URL.createObjectURL(blob); - }); - }; - - // eslint-disable-next-line - event.data.data = await createImageBitmap(event.data.data); - } + this.frames[event.data.index] = event.data.data; - this._frames[event.data.index] = event.data.data; - - if (this._decodingBlocks[`${start}:${end}`].resolveCallback) { - this._decodingBlocks[`${start}:${end}`].resolveCallback(event.data.index); + const { resolveCallback } = this.blocksAreBeingDecoded[`${start}:${end}`]; + if (resolveCallback) { + resolveCallback(event.data.index); } - if (event.data.index in this._promisedFrames) { - this._promisedFrames[event.data.index].resolve(this._frames[event.data.index]); - delete this._promisedFrames[event.data.index]; + if (event.data.index in this.promisedFrames) { + const { resolve } = this.promisedFrames[event.data.index]; + delete this.promisedFrames[event.data.index]; + resolve(this.frames[event.data.index]); } if (index === end) { worker.terminate(); - delete this._decodingBlocks[`${start}:${end}`]; - this._decodeThreadCount--; + this.currentDecodingThreads--; + delete this.blocksAreBeingDecoded[`${start}:${end}`]; } index++; }; - const dimension = this._dimension; + + worker.onerror = (e: ErrorEvent) => { + for (let i = start; i <= end; i++) { + if (i in this.promisedFrames) { + const { reject } = this.promisedFrames[i]; + delete this.promisedFrames[i]; + reject(); + } + } + if (this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback) { + this.blocksAreBeingDecoded[`${start}:${end}`].rejectCallback(e); + } + this.currentDecodingThreads--; + worker.terminate(); + }; + worker.postMessage({ block, start, end, - dimension, - dimension2D: DimensionType.DIM_2D, + dimension: this.dimension, + dimension2D: DimensionType.DIMENSION_2D, }); - this._decodeThreadCount++; } } finally { release(); } } - get decodeThreadCount() { - return this._decodeThreadCount; - } - - get decodedBlocksCacheSize() { - return this._decodedBlocksCacheSize; + get decodedBlocksCacheSize(): number { + return this.cachedDecodedBlocksLimit; } /* Method returns a list of cached ranges Is an array of strings like "start:end" */ - get cachedFrames() { - return [...this._blocksRanges].sort((a, b) => a.split(':')[0] - b.split(':')[0]); + get cachedFrames(): string[] { + return [...this.blocksRanges].sort((a, b) => +a.split(':')[0] - +b.split(':')[0]); } } diff --git a/cvat-data/src/ts/unzip_imgs.worker.js b/cvat-data/src/ts/unzip_imgs.worker.js index 355106b8b239..a4ed18f78b33 100644 --- a/cvat-data/src/ts/unzip_imgs.worker.js +++ b/cvat-data/src/ts/unzip_imgs.worker.js @@ -1,7 +1,9 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT +// eslint-disable-next-line @typescript-eslint/no-var-requires const JSZip = require('jszip'); onmessage = (e) => { @@ -19,8 +21,7 @@ onmessage = (e) => { _zip.file(relativePath) .async('blob') .then((fileData) => { - // eslint-disable-next-line no-restricted-globals - if (dimension === dimension2D && self.createImageBitmap) { + if (dimension === dimension2D) { createImageBitmap(fileData).then((img) => { postMessage({ fileName: relativePath, @@ -33,12 +34,11 @@ onmessage = (e) => { fileName: relativePath, index: fileIndex, data: fileData, - isRaw: true, }); } }); } }); - }); + }).catch((error) => postMessage({ error })); } }; diff --git a/cvat-data/webpack.config.js b/cvat-data/webpack.config.js index 71f8663f72e3..c90d0f3d6f54 100644 --- a/cvat-data/webpack.config.js +++ b/cvat-data/webpack.config.js @@ -13,7 +13,7 @@ const cvatData = { target: 'web', mode: 'production', entry: { - 'cvat-data': './src/js/cvat-data.ts', + 'cvat-data': './src/ts/cvat-data.ts', }, output: { path: path.resolve(__dirname, 'dist'), @@ -66,7 +66,7 @@ const cvatData = { }, ], }, - plugins: [new CopyPlugin({ patterns: ['./src/js/3rdparty/avc.wasm'] })], + plugins: [new CopyPlugin({ patterns: ['./src/ts/3rdparty/avc.wasm'] })], }; module.exports = cvatData; diff --git a/cvat-ui/package.json b/cvat-ui/package.json index a681c0fc67bc..331319826574 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.46.1", + "version": "1.47.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { @@ -26,8 +26,8 @@ "@types/react": "^16.14.15", "@types/react-color": "^3.0.5", "@types/react-dom": "^16.9.14", + "@types/react-grid-layout": "^1.3.2", "@types/react-redux": "^7.1.18", - "@types/react-resizable": "^3.0.1", "@types/react-router": "^5.1.16", "@types/react-router-dom": "^5.1.9", "@types/react-share": "^3.0.3", @@ -50,10 +50,10 @@ "react-color": "^2.19.3", "react-cookie": "^4.0.3", "react-dom": "^16.14.0", + "react-grid-layout": "^1.3.4", "react-markdown": "^8.0.4", "react-moment": "^1.1.1", "react-redux": "^8.0.2", - "react-resizable": "^3.0.4", "react-router": "^5.1.0", "react-router-dom": "^5.1.0", "react-share": "^4.4.0", diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 5f9ff078ff15..4effce3319d2 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -125,7 +125,6 @@ export enum AnnotationActionTypes { SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS', SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS', SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED', - SAVE_UPDATE_ANNOTATIONS_STATUS = 'SAVE_UPDATE_ANNOTATIONS_STATUS', SWITCH_PLAY = 'SWITCH_PLAY', CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY', DRAG_CANVAS = 'DRAG_CANVAS', @@ -196,10 +195,6 @@ export enum AnnotationActionTypes { GET_PREDICTIONS = 'GET_PREDICTIONS', GET_PREDICTIONS_FAILED = 'GET_PREDICTIONS_FAILED', GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS', - HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE', - GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE', - GET_CONTEXT_IMAGE_SUCCESS = 'GET_CONTEXT_IMAGE_SUCCESS', - GET_CONTEXT_IMAGE_FAILED = 'GET_CONTEXT_IMAGE_FAILED', SWITCH_NAVIGATION_BLOCKED = 'SWITCH_NAVIGATION_BLOCKED', DELETE_FRAME = 'DELETE_FRAME', DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS', @@ -700,7 +695,7 @@ export function changeFrameAsync( number: currentState.annotation.player.frame.number, data: currentState.annotation.player.frame.data, filename: currentState.annotation.player.frame.filename, - hasRelatedContext: currentState.annotation.player.frame.hasRelatedContext, + relatedFiles: currentState.annotation.player.frame.relatedFiles, delay: currentState.annotation.player.frame.delay, changeTime: currentState.annotation.player.frame.changeTime, states: currentState.annotation.annotations.states, @@ -767,7 +762,7 @@ export function changeFrameAsync( number: toFrame, data, filename: data.filename, - hasRelatedContext: data.hasRelatedContext, + relatedFiles: data.relatedFiles, states, minZ, maxZ, @@ -1046,7 +1041,7 @@ export function getJobAsync( states, frameNumber, frameFilename: frameData.filename, - frameHasRelatedContext: frameData.hasRelatedContext, + relatedFiles: frameData.relatedFiles, frameData, colors, filters, @@ -1113,19 +1108,8 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi try { const saveJobEvent = await sessionInstance.logger.log(LogType.saveJob, {}, true); - dispatch({ - type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS, - payload: { status: 'Saving frames' }, - }); await sessionInstance.frames.save(); - await sessionInstance.annotations.save((status: string) => { - dispatch({ - type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS, - payload: { - status, - }, - }); - }); + await sessionInstance.annotations.save(); await saveJobEvent.close(); await sessionInstance.logger.log(LogType.sendTaskInfo, await jobInfoGenerator(sessionInstance)); dispatch(saveLogsAsync()); @@ -1667,40 +1651,6 @@ export function switchPredictor(predictorEnabled: boolean): AnyAction { }, }; } -export function hideShowContextImage(hidden: boolean): AnyAction { - return { - type: AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE, - payload: { - hidden, - }, - }; -} - -export function getContextImageAsync(): ThunkAction { - return async (dispatch: ActionCreator): Promise => { - const state: CombinedState = getStore().getState(); - const { instance: job } = state.annotation.job; - const { number: frameNumber } = state.annotation.player.frame; - - try { - dispatch({ - type: AnnotationActionTypes.GET_CONTEXT_IMAGE, - payload: {}, - }); - - const contextImageData = await job.frames.contextImage(frameNumber); - dispatch({ - type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS, - payload: { contextImageData }, - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED, - payload: { error }, - }); - } - }; -} export function switchNavigationBlocked(navigationBlocked: boolean): AnyAction { return { diff --git a/cvat-ui/src/actions/boundaries-actions.ts b/cvat-ui/src/actions/boundaries-actions.ts index faa1cac4f8a3..4ebec2168ac2 100644 --- a/cvat-ui/src/actions/boundaries-actions.ts +++ b/cvat-ui/src/actions/boundaries-actions.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -24,7 +24,7 @@ export const boundariesActions = { openTime: number; frameNumber: number; frameFilename: string; - frameHasRelatedContext: boolean; + relatedFiles: boolean; colors: string[]; filters: string[]; frameData: any; @@ -58,7 +58,7 @@ export function resetAfterErrorAsync(): ThunkAction { openTime: state.annotation.job.openTime || Date.now(), frameNumber, frameFilename: frameData.filename, - frameHasRelatedContext: frameData.hasRelatedContext, + relatedFiles: frameData.relatedFiles, colors, filters: [], frameData, diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 7ec4e42e414e..643b0859e2cf 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,7 +7,6 @@ $grid-unit-size: 8px; $header-height: $grid-unit-size * 6; -$layout-sm-grid-size: $grid-unit-size * 0.5; $layout-lg-grid-size: $grid-unit-size * 2; $layout-sm-grid-color: rgba(0, 0, 0, 0.15); $layout-lg-grid-color: rgba(0, 0, 0, 0.15); diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 7cb79b706070..e8ff662cd9d8 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -139,31 +140,13 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { - {workspace === Workspace.STANDARD3D && ( - - - - )} - {workspace === Workspace.STANDARD && ( - - - - )} - {workspace === Workspace.ATTRIBUTE_ANNOTATION && ( - - - - )} - {workspace === Workspace.TAG_ANNOTATION && ( - - - - )} - {workspace === Workspace.REVIEW_WORKSPACE && ( - - - - )} + + {workspace === Workspace.STANDARD3D && } + {workspace === Workspace.STANDARD && } + {workspace === Workspace.ATTRIBUTE_ANNOTATION && } + {workspace === Workspace.TAG_ANNOTATION && } + {workspace === Workspace.REVIEW_WORKSPACE && } + diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx index e6c4244b335c..2c35588de3ff 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -11,8 +11,6 @@ import Text from 'antd/lib/typography/Text'; import { filterApplicableLabels } from 'utils/filter-applicable-labels'; import { Label } from 'cvat-core-wrapper'; -import { Canvas } from 'cvat-canvas-wrapper'; -import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { LogType } from 'cvat-logger'; import { activateObject as activateObjectAction, @@ -24,7 +22,6 @@ import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { ThunkDispatch } from 'utils/redux'; import AppearanceBlock from 'components/annotation-page/appearance-block'; import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons'; -import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image'; import { CombinedState, ObjectType } from 'reducers'; import AttributeEditor from './attribute-editor'; import AttributeSwitcher from './attribute-switcher'; @@ -39,7 +36,6 @@ interface StateToProps { jobInstance: any; keyMap: KeyMap; normalizedKeyMap: Record; - canvasInstance: Canvas | Canvas3d; canvasIsReady: boolean; curZLayer: number; } @@ -64,7 +60,7 @@ function mapStateToProps(state: CombinedState): StateToProps { zLayer: { cur }, }, job: { instance: jobInstance, labels }, - canvas: { instance: canvasInstance, ready: canvasIsReady }, + canvas: { ready: canvasIsReady }, }, shortcuts: { keyMap, normalizedKeyMap }, } = state; @@ -77,7 +73,6 @@ function mapStateToProps(state: CombinedState): StateToProps { states, keyMap, normalizedKeyMap, - canvasInstance, canvasIsReady, curZLayer: cur, }; @@ -109,7 +104,6 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. activateObject, keyMap, normalizedKeyMap, - canvasInstance, canvasIsReady, curZLayer, } = props; @@ -129,8 +123,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. const listener = (event: TransitionEvent): void => { if (event.target && event.propertyName === 'width' && event.target === collapser) { - canvasInstance.fitCanvas(); - canvasInstance.fit(); + window.dispatchEvent(new Event('resize')); (collapser as HTMLElement).removeEventListener('transitionend', listener as any); } }; @@ -139,7 +132,6 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. (collapser as HTMLElement).addEventListener('transitionend', listener as any); } - adjustContextImagePosition(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed); }; diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx index 7a50b02d7665..6cfe835651c8 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,13 +7,13 @@ import './styles.scss'; import React from 'react'; import Layout from 'antd/lib/layout'; -import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; import AttributeAnnotationSidebar from './attribute-annotation-sidebar/attribute-annotation-sidebar'; export default function AttributeAnnotationWorkspace(): JSX.Element { return ( - + ); diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx b/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx deleted file mode 100644 index 783460e600c3..000000000000 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper3D.tsx +++ /dev/null @@ -1,586 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import React, { - ReactElement, SyntheticEvent, useEffect, useReducer, useRef, -} from 'react'; -import Layout from 'antd/lib/layout/layout'; -import { - ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined, -} from '@ant-design/icons'; -import { ResizableBox } from 'react-resizable'; -import { - ColorBy, ContextMenuType, ObjectType, Workspace, -} from 'reducers'; -import { - CameraAction, Canvas3d, ViewType, ViewsDOM, -} from 'cvat-canvas3d-wrapper'; -import { Canvas } from 'cvat-canvas-wrapper'; -import ContextImage from 'components/annotation-page/standard-workspace/context-image/context-image'; -import CVATTooltip from 'components/common/cvat-tooltip'; -import { LogType } from 'cvat-logger'; -import { getCore } from 'cvat-core-wrapper'; - -const cvat = getCore(); - -interface Props { - opacity: number; - selectedOpacity: number; - outlined: boolean; - outlineColor: string; - colorBy: ColorBy; - frameFetching: boolean; - canvasInstance: Canvas3d | Canvas; - jobInstance: any; - frameData: any; - annotations: any[]; - contextMenuVisibility: boolean; - activeLabelID: number; - activeObjectType: ObjectType; - activatedStateID: number | null; - onSetupCanvas: () => void; - onGroupObjects: (enabled: boolean) => void; - onResetCanvas(): void; - onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onActivateObject(activatedStateID: number | null): void; - onUpdateAnnotations(states: any[]): void; - onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; - onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onEditShape: (enabled: boolean) => void; - onDragCanvas: (enabled: boolean) => void; - onShapeDrawn: () => void; - workspace: Workspace; - frame: number; - resetZoom: boolean; -} - -interface ViewSize { - fullHeight: number; - fullWidth: number; - vertical: number; - top: number; - side: number; - front: number; -} - -function viewSizeReducer( - state: ViewSize, - action: { type: ViewType | 'set' | 'resize'; 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; - case 'resize': { - const canvasPerspectiveContainer = document.getElementById('cvat-canvas3d-perspective'); - let midState = { ...state }; - if (canvasPerspectiveContainer) { - if (state.fullHeight !== canvas3dContainer.clientHeight) { - const diff = canvas3dContainer.clientHeight - state.fullHeight; - midState = { - ...midState, - fullHeight: canvas3dContainer.clientHeight, - vertical: state.vertical + diff, - }; - } - if (state.fullWidth !== canvasPerspectiveContainer.clientWidth) { - const oldWidth = state.fullWidth; - const width = canvasPerspectiveContainer.clientWidth; - midState = { - ...midState, - fullWidth: width, - top: (state.top / oldWidth) * width, - side: (state.side / oldWidth) * width, - front: (state.front / oldWidth) * width, - }; - } - return midState; - } - return state; - } - default: - throw new Error(); - } - } - return state; -} - -const CanvasWrapperComponent = (props: Props): ReactElement => { - const animateId = useRef(0); - const [viewSize, setViewSize] = useReducer(viewSizeReducer, { - fullHeight: 0, - fullWidth: 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 { - opacity, - outlined, - outlineColor, - selectedOpacity, - colorBy, - contextMenuVisibility, - frameData, - onResetCanvas, - onSetupCanvas, - annotations, - frame, - jobInstance, - activeLabelID, - activatedStateID, - resetZoom, - activeObjectType, - onShapeDrawn, - onCreateAnnotations, - frameFetching, - } = props; - const { canvasInstance } = props as { canvasInstance: Canvas3d }; - - const onCanvasSetup = (): void => { - onSetupCanvas(); - }; - - const onCanvasDragStart = (): void => { - const { onDragCanvas } = props; - onDragCanvas(true); - }; - - const onCanvasDragDone = (): void => { - const { onDragCanvas } = props; - onDragCanvas(false); - }; - - const animateCanvas = (): void => { - canvasInstance.render(); - animateId.current = requestAnimationFrame(animateCanvas); - }; - - const updateCanvas = (): void => { - if (frameData !== null) { - canvasInstance.setup( - frameData, - annotations.filter((e) => e.objectType !== ObjectType.TAG), - ); - } - }; - - const onCanvasCancel = (): void => { - onResetCanvas(); - }; - - const onCanvasShapeDrawn = (event: any): void => { - if (!event.detail.continue) { - onShapeDrawn(); - } - - const { state, duration } = event.detail; - const isDrawnFromScratch = !state.label; - if (isDrawnFromScratch) { - jobInstance.logger.log(LogType.drawObject, { count: 1, duration }); - } else { - jobInstance.logger.log(LogType.pasteObject, { count: 1, duration }); - } - - state.objectType = state.objectType || activeObjectType; - state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0]; - state.occluded = state.occluded || false; - state.frame = frame; - state.zOrder = 0; - const objectState = new cvat.classes.ObjectState(state); - onCreateAnnotations(jobInstance, frame, [objectState]); - }; - - const onCanvasClick = (e: MouseEvent): void => { - const { onUpdateContextMenu } = props; - if (contextMenuVisibility) { - onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE); - } - }; - - const initialSetup = (): void => { - const canvasInstanceDOM = canvasInstance.html() as ViewsDOM; - canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup); - canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel); - canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart); - canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone); - canvasInstance.configure({ resetZoom }); - }; - - const keyControlsKeyDown = (key: KeyboardEvent): void => { - canvasInstance.keyControls(key); - }; - - const keyControlsKeyUp = (key: KeyboardEvent): void => { - if (key.code === 'ControlLeft') { - canvasInstance.keyControls(key); - } - }; - - const onCanvasShapeSelected = (event: any): void => { - const { onActivateObject } = props; - const { clientID } = event.detail; - onActivateObject(clientID); - canvasInstance.activate(clientID); - }; - - const onCanvasEditDone = (event: any): void => { - const { onEditShape, onUpdateAnnotations } = props; - onEditShape(false); - const { state, points } = event.detail; - state.points = points; - onUpdateAnnotations([state]); - }; - - useEffect(() => { - const canvasInstanceDOM = canvasInstance.html(); - if ( - perspectiveView && - perspectiveView.current && - topView && - topView.current && - sideView && - sideView.current && - frontView && - frontView.current - ) { - perspectiveView.current.appendChild(canvasInstanceDOM.perspective); - 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, - fullWidth: canvas3dContainer.clientWidth, - vertical: canvas3dContainer.clientHeight / 2, - top: width, - side: width, - front: width, - }, - }); - } - } - - document.addEventListener('keydown', keyControlsKeyDown); - document.addEventListener('keyup', keyControlsKeyUp); - - initialSetup(); - updateCanvas(); - animateCanvas(); - - return () => { - canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup); - canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel); - canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart); - canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone); - document.removeEventListener('keydown', keyControlsKeyDown); - document.removeEventListener('keyup', keyControlsKeyUp); - cancelAnimationFrame(animateId.current); - }; - }, []); - - useEffect(() => { - canvasInstance.activate(activatedStateID); - }, [activatedStateID]); - - useEffect(() => { - canvasInstance.configure({ resetZoom }); - }, [resetZoom]); - - const updateShapesView = (): void => { - (canvasInstance as Canvas3d).configureShapes({ - opacity, - outlined, - outlineColor, - selectedOpacity, - colorBy, - }); - }; - - const onContextMenu = (event: any): void => { - const { onUpdateContextMenu, onActivateObject } = props; - onActivateObject(event.detail.clientID); - onUpdateContextMenu( - event.detail.clientID !== null, - event.detail.clientX, - event.detail.clientY, - ContextMenuType.CANVAS_SHAPE, - ); - }; - - const onResize = (): void => { - setViewSize({ - type: 'resize', - }); - }; - - const onCanvasObjectsGroupped = (event: any): void => { - const { onGroupAnnotations, onGroupObjects } = props; - - onGroupObjects(false); - - const { states } = event.detail; - onGroupAnnotations(jobInstance, frame, states); - }; - - useEffect(() => { - updateShapesView(); - }, [opacity, outlined, outlineColor, selectedOpacity, colorBy]); - - useEffect(() => { - const canvasInstanceDOM = canvasInstance.html() as ViewsDOM; - updateCanvas(); - canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn); - canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected); - canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone); - canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu); - canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick); - canvasInstanceDOM.perspective.addEventListener('canvas.fit', onResize); - canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped); - window.addEventListener('resize', onResize); - - return () => { - canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn); - canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected); - canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone); - canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu); - canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick); - canvasInstanceDOM.perspective.removeEventListener('canvas.fit', onResize); - canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped); - window.removeEventListener('resize', onResize); - }; - }, [frameData, annotations, activeLabelID, contextMenuVisibility]); - - const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => { - canvasInstance.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey })); - }; - - const ArrowGroup = (): ReactElement => ( - - - - -
- - - - - - - - - -
- ); - - const ControlGroup = (): ReactElement => ( - - - - - - - - - - -
- - - - - - - - - -
- ); - - return ( - - - } - onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.PERSPECTIVE, e })} - > - <> - {frameFetching ? ( - - - - ) : null} -
-
- - -
- - -
- } - onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.TOP, e })} - > -
-
TOP
-
-
- - } - onResize={(e: SyntheticEvent) => setViewSize({ type: ViewType.SIDE, e })} - > -
-
SIDE
-
-
- -
-
FRONT
-
-
-
- - ); -}; - -export default React.memo(CanvasWrapperComponent); diff --git a/cvat-ui/src/components/annotation-page/canvas/grid-layout/canvas-layout.conf.tsx b/cvat-ui/src/components/annotation-page/canvas/grid-layout/canvas-layout.conf.tsx new file mode 100644 index 000000000000..f5059b22f7b9 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/grid-layout/canvas-layout.conf.tsx @@ -0,0 +1,139 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +export interface ItemLayout { + viewType: ViewType; + offset: number[]; + x: number; + y: number; + w: number; + h: number; + viewIndex?: string; +} + +export enum ViewType { + CANVAS = 'canvas', + CANVAS_3D = 'canvas3D', + CANVAS_3D_TOP = 'canvas3DTop', + CANVAS_3D_SIDE = 'canvas3DSide', + CANVAS_3D_FRONT = 'canvas3DFront', + RELATED_IMAGE = 'relatedImage', +} + +const defaultLayout: { + '2D': { + [index: string]: ItemLayout[]; + }; + '3D': { + [index: string]: ItemLayout[]; + }; +} = { '2D': {}, '3D': {} }; + +defaultLayout['2D']['0'] = [{ + viewType: ViewType.CANVAS, + offset: [0], + x: 0, + y: 0, + w: 12, + h: 12, +}]; + +defaultLayout['2D']['1'] = [ + { ...defaultLayout['2D']['0'][0], w: 9 }, { + viewType: ViewType.RELATED_IMAGE, + offset: [0, 0], + x: 9, + y: 0, + w: 3, + h: 4, + viewIndex: '0', + }, +]; + +defaultLayout['2D']['2'] = [ + ...defaultLayout['2D']['1'], { + ...defaultLayout['2D']['1'][1], + viewType: ViewType.RELATED_IMAGE, + viewIndex: '1', + offset: [0, 1], + y: 4, + }, +]; + +defaultLayout['2D']['3'] = [ + ...defaultLayout['2D']['2'], { + ...defaultLayout['2D']['2'][2], + viewIndex: '2', + offset: [0, 2], + y: 8, + }, +]; + +defaultLayout['3D']['0'] = [{ + viewType: ViewType.CANVAS_3D, + offset: [0], + x: 0, + y: 0, + w: 12, + h: 9, +}, { + viewType: ViewType.CANVAS_3D_TOP, + offset: [0], + x: 0, + y: 9, + w: 4, + h: 3, +}, { + viewType: ViewType.CANVAS_3D_SIDE, + offset: [0], + x: 4, + y: 9, + w: 4, + h: 3, +}, { + viewType: ViewType.CANVAS_3D_FRONT, + offset: [0], + x: 8, + y: 9, + w: 4, + h: 3, +}]; + +defaultLayout['3D']['1'] = [ + { ...defaultLayout['3D']['0'][0], w: 9 }, + { ...defaultLayout['3D']['0'][1], w: 3 }, + { ...defaultLayout['3D']['0'][2], x: 3, w: 3 }, + { ...defaultLayout['3D']['0'][3], x: 6, w: 3 }, + { + viewType: ViewType.RELATED_IMAGE, + offset: [0, 0], + x: 9, + y: 0, + w: 3, + h: 4, + viewIndex: '0', + }, +]; + +defaultLayout['3D']['2'] = [ + ...defaultLayout['3D']['1'], + { + ...defaultLayout['3D']['1'][4], + viewIndex: '1', + offset: [0, 1], + y: 4, + }, +]; + +defaultLayout['3D']['3'] = [ + ...defaultLayout['3D']['2'], + { + ...defaultLayout['3D']['2'][5], + viewIndex: '2', + offset: [0, 2], + y: 8, + }, +]; + +export default defaultLayout; diff --git a/cvat-ui/src/components/annotation-page/canvas/grid-layout/canvas-layout.tsx b/cvat-ui/src/components/annotation-page/canvas/grid-layout/canvas-layout.tsx new file mode 100644 index 000000000000..17c97d3c9e7c --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/grid-layout/canvas-layout.tsx @@ -0,0 +1,365 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import 'react-grid-layout/css/styles.css'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import RGL, { WidthProvider } from 'react-grid-layout'; +import PropTypes from 'prop-types'; +import { isEqual } from 'lodash'; +import Layout from 'antd/lib/layout'; +import { + CloseOutlined, + DragOutlined, + FullscreenExitOutlined, + FullscreenOutlined, + PicCenterOutlined, + PlusOutlined, + ReloadOutlined, +} from '@ant-design/icons'; + +import consts from 'consts'; +import { DimensionType, CombinedState } from 'reducers'; +import CanvasWrapperComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-wrapper'; +import CanvasWrapper3DComponent, { + PerspectiveViewComponent, + TopViewComponent, + SideViewComponent, + FrontViewComponent, +} from 'components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D'; +import ContextImage from 'components/annotation-page/canvas/views/context-image/context-image'; +import CVATTooltip from 'components/common/cvat-tooltip'; +import defaultLayout, { ItemLayout, ViewType } from './canvas-layout.conf'; + +const ReactGridLayout = WidthProvider(RGL); + +const ViewFabric = (itemLayout: ItemLayout): JSX.Element => { + const { viewType: type, offset } = itemLayout; + + let component = null; + switch (type) { + case ViewType.CANVAS: + component = ; + break; + case ViewType.CANVAS_3D: + component = ; + break; + case ViewType.RELATED_IMAGE: + component = ; + break; + case ViewType.CANVAS_3D_FRONT: + component = ; + break; + case ViewType.CANVAS_3D_SIDE: + component = ; + break; + case ViewType.CANVAS_3D_TOP: + component = ; + break; + default: + component =
Undefined view
; + } + + return component; +}; + +const fitLayout = (type: DimensionType, layoutConfig: ItemLayout[]): ItemLayout[] => { + const updatedLayout: ItemLayout[] = []; + + const relatedViews = layoutConfig + .filter((item: ItemLayout) => item.viewType === ViewType.RELATED_IMAGE); + const relatedViewsCols = relatedViews.length > 6 ? 2 : 1; + const height = Math.floor(consts.CANVAS_WORKSPACE_ROWS / (relatedViews.length / relatedViewsCols)); + relatedViews.forEach((view: ItemLayout, i: number) => { + updatedLayout.push({ + ...view, + h: height, + w: relatedViews.length > 6 ? 2 : 3, + x: relatedViewsCols === 1 ? 9 : 8 + (i % 2) * 2, + y: height * i, + }); + }); + + let widthAvail = consts.CANVAS_WORKSPACE_COLS; + if (updatedLayout.length > 0) { + widthAvail -= updatedLayout[0].w * relatedViewsCols; + } + + if (type === DimensionType.DIM_2D) { + const canvas = layoutConfig + .find((item: ItemLayout) => item.viewType === ViewType.CANVAS) as ItemLayout; + updatedLayout.push({ + ...canvas, + x: 0, + y: 0, + w: widthAvail, + h: consts.CANVAS_WORKSPACE_ROWS, + }); + } else { + const canvas = layoutConfig + .find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D) as ItemLayout; + const top = layoutConfig + .find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_TOP) as ItemLayout; + const side = layoutConfig + .find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_SIDE) as ItemLayout; + const front = layoutConfig + .find((item: ItemLayout) => item.viewType === ViewType.CANVAS_3D_FRONT) as ItemLayout; + const helpfulCanvasViewHeight = 3; + updatedLayout.push({ + ...canvas, + x: 0, + y: 0, + w: widthAvail, + h: consts.CANVAS_WORKSPACE_ROWS - helpfulCanvasViewHeight, + }, { + ...top, + x: 0, + y: consts.CANVAS_WORKSPACE_ROWS, + w: Math.ceil(widthAvail / 3), + h: helpfulCanvasViewHeight, + }, + { + ...side, + x: Math.ceil(widthAvail / 3), + y: consts.CANVAS_WORKSPACE_ROWS, + w: Math.ceil(widthAvail / 3), + h: helpfulCanvasViewHeight, + }, + { + ...front, + x: Math.ceil(widthAvail / 3) * 2, + y: consts.CANVAS_WORKSPACE_ROWS, + w: Math.floor(widthAvail / 3), + h: helpfulCanvasViewHeight, + }); + } + + return updatedLayout; +}; + +function CanvasLayout({ type }: { type?: DimensionType }): JSX.Element { + const relatedFiles = useSelector((state: CombinedState) => state.annotation.player.frame.relatedFiles); + const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance); + const canvasBackgroundColor = useSelector((state: CombinedState) => state.settings.player.canvasBackgroundColor); + + const computeRowHeight = (): number => { + const container = window.document.getElementsByClassName('cvat-annotation-header')[0]; + let containerHeight = window.innerHeight; + if (container) { + containerHeight = window.innerHeight - container.getBoundingClientRect().bottom; + // https://github.com/react-grid-layout/react-grid-layout/issues/628#issuecomment-1228453084 + return Math.floor( + (containerHeight - consts.CANVAS_WORKSPACE_MARGIN * (consts.CANVAS_WORKSPACE_ROWS)) / + consts.CANVAS_WORKSPACE_ROWS, + ); + } + + return 0; + }; + + const getLayout = useCallback(() => ( + defaultLayout[(type as DimensionType).toUpperCase() as '2D' | '3D'][Math.min(relatedFiles, 3)] + ), [type, relatedFiles]); + + const [layoutConfig, setLayoutConfig] = useState(getLayout()); + const [rowHeight, setRowHeight] = useState(Math.floor(computeRowHeight())); + const [fullscreenKey, setFullscreenKey] = useState(''); + + const fitCanvas = useCallback(() => { + if (canvasInstance) { + canvasInstance.fitCanvas(); + canvasInstance.fit(); + } + }, [canvasInstance]); + + useEffect(() => { + const onResize = (): void => { + setRowHeight(computeRowHeight()); + fitCanvas(); + const [el] = window.document.getElementsByClassName('cvat-canvas-grid-root'); + if (el) { + el.addEventListener('transitionend', () => { + fitCanvas(); + }, { once: true }); + } + }; + + window.addEventListener('resize', onResize); + return () => { + window.removeEventListener('resize', onResize); + }; + }, [fitCanvas]); + + useEffect(() => { + setRowHeight(computeRowHeight()); + }, []); + + useEffect(() => { + window.dispatchEvent(new Event('resize')); + }, [layoutConfig]); + + const children = layoutConfig.map((value: ItemLayout) => ViewFabric(value)); + const layout = layoutConfig.map((value: ItemLayout) => ({ + x: value.x, + y: value.y, + w: value.w, + h: value.h, + i: typeof (value.viewIndex) !== 'undefined' ? `${value.viewType}_${value.viewIndex}` : `${value.viewType}`, + })); + + return ( + + { !!rowHeight && ( + { + const transformedLayout = layoutConfig.map((itemLayout: ItemLayout, i: number): ItemLayout => ({ + ...itemLayout, + x: updatedLayout[i].x, + y: updatedLayout[i].y, + w: updatedLayout[i].w, + h: updatedLayout[i].h, + })); + + if (!isEqual(layoutConfig, transformedLayout)) { + setLayoutConfig(transformedLayout); + } + }} + resizeHandle={(_: any, ref: React.MutableRefObject) => ( +
+ )} + draggableHandle='.cvat-grid-item-drag-handler' + > + { children.map((child: JSX.Element, idx: number): JSX.Element => { + const { viewType, viewIndex } = layoutConfig[idx]; + const key = typeof viewIndex !== 'undefined' ? `${viewType}_${viewIndex}` : `${viewType}`; + return ( +
+ + { + if (viewType === ViewType.RELATED_IMAGE) { + setLayoutConfig( + layoutConfig + .filter((item: ItemLayout) => !( + item.viewType === viewType && item.viewIndex === viewIndex + )), + ); + } + }} + /> + {fullscreenKey === key ? ( + { + window.dispatchEvent(new Event('resize')); + setFullscreenKey(''); + }} + /> + ) : ( + { + window.dispatchEvent(new Event('resize')); + setFullscreenKey(key); + }} + /> + )} + + { child } +
+ ); + }) } + + )} + { type === DimensionType.DIM_3D && } +
+ + { + setLayoutConfig(fitLayout(type as DimensionType, layoutConfig)); + window.dispatchEvent(new Event('resize')); + }} + /> + + + { + const MAXIMUM_RELATED = 12; + const existingRelated = layoutConfig + .filter((configItem: ItemLayout) => configItem.viewType === ViewType.RELATED_IMAGE); + + if (existingRelated.length >= MAXIMUM_RELATED) { + return; + } + + if (existingRelated.length === 0) { + setLayoutConfig(defaultLayout[type?.toUpperCase() as '2D' | '3D']['1']); + return; + } + + const viewIndexes = existingRelated + .map((item: ItemLayout) => +(item.viewIndex as string)).sort(); + const max = Math.max(...viewIndexes); + let viewIndex = max + 1; + for (let i = 0; i < max + 1; i++) { + if (!viewIndexes.includes(i)) { + viewIndex = i; + break; + } + } + + const latest = existingRelated[existingRelated.length - 1]; + const copy = { ...latest, offset: [0, viewIndex], viewIndex: `${viewIndex}` }; + setLayoutConfig(fitLayout(type as DimensionType, [...layoutConfig, copy])); + window.dispatchEvent(new Event('resize')); + }} + /> + + + { + setLayoutConfig([...getLayout()]); + window.dispatchEvent(new Event('resize')); + }} + /> + +
+ + ); +} + +CanvasLayout.defaultProps = { + type: DimensionType.DIM_2D, +}; + +CanvasLayout.PropType = { + type: PropTypes.oneOf(Object.values(DimensionType)), +}; + +export default React.memo(CanvasLayout); diff --git a/cvat-ui/src/components/annotation-page/canvas/grid-layout/styles.scss b/cvat-ui/src/components/annotation-page/canvas/grid-layout/styles.scss new file mode 100644 index 000000000000..97e444ba16c8 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/grid-layout/styles.scss @@ -0,0 +1,102 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import 'base.scss'; + +.cvat-canvas-grid-root { + position: relative; +} + +.cvat-grid-layout-common-setups { + position: absolute; + top: 0; + right: 50%; + transform: translate(0, calc($grid-unit-size * 12 - 1px)); + z-index: 1000; + background: $background-color-2; + line-height: $grid-unit-size * 3; + height: calc($grid-unit-size * 3 + 1px); + padding-bottom: $grid-unit-size; + padding-right: $grid-unit-size; + padding-left: $grid-unit-size; + border-radius: 0 0 4px 4px; + border-bottom: 1px solid $border-color-1; + border-right: 1px solid $border-color-1; + border-left: 1px solid $border-color-1; + + > span { + margin-right: $grid-unit-size * 2; + + &:last-child { + margin-right: 0; + } + } +} + +.cvat-canvas-grid-item { + background-color: rgba(241, 241, 241, 0.7); + border-radius: 4px; + + &.react-grid-item.cssTransforms { + transition-property: all; + } + + &.cvat-canvas-grid-fullscreen-item { + width: 100% !important; + height: 100% !important; + padding-right: $grid-unit-size; + transform: translate(4px, 4px) !important; + z-index: 1; + + .cvat-grid-item-resize-handler.react-resizable-handle, + .cvat-grid-item-drag-handler { + visibility: hidden; + } + } + + .cvat-grid-item-drag-handler, + .cvat-grid-item-fullscreen-handler, + .cvat-grid-item-close-button { + position: absolute; + top: $grid-unit-size; + z-index: 1000; + font-size: 16px; + background: $header-color; + border-radius: 2px; + opacity: 0.6; + transition: all 200ms; + + &:hover { + opacity: 0.9; + } + + &.cvat-grid-item-drag-handler { + left: $grid-unit-size * 4; + cursor: move; + } + + &.cvat-grid-item-fullscreen-handler { + left: $grid-unit-size; + } + + &.cvat-grid-item-close-button { + right: $grid-unit-size; + } + } + + .cvat-grid-item-resize-handler.react-resizable-handle { + bottom: -3px; + right: -3px; + cursor: se-resize; + + &::after { + bottom: 0; + right: 0; + width: 9px; + height: 10px; + border-right: 2px solid rgba(0, 0, 0, 1); + border-bottom: 2px solid rgba(0, 0, 0, 1); + } + } +} diff --git a/cvat-ui/src/components/annotation-page/canvas/brush-toolbox-styles.scss b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-toolbox-styles.scss similarity index 93% rename from cvat-ui/src/components/annotation-page/canvas/brush-toolbox-styles.scss rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-toolbox-styles.scss index 31060b9afd2e..488c561390f3 100644 --- a/cvat-ui/src/components/annotation-page/canvas/brush-toolbox-styles.scss +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-toolbox-styles.scss @@ -1,8 +1,8 @@ -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -@import '../../../base.scss'; +@import 'base.scss'; .cvat-brush-tools-toolbox { position: absolute; diff --git a/cvat-ui/src/components/annotation-page/canvas/brush-tools.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx similarity index 100% rename from cvat-ui/src/components/annotation-page/canvas/brush-tools.tsx rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-context-menu.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx similarity index 100% rename from cvat-ui/src/components/annotation-page/canvas/canvas-context-menu.tsx rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-context-menu.tsx diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-point-context-menu.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu.tsx similarity index 100% rename from cvat-ui/src/components/annotation-page/canvas/canvas-point-context-menu.tsx rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu.tsx diff --git a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx similarity index 77% rename from cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx index 58eda99dbfc9..83acf0258da2 100644 --- a/cvat-ui/src/components/annotation-page/canvas/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx @@ -1,17 +1,18 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React from 'react'; -import Layout from 'antd/lib/layout'; +import { connect } from 'react-redux'; import Slider from 'antd/lib/slider'; +import Spin from 'antd/lib/spin'; import Dropdown from 'antd/lib/dropdown'; import { PlusCircleOutlined, UpOutlined } from '@ant-design/icons'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { - ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType, + ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType, ActiveControl, CombinedState, } from 'reducers'; import { LogType } from 'cvat-logger'; import { Canvas } from 'cvat-canvas-wrapper'; @@ -20,16 +21,46 @@ import { getCore } from 'cvat-core-wrapper'; import consts from 'consts'; import CVATTooltip from 'components/common/cvat-tooltip'; import FrameTags from 'components/annotation-page/tag-annotation-workspace/frame-tags'; +import { + confirmCanvasReady, + dragCanvas, + zoomCanvas, + resetCanvas, + shapeDrawn, + mergeObjects, + groupObjects, + splitTrack, + editShape, + updateAnnotationsAsync, + createAnnotationsAsync, + mergeAnnotationsAsync, + groupAnnotationsAsync, + splitAnnotationsAsync, + activateObject, + updateCanvasContextMenu, + addZLayer, + switchZLayer, + fetchAnnotationsAsync, + getDataFailed, +} from 'actions/annotation-actions'; +import { + switchGrid, + changeGridColor, + changeGridOpacity, + changeBrightnessLevel, + changeContrastLevel, + changeSaturationLevel, + switchAutomaticBordering, +} from 'actions/settings-actions'; +import { reviewActions } from 'actions/review-actions'; + import ImageSetupsContent from './image-setups-content'; import BrushTools from './brush-tools'; -import ContextImage from '../standard-workspace/context-image/context-image'; const cvat = getCore(); - const MAX_DISTANCE_TO_OPEN_SHAPE = 50; -interface Props { - sidebarCollapsed: boolean; +interface StateToProps { canvasInstance: Canvas | Canvas3d | null; jobInstance: any; activatedStateID: number | null; @@ -38,7 +69,7 @@ interface Props { annotations: any[]; frameData: any; frameAngle: number; - frameFetching: boolean; + canvasIsReady: boolean; frame: number; opacity: number; colorBy: ColorBy; @@ -53,9 +84,6 @@ interface Props { gridOpacity: number; activeLabelID: number; activeObjectType: ObjectType; - curZLayer: number; - minZLayer: number; - maxZLayer: number; brightnessLevel: number; contrastLevel: number; saturationLevel: number; @@ -69,27 +97,32 @@ interface Props { textContent: string; showAllInterpolationTracks: boolean; workspace: Workspace; + minZLayer: number; + maxZLayer: number; + curZLayer: number; automaticBordering: boolean; intelligentPolygonCrop: boolean; - keyMap: KeyMap; - canvasBackgroundColor: string; switchableAutomaticBordering: boolean; + keyMap: KeyMap; showTagsOnFrame: boolean; - onSetupCanvas: () => void; +} + +interface DispatchToProps { + onSetupCanvas(): void; onDragCanvas: (enabled: boolean) => void; onZoomCanvas: (enabled: boolean) => void; + onResetCanvas: () => void; + onShapeDrawn: () => void; onMergeObjects: (enabled: boolean) => void; onGroupObjects: (enabled: boolean) => void; onSplitTrack: (enabled: boolean) => void; onEditShape: (enabled: boolean) => void; - onShapeDrawn: () => void; - onResetCanvas: () => void; onUpdateAnnotations(states: any[]): void; onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; - onActivateObject(activatedStateID: number | null, activatedElementID?: number | null): void; + onActivateObject: (activatedStateID: number | null, activatedElementID: number | null) => void; onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; onAddZLayer(): void; onSwitchZLayer(cur: number): void; @@ -105,7 +138,210 @@ interface Props { onStartIssue(position: number[]): void; } -export default class CanvasWrapperComponent extends React.PureComponent { +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + canvas: { activeControl, instance: canvasInstance, ready: canvasIsReady }, + drawing: { activeLabelID, activeObjectType }, + job: { instance: jobInstance }, + player: { + frame: { data: frameData, number: frame }, + frameAngles, + }, + annotations: { + states: annotations, + activatedStateID, + activatedElementID, + activatedAttributeID, + zLayer: { cur: curZLayer, min: minZLayer, max: maxZLayer }, + }, + workspace, + }, + settings: { + player: { + grid, + gridSize, + gridColor, + gridOpacity, + brightnessLevel, + contrastLevel, + saturationLevel, + resetZoom, + smoothImage, + }, + workspace: { + aamZoomMargin, + showObjectsTextAlways, + showAllInterpolationTracks, + showTagsOnFrame, + automaticBordering, + intelligentPolygonCrop, + textFontSize, + controlPointsSize, + textPosition, + textContent, + }, + shapes: { + opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections, + }, + }, + shortcuts: { keyMap }, + } = state; + + return { + canvasInstance, + jobInstance, + frameData, + frameAngle: frameAngles[frame - jobInstance.startFrame], + canvasIsReady, + frame, + activatedStateID, + activatedElementID, + activatedAttributeID, + annotations, + opacity: opacity / 100, + colorBy, + selectedOpacity: selectedOpacity / 100, + outlined, + outlineColor, + showBitmap, + showProjections, + grid, + gridSize, + gridColor, + gridOpacity: gridOpacity / 100, + activeLabelID, + activeObjectType, + brightnessLevel: brightnessLevel / 100, + contrastLevel: contrastLevel / 100, + saturationLevel: saturationLevel / 100, + resetZoom, + smoothImage, + aamZoomMargin, + showObjectsTextAlways, + textFontSize, + controlPointsSize, + textPosition, + textContent, + showAllInterpolationTracks, + showTagsOnFrame, + curZLayer, + minZLayer, + maxZLayer, + automaticBordering, + intelligentPolygonCrop, + workspace, + keyMap, + switchableAutomaticBordering: + activeControl === ActiveControl.DRAW_POLYGON || + activeControl === ActiveControl.DRAW_POLYLINE || + activeControl === ActiveControl.DRAW_MASK || + activeControl === ActiveControl.EDIT, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onSetupCanvas(): void { + dispatch(confirmCanvasReady()); + }, + onDragCanvas(enabled: boolean): void { + dispatch(dragCanvas(enabled)); + }, + onZoomCanvas(enabled: boolean): void { + dispatch(zoomCanvas(enabled)); + }, + onResetCanvas(): void { + dispatch(resetCanvas()); + }, + onShapeDrawn(): void { + dispatch(shapeDrawn()); + }, + onMergeObjects(enabled: boolean): void { + dispatch(mergeObjects(enabled)); + }, + onGroupObjects(enabled: boolean): void { + dispatch(groupObjects(enabled)); + }, + onSplitTrack(enabled: boolean): void { + dispatch(splitTrack(enabled)); + }, + onEditShape(enabled: boolean): void { + dispatch(editShape(enabled)); + }, + onUpdateAnnotations(states: any[]): void { + dispatch(updateAnnotationsAsync(states)); + }, + onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { + dispatch(createAnnotationsAsync(sessionInstance, frame, states)); + }, + onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void { + dispatch(mergeAnnotationsAsync(sessionInstance, frame, states)); + }, + onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void { + dispatch(groupAnnotationsAsync(sessionInstance, frame, states)); + }, + onSplitAnnotations(sessionInstance: any, frame: number, state: any): void { + dispatch(splitAnnotationsAsync(sessionInstance, frame, state)); + }, + onActivateObject(activatedStateID: number | null, activatedElementID: number | null = null): void { + if (activatedStateID === null) { + dispatch(updateCanvasContextMenu(false, 0, 0)); + } + + dispatch(activateObject(activatedStateID, activatedElementID, null)); + }, + onUpdateContextMenu( + visible: boolean, + left: number, + top: number, + type: ContextMenuType, + pointID?: number, + ): void { + dispatch(updateCanvasContextMenu(visible, left, top, pointID, type)); + }, + onAddZLayer(): void { + dispatch(addZLayer()); + }, + onSwitchZLayer(cur: number): void { + dispatch(switchZLayer(cur)); + }, + onChangeBrightnessLevel(level: number): void { + dispatch(changeBrightnessLevel(level)); + }, + onChangeContrastLevel(level: number): void { + dispatch(changeContrastLevel(level)); + }, + onChangeSaturationLevel(level: number): void { + dispatch(changeSaturationLevel(level)); + }, + onChangeGridOpacity(opacity: number): void { + dispatch(changeGridOpacity(opacity)); + }, + onChangeGridColor(color: GridColor): void { + dispatch(changeGridColor(color)); + }, + onSwitchGrid(enabled: boolean): void { + dispatch(switchGrid(enabled)); + }, + onSwitchAutomaticBordering(enabled: boolean): void { + dispatch(switchAutomaticBordering(enabled)); + }, + onFetchAnnotation(): void { + dispatch(fetchAnnotationsAsync()); + }, + onGetDataFailed(error: any): void { + dispatch(getDataFailed(error)); + }, + onStartIssue(position: number[]): void { + dispatch(reviewActions.startIssue(position)); + }, + }; +} + +type Props = StateToProps & DispatchToProps; + +class CanvasWrapperComponent extends React.PureComponent { public componentDidMount(): void { const { automaticBordering, @@ -163,7 +399,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { frameData, frameAngle, annotations, - sidebarCollapsed, activatedStateID, curZLayer, resetZoom, @@ -176,7 +411,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { contrastLevel, saturationLevel, workspace, - frameFetching, showObjectsTextAlways, textFontSize, controlPointsSize, @@ -186,7 +420,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { automaticBordering, intelligentPolygonCrop, showProjections, - canvasBackgroundColor, colorBy, onFetchAnnotation, } = this.props; @@ -230,19 +463,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { onFetchAnnotation(); } - if (prevProps.sidebarCollapsed !== sidebarCollapsed) { - const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar'); - if (sidebar) { - sidebar.addEventListener( - 'transitionend', - () => { - canvasInstance.fitCanvas(); - }, - { once: true }, - ); - } - } - if (prevProps.activatedStateID !== null && prevProps.activatedStateID !== activatedStateID) { canvasInstance.activate(null); } @@ -312,26 +532,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { } } - if (frameFetching !== prevProps.frameFetching) { - const loadingAnimation = window.document.getElementById('cvat_canvas_loading_animation'); - if (loadingAnimation) { - if (frameFetching) { - loadingAnimation.classList.remove('cvat_canvas_hidden'); - } else { - loadingAnimation.classList.add('cvat_canvas_hidden'); - } - } - } - - if (prevProps.canvasBackgroundColor !== canvasBackgroundColor) { - const canvasWrapperElement = window.document - .getElementsByClassName('cvat-canvas-container') - .item(0) as HTMLElement | null; - if (canvasWrapperElement) { - canvasWrapperElement.style.backgroundColor = canvasBackgroundColor; - } - } - this.activateOnCanvas(); } @@ -365,8 +565,6 @@ export default class CanvasWrapperComponent extends React.PureComponent { canvasInstance.html().removeEventListener('canvas.splitted', this.onCanvasTrackSplitted); canvasInstance.html().removeEventListener('canvas.error', this.onCanvasErrorOccurrence); - - window.removeEventListener('resize', this.fitCanvas); } private onCanvasErrorOccurrence = (event: any): void => { @@ -458,19 +656,12 @@ export default class CanvasWrapperComponent extends React.PureComponent { onSplitAnnotations(jobInstance, frame, state); }; - private fitCanvas = (): void => { - const { canvasInstance } = this.props; - if (canvasInstance) { - canvasInstance.fitCanvas(); - } - }; - private onCanvasMouseDown = (e: MouseEvent): void => { const { workspace, activatedStateID, onActivateObject } = this.props; if ((e.target as HTMLElement).tagName === 'svg' && e.button !== 2) { if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) { - onActivateObject(null); + onActivateObject(null, null); } } }; @@ -526,7 +717,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { // and triggers this event // in this case we do not need to update our state if (state.clientID === activatedStateID) { - onActivateObject(null); + onActivateObject(null, null); } }; @@ -557,7 +748,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { private onCanvasEditStart = (): void => { const { onActivateObject, onEditShape } = this.props; - onActivateObject(null); + onActivateObject(null, null); onEditShape(true); }; @@ -683,14 +874,9 @@ export default class CanvasWrapperComponent extends React.PureComponent { brightnessLevel, contrastLevel, saturationLevel, - canvasBackgroundColor, } = this.props; const { canvasInstance } = this.props as { canvasInstance: Canvas }; - // Size - window.addEventListener('resize', this.fitCanvas); - this.fitCanvas(); - // Grid const gridElement = window.document.getElementById('cvat_canvas_grid'); const gridPattern = window.document.getElementById('cvat_canvas_grid_pattern'); @@ -707,18 +893,13 @@ export default class CanvasWrapperComponent extends React.PureComponent { CSSImageFilter: `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`, }); - const canvasWrapperElement = window.document - .getElementsByClassName('cvat-canvas-container') - .item(0) as HTMLElement | null; - if (canvasWrapperElement) { - canvasWrapperElement.style.backgroundColor = canvasBackgroundColor; - } // Events canvasInstance.html().addEventListener( 'canvas.setup', () => { const { activatedStateID, activatedAttributeID } = this.props; + canvasInstance.fitCanvas(); canvasInstance.fit(); canvasInstance.activate(activatedStateID, activatedAttributeID); }, @@ -763,6 +944,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { switchableAutomaticBordering, automaticBordering, showTagsOnFrame, + canvasIsReady, onSwitchAutomaticBordering, onSwitchZLayer, onAddZLayer, @@ -788,13 +970,20 @@ export default class CanvasWrapperComponent extends React.PureComponent { }; return ( - + <> {/* This element doesn't have any props So, React isn't going to rerender it And it's a reason why cvat-canvas appended in mount function works */} + { + !canvasIsReady && ( +
+ +
+ ) + }
{ }} /> - }> @@ -832,8 +1020,9 @@ export default class CanvasWrapperComponent extends React.PureComponent {
) : null} - ; -
+ ); } } + +export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent); diff --git a/cvat-ui/src/components/annotation-page/canvas/draggable-hoc.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/draggable-hoc.tsx similarity index 100% rename from cvat-ui/src/components/annotation-page/canvas/draggable-hoc.tsx rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/draggable-hoc.tsx diff --git a/cvat-ui/src/components/annotation-page/canvas/image-setups-content.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/image-setups-content.tsx similarity index 100% rename from cvat-ui/src/components/annotation-page/canvas/image-setups-content.tsx rename to cvat-ui/src/components/annotation-page/canvas/views/canvas2d/image-setups-content.tsx diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx new file mode 100644 index 000000000000..d00a75932c2d --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx @@ -0,0 +1,593 @@ +// Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { + ReactElement, useEffect, useRef, +} from 'react'; +import { connect, useSelector } from 'react-redux'; +import { + ArrowDownOutlined, ArrowLeftOutlined, ArrowRightOutlined, ArrowUpOutlined, +} from '@ant-design/icons'; +import Spin from 'antd/lib/spin'; + +import { + activateObject, + confirmCanvasReady, + createAnnotationsAsync, + dragCanvas, + editShape, + groupAnnotationsAsync, + groupObjects, + resetCanvas, + shapeDrawn, + updateAnnotationsAsync, + updateCanvasContextMenu, +} from 'actions/annotation-actions'; +import { + ColorBy, CombinedState, ContextMenuType, ObjectType, Workspace, +} from 'reducers'; +import { CameraAction, Canvas3d, ViewsDOM } from 'cvat-canvas3d-wrapper'; + +import CVATTooltip from 'components/common/cvat-tooltip'; +import { LogType } from 'cvat-logger'; +import { getCore } from 'cvat-core-wrapper'; + +const cvat = getCore(); + +interface StateToProps { + opacity: number; + selectedOpacity: number; + outlined: boolean; + outlineColor: string; + colorBy: ColorBy; + frameFetching: boolean; + canvasInstance: Canvas3d; + jobInstance: any; + frameData: any; + annotations: any[]; + contextMenuVisibility: boolean; + activeLabelID: number | null; + activatedStateID: number | null; + activeObjectType: ObjectType; + workspace: Workspace; + frame: number; + resetZoom: boolean; +} + +interface DispatchToProps { + onDragCanvas: (enabled: boolean) => void; + onSetupCanvas(): void; + onGroupObjects: (enabled: boolean) => void; + onResetCanvas(): void; + onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onUpdateAnnotations(states: any[]): void; + onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; + onActivateObject: (activatedStateID: number | null) => void; + onShapeDrawn: () => void; + onEditShape: (enabled: boolean) => void; + onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; +} + +function mapStateToProps(state: CombinedState): StateToProps { + const { + annotation: { + canvas: { + instance: canvasInstance, + contextMenu: { visible: contextMenuVisibility }, + }, + drawing: { activeLabelID, activeObjectType }, + job: { instance: jobInstance }, + player: { + frame: { data: frameData, number: frame, fetching: frameFetching }, + }, + annotations: { + states: annotations, + activatedStateID, + }, + workspace, + }, + settings: { + player: { + resetZoom, + }, + shapes: { + opacity, colorBy, selectedOpacity, outlined, outlineColor, + }, + }, + } = state; + + return { + canvasInstance: canvasInstance as Canvas3d, + jobInstance, + frameData, + contextMenuVisibility, + annotations, + frameFetching, + frame, + opacity, + colorBy, + selectedOpacity, + outlined, + outlineColor, + activeLabelID, + activatedStateID, + activeObjectType, + resetZoom, + workspace, + }; +} + +function mapDispatchToProps(dispatch: any): DispatchToProps { + return { + onDragCanvas(enabled: boolean): void { + dispatch(dragCanvas(enabled)); + }, + onSetupCanvas(): void { + dispatch(confirmCanvasReady()); + }, + onResetCanvas(): void { + dispatch(resetCanvas()); + }, + onGroupObjects(enabled: boolean): void { + dispatch(groupObjects(enabled)); + }, + onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { + dispatch(createAnnotationsAsync(sessionInstance, frame, states)); + }, + onShapeDrawn(): void { + dispatch(shapeDrawn()); + }, + onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void { + dispatch(groupAnnotationsAsync(sessionInstance, frame, states)); + }, + onActivateObject(activatedStateID: number | null): void { + if (activatedStateID === null) { + dispatch(updateCanvasContextMenu(false, 0, 0)); + } + + dispatch(activateObject(activatedStateID, null, null)); + }, + onEditShape(enabled: boolean): void { + dispatch(editShape(enabled)); + }, + onUpdateAnnotations(states: any[]): void { + dispatch(updateAnnotationsAsync(states)); + }, + onUpdateContextMenu( + visible: boolean, + left: number, + top: number, + type: ContextMenuType, + pointID?: number, + ): void { + dispatch(updateCanvasContextMenu(visible, left, top, pointID, type)); + }, + }; +} + +type Props = StateToProps & DispatchToProps; + +const Spinner = React.memo(() => ( +
+ +
+)); + +export const PerspectiveViewComponent = React.memo( + (): JSX.Element => { + const ref = useRef(null); + const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d); + const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready); + + const screenKeyControl = (code: CameraAction, altKey: boolean, shiftKey: boolean): void => { + canvas.keyControls(new KeyboardEvent('keydown', { code, altKey, shiftKey })); + }; + + const ArrowGroup = (): ReactElement => ( + + + + +
+ + + + + + + + + +
+ ); + + const ControlGroup = (): ReactElement => ( + + + + + + + + + + +
+ + + + + + + + + +
+ ); + + useEffect(() => { + if (ref.current) { + ref.current.appendChild(canvas.html().perspective); + } + }, []); + + return ( +
+ { !canvasIsReady && } +
+ + +
+ ); + }, +); + +export const TopViewComponent = React.memo( + (): JSX.Element => { + const ref = useRef(null); + const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d); + const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready); + + useEffect(() => { + if (ref.current) { + ref.current.appendChild(canvas.html().top); + } + }, []); + + return ( +
+ { !canvasIsReady && } +
Top
+
+
+ ); + }, +); + +export const SideViewComponent = React.memo( + (): JSX.Element => { + const ref = useRef(null); + const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d); + const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready); + + useEffect(() => { + if (ref.current) { + ref.current.appendChild(canvas.html().side); + } + }, []); + + return ( +
+ { !canvasIsReady && } +
Side
+
+
+ ); + }, +); + +export const FrontViewComponent = React.memo( + (): JSX.Element => { + const ref = useRef(null); + const canvas = useSelector((state: CombinedState) => state.annotation.canvas.instance as Canvas3d); + const canvasIsReady = useSelector((state: CombinedState) => state.annotation.canvas.ready); + + useEffect(() => { + if (ref.current) { + ref.current.appendChild(canvas.html().front); + } + }, []); + + return ( +
+ { !canvasIsReady && } +
Front
+
+
+ ); + }, +); + +const Canvas3DWrapperComponent = React.memo((props: Props): ReactElement => { + const animateId = useRef(0); + + const { + opacity, + outlined, + outlineColor, + selectedOpacity, + colorBy, + contextMenuVisibility, + frameData, + onResetCanvas, + onSetupCanvas, + annotations, + frame, + jobInstance, + activeLabelID, + activatedStateID, + resetZoom, + activeObjectType, + onShapeDrawn, + onCreateAnnotations, + } = props; + const { canvasInstance } = props as { canvasInstance: Canvas3d }; + + const onCanvasSetup = (): void => { + onSetupCanvas(); + }; + + const onCanvasDragStart = (): void => { + const { onDragCanvas } = props; + onDragCanvas(true); + }; + + const onCanvasDragDone = (): void => { + const { onDragCanvas } = props; + onDragCanvas(false); + }; + + const animateCanvas = (): void => { + canvasInstance.render(); + animateId.current = requestAnimationFrame(animateCanvas); + }; + + const updateCanvas = (): void => { + if (frameData !== null) { + canvasInstance.setup( + frameData, + annotations.filter((e) => e.objectType !== ObjectType.TAG), + ); + } + }; + + const onCanvasCancel = (): void => { + onResetCanvas(); + }; + + const onCanvasShapeDrawn = (event: any): void => { + if (!event.detail.continue) { + onShapeDrawn(); + } + + const { state, duration } = event.detail; + const isDrawnFromScratch = !state.label; + if (isDrawnFromScratch) { + jobInstance.logger.log(LogType.drawObject, { count: 1, duration }); + } else { + jobInstance.logger.log(LogType.pasteObject, { count: 1, duration }); + } + + state.objectType = state.objectType || activeObjectType; + state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0]; + state.occluded = state.occluded || false; + state.frame = frame; + state.zOrder = 0; + const objectState = new cvat.classes.ObjectState(state); + onCreateAnnotations(jobInstance, frame, [objectState]); + }; + + const onCanvasClick = (e: MouseEvent): void => { + const { onUpdateContextMenu } = props; + if (contextMenuVisibility) { + onUpdateContextMenu(false, e.clientX, e.clientY, ContextMenuType.CANVAS_SHAPE); + } + }; + + const initialSetup = (): void => { + const canvasInstanceDOM = canvasInstance.html() as ViewsDOM; + canvasInstanceDOM.perspective.addEventListener('canvas.setup', onCanvasSetup); + canvasInstanceDOM.perspective.addEventListener('canvas.canceled', onCanvasCancel); + canvasInstanceDOM.perspective.addEventListener('canvas.dragstart', onCanvasDragStart); + canvasInstanceDOM.perspective.addEventListener('canvas.dragstop', onCanvasDragDone); + canvasInstance.configure({ resetZoom }); + }; + + const keyControlsKeyDown = (key: KeyboardEvent): void => { + canvasInstance.keyControls(key); + }; + + const keyControlsKeyUp = (key: KeyboardEvent): void => { + if (key.code === 'ControlLeft') { + canvasInstance.keyControls(key); + } + }; + + const onCanvasShapeSelected = (event: any): void => { + const { onActivateObject } = props; + const { clientID } = event.detail; + onActivateObject(clientID); + canvasInstance.activate(clientID); + }; + + const onCanvasEditDone = (event: any): void => { + const { onEditShape, onUpdateAnnotations } = props; + onEditShape(false); + const { state, points } = event.detail; + state.points = points; + onUpdateAnnotations([state]); + }; + + useEffect(() => { + const canvasInstanceDOM = canvasInstance.html(); + + document.addEventListener('keydown', keyControlsKeyDown); + document.addEventListener('keyup', keyControlsKeyUp); + + initialSetup(); + updateCanvas(); + animateCanvas(); + + return () => { + canvasInstanceDOM.perspective.removeEventListener('canvas.setup', onCanvasSetup); + canvasInstanceDOM.perspective.removeEventListener('canvas.canceled', onCanvasCancel); + canvasInstanceDOM.perspective.removeEventListener('canvas.dragstart', onCanvasDragStart); + canvasInstanceDOM.perspective.removeEventListener('canvas.dragstop', onCanvasDragDone); + document.removeEventListener('keydown', keyControlsKeyDown); + document.removeEventListener('keyup', keyControlsKeyUp); + cancelAnimationFrame(animateId.current); + }; + }, []); + + useEffect(() => { + canvasInstance.activate(activatedStateID); + }, [activatedStateID]); + + useEffect(() => { + canvasInstance.configure({ resetZoom }); + }, [resetZoom]); + + const updateShapesView = (): void => { + (canvasInstance as Canvas3d).configureShapes({ + opacity, + outlined, + outlineColor, + selectedOpacity, + colorBy, + }); + }; + + const onContextMenu = (event: any): void => { + const { onUpdateContextMenu, onActivateObject } = props; + onActivateObject(event.detail.clientID); + onUpdateContextMenu( + event.detail.clientID !== null, + event.detail.clientX, + event.detail.clientY, + ContextMenuType.CANVAS_SHAPE, + ); + }; + + const onCanvasObjectsGroupped = (event: any): void => { + const { onGroupAnnotations, onGroupObjects } = props; + + onGroupObjects(false); + + const { states } = event.detail; + onGroupAnnotations(jobInstance, frame, states); + }; + + useEffect(() => { + updateShapesView(); + }, [opacity, outlined, outlineColor, selectedOpacity, colorBy]); + + useEffect(() => { + const canvasInstanceDOM = canvasInstance.html() as ViewsDOM; + updateCanvas(); + canvasInstanceDOM.perspective.addEventListener('canvas.drawn', onCanvasShapeDrawn); + canvasInstanceDOM.perspective.addEventListener('canvas.selected', onCanvasShapeSelected); + canvasInstanceDOM.perspective.addEventListener('canvas.edited', onCanvasEditDone); + canvasInstanceDOM.perspective.addEventListener('canvas.contextmenu', onContextMenu); + canvasInstanceDOM.perspective.addEventListener('click', onCanvasClick); + canvasInstanceDOM.perspective.addEventListener('canvas.groupped', onCanvasObjectsGroupped); + + return () => { + canvasInstanceDOM.perspective.removeEventListener('canvas.drawn', onCanvasShapeDrawn); + canvasInstanceDOM.perspective.removeEventListener('canvas.selected', onCanvasShapeSelected); + canvasInstanceDOM.perspective.removeEventListener('canvas.edited', onCanvasEditDone); + canvasInstanceDOM.perspective.removeEventListener('canvas.contextmenu', onContextMenu); + canvasInstanceDOM.perspective.removeEventListener('click', onCanvasClick); + canvasInstanceDOM.perspective.removeEventListener('canvas.groupped', onCanvasObjectsGroupped); + }; + }, [frameData, annotations, activeLabelID, contextMenuVisibility]); + + return <>; +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Canvas3DWrapperComponent); diff --git a/cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/styles.scss similarity index 54% rename from cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss rename to cvat-ui/src/components/annotation-page/canvas/views/canvas3d/styles.scss index b4ab69aecafe..4414ecb73244 100644 --- a/cvat-ui/src/components/annotation-page/standard3D-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/styles.scss @@ -1,15 +1,10 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @import 'base.scss'; -.cvat-canvas-container-overflow { - overflow: hidden; - width: 100%; - height: 100%; -} - .cvat-canvas3d-perspective { height: 100%; width: 100%; @@ -77,87 +72,20 @@ } .cvat-canvas3d-fullsize { - position: relative; width: 100%; - height: 100%; -} - -.cvat-canvas3d-view-slider { - position: absolute; - margin-left: auto; - width: $grid-unit-size * 10; - margin-right: auto; - right: 0; - top: 0; - left: 0; - background-color: grey; - height: $grid-unit-size * 0.5; + height: calc(100% - $grid-unit-size * 3); } .cvat-canvas3d-header { - height: $grid-unit-size * 4; + height: $grid-unit-size * 3; width: 100%; background-color: $background-color-2; 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 * 0.5; - cursor: ns-resize; -} - -.cvat-resizable-handle-vertical-side { - position: absolute; - width: $grid-unit-size * 0.5; - 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 * 0.5; - margin-right: auto; - top: $grid-unit-size * 4.5; - right: 0; - bottom: 0; - background-color: grey; - height: 100%; - cursor: ew-resize; -} - -#cvat_canvas_loading_animation { - z-index: 1; - position: absolute; +.cvat-canvas-container-overflow { + overflow: hidden; width: 100%; height: 100%; } - -#cvat_canvas_loading_circle { - fill-opacity: 0; - stroke: #09c; - stroke-width: 3px; - stroke-dasharray: 50; - animation: loadingAnimation 1s linear infinite; -} - -.cvat_canvas_hidden { - display: none; -} diff --git a/cvat-ui/src/components/annotation-page/canvas/views/context-image/context-image-selector.tsx b/cvat-ui/src/components/annotation-page/canvas/views/context-image/context-image-selector.tsx new file mode 100644 index 000000000000..05779380394e --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/views/context-image/context-image-selector.tsx @@ -0,0 +1,87 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useRef, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import Text from 'antd/lib/typography/Text'; +import { CloseOutlined } from '@ant-design/icons'; + +interface Props { + images: Record; + offset: number; + onChangeOffset: (offset: number) => void; + onClose: () => void; +} + +function CanvasWithRef({ + image, isActive, onClick, name, +}: { image: ImageBitmap, name: string, isActive: boolean, onClick: () => void }): JSX.Element { + const ref = useRef(null); + + useEffect((): void => { + if (ref.current) { + const context = ref.current.getContext('2d'); + if (context) { + ref.current.width = image.width; + ref.current.height = image.height; + context.drawImage(image, 0, 0); + } + } + }, [image, ref]); + + return ( +
+ {name} + +
+ ); +} + +function ContextImageSelector(props: Props): React.ReactPortal { + const { + images, offset, onChangeOffset, onClose, + } = props; + + const keys = Object.keys(images).sort(); + + return ReactDOM.createPortal(( +
+
+
+ + Click the image to display it as a context image + + +
+
+ { keys.map((key, i: number) => ( + { + onChangeOffset(i); + onClose(); + }} + key={i} + /> + ))} +
+
+
+ ), window.document.body); +} + +ContextImageSelector.PropType = { + images: PropTypes.arrayOf(PropTypes.string), + offset: PropTypes.number, + onChangeOffset: PropTypes.func, + onClose: PropTypes.func, +}; + +export default React.memo(ContextImageSelector); diff --git a/cvat-ui/src/components/annotation-page/canvas/views/context-image/context-image.tsx b/cvat-ui/src/components/annotation-page/canvas/views/context-image/context-image.tsx new file mode 100644 index 000000000000..f21e646b931f --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/views/context-image/context-image.tsx @@ -0,0 +1,128 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; +import React, { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import notification from 'antd/lib/notification'; +import Spin from 'antd/lib/spin'; +import Text from 'antd/lib/typography/Text'; +import { SettingOutlined } from '@ant-design/icons'; + +import CVATTooltop from 'components/common/cvat-tooltip'; +import { CombinedState } from 'reducers'; +import ContextImageSelector from './context-image-selector'; + +interface Props { + offset: number[]; +} + +function ContextImage(props: Props): JSX.Element { + const { offset } = props; + const defaultFrameOffset = (offset[0] || 0); + const defaultContextImageOffset = (offset[1] || 0); + + const canvasRef = useRef(null); + const job = useSelector((state: CombinedState) => state.annotation.job.instance); + const { number: frame, relatedFiles } = useSelector((state: CombinedState) => state.annotation.player.frame); + const frameIndex = frame + defaultFrameOffset; + + const [contextImageData, setContextImageData] = useState>({}); + const [fetching, setFetching] = useState(false); + const [contextImageOffset, setContextImageOffset] = useState( + Math.min(defaultContextImageOffset, relatedFiles), + ); + + const [hasError, setHasError] = useState(false); + const [showSelector, setShowSelector] = useState(false); + + useEffect(() => { + let unmounted = false; + const promise = job.frames.contextImage(frameIndex); + setFetching(true); + promise.then((imageBitmaps: Record) => { + if (!unmounted) { + setContextImageData(imageBitmaps); + } + }).catch((error: any) => { + if (!unmounted) { + setHasError(true); + notification.error({ + message: `Could not fetch context images. Frame: ${frameIndex}`, + description: error.toString(), + }); + } + }).finally(() => { + if (!unmounted) { + setFetching(false); + } + }); + + return () => { + setContextImageData({}); + unmounted = true; + }; + }, [frameIndex]); + + useEffect(() => { + if (canvasRef.current) { + const sortedKeys = Object.keys(contextImageData).sort(); + const key = sortedKeys[contextImageOffset]; + const image = contextImageData[key]; + const context = canvasRef.current.getContext('2d'); + if (context && image) { + canvasRef.current.width = image.width; + canvasRef.current.height = image.height; + context.drawImage(image, 0, 0); + } + } + }, [contextImageData, contextImageOffset, canvasRef]); + + const contextImageName = Object.keys(contextImageData).sort()[contextImageOffset]; + return ( +
+
+ { relatedFiles > 1 && ( + { + setShowSelector(true); + }} + /> + )} +
+ + {contextImageName} + +
+
+ { (hasError || + (!fetching && contextImageOffset >= Object.keys(contextImageData).length)) && No data } + { fetching && } + { + contextImageOffset < Object.keys(contextImageData).length && + + } + { showSelector && ( + { + setContextImageOffset(newContextImageOffset); + }} + onClose={() => { + setShowSelector(false); + }} + /> + )} +
+ ); +} + +ContextImage.PropType = { + offset: PropTypes.arrayOf(PropTypes.number), +}; + +export default React.memo(ContextImage); diff --git a/cvat-ui/src/components/annotation-page/canvas/views/context-image/styles.scss b/cvat-ui/src/components/annotation-page/canvas/views/context-image/styles.scss new file mode 100644 index 000000000000..31d1488bd712 --- /dev/null +++ b/cvat-ui/src/components/annotation-page/canvas/views/context-image/styles.scss @@ -0,0 +1,148 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import 'base.scss'; + +.cvat-context-image-wrapper { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + + > .ant-spin { + position: absolute; + top: 50%; + transform: translate(0, -50%); + } + + > .ant-typography { + top: 50%; + position: absolute; + } + + .cvat-context-image-header { + position: absolute; + height: $grid-unit-size * 4; + border-radius: 4px 4px 0 0; + width: 100%; + text-align: center; + z-index: 1; + background: $header-color; + overflow: hidden; + + > .cvat-context-image-title { + width: calc(100% - $grid-unit-size * 13); + margin-right: $grid-unit-size * 7; + margin-left: $grid-unit-size * 7; + + > span.ant-typography { + font-size: 12px; + line-height: $grid-unit-size * 4; + word-break: break-all; + } + } + + > .cvat-context-image-setup-button { + font-size: 16px; + opacity: 0; + transition: all 200ms; + position: absolute; + top: $grid-unit-size; + right: $grid-unit-size * 4; + } + + > .cvat-context-image-close-button { + font-size: 16px; + opacity: 0; + transition: all 200ms; + position: absolute; + top: $grid-unit-size; + right: $grid-unit-size; + } + } + + > canvas { + object-fit: contain; + position: relative; + top: calc(50% + $grid-unit-size * 2); + transform: translateY(-50%); + width: 100%; + height: calc(100% - $grid-unit-size * 4); + } + + &:hover { + > .cvat-context-image-header > .cvat-context-image-setup-button { + opacity: 0.6; + + &:hover { + opacity: 0.9; + } + } + } +} + +.cvat-context-image-overlay { + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1000; + background: rgba(255, 255, 255, 0.25); + position: absolute; + justify-content: space-around; + align-items: center; + display: flex; + + .cvat-context-image-gallery { + width: 80%; + max-height: 80%; + position: relative; + background: white; + padding: $grid-unit-size; + display: block; + justify-content: space-between; + overflow: hidden; + overflow-y: auto; + + .cvat-context-image-gallery-items { + display: block; + + .cvat-context-image-gallery-item { + text-align: center; + padding: $grid-unit-size; + opacity: 0.6; + width: 25%; + float: left; + + &.cvat-context-image-gallery-item-current { + opacity: 1; + } + + &:hover { + opacity: 0.9; + } + + > canvas { + width: 100%; + } + } + } + + .cvat-context-image-gallery-header { + text-align: center; + + .cvat-context-image-close-button { + &:hover { + opacity: 0.9; + } + + transition: all 200ms; + opacity: 0.6; + position: absolute; + top: $grid-unit-size; + right: $grid-unit-size; + } + } + } +} diff --git a/cvat-ui/src/components/annotation-page/review-workspace/review-workspace.tsx b/cvat-ui/src/components/annotation-page/review-workspace/review-workspace.tsx index cae74fefc0f0..bd75dd1409a2 100644 --- a/cvat-ui/src/components/annotation-page/review-workspace/review-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/review-workspace/review-workspace.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,8 +7,8 @@ import './styles.scss'; import React from 'react'; import Layout from 'antd/lib/layout'; -import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; import ControlsSideBarContainer from 'containers/annotation-page/review-workspace/controls-side-bar/controls-side-bar'; +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; @@ -17,7 +18,7 @@ export default function ReviewWorkspaceComponent(): JSX.Element { return ( - + } /> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/context-image/context-image.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/context-image/context-image.tsx deleted file mode 100644 index 229635a88fcc..000000000000 --- a/cvat-ui/src/components/annotation-page/standard-workspace/context-image/context-image.tsx +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React, { useEffect, useState } from 'react'; -import notification from 'antd/lib/notification'; -import { useDispatch, useSelector } from 'react-redux'; -import { QuestionCircleOutlined, ShrinkOutlined } from '@ant-design/icons'; -import Spin from 'antd/lib/spin'; -import Image from 'antd/lib/image'; - -import { CombinedState } from 'reducers'; -import { hideShowContextImage, getContextImageAsync } from 'actions/annotation-actions'; -import CVATTooltip from 'components/common/cvat-tooltip'; - -export function adjustContextImagePosition(sidebarCollapsed: boolean): void { - const element = window.document.getElementsByClassName('cvat-context-image-wrapper')[0] as - | HTMLDivElement - | undefined; - if (element) { - if (sidebarCollapsed) { - element.style.right = '40px'; - } else { - element.style.right = ''; - } - } -} - -function ContextImage(): JSX.Element | null { - const dispatch = useDispatch(); - const { number: frame, hasRelatedContext } = useSelector((state: CombinedState) => state.annotation.player.frame); - const { data: contextImageData, hidden: contextImageHidden, fetching: contextImageFetching } = useSelector( - (state: CombinedState) => state.annotation.player.contextImage, - ); - const [requested, setRequested] = useState(false); - - useEffect(() => { - if (requested) { - setRequested(false); - } - }, [frame, contextImageData]); - - useEffect(() => { - if (hasRelatedContext && !contextImageHidden && !requested) { - dispatch(getContextImageAsync()); - setRequested(true); - } - }, [contextImageHidden, requested, hasRelatedContext]); - - if (!hasRelatedContext) { - return null; - } - - return ( -
-
- {contextImageFetching ? : null} - {contextImageHidden ? ( - - dispatch(hideShowContextImage(false))} - /> - - ) : ( - <> - dispatch(hideShowContextImage(true))} - /> - { - notification.error({ - message: 'Could not display context image', - description: `Source is ${ - contextImageData === null ? 'empty' : contextImageData.slice(0, 100) - }`, - }); - }} - className='cvat-context-image' - /> - - )} -
- ); -} - -export default React.memo(ContextImage); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx index b4239e5fcf43..0042bcb67799 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -11,11 +12,8 @@ import Text from 'antd/lib/typography/Text'; import Tabs from 'antd/lib/tabs'; import Layout from 'antd/lib/layout'; -import { Canvas } from 'cvat-canvas-wrapper'; -import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { CombinedState, DimensionType } from 'reducers'; import LabelsList from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list'; -import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image'; import { collapseSidebar as collapseSidebarAction } from 'actions/annotation-actions'; import AppearanceBlock from 'components/annotation-page/appearance-block'; import IssuesListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/issues-list'; @@ -26,7 +24,6 @@ interface OwnProps { interface StateToProps { sidebarCollapsed: boolean; - canvasInstance: Canvas | Canvas3d; jobInstance: any; } @@ -38,14 +35,12 @@ function mapStateToProps(state: CombinedState): StateToProps { const { annotation: { sidebarCollapsed, - canvas: { instance: canvasInstance }, job: { instance: jobInstance }, }, } = state; return { sidebarCollapsed, - canvasInstance, jobInstance, }; } @@ -60,15 +55,14 @@ function mapDispatchToProps(dispatch: Dispatch): DispatchToProps { function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { const { - sidebarCollapsed, canvasInstance, collapseSidebar, objectsList, jobInstance, + sidebarCollapsed, collapseSidebar, objectsList, jobInstance, } = props; const collapse = (): void => { const [collapser] = window.document.getElementsByClassName('cvat-objects-sidebar'); const listener = (event: TransitionEvent): void => { if (event.target && event.propertyName === 'width' && event.target === collapser) { - canvasInstance.fitCanvas(); - canvasInstance.fit(); + window.dispatchEvent(new Event('resize')); (collapser as HTMLElement).removeEventListener('transitionend', listener as any); } }; @@ -77,7 +71,6 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E (collapser as HTMLElement).addEventListener('transitionend', listener as any); } - adjustContextImagePosition(!sidebarCollapsed); collapseSidebar(); }; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx index 096d62fe5c0d..3c90aa95d011 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/standard-workspace.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporations +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,12 +7,12 @@ import './styles.scss'; import React from 'react'; import Layout from 'antd/lib/layout'; -import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; -import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/canvas-point-context-menu'; +import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu'; import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator'; import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; @@ -21,7 +21,7 @@ export default function StandardWorkspaceComponent(): JSX.Element { return ( - + } /> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index ef788cf583cd..5980c704f829 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,55 +7,11 @@ .cvat-standard-workspace.ant-layout { height: 100%; -} - -.cvat-context-image-wrapper { - height: auto; - width: $grid-unit-size * 32; - position: absolute; - top: $grid-unit-size; - right: $grid-unit-size; - z-index: 100; - background: black; - display: flex; - flex-direction: column; - justify-content: space-between; - user-select: none; - > .cvat-context-image-wrapper-header { - height: $grid-unit-size * 4; - width: 100%; - z-index: 101; - background: rgba(0, 0, 0, 0.2); - position: absolute; - top: 0; - left: 0; + > .ant-layout-content { + overflow-y: hidden; + overflow-x: hidden; } - - > .ant-image { - margin: $grid-unit-size * 0.5; - } - - > span { - position: absolute; - font-size: 18px; - top: 7px; - right: 7px; - z-index: 102; - color: white; - - &:hover { - > svg { - transform: scale(1.2); - } - } - } -} - -.cvat-context-image { - width: 100%; - height: auto; - display: block; } .cvat-objects-sidebar-sider { diff --git a/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/photo-context.tsx b/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/photo-context.tsx deleted file mode 100644 index 5ebb696aeb2e..000000000000 --- a/cvat-ui/src/components/annotation-page/standard3D-workspace/controls-side-bar/photo-context.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// -// SPDX-License-Identifier: MIT - -import React from 'react'; -import CameraIcon from '@ant-design/icons/CameraOutlined'; - -import CVATTooltip from 'components/common/cvat-tooltip'; -import { Canvas3d } from 'cvat-canvas3d-wrapper'; -import { Canvas } from 'cvat-canvas-wrapper'; -import { ActiveControl } from 'reducers'; - -interface Props { - canvasInstance: Canvas3d | Canvas; - activeControl: ActiveControl; - hideShowContextImage: (hidden: boolean) => void; - contextImageHide: boolean; -} - -function PhotoContextControl(props: Props): JSX.Element { - const { activeControl, contextImageHide, hideShowContextImage } = props; - - return ( - - { - hideShowContextImage(!contextImageHide); - }} - /> - - ); -} - -export default React.memo(PhotoContextControl); diff --git a/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx b/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx index 18f70140b0a2..899c4912fdbd 100644 --- a/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/standard3D-workspace/standard3D-workspace.tsx @@ -1,17 +1,18 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import './styles.scss'; import React from 'react'; import Layout from 'antd/lib/layout'; -import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper3D'; +import { DimensionType } from 'reducers'; import ControlsSideBarContainer from 'containers/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar'; import ObjectSideBarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar'; import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list'; import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas-context-menu'; -import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/canvas-point-context-menu'; +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; +import CanvasPointContextMenuComponent from 'components/annotation-page/canvas/views/canvas2d/canvas-point-context-menu'; import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; import PropagateConfirmComponent from 'components/annotation-page/standard-workspace/propagate-confirm'; @@ -19,7 +20,7 @@ export default function StandardWorkspace3DComponent(): JSX.Element { return ( - + } /> diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index fcdfe480e772..f776f43bd766 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -181,6 +182,8 @@ button.cvat-predictor-button { .cvat-player-filename-wrapper { max-width: $grid-unit-size * 30; + max-height: $grid-unit-size * 3; + line-height: $grid-unit-size * 3; overflow: hidden; text-overflow: ellipsis; user-select: none; @@ -517,3 +520,9 @@ button.cvat-predictor-button { } } } + +.cvat-saving-job-modal { + span.anticon { + margin-left: $grid-unit-size * 2; + } +} diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss index dc0e1ebd8e47..cde4dcf8614f 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -10,14 +11,14 @@ .cvat-tag-annotation-sidebar:not(.ant-layout-sider-collapsed) { background: $background-color-2; - padding: $grid-unit-size * 0.5; - padding-left: $grid-unit-size * 1.25; + padding: $grid-unit-size; + padding-left: $grid-unit-size; overflow-y: auto; } .cvat-tag-annotation-sidebar-label-select { - padding-top: $grid-unit-size * 1.25; - padding-bottom: $grid-unit-size * 1.8; + padding-top: $grid-unit-size; + padding-bottom: $grid-unit-size * 2; > .ant-col > .ant-select { width: $grid-unit-size * 25; @@ -25,16 +26,16 @@ } .cvat-tag-annotation-sidebar-shortcut-help { - padding-top: $grid-unit-size * 1.8; + padding-top: $grid-unit-size; text-align: center; } .cvat-tag-annotation-sidebar-checkbox-skip-frame { - padding-bottom: $grid-unit-size * 1.8; + padding-bottom: $grid-unit-size; } .cvat-tag-annotation-label-selects { - padding-top: $grid-unit-size * 1.25; + padding-top: $grid-unit-size; .ant-select { width: $grid-unit-size * 29; @@ -42,7 +43,7 @@ } .cvat-tag-annotation-shortcut-key { - margin-left: $grid-unit-size * 1.25; + margin-left: $grid-unit-size; } } @@ -52,14 +53,13 @@ .cvat-frame-tags { .ant-tag { - margin: $grid-unit-size * 0.25; display: inline-flex; justify-content: center; align-items: center; .ant-tag-close-icon { - margin-left: $grid-unit-size * 0.5; - font-size: $grid-unit-size * 1.5; + margin-left: $grid-unit-size; + font-size: 12px; } } } @@ -68,7 +68,7 @@ @extend .cvat-frame-tags; position: absolute; - top: $layout-sm-grid-size; + top: $grid-unit-size * 4; left: $grid-unit-size; z-index: 3; @@ -78,11 +78,11 @@ } .cvat-tag-annotation-sidebar-tag-label { - margin-top: $grid-unit-size * 1.8; + margin-top: $grid-unit-size * 2; } .cvat-add-tag-button { - margin-left: $grid-unit-size * 1.25; - width: $grid-unit-size * 3.5; - height: $grid-unit-size * 3.5; + margin-left: $grid-unit-size; + width: $grid-unit-size * 4; + height: $grid-unit-size * 3; } diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx index c67258a55541..994cb2ea127d 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-sidebar/tag-annotation-sidebar.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -22,12 +22,9 @@ import { changeFrameAsync, rememberObject, } from 'actions/annotation-actions'; -import { Canvas } from 'cvat-canvas-wrapper'; -import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { getCore, Label, LabelType } from 'cvat-core-wrapper'; import { CombinedState, ObjectType } from 'reducers'; import { filterApplicableForType } from 'utils/filter-applicable-labels'; -import { adjustContextImagePosition } from 'components/annotation-page/standard-workspace/context-image/context-image'; import LabelSelector from 'components/label-selector/label-selector'; import isAbleToChangeFrame from 'utils/is-able-to-change-frame'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; @@ -39,7 +36,6 @@ interface StateToProps { states: any[]; labels: any[]; jobInstance: any; - canvasInstance: Canvas | Canvas3d; frameNumber: number; keyMap: KeyMap; normalizedKeyMap: Record; @@ -61,7 +57,6 @@ function mapStateToProps(state: CombinedState): StateToProps { }, annotations: { states }, job: { instance: jobInstance, labels }, - canvas: { instance: canvasInstance }, }, shortcuts: { keyMap, normalizedKeyMap }, } = state; @@ -70,7 +65,6 @@ function mapStateToProps(state: CombinedState): StateToProps { jobInstance, labels, states, - canvasInstance: canvasInstance as Canvas | Canvas3d, frameNumber, keyMap, normalizedKeyMap, @@ -102,7 +96,6 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen removeObject, jobInstance, changeFrame, - canvasInstance, frameNumber, onRememberObject, createAnnotations, @@ -143,8 +136,7 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen (event as TransitionEvent).propertyName === 'width' && ((event.target as any).classList as DOMTokenList).contains('cvat-tag-annotation-sidebar') ) { - canvasInstance.fitCanvas(); - canvasInstance.fit(); + window.dispatchEvent(new Event('resize')); } }; @@ -234,7 +226,6 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen ant-layout-sider-zero-width-trigger ant-layout-sider-zero-width-trigger-left`} onClick={() => { - adjustContextImagePosition(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed); }} > @@ -256,7 +247,6 @@ function TagAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Elemen ant-layout-sider-zero-width-trigger ant-layout-sider-zero-width-trigger-left`} onClick={() => { - adjustContextImagePosition(!sidebarCollapsed); setSidebarCollapsed(!sidebarCollapsed); }} > diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx index e4d202d6b446..ed1b1767826d 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/tag-annotation-workspace.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -6,14 +7,14 @@ import './styles.scss'; import React from 'react'; import Layout from 'antd/lib/layout'; -import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; +import CanvasLayout from 'components/annotation-page/canvas/grid-layout/canvas-layout'; import RemoveConfirmComponent from 'components/annotation-page/standard-workspace/remove-confirm'; import TagAnnotationSidebar from './tag-annotation-sidebar/tag-annotation-sidebar'; export default function TagAnnotationWorkspace(): JSX.Element { return ( - + diff --git a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx index e692f20ba951..5ddfb5adbbff 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/left-group.tsx @@ -1,13 +1,14 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React from 'react'; import { Col } from 'antd/lib/grid'; -import Icon, { StopOutlined, CheckCircleOutlined } from '@ant-design/icons'; +import Icon, { StopOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons'; import Modal from 'antd/lib/modal'; import Button from 'antd/lib/button'; -import Timeline from 'antd/lib/timeline'; +import Text from 'antd/lib/typography/Text'; import Dropdown from 'antd/lib/dropdown'; import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotation-menu'; @@ -19,7 +20,6 @@ import CVATTooltip from 'components/common/cvat-tooltip'; interface Props { saving: boolean; - savingStatuses: string[]; undoAction?: string; redoAction?: string; saveShortcut: string; @@ -39,7 +39,6 @@ interface Props { function LeftGroup(props: Props): JSX.Element { const { saving, - savingStatuses, undoAction, redoAction, saveShortcut, @@ -71,12 +70,9 @@ function LeftGroup(props: Props): JSX.Element { return ( <> - - - {savingStatuses.slice(0, -1).map((status: string, id: number) => ( - {status} - ))} - + + CVAT is working to save annotations, please wait + }> diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index d238c94adaf9..3d66e7ba0abe 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -17,7 +18,6 @@ import RightGroup from './right-group'; interface Props { playing: boolean; saving: boolean; - savingStatuses: string[]; frameNumber: number; frameFilename: string; frameDeleted: boolean; @@ -75,7 +75,6 @@ interface Props { export default function AnnotationTopBarComponent(props: Props): JSX.Element { const { saving, - savingStatuses, undoAction, redoAction, playing, @@ -135,7 +134,6 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { void; - onZoomCanvas: (enabled: boolean) => void; - onResetCanvas: () => void; - onShapeDrawn: () => void; - onMergeObjects: (enabled: boolean) => void; - onGroupObjects: (enabled: boolean) => void; - onSplitTrack: (enabled: boolean) => void; - onEditShape: (enabled: boolean) => void; - onUpdateAnnotations(states: any[]): void; - onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onSplitAnnotations(sessionInstance: any, frame: number, state: any): void; - onActivateObject: (activatedStateID: number | null, activatedElementID: number | null) => void; - onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; - onAddZLayer(): void; - onSwitchZLayer(cur: number): void; - onChangeBrightnessLevel(level: number): void; - onChangeContrastLevel(level: number): void; - onChangeSaturationLevel(level: number): void; - onChangeGridOpacity(opacity: number): void; - onChangeGridColor(color: GridColor): void; - onSwitchGrid(enabled: boolean): void; - onSwitchAutomaticBordering(enabled: boolean): void; - onFetchAnnotation(): void; - onGetDataFailed(error: any): void; - onStartIssue(position: number[]): void; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const { - annotation: { - canvas: { activeControl, instance: canvasInstance }, - drawing: { activeLabelID, activeObjectType }, - job: { instance: jobInstance }, - player: { - frame: { data: frameData, number: frame, fetching: frameFetching }, - frameAngles, - }, - annotations: { - states: annotations, - activatedStateID, - activatedElementID, - activatedAttributeID, - zLayer: { cur: curZLayer, min: minZLayer, max: maxZLayer }, - }, - sidebarCollapsed, - workspace, - }, - settings: { - player: { - canvasBackgroundColor, - grid, - gridSize, - gridColor, - gridOpacity, - brightnessLevel, - contrastLevel, - saturationLevel, - resetZoom, - smoothImage, - }, - workspace: { - aamZoomMargin, - showObjectsTextAlways, - showAllInterpolationTracks, - showTagsOnFrame, - automaticBordering, - intelligentPolygonCrop, - textFontSize, - controlPointsSize, - textPosition, - textContent, - }, - shapes: { - opacity, colorBy, selectedOpacity, outlined, outlineColor, showBitmap, showProjections, - }, - }, - shortcuts: { keyMap }, - } = state; - - return { - sidebarCollapsed, - canvasInstance, - jobInstance, - frameData, - frameAngle: frameAngles[frame - jobInstance.startFrame], - frameFetching, - frame, - activatedStateID, - activatedElementID, - activatedAttributeID, - annotations, - opacity: opacity / 100, - colorBy, - selectedOpacity: selectedOpacity / 100, - outlined, - outlineColor, - showBitmap, - showProjections, - grid, - gridSize, - gridColor, - gridOpacity: gridOpacity / 100, - activeLabelID, - activeObjectType, - brightnessLevel: brightnessLevel / 100, - contrastLevel: contrastLevel / 100, - saturationLevel: saturationLevel / 100, - resetZoom, - smoothImage, - aamZoomMargin, - showObjectsTextAlways, - textFontSize, - controlPointsSize, - textPosition, - textContent, - showAllInterpolationTracks, - showTagsOnFrame, - curZLayer, - minZLayer, - maxZLayer, - automaticBordering, - intelligentPolygonCrop, - workspace, - keyMap, - canvasBackgroundColor, - switchableAutomaticBordering: - activeControl === ActiveControl.DRAW_POLYGON || - activeControl === ActiveControl.DRAW_POLYLINE || - activeControl === ActiveControl.DRAW_MASK || - activeControl === ActiveControl.EDIT, - }; -} - -function mapDispatchToProps(dispatch: any): DispatchToProps { - return { - onSetupCanvas(): void { - dispatch(confirmCanvasReady()); - }, - onDragCanvas(enabled: boolean): void { - dispatch(dragCanvas(enabled)); - }, - onZoomCanvas(enabled: boolean): void { - dispatch(zoomCanvas(enabled)); - }, - onResetCanvas(): void { - dispatch(resetCanvas()); - }, - onShapeDrawn(): void { - dispatch(shapeDrawn()); - }, - onMergeObjects(enabled: boolean): void { - dispatch(mergeObjects(enabled)); - }, - onGroupObjects(enabled: boolean): void { - dispatch(groupObjects(enabled)); - }, - onSplitTrack(enabled: boolean): void { - dispatch(splitTrack(enabled)); - }, - onEditShape(enabled: boolean): void { - dispatch(editShape(enabled)); - }, - onUpdateAnnotations(states: any[]): void { - dispatch(updateAnnotationsAsync(states)); - }, - onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { - dispatch(createAnnotationsAsync(sessionInstance, frame, states)); - }, - onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void { - dispatch(mergeAnnotationsAsync(sessionInstance, frame, states)); - }, - onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void { - dispatch(groupAnnotationsAsync(sessionInstance, frame, states)); - }, - onSplitAnnotations(sessionInstance: any, frame: number, state: any): void { - dispatch(splitAnnotationsAsync(sessionInstance, frame, state)); - }, - onActivateObject(activatedStateID: number | null, activatedElementID: number | null = null): void { - if (activatedStateID === null) { - dispatch(updateCanvasContextMenu(false, 0, 0)); - } - - dispatch(activateObject(activatedStateID, activatedElementID, null)); - }, - onUpdateContextMenu( - visible: boolean, - left: number, - top: number, - type: ContextMenuType, - pointID?: number, - ): void { - dispatch(updateCanvasContextMenu(visible, left, top, pointID, type)); - }, - onAddZLayer(): void { - dispatch(addZLayer()); - }, - onSwitchZLayer(cur: number): void { - dispatch(switchZLayer(cur)); - }, - onChangeBrightnessLevel(level: number): void { - dispatch(changeBrightnessLevel(level)); - }, - onChangeContrastLevel(level: number): void { - dispatch(changeContrastLevel(level)); - }, - onChangeSaturationLevel(level: number): void { - dispatch(changeSaturationLevel(level)); - }, - onChangeGridOpacity(opacity: number): void { - dispatch(changeGridOpacity(opacity)); - }, - onChangeGridColor(color: GridColor): void { - dispatch(changeGridColor(color)); - }, - onSwitchGrid(enabled: boolean): void { - dispatch(switchGrid(enabled)); - }, - onSwitchAutomaticBordering(enabled: boolean): void { - dispatch(switchAutomaticBordering(enabled)); - }, - onFetchAnnotation(): void { - dispatch(fetchAnnotationsAsync()); - }, - onGetDataFailed(error: any): void { - dispatch(getDataFailed(error)); - }, - onStartIssue(position: number[]): void { - dispatch(reviewActions.startIssue(position)); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent); diff --git a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx b/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx deleted file mode 100644 index cb64e7e24c73..000000000000 --- a/cvat-ui/src/containers/annotation-page/canvas/canvas-wrapper3D.tsx +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import { connect } from 'react-redux'; - -import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper3D'; -import { - activateObject, - confirmCanvasReady, - createAnnotationsAsync, - dragCanvas, - editShape, - groupAnnotationsAsync, - groupObjects, - resetCanvas, - shapeDrawn, - updateAnnotationsAsync, - updateCanvasContextMenu, -} from 'actions/annotation-actions'; - -import { - ColorBy, - CombinedState, - ContextMenuType, - ObjectType, - Workspace, -} from 'reducers'; - -import { Canvas3d } from 'cvat-canvas3d-wrapper'; -import { Canvas } from 'cvat-canvas-wrapper'; - -interface StateToProps { - opacity: number; - selectedOpacity: number; - outlined: boolean; - outlineColor: string; - colorBy: ColorBy; - frameFetching: boolean; - canvasInstance: Canvas3d | Canvas; - jobInstance: any; - frameData: any; - annotations: any[]; - contextMenuVisibility: boolean; - activeLabelID: number; - activatedStateID: number | null; - activeObjectType: ObjectType; - workspace: Workspace; - frame: number; - resetZoom: boolean; -} - -interface DispatchToProps { - onDragCanvas: (enabled: boolean) => void; - onSetupCanvas(): void; - onGroupObjects: (enabled: boolean) => void; - onResetCanvas(): void; - onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onUpdateAnnotations(states: any[]): void; - onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void; - onActivateObject: (activatedStateID: number | null) => void; - onShapeDrawn: () => void; - onEditShape: (enabled: boolean) => void; - onUpdateContextMenu(visible: boolean, left: number, top: number, type: ContextMenuType, pointID?: number): void; -} - -function mapStateToProps(state: CombinedState): StateToProps { - const { - annotation: { - canvas: { - instance: canvasInstance, - contextMenu: { visible: contextMenuVisibility }, - }, - drawing: { activeLabelID, activeObjectType }, - job: { instance: jobInstance }, - player: { - frame: { data: frameData, number: frame, fetching: frameFetching }, - }, - annotations: { - states: annotations, - activatedStateID, - }, - workspace, - }, - settings: { - player: { - resetZoom, - }, - shapes: { - opacity, colorBy, selectedOpacity, outlined, outlineColor, - }, - }, - } = state; - - return { - canvasInstance, - jobInstance, - frameData, - contextMenuVisibility, - annotations, - frameFetching, - frame, - opacity, - colorBy, - selectedOpacity, - outlined, - outlineColor, - activeLabelID, - activatedStateID, - activeObjectType, - resetZoom, - workspace, - }; -} - -function mapDispatchToProps(dispatch: any): DispatchToProps { - return { - onDragCanvas(enabled: boolean): void { - dispatch(dragCanvas(enabled)); - }, - onSetupCanvas(): void { - dispatch(confirmCanvasReady()); - }, - onResetCanvas(): void { - dispatch(resetCanvas()); - }, - onGroupObjects(enabled: boolean): void { - dispatch(groupObjects(enabled)); - }, - onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void { - dispatch(createAnnotationsAsync(sessionInstance, frame, states)); - }, - onShapeDrawn(): void { - dispatch(shapeDrawn()); - }, - onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void { - dispatch(groupAnnotationsAsync(sessionInstance, frame, states)); - }, - onActivateObject(activatedStateID: number | null): void { - if (activatedStateID === null) { - dispatch(updateCanvasContextMenu(false, 0, 0)); - } - - dispatch(activateObject(activatedStateID, null, null)); - }, - onEditShape(enabled: boolean): void { - dispatch(editShape(enabled)); - }, - onUpdateAnnotations(states: any[]): void { - dispatch(updateAnnotationsAsync(states)); - }, - onUpdateContextMenu( - visible: boolean, - left: number, - top: number, - type: ContextMenuType, - pointID?: number, - ): void { - dispatch(updateCanvasContextMenu(visible, left, top, pointID, type)); - }, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(CanvasWrapperComponent); diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 34b321ad890d..1f143ac818b5 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -56,7 +57,6 @@ interface StateToProps { playing: boolean; saving: boolean; canvasIsReady: boolean; - savingStatuses: string[]; undoAction?: string; redoAction?: string; autoSave: boolean; @@ -106,7 +106,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, }, annotations: { - saving: { uploading: saving, statuses: savingStatuses, forceExit }, + saving: { uploading: saving, forceExit }, history, }, job: { instance: jobInstance }, @@ -135,7 +135,6 @@ function mapStateToProps(state: CombinedState): StateToProps { playing, canvasIsReady, saving, - savingStatuses, frameNumber, frameFilename, jobInstance, @@ -607,7 +606,6 @@ class AnnotationTopBarContainer extends React.PureComponent { const { playing, saving, - savingStatuses, jobInstance, jobInstance: { startFrame, stopFrame }, frameNumber, @@ -751,7 +749,6 @@ class AnnotationTopBarContainer extends React.PureComponent { workspace={workspace} playing={playing} saving={saving} - savingStatuses={savingStatuses} startFrame={startFrame} stopFrame={stopFrame} frameNumber={frameNumber} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index e05625074f65..c704d49361ef 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -65,7 +65,7 @@ const defaultState: AnnotationState = { number: 0, filename: '', data: null, - hasRelatedContext: false, + relatedFiles: 0, fetching: false, delay: 0, changeTime: null, @@ -73,11 +73,6 @@ const defaultState: AnnotationState = { playing: false, frameAngles: [], navigationBlocked: false, - contextImage: { - fetching: false, - data: null, - hidden: false, - }, }, drawing: { activeShapeType: ShapeType.RECTANGLE, @@ -91,7 +86,6 @@ const defaultState: AnnotationState = { saving: { forceExit: false, uploading: false, - statuses: [], }, collapsed: {}, collapsedAll: true, @@ -160,7 +154,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { openTime, frameNumber: number, frameFilename: filename, - frameHasRelatedContext, + relatedFiles, colors, filters, frameData: data, @@ -213,7 +207,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { frame: { ...state.player.frame, filename, - hasRelatedContext: frameHasRelatedContext, + relatedFiles, number, data, }, @@ -277,7 +271,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { number, data, filename, - hasRelatedContext, + relatedFiles, states, minZ, maxZ, @@ -293,16 +287,12 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { frame: { data, filename, - hasRelatedContext, + relatedFiles, number, fetching: false, changeTime, delay, }, - contextImage: { - ...state.player.contextImage, - ...(state.player.frame.number === number ? {} : { data: null }), - }, }, annotations: { ...state.annotations, @@ -351,7 +341,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { saving: { ...state.annotations.saving, uploading: true, - statuses: [], }, }, }; @@ -385,20 +374,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } - case AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS: { - const { status } = action.payload; - - return { - ...state, - annotations: { - ...state.annotations, - saving: { - ...state.annotations.saving, - statuses: [...state.annotations.saving.statuses, status], - }, - }, - }; - } case AnnotationActionTypes.SWITCH_PLAY: { const { playing } = action.payload; @@ -1188,58 +1163,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { }, }; } - case AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE: { - const { hidden } = action.payload; - return { - ...state, - player: { - ...state.player, - contextImage: { - ...state.player.contextImage, - hidden, - }, - }, - }; - } - case AnnotationActionTypes.GET_CONTEXT_IMAGE: { - return { - ...state, - player: { - ...state.player, - contextImage: { - ...state.player.contextImage, - fetching: true, - }, - }, - }; - } - case AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS: { - const { contextImageData } = action.payload; - - return { - ...state, - player: { - ...state.player, - contextImage: { - ...state.player.contextImage, - fetching: false, - data: contextImageData, - }, - }, - }; - } - case AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED: { - return { - ...state, - player: { - ...state.player, - contextImage: { - ...state.player.contextImage, - fetching: false, - }, - }, - }; - } case AnnotationActionTypes.SWITCH_NAVIGATION_BLOCKED: { return { ...state, diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 8adf2b1075bb..4e4c110c5355 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -457,7 +457,6 @@ export interface NotificationsState { saving: null | ErrorState; jobFetching: null | ErrorState; frameFetching: null | ErrorState; - contextImageFetching: null | ErrorState; changingLabelColor: null | ErrorState; updating: null | ErrorState; creating: null | ErrorState; @@ -669,7 +668,7 @@ export interface AnnotationState { frame: { number: number; filename: string; - hasRelatedContext: boolean; + relatedFiles: number; data: any | null; fetching: boolean; delay: number; @@ -678,11 +677,6 @@ export interface AnnotationState { navigationBlocked: boolean; playing: boolean; frameAngles: number[]; - contextImage: { - fetching: boolean; - data: string | null; - hidden: boolean; - }; }; drawing: { activeInteractor?: Model | OpenCVTool; @@ -710,7 +704,6 @@ export interface AnnotationState { saving: { forceExit: boolean; uploading: boolean; - statuses: string[]; }; zLayer: { min: number; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 80134793ec9f..1605af2549f3 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -85,7 +85,6 @@ const defaultState: NotificationsState = { saving: null, jobFetching: null, frameFetching: null, - contextImageFetching: null, changingLabelColor: null, updating: null, creating: null, @@ -835,21 +834,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED: { - return { - ...state, - errors: { - ...state.errors, - annotation: { - ...state.errors.annotation, - contextImageFetching: { - message: 'Could not fetch context image from the server', - reason: action.payload.error, - }, - }, - }, - }; - } case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: { return { ...state, diff --git a/cvat/apps/engine/cache.py b/cvat/apps/engine/cache.py index d1b19ef3578b..cc8460c32552 100644 --- a/cvat/apps/engine/cache.py +++ b/cvat/apps/engine/cache.py @@ -3,9 +3,12 @@ # SPDX-License-Identifier: MIT import os +import io +import zipfile from io import BytesIO from datetime import datetime from tempfile import NamedTemporaryFile +import cv2 import pytz from django.core.cache import cache @@ -16,7 +19,7 @@ from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter, ImageDatasetManifestReader, VideoDatasetManifestReader) -from cvat.apps.engine.models import DataChoice, StorageChoice +from cvat.apps.engine.models import DataChoice, StorageChoice, Image from cvat.apps.engine.models import DimensionType from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials from cvat.apps.engine.utils import md5_hash @@ -34,7 +37,8 @@ def _get_or_set_cache_item(key, create_function): item = cache.get(key) if not item: item = create_function() - cache.set(key, item) + if item[0]: + cache.set(key, item) return item @@ -62,13 +66,20 @@ def get_cloud_preview_with_mime(self, db_storage): return item + def get_frame_context_images(self, db_data, frame_number): + item = self._get_or_set_cache_item( + key=f'context_image_{db_data.id}_{frame_number}', + create_function=lambda: self._prepare_context_image(db_data, frame_number) + ) + + return item + @staticmethod def _get_frame_provider(): from cvat.apps.engine.frame_provider import FrameProvider # TODO: remove circular dependency return FrameProvider def _prepare_chunk_buff(self, db_data, quality, chunk_number): - FrameProvider = self._get_frame_provider() writer_classes = { @@ -183,3 +194,25 @@ def _prepare_cloud_preview(self, db_storage): mime_type = mimetypes.guess_type(preview_path)[0] return buff, mime_type + + def _prepare_context_image(self, db_data, frame_number): + zip_buffer = io.BytesIO() + try: + image = Image.objects.get(data_id=db_data.id, frame=frame_number) + except Image.DoesNotExist: + return None, None + with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file: + if not image.related_files.count(): + return None, None + common_path = os.path.commonpath(list(map(lambda x: str(x.path), image.related_files.all()))) + for i in image.related_files.all(): + path = os.path.realpath(str(i.path)) + name = os.path.relpath(str(i.path), common_path) + image = cv2.imread(path) + success, result = cv2.imencode('.JPEG', image) + if not success: + raise Exception('Failed to encode image to ".jpeg" format') + zip_file.writestr(f'{name}.jpg', result.tobytes()) + buff = zip_buffer.getvalue() + mime_type = 'application/zip' + return buff, mime_type diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 71a211b9b572..a718bd48efe3 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -1,5 +1,5 @@ # Copyright (C) 2019-2022 Intel Corporation -# Copyright (C) 2022 CVAT.ai Corporation +# Copyright (C) 2022-2023 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -924,7 +924,7 @@ class FrameMetaSerializer(serializers.Serializer): width = serializers.IntegerField() height = serializers.IntegerField() name = serializers.CharField(max_length=1024) - has_related_context = serializers.BooleanField() + related_files = serializers.IntegerField() class PluginsSerializer(serializers.Serializer): GIT_INTEGRATION = serializers.BooleanField() diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 1bb8dd6bb68d..f9ae87a6de53 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -1,5 +1,5 @@ # Copyright (C) 2018-2022 Intel Corporation -# Copyright (C) 2022 CVAT.ai Corporation +# Copyright (C) 2022-2023 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -13,7 +13,6 @@ from distutils.util import strtobool from tempfile import mkstemp -import cv2 from django.db.models.query import Prefetch from django.shortcuts import get_object_or_404 import django_rq @@ -48,7 +47,7 @@ from cvat.apps.engine.media_extractors import get_mime from cvat.apps.engine.models import ( Job, Task, Project, Issue, Data, - Comment, StorageMethodChoice, StorageChoice, Image, + Comment, StorageMethodChoice, StorageChoice, CloudProviderChoice, Location ) from cvat.apps.engine.models import CloudStorage as CloudStorageModel @@ -636,7 +635,6 @@ def _get_rq_response(queue, job_id): return response - class DataChunkGetter: def __init__(self, data_type, data_num, data_quality, task_dim): possible_data_type_values = ('chunk', 'frame', 'preview', 'context_image') @@ -696,20 +694,14 @@ def __call__(self, request, start, stop, db_data): return HttpResponse(buf.getvalue(), content_type=mime) elif self.type == 'context_image': - if not (start <= self.number <= stop): - raise ValidationError('The frame number should be in ' + - f'[{start}, {stop}] range') - - image = Image.objects.get(data_id=db_data.id, frame=self.number) - for i in image.related_files.all(): - path = os.path.realpath(str(i.path)) - image = cv2.imread(path) - success, result = cv2.imencode('.JPEG', image) - if not success: - raise Exception('Failed to encode image to ".jpeg" format') - return HttpResponse(io.BytesIO(result.tobytes()), content_type='image/jpeg') - return Response(data='No context image related to the frame', - status=status.HTTP_404_NOT_FOUND) + if start <= self.number <= stop: + cache = MediaCache(self.dimension) + buff, mime = cache.get_frame_context_images(db_data, self.number) + if not buff: + return HttpResponseNotFound() + return HttpResponse(io.BytesIO(buff), content_type=mime) + raise ValidationError('The frame number should be in ' + + f'[{start}, {stop}] range') else: return Response(data='unknown data type {}.'.format(self.type), status=status.HTTP_400_BAD_REQUEST) @@ -1275,7 +1267,7 @@ def metadata(self, request, pk): 'width': item.width, 'height': item.height, 'name': item.path, - 'has_related_context': hasattr(item, 'related_files') and item.related_files.exists() + 'related_files': item.related_files.count() if hasattr(item, 'related_files') else 0 } for item in media] db_data = db_task.data @@ -1747,7 +1739,7 @@ def metadata(self, request, pk): 'width': item.width, 'height': item.height, 'name': item.path, - 'has_related_context': hasattr(item, 'related_files') and item.related_files.exists() + 'related_files': item.related_files.count() if hasattr(item, 'related_files') else 0 } for item in media] db_data.frames = frame_meta diff --git a/tests/cypress/integration/actions_objects2/case_115_ellipse_shape_track_label.js b/tests/cypress/integration/actions_objects2/case_115_ellipse_shape_track_label.js index 658dd0be2269..942a43bf83d3 100644 --- a/tests/cypress/integration/actions_objects2/case_115_ellipse_shape_track_label.js +++ b/tests/cypress/integration/actions_objects2/case_115_ellipse_shape_track_label.js @@ -73,7 +73,7 @@ context('Actions on ellipse.', () => { it('Ellipse rotation/interpolation.', () => { Cypress.config('scrollBehavior', false); cy.get('.cvat-player-last-button').click(); - cy.shapeRotate('#cvat_canvas_shape_4', '19.5'); + cy.shapeRotate('#cvat_canvas_shape_4', '19.7'); testCompareRotate('cvat_canvas_shape_4', 0); // Rotation with shift cy.shapeRotate('#cvat_canvas_shape_4', '15.0', true); diff --git a/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js b/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js index cbfd1b09e1c3..7c3bcd1b24eb 100644 --- a/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js +++ b/tests/cypress/integration/actions_tasks2/case_101_opencv_basic_actions.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -35,8 +36,8 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', () labelName, tracker: 'TrackerMIL', pointsMap: [ - { x: 440, y: 45 }, - { x: 650, y: 150 }, + { x: 430, y: 40 }, + { x: 640, y: 145 }, ], }; @@ -211,10 +212,10 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', () // On each frame text is moved by 5px on x and y axis, // so we expect shape to be close to real text positions cy.get('#cvat_canvas_shape_3').invoke('attr', 'x').then((xVal) => { - expect(parseFloat(xVal)).to.be.closeTo(x + (i - 1) * 5, 1.0); + expect(parseFloat(xVal)).to.be.closeTo(x + (i - 1) * 5, 2.0); }); cy.get('#cvat_canvas_shape_3').invoke('attr', 'y').then((yVal) => { - expect(parseFloat(yVal)).to.be.closeTo(y + (i - 1) * 5, 1.0); + expect(parseFloat(yVal)).to.be.closeTo(y + (i - 1) * 5, 2.0); }); cy.get('#cvat-objects-sidebar-state-item-3') .should('contain', 'RECTANGLE TRACK') diff --git a/tests/cypress/integration/actions_tasks2/case_111_settings_text_size_position_label_content.js b/tests/cypress/integration/actions_tasks2/case_111_settings_text_size_position_label_content.js index 007c60ad57cf..8fe9895b2df0 100644 --- a/tests/cypress/integration/actions_tasks2/case_111_settings_text_size_position_label_content.js +++ b/tests/cypress/integration/actions_tasks2/case_111_settings_text_size_position_label_content.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -45,29 +46,28 @@ context('Settings. Text size/position. Text labels content.', () => { let textTopPosition = 0; let getText; - cy.get(shape).then(($shape) => { - shapeLeftPosition = Math.trunc($shape.position().left); - shapeTopPosition = Math.trunc($shape.position().top); - if (shape === '#cvat_canvas_shape_1') { - shapeWidth = $shape.attr('width'); - shapeHeight = $shape.attr('height'); - } else { - const points = $shape.attr('points').split(' '); - shapeWidth = +points[1].split(',')[0] - +points[0].split(',')[0]; - shapeHeight = +points[2].split(',')[1] - +points[0].split(',')[1]; - } + cy.get(shape).then(([shapeObj]) => { + const shapeBBox = shapeObj.getBoundingClientRect(); + shapeLeftPosition = shapeBBox.left; + shapeTopPosition = shapeBBox.top; + shapeWidth = shapeBBox.width; + shapeHeight = shapeBBox.height; + if (shape === '#cvat_canvas_shape_1') { getText = cy.get('.cvat_canvas_text').first(); } else { getText = cy.get('.cvat_canvas_text').last(); } - getText.then(($text) => { - textLeftPosition = Math.trunc($text.position().left); - textTopPosition = Math.trunc($text.position().top); + + getText.then(([textObj]) => { + const textBBox = textObj.getBoundingClientRect(); + textLeftPosition = textBBox.left; + textTopPosition = textBBox.top; + if (expectedPosition === 'outside') { // Text outside the shape of the right. Slightly below the shape upper edge. expect(+shapeLeftPosition + +shapeWidth).lessThan(+textLeftPosition); - expect(+textTopPosition).to.be.within(+shapeTopPosition, +shapeTopPosition + 10); + expect(+textTopPosition).to.be.within(+shapeTopPosition, +shapeTopPosition + 15); } else { // Text inside the shape expect(+shapeLeftPosition + +shapeWidth / 2).greaterThan(+textLeftPosition); @@ -108,6 +108,21 @@ context('Settings. Text size/position. Text labels content.', () => { }); describe(`Testing case "${caseId}"`, () => { + it('Text font size.', () => { + cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 14px;'); + cy.openSettings(); + + // Change the text size to 16 + cy.get('.cvat-workspace-settings-text-size') + .find('input') + .should('have.attr', 'value', '14') + .clear() + .type('10') + .should('have.attr', 'value', '10'); + cy.closeSettings(); + cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 10px;'); + }); + it('Text position.', () => { testTextPosition('#cvat_canvas_shape_1', 'outside'); testTextPosition('#cvat_canvas_shape_2', 'outside'); @@ -127,21 +142,6 @@ context('Settings. Text size/position. Text labels content.', () => { testTextPosition('#cvat_canvas_shape_2', 'inside'); }); - it('Text font size.', () => { - cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 14px;'); - cy.openSettings(); - - // Change the text size to 16 - cy.get('.cvat-workspace-settings-text-size') - .find('input') - .should('have.attr', 'value', '14') - .clear() - .type('16') - .should('have.attr', 'value', '16'); - cy.closeSettings(); - cy.get('.cvat_canvas_text').should('have.attr', 'style', 'font-size: 16px;'); - }); - it('Text labels content.', () => { cy.openSettings(); cy.get('.cvat-workspace-settings-text-content').within(() => { diff --git a/tests/cypress/integration/actions_tasks2/case_21_canvas_color_feature.js b/tests/cypress/integration/actions_tasks2/case_21_canvas_color_feature.js index 6ba5e100d74c..bfbf055c4722 100644 --- a/tests/cypress/integration/actions_tasks2/case_21_canvas_color_feature.js +++ b/tests/cypress/integration/actions_tasks2/case_21_canvas_color_feature.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -29,10 +30,11 @@ context('Canvas color feature', () => { .click() .should('have.css', 'background-color') .then((colorPickerBgValue) => { - cy.get('.cvat-canvas-container') + cy.get('.cvat-canvas-grid-root') .should('have.css', 'background-color') .then((canvasBgColor) => { - //For each color change, compare the value with the css value background-color of .cvat-canvas-container + // For each color change compare + // the value with the css value background-color of .cvat-canvas-grid-root expect(colorPickerBgValue).to.be.equal(canvasBgColor); }); }); diff --git a/tests/cypress/integration/actions_tasks2/case_30_collapse_sidebar_appearance.js b/tests/cypress/integration/actions_tasks2/case_30_collapse_sidebar_appearance.js index 0d0d3f115241..850699517903 100644 --- a/tests/cypress/integration/actions_tasks2/case_30_collapse_sidebar_appearance.js +++ b/tests/cypress/integration/actions_tasks2/case_30_collapse_sidebar_appearance.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,7 +14,7 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi const createRectangleShape2Points = { points: 'By 2 Points', type: 'Shape', - labelName: labelName, + labelName, firstX: 250, firstY: 350, secondX: 350, @@ -21,11 +22,10 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi }; function checkEqualBackground() { - cy.get('#cvat_canvas_background') - .should('have.css', 'left') - .and((currentValueLeftBackground) => { - currentValueLeftBackground = Number(currentValueLeftBackground.match(/\d+/)); - expect(currentValueLeftBackground).to.be.eq(defaultValueLeftBackground); + cy.get('.cvat-canvas-grid-root') + .then((el) => { + expect(el[0].getBoundingClientRect().left) + .to.be.eq(defaultValueLeftBackground); }); } @@ -34,10 +34,9 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi cy.createRectangle(createRectangleShape2Points); // get default left value from background - cy.get('#cvat_canvas_background') - .should('have.css', 'left') - .then((currentValueLeftBackground) => { - defaultValueLeftBackground = Number(currentValueLeftBackground.match(/\d+/)); + cy.get('.cvat-canvas-grid-root') + .then((el) => { + defaultValueLeftBackground = el[0].getBoundingClientRect().left; }); }); @@ -46,28 +45,15 @@ context('Collapse sidebar/appearance. Check issue 3250 (empty sidebar after resi // hide sidebar cy.get('.cvat-objects-sidebar-sider').click(); cy.get('.cvat-objects-sidebar').should('not.be.visible'); - cy.get('#cvat_canvas_background') - .should('have.css', 'left') - .and((currentValueLeftBackground) => { - currentValueLeftBackground = Number(currentValueLeftBackground.match(/\d+/)); - expect(currentValueLeftBackground).to.be.greaterThan(defaultValueLeftBackground); - }); - - // Check issue 3250 - cy.get('#cvat_canvas_content').invoke('attr', 'style').then((canvasContainerStyle) => { - cy.viewport(2999, 2999); // Resize window - cy.get('#cvat_canvas_content').should('have.attr', 'style').and('not.equal', canvasContainerStyle); - cy.viewport(Cypress.config('viewportWidth'), Cypress.config('viewportHeight')); // Return to the original size - cy.get('#cvat_canvas_content').should('have.attr', 'style').and('equal', canvasContainerStyle); - }); // unhide sidebar cy.get('.cvat-objects-sidebar-sider').click(); cy.get('.cvat-objects-sidebar').should('be.visible'); checkEqualBackground(); - // Before the issue fix the sidebar item did not appear accordingly it was not possible to activate the shape through the sidebar item - cy.get(`#cvat-objects-sidebar-state-item-1`).trigger('mouseover'); + // Before the issue fix the sidebar item did not appear accordingly + // it was not possible to activate the shape through the sidebar item + cy.get('#cvat-objects-sidebar-state-item-1').trigger('mouseover'); cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); }); diff --git a/tests/cypress/integration/actions_tasks2/case_32_attribute_annotation_mode_zoom_margin_feature.js b/tests/cypress/integration/actions_tasks2/case_32_attribute_annotation_mode_zoom_margin_feature.js index 86afb9501160..bc77b67ac147 100644 --- a/tests/cypress/integration/actions_tasks2/case_32_attribute_annotation_mode_zoom_margin_feature.js +++ b/tests/cypress/integration/actions_tasks2/case_32_attribute_annotation_mode_zoom_margin_feature.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -11,7 +12,7 @@ context('Attribute annotation mode (AAM) zoom margin feature', () => { const rectangleShape2Points = { points: 'By 2 Points', type: 'Shape', - labelName: labelName, + labelName, firstX: 100, firstY: 100, secondX: 150, @@ -43,6 +44,8 @@ context('Attribute annotation mode (AAM) zoom margin feature', () => { describe(`Testing case "${caseId}"`, () => { it('Change AAM zoom margin on workspace with rectangle', () => { + cy.get('.cvat-attribute-annotation-sidebar-object-switcher-right').click(); + cy.get('.cvat-attribute-annotation-sidebar-object-switcher-left').click(); cy.get('.cvat-attribute-annotation-sidebar-object-switcher').should('contain', `${labelName} 1 [1/2]`); cy.getScaleValue().then((scaleBeforeChangeZoomMargin) => { changeSettingsZoomMargin(150); diff --git a/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js b/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js index dc5ff8ba4c42..37360f11bb48 100644 --- a/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js +++ b/tests/cypress/integration/actions_tasks2/case_97_export_import_task.js @@ -61,7 +61,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => { .trigger('mousemove') .trigger('mouseover'); cy.get('.svg_select_points_rot').trigger('mousedown', { button: 0 }); - cy.get('.cvat-canvas-container').trigger('mousemove', 340, 150); + cy.get('.cvat-canvas-container').trigger('mousemove', 345, 150); cy.get('.cvat-canvas-container').trigger('mouseup'); cy.get('#cvat_canvas_shape_1').should('have.attr', 'transform'); cy.document().then((doc) => { diff --git a/tests/cypress/integration/actions_tasks3/case_90_context_image.js b/tests/cypress/integration/actions_tasks3/case_90_context_image.js index a2cf22707d3c..4bac12c98d3d 100644 --- a/tests/cypress/integration/actions_tasks3/case_90_context_image.js +++ b/tests/cypress/integration/actions_tasks3/case_90_context_image.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -11,36 +12,6 @@ context('Context images for 2D tasks.', () => { const attrName = `Attr for ${labelName}`; const textDefaultValue = 'color'; const pathToArchive = `../../${__dirname}/assets/case_90/case_90_context_image.zip`; - const createRectangleShape2Points = { - points: 'By 2 Points', - type: 'Shape', - labelName: labelName, - firstX: 250, - firstY: 350, - secondX: 350, - secondY: 450, - }; - - function previewRotate(directionRotation, expectedDeg) { - if (directionRotation === 'right') { - cy.get('[data-icon="rotate-right"]').click(); - } else { - cy.get('[data-icon="rotate-left"]').click(); - } - cy.get('.ant-image-preview-img').should('have.attr', 'style').and('contain', `rotate(${expectedDeg}deg)`); - } - - function previewScaleWheel(zoom, expectedScaleValue) { - cy.get('.ant-image-preview-img') - .trigger('wheel', { deltaY: zoom }) - .should('have.attr', 'style') - .and('contain', `scale3d(${expectedScaleValue})`); - } - - function previewScaleButton(zoom, expectedScaleValue) { - cy.get(`[data-icon="zoom-${zoom}"]`).click(); - cy.get('.ant-image-preview-img').should('have.attr', 'style').and('contain', `scale3d(${expectedScaleValue})`); - } before(() => { cy.visit('auth/login'); @@ -56,65 +27,10 @@ context('Context images for 2D tasks.', () => { describe(`Testing case "${caseId}"`, () => { it('Check a context image.', () => { - cy.get('.cvat-context-image').should('exist').and('be.visible'); + cy.get('.cvat-context-image-wrapper').should('exist').and('be.visible'); cy.get('.cvat-player-next-button').click(); - cy.get('.cvat-context-image').should('exist').and('be.visible'); // Check a context image on the second frame + cy.get('.cvat-context-image-wrapper').should('exist').and('be.visible'); // Check a context image on the second frame cy.get('.cvat-player-previous-button').click(); - cy.get('.cvat-context-image-switcher').click(); // Hide a context image - cy.get('.cvat-context-image').should('not.exist'); - cy.get('.cvat-context-image-switcher').click(); // Unhide - cy.get('.cvat-context-image').should('exist').and('be.visible'); - }); - - it('Preview a context image. Rotate.', () => { - let degRight = 0; - let degLeft = 360; - cy.contains('Preview').click(); - cy.get('.ant-image-preview-mask').should('exist'); - for (let numberSpins = 0; numberSpins < 4; numberSpins++) { - degRight += 90; - previewRotate('right', String(degRight)); - } - for (let numberSpins = 0; numberSpins < 4; numberSpins++) { - degLeft -= 90; - previewRotate('left', String(degLeft)); - } - }); - - it('Preview a context image. Scale.', () => { - previewScaleWheel(-1, '2, 2, 1'); - previewScaleWheel(1, '1, 1, 1'); - previewScaleButton('in', '2, 2, 1'); - previewScaleButton('out', '1, 1, 1'); - }); - - it('Preview a context image. Move.', () => { - cy.get('.ant-image-preview-img-wrapper') - .should('have.attr', 'style') - .then((translate3d) => { - cy.get('.ant-image-preview-img').trigger('mousedown', { button: 0 }); - cy.get('.ant-image-preview-moving').should('exist'); - cy.get('.ant-image-preview-wrap').trigger('mousemove', 300, 300); - cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('not.equal', translate3d); - cy.get('.ant-image-preview-img').trigger('mouseup'); - cy.get('.ant-image-preview-moving').should('not.exist'); - cy.get('.ant-image-preview-img-wrapper').should('have.attr', 'style').and('equal', translate3d); - }); - }); - - it('Preview a context image. Cancel preview.', () => { - cy.get('.ant-image-preview-wrap').type('{Esc}'); - cy.get('.ant-image-preview-wrap').should('have.attr', 'style').and('contain', 'display: none'); - }); - - it('Checking issue "Context image disappears after undo/redo".', () => { - cy.createRectangle(createRectangleShape2Points); - cy.contains('.cvat-annotation-header-button', 'Undo').click(); - cy.get('.cvat-context-image').should('have.attr', 'src'); - cy.get('#cvat_canvas_shape_1').should('not.exist'); - cy.contains('.cvat-annotation-header-button', 'Redo').click(); - cy.get('.cvat-context-image').should('have.attr', 'src'); - cy.get('#cvat_canvas_shape_1').should('exist'); }); }); }); diff --git a/tests/cypress/integration/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js b/tests/cypress/integration/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js index 324ab9b66da4..0f189ff0214c 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js +++ b/tests/cypress/integration/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -14,22 +14,22 @@ context('Canvas 3D functionality. Grouping.', () => { const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js'; const firstCuboidCreationParams = { labelName, - x: 480, - y: 150, + x: 400, + y: 200, }; const secondCuboidCreationParams = { labelName, - x: 480, - y: 200, + x: 400, + y: 280, }; const thirdCuboidCreationParams = { labelName, - x: 530, - y: 150, + x: 500, + y: 280, }; const fourthCuboidCreationParams = { labelName, - x: 530, + x: 500, y: 200, }; const yellowHex = 'fcbe03'; @@ -64,8 +64,8 @@ context('Canvas 3D functionality. Grouping.', () => { describe(`Testing case "${caseId}"`, () => { it('Grouping two cuboids.', () => { cy.get('.cvat-group-control').click(); - cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 480, 200).click(480, 200); - cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 530, 150).click(530, 150); + cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 400, 280).click(400, 280); + cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 500, 280).click(500, 280); cy.get('.cvat-group-control').click(); cy.changeAppearance('Group'); cy.get('#cvat-objects-sidebar-state-item-1').invoke('attr', 'style').then((bgColorItem1) => { @@ -105,8 +105,8 @@ context('Canvas 3D functionality. Grouping.', () => { it('Reset group.', () => { cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_before_reset_group'); cy.get('.cvat-group-control').click(); - cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 480, 200).click(480, 200); - cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 530, 150).click(530, 150); + cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 400, 280).click(400, 280); + cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 500, 280).click(500, 280); cy.get('body').type('{Shift}g'); cy.get('#cvat-objects-sidebar-state-item-2').invoke('attr', 'style').then((bgColorItem2) => { expect(bgColorItem).to.be.equal(bgColorItem2); diff --git a/tests/cypress/integration/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js b/tests/cypress/integration/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js index d262052c7580..14a34e91f487 100644 --- a/tests/cypress/integration/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js +++ b/tests/cypress/integration/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js @@ -1,7 +1,10 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT +/* eslint-disable cypress/no-unnecessary-waiting */ + /// import { taskName, labelName } from '../../support/const_canvas3d'; @@ -11,8 +14,9 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => { const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_88_canvas3d_functionality_save_job_remove_annotation.js'; const cuboidCreationParams = { - labelName: labelName, + labelName, }; + const waitTime = 2000; before(() => { cy.openTask(taskName); @@ -24,20 +28,14 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => { describe(`Testing case "${caseId}"`, () => { it('Save a job. Reopen the job.', () => { - const waitTime = 1000; - cy.wait(waitTime); cy.saveJob('PATCH', 200, 'saveJob'); - cy.wait(waitTime); cy.goToTaskList(); - cy.wait(waitTime); cy.openTaskJob(taskName); - cy.wait(waitTime); // Waiting for the point cloud to display cy.get('.cvat-objects-sidebar-state-item').then((sidebarStateItem) => { expect(sidebarStateItem.length).to.be.equal(1); }); cy.wait(waitTime); cy.customScreenshot('.cvat-canvas3d-topview', 'canvas3d_topview_after_reopen_job'); - cy.wait(waitTime); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_topview_before_all.png`, `${screenshotsPath}/canvas3d_topview_after_reopen_job.png`, @@ -49,6 +47,7 @@ context('Canvas 3D functionality. Save a job. Remove annotations.', () => { cy.saveJob('PUT'); cy.contains('Saving changes on the server').should('be.hidden'); cy.get('.cvat-objects-sidebar-state-item').should('not.exist'); + cy.wait(waitTime); cy.customScreenshot('.cvat-canvas3d-topview', 'canvas3d_topview_after_remove_annotations'); cy.compareImagesAndCheckResult( `${screenshotsPath}/canvas3d_topview_after_reopen_job.png`, diff --git a/tests/cypress/integration/canvas3d_functionality_2/case_56_canvas3d_functionality_basic_actions.js b/tests/cypress/integration/canvas3d_functionality_2/case_56_canvas3d_functionality_basic_actions.js index d54f162bf181..349c0575ed47 100644 --- a/tests/cypress/integration/canvas3d_functionality_2/case_56_canvas3d_functionality_basic_actions.js +++ b/tests/cypress/integration/canvas3d_functionality_2/case_56_canvas3d_functionality_basic_actions.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -41,7 +41,7 @@ context('Canvas 3D functionality. Basic actions.', () => { function testPerspectiveChangeOnWheel(screenshotNameBefore, screenshotNameAfter) { cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameBefore); for (let i = 0; i < 3; i++) { - cy.get('.cvat-canvas3d-perspective').trigger('wheel', { deltaY: -50 }); + cy.get('.cvat-canvas3d-perspective canvas').trigger('wheel', { deltaY: -50 }); } cy.customScreenshot('.cvat-canvas3d-perspective', screenshotNameAfter); cy.compareImagesAndCheckResult( @@ -52,9 +52,12 @@ context('Canvas 3D functionality. Basic actions.', () => { function testTopSideFrontChangeOnWheel(element, screenshotNameBefore, screenshotNameAfter) { cy.customScreenshot(element, screenshotNameBefore); - for (let i = 0; i < 3; i++) { - cy.get(element).trigger('wheel', { deltaY: -100 }); - } + cy.get(element).within(() => { + for (let i = 0; i < 3; i++) { + cy.get('.cvat-canvas3d-fullsize canvas').trigger('wheel', { deltaY: -100 }); + } + }); + cy.customScreenshot(element, screenshotNameAfter); cy.compareImagesAndCheckResult( `${screenshotsPath}/${screenshotNameBefore}.png`, @@ -63,10 +66,7 @@ context('Canvas 3D functionality. Basic actions.', () => { } function testContextImage() { - cy.get('.cvat-context-image-wrapper img').should('exist').and('be.visible'); - cy.get('.cvat-context-image-switcher').click(); // Context image hide - cy.get('.cvat-context-image-wrapper img').should('not.exist'); - cy.get('.cvat-context-image-switcher').click(); // Context image show + cy.get('.cvat-context-image-wrapper canvas').should('exist').and('be.visible'); } function testControlButtonTooltip(button, expectedTooltipText) { diff --git a/tests/cypress/integration/canvas3d_functionality_2/case_62_canvas3d_functionality_views_resize.js b/tests/cypress/integration/canvas3d_functionality_2/case_62_canvas3d_functionality_views_resize.js deleted file mode 100644 index 6112331ad874..000000000000 --- a/tests/cypress/integration/canvas3d_functionality_2/case_62_canvas3d_functionality_views_resize.js +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (C) 2021-2022 Intel Corporation -// -// SPDX-License-Identifier: MIT - -/// - -import { taskName } from '../../support/const_canvas3d'; - -context('Canvas 3D functionality. Resize views.', () => { - const caseId = '62'; - let widthHeightArrBeforeResize = []; - let widthHeightArrAfterResize = []; - - function getViewWidthHeight(element, arrToPush) { - cy.get(element) - .find('canvas') - .invoke('attr', 'width') - .then(($topviewWidth) => { - cy.get(element) - .find('canvas') - .invoke('attr', 'height') - .then(($topviewHeight) => { - arrToPush.push([$topviewWidth, $topviewHeight]); - }); - }); - } - - before(() => { - cy.openTaskJob(taskName); - getViewWidthHeight('.cvat-canvas3d-perspective', widthHeightArrBeforeResize); - getViewWidthHeight('.cvat-canvas3d-topview', widthHeightArrBeforeResize); - getViewWidthHeight('.cvat-canvas3d-sideview', widthHeightArrBeforeResize); - getViewWidthHeight('.cvat-canvas3d-frontview', widthHeightArrBeforeResize); - }); - - describe(`Testing case "${caseId}"`, () => { - it('Resizing perspective.', () => { - cy.get('.cvat-resizable-handle-horizontal').trigger('mousedown', { button: 0, scrollBehavior: false }); - cy.get('.cvat-canvas3d-perspective') - .trigger('mousemove', 600, 300, { scrollBehavior: false }) - .trigger('mouseup'); - getViewWidthHeight('.cvat-canvas3d-perspective', widthHeightArrAfterResize); - }); - - it('Resizing topview.', () => { - cy.get('.cvat-resizable-handle-vertical-top').trigger('mousedown', { button: 0, scrollBehavior: false }); - cy.get('.cvat-canvas3d-topview') - .trigger('mousemove', 200, 200, { scrollBehavior: false }) - .trigger('mouseup'); - getViewWidthHeight('.cvat-canvas3d-topview', widthHeightArrAfterResize); - }); - - it('Resizing sideview.', () => { - cy.get('.cvat-resizable-handle-vertical-side').trigger('mousedown', { button: 0, scrollBehavior: false }); - cy.get('.cvat-canvas3d-frontview') - .trigger('mousemove', 200, 200, { scrollBehavior: false }) - .trigger('mouseup'); - getViewWidthHeight('.cvat-canvas3d-sideview', widthHeightArrAfterResize); - getViewWidthHeight('.cvat-canvas3d-frontview', widthHeightArrAfterResize); - }); - - it('Checking for elements resizing.', () => { - expect(widthHeightArrBeforeResize[0][0]).to.be.equal(widthHeightArrAfterResize[0][0]); // Width of cvat-canvas3d-perspective before and after didn't change - expect(widthHeightArrBeforeResize[0][1]).not.be.equal(widthHeightArrAfterResize[0][1]); // Height of cvat-canvas3d-perspective changed - expect(widthHeightArrAfterResize[1][1]) - .to.be.equal(widthHeightArrAfterResize[2][1]) - .to.be.equal(widthHeightArrAfterResize[3][1]); // Top/side/front has equal height after changes - [ - [widthHeightArrBeforeResize[1][0], widthHeightArrAfterResize[1][0]], - [widthHeightArrBeforeResize[2][0], widthHeightArrAfterResize[2][0]], - [widthHeightArrBeforeResize[3][0], widthHeightArrAfterResize[3][0]], - ].forEach(([widthBefore, widthAfter]) => { - expect(widthBefore).not.be.equal(widthAfter); // Width of top/side/front changed - }); - [ - [widthHeightArrBeforeResize[1][1], widthHeightArrAfterResize[1][1]], - [widthHeightArrBeforeResize[2][1], widthHeightArrAfterResize[2][1]], - [widthHeightArrBeforeResize[3][1], widthHeightArrAfterResize[3][1]], - ].forEach(([heightBefore, heightAfter]) => { - expect(heightBefore).not.be.equal(heightAfter); // Height of top/side/front changed - }); - }); - }); -}); diff --git a/tests/cypress/integration/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js b/tests/cypress/integration/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js index ffe705dddc07..3f14bfe4c2b3 100644 --- a/tests/cypress/integration/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js +++ b/tests/cypress/integration/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js @@ -11,7 +11,6 @@ import { taskName, labelName } from '../../support/const_canvas3d'; context('Canvas 3D functionality. Opacity. Outlined borders.', () => { const caseId = '82'; - const screenshotsPath = 'cypress/screenshots/canvas3d_functionality_2/case_82_canvas3d_functionality_cuboid_opacity_outlined_borders.js'; const cuboidCreationParams = { labelName, x: 500, @@ -21,48 +20,48 @@ context('Canvas 3D functionality. Opacity. Outlined borders.', () => { before(() => { cy.openTask(taskName); cy.openJob(); - cy.wait(1000); // Waiting for the point cloud to display + cy.wait(2000); // Waiting for the point cloud to display cy.create3DCuboid(cuboidCreationParams); cy.get('.cvat-canvas3d-perspective').trigger('mousemove').click(); // Deactivate the cuboiud - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_deactivate_cuboid'); }); + const getScene = (el) => el.scene.children[0]; + const getFirstChild = (el) => getScene(el).children[0]; + const getWireframe = (el) => getFirstChild(el).children[0]; + describe(`Testing case "${caseId}"`, () => { it('Change opacity to 100. To 0.', () => { cy.get('.cvat-appearance-opacity-slider').click('right'); cy.get('.cvat-appearance-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 100); - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_opacty_100'); - cy.compareImagesAndCheckResult( - `${screenshotsPath}/canvas3d_perspective_deactivate_cuboid.png`, - `${screenshotsPath}/canvas3d_perspective_opacty_100.png`, - ); + cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => { + expect(getFirstChild(el).material.opacity).to.equal(1); + }); + cy.get('.cvat-appearance-opacity-slider').click('left'); cy.get('.cvat-appearance-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 0); - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_opacty_0'); - cy.compareImagesAndCheckResult( - `${screenshotsPath}/canvas3d_perspective_opacty_100.png`, - `${screenshotsPath}/canvas3d_perspective_opacty_0.png`, - ); + cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => { + expect(getFirstChild(el).material.opacity).to.equal(0); + }); + + cy.get('body').click(); }); it('Change selected opacity to 100. To 0.', () => { cy.get('.cvat-appearance-selected-opacity-slider').click('right'); cy.get('.cvat-appearance-selected-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 100); + cy.get('body').click(); cy.get('.cvat-canvas3d-perspective').trigger('mousemove').trigger('mousemove', 500, 250).wait(1000); // Waiting for the cuboid activation - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_selected_opacty_100'); - cy.compareImagesAndCheckResult( - `${screenshotsPath}/canvas3d_perspective_opacty_100.png`, - `${screenshotsPath}/canvas3d_perspective_selected_opacty_100.png`, - true, // No diff between the images - ); + + cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => { + expect(el.scene.children[0].children[0].material.opacity).to.equal(1); + }); + cy.get('.cvat-appearance-selected-opacity-slider').click('left'); cy.get('.cvat-appearance-selected-opacity-slider').find('[role="slider"]').should('have.attr', 'aria-valuenow', 0); - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_selected_opacty_0'); - cy.compareImagesAndCheckResult( - `${screenshotsPath}/canvas3d_perspective_opacty_0.png`, - `${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`, - true, // No diff between the images - ); + + cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => { + expect(getFirstChild(el).material.opacity).to.equal(0); + }); }); it('Enable/disable outlined borders.', () => { @@ -72,18 +71,14 @@ context('Canvas 3D functionality. Opacity. Outlined borders.', () => { cy.get('div[title="#ff007c"]').click(); cy.contains('Ok').click(); }); - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_enable_outlined_borders'); - cy.compareImagesAndCheckResult( - `${screenshotsPath}/canvas3d_perspective_enable_outlined_borders.png`, - `${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`, - ); + cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => { + expect({ ...getWireframe(el).material.color }).to.deep.equal({ r: 1, g: 0, b: 0.48627450980392156 }); + }); + cy.get('.cvat-appearance-outlinded-borders-checkbox').find('[type="checkbox"]').uncheck().should('not.be.checked'); - cy.customScreenshot('.cvat-canvas3d-perspective', 'canvas3d_perspective_disable_outlined_borders'); - cy.compareImagesAndCheckResult( - `${screenshotsPath}/canvas3d_perspective_disable_outlined_borders.png`, - `${screenshotsPath}/canvas3d_perspective_selected_opacty_0.png`, - true, // No diff between the images - ); + cy.get('.cvat-canvas3d-perspective canvas').then(([el]) => { + expect({ ...getWireframe(el).material.color }).to.deep.equal({ ...getFirstChild(el).material.color }); + }); }); }); }); diff --git a/tests/cypress/support/commands_canvas3d.js b/tests/cypress/support/commands_canvas3d.js index 3343429ed1ca..e94734696783 100644 --- a/tests/cypress/support/commands_canvas3d.js +++ b/tests/cypress/support/commands_canvas3d.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -29,14 +29,13 @@ Cypress.Commands.add('create3DCuboid', (cuboidCreationParams) => { }); Cypress.Commands.add('customScreenshot', (element, screenshotName) => { - let getEl; - let padding; - if (element.includes('perspective')) { - getEl = cy.get(element); - padding = -130; - } else { - getEl = cy.get(element).find('.cvat-canvas3d-fullsize'); - padding = -40; - } - getEl.screenshot(screenshotName, { padding }); + cy.get(`${element} canvas`).then(([$el]) => ($el.getBoundingClientRect())).then((rect) => { + cy.screenshot(screenshotName, { + overwrite: true, + capture: 'fullPage', + clip: { + x: rect.x, y: rect.y, width: rect.width, height: rect.height, + }, + }); + }); }); diff --git a/yarn.lock b/yarn.lock index e0ce0e21dadb..b55a887f345e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1884,7 +1884,7 @@ resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.0.5.tgz#9262f269de36f1e9248aeb9dee0ee9d10065e043" integrity sha512-gnaNmo1OJiYNBFAZMZdqLZ3hKx2ee4ksAzqhKWBxuQ61PmhINHMcvIqsGmyCD1WFKCkwRt9NFhMSmKE6AgYY+w== -"@types/prettier@^2.0.0", "@types/prettier@^2.1.5": +"@types/prettier@2.4.1", "@types/prettier@^2.0.0", "@types/prettier@^2.1.5": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== @@ -1917,31 +1917,31 @@ "@types/react" "*" "@types/reactcss" "*" -"@types/react-dom@^16.9.14": - version "16.9.17" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.17.tgz#29100cbcc422d7b7dba7de24bb906de56680dd34" - integrity sha512-qSRyxEsrm5btPXnowDOs5jSkgT8ldAA0j6Qp+otHUh+xHzy3sXmgNfyhucZjAjkgpdAUw9rJe0QRtX/l+yaS4g== +"@types/react-dom@^16.9.14", "@types/react-dom@^18.0.5": + version "18.0.10" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352" + integrity sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg== dependencies: - "@types/react" "^16" + "@types/react" "*" -"@types/react-redux@^7.1.18": - version "7.1.24" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" - integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== +"@types/react-grid-layout@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.2.tgz#9f195666a018a5ae2b773887e3b552cb4378d67f" + integrity sha512-ZzpBEOC1JTQ7MGe1h1cPKSLP4jSWuxc+yvT4TsAlEW9+EFPzAf8nxQfFd7ea9gL17Em7PbwJZAsiwfQQBUklZQ== dependencies: - "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" -"@types/react-resizable@^3.0.1": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/react-resizable/-/react-resizable-3.0.2.tgz#3c914be6b02c8d6864b82ffb6461b2e8a771fb75" - integrity sha512-4rHjZDQmSpFqRlNzlcnF5tpOG5fBcMuDlvD+qT3XHAJLKGx/FC3iDQ9li9tHW53ecWwZzHTPCGvz5vNWQN+v/Q== +"@types/react-redux@^7.1.18", "@types/react-redux@^7.1.24": + version "7.1.25" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.25.tgz#de841631205b24f9dfb4967dd4a7901e048f9a88" + integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg== dependencies: + "@types/hoist-non-react-statics" "^3.3.0" "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" -"@types/react-router-dom@^5.1.9": +"@types/react-router-dom@^5.1.9", "@types/react-router-dom@^5.3.3": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== @@ -1965,19 +1965,10 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "17.0.48" - resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.48.tgz#a4532a8b91d7b27b8768b6fc0c3bccb760d15a6c" - integrity sha512-zJ6IYlJ8cYYxiJfUaZOQee4lh99mFihBoqkOSEGV+dFi9leROW6+PgstzQ+w3gWTnUfskALtQPGHK6dYmPj+2A== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@^16", "@types/react@^16.14.15": - version "16.14.34" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.34.tgz#d129324ffda312044e1c47aab18696e4ed493282" - integrity sha512-b99nWeGGReLh6aKBppghVqp93dFJtgtDOzc8NXM6hewD8PQ2zZG5kBLgbx+VJr7Q7WBMjHxaIl3dwpwwPIUgyA== +"@types/react@*", "@types/react@^16", "@types/react@^16.14.15", "@types/react@^17.0.30": + version "17.0.52" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.52.tgz#10d8b907b5c563ac014a541f289ae8eaa9bf2e9b" + integrity sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -4107,7 +4098,7 @@ custom-error-instance@2.1.1: integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg== "cvat-canvas3d@link:./cvat-canvas3d": - version "0.0.6" + version "0.0.7" dependencies: "@types/three" "^0.125.3" camera-controls "^1.25.3" @@ -4115,7 +4106,7 @@ custom-error-instance@2.1.1: three "^0.126.1" "cvat-canvas@link:./cvat-canvas": - version "2.16.1" + version "2.16.2" dependencies: "@types/fabric" "^4.5.7" "@types/polylabel" "^1.0.5" @@ -4128,7 +4119,7 @@ custom-error-instance@2.1.1: svg.select.js "3.0.1" "cvat-core@link:./cvat-core": - version "7.4.0" + version "8.0.0" dependencies: axios "^0.27.2" browser-or-node "^2.0.0" @@ -4145,7 +4136,7 @@ custom-error-instance@2.1.1: tus-js-client "^3.0.1" "cvat-data@link:./cvat-data": - version "1.0.2" + version "1.1.0" dependencies: async-mutex "^0.4.0" jszip "3.10.1" @@ -7906,6 +7897,11 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ== +lodash.isequal@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -10533,7 +10529,7 @@ react-dom@^16.14.0: prop-types "^15.6.2" scheduler "^0.19.1" -react-draggable@^4.0.3: +react-draggable@^4.0.0, react-draggable@^4.0.3: version "4.4.5" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c" integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g== @@ -10541,6 +10537,17 @@ react-draggable@^4.0.3: clsx "^1.1.1" prop-types "^15.8.1" +react-grid-layout@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff" + integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw== + dependencies: + clsx "^1.1.1" + lodash.isequal "^4.0.0" + prop-types "^15.8.1" + react-draggable "^4.0.0" + react-resizable "^3.0.4" + react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"