diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index f3ec1d48262..a74f70c5437 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -5,38 +5,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - id: files - uses: tj-actions/changed-files@v41.0.0 - with: - files: | - cvat-sdk/**/*.py - cvat-cli/**/*.py - tests/python/**/*.py - cvat/apps/quality_control/**/*.py - cvat/apps/analytics_report/**/*.py - dir_names: true - name: Run checks - env: - PR_FILES_AM: ${{ steps.files.outputs.added_modified }} - PR_FILES_RENAMED: ${{ steps.files.outputs.renamed }} run: | - # If different modules use different Black configs, - # we need to run Black for each python component group separately. - # Otherwise, they all will use the same config. + pipx install $(grep "^black" ./cvat-cli/requirements/development.txt) - UPDATED_DIRS="${{steps.files.outputs.all_changed_files}}" + echo "Black version: $(black --version)" - if [[ ! -z $UPDATED_DIRS ]]; then - pipx install $(egrep "black.*" ./cvat-cli/requirements/development.txt) - - echo "Black version: "$(black --version) - echo "The dirs will be checked: $UPDATED_DIRS" - EXIT_CODE=0 - for DIR in $UPDATED_DIRS; do - black --check --diff $DIR || EXIT_CODE=$(($? | $EXIT_CODE)) || true - done - exit $EXIT_CODE - else - echo "No files with the \"py\" extension found" - fi + black --check --diff . diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9a4fd9584..42d2893080b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## \[2.21.3\] - 2024-10-31 + +### Changed + +- CLI no longer prints the stack trace in case of HTTP errors + () + +### Removed + +- Dropped support for Python 3.8 since its EOL was on 2024-10-07 + () + +### Fixed + +- Requests page crush with `Cannot read property 'target' of undefined` error + () + +- Tags in ground truth job were displayed as `tag (GT)` + () + +- Tags in ground truth job couldn't be deleted via `x` button + () + +- Exception 'Canvas is busy' when change frame during drag/resize a track + () + +- A shape gets shifted if auto save triggered during dragging + () + ## \[2.21.2\] - 2024-10-24 diff --git a/changelog.d/20241018_142148_klakhov_hide_mask.md b/changelog.d/20241018_142148_klakhov_hide_mask.md new file mode 100644 index 00000000000..4c79bfdacfa --- /dev/null +++ b/changelog.d/20241018_142148_klakhov_hide_mask.md @@ -0,0 +1,3 @@ +### Added + +- Feature to hide a mask during editing () diff --git a/changelog.d/20241022_121638_klakhov_fix_request_status_crush.md b/changelog.d/20241022_121638_klakhov_fix_request_status_crush.md deleted file mode 100644 index 082b59a70d4..00000000000 --- a/changelog.d/20241022_121638_klakhov_fix_request_status_crush.md +++ /dev/null @@ -1,4 +0,0 @@ -### Fixed - -- Requests page crush with `Cannot read property 'target' of undefined` error - () diff --git a/changelog.d/20241023_120236_klakhov_improve_gt_tags.md b/changelog.d/20241023_120236_klakhov_improve_gt_tags.md deleted file mode 100644 index 79c1e724a35..00000000000 --- a/changelog.d/20241023_120236_klakhov_improve_gt_tags.md +++ /dev/null @@ -1,7 +0,0 @@ -### Fixed - -- Tags in ground truth job were displayed as `tag (GT)` - () - -- Tags in ground truth job couldn't be deleted via `x` button - () diff --git a/changelog.d/20241028_140908_sekachev.bs.md b/changelog.d/20241028_140908_sekachev.bs.md deleted file mode 100644 index 507346e3d05..00000000000 --- a/changelog.d/20241028_140908_sekachev.bs.md +++ /dev/null @@ -1,4 +0,0 @@ -### Fixed - -- Exception 'Canvas is busy' when change frame during drag/resize a track - () diff --git a/changelog.d/20241028_140945_sekachev.bs.md b/changelog.d/20241028_140945_sekachev.bs.md deleted file mode 100644 index fd401e390a1..00000000000 --- a/changelog.d/20241028_140945_sekachev.bs.md +++ /dev/null @@ -1,4 +0,0 @@ -### Fixed - -- A shape gets shifted if auto save triggered during dragging - () diff --git a/changelog.d/20241029_105216_maria_do_not_export_honeypots_when_exporting_project.md b/changelog.d/20241029_105216_maria_do_not_export_honeypots_when_exporting_project.md new file mode 100644 index 00000000000..e6d8950a55b --- /dev/null +++ b/changelog.d/20241029_105216_maria_do_not_export_honeypots_when_exporting_project.md @@ -0,0 +1,4 @@ +### Fixed + +- Exporting projects with tasks containing honeypots. Honeypots are no longer exported. + () diff --git a/changelog.d/20241029_113229_maria_drop_python_3_8_support.md b/changelog.d/20241029_113229_maria_drop_python_3_8_support.md deleted file mode 100644 index ca57734aa1d..00000000000 --- a/changelog.d/20241029_113229_maria_drop_python_3_8_support.md +++ /dev/null @@ -1,4 +0,0 @@ -### Removed - -- Dropped support for Python 3.8 since its EOL was on 2024-10-07 - () diff --git a/changelog.d/20241029_120317_dmitrii.lavrukhin_remove_business.md b/changelog.d/20241029_120317_dmitrii.lavrukhin_remove_business.md new file mode 100644 index 00000000000..654fc263dbb --- /dev/null +++ b/changelog.d/20241029_120317_dmitrii.lavrukhin_remove_business.md @@ -0,0 +1,4 @@ +### Removed + +- Removed unused business group + () diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 2b24ff47e34..c89e7506854 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.20.9", + "version": "2.20.10", "type": "module", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 2c7a1f08d20..0ad62484c14 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -96,6 +96,7 @@ export interface Configuration { controlPointsSize?: number; outlinedBorders?: string | false; resetZoom?: boolean; + hideEditedObject?: boolean; } export interface BrushTool { @@ -416,6 +417,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { textPosition: consts.DEFAULT_SHAPE_TEXT_POSITION, textContent: consts.DEFAULT_SHAPE_TEXT_CONTENT, undefinedAttrValue: consts.DEFAULT_UNDEFINED_ATTR_VALUE, + hideEditedObject: false, }, imageBitmap: false, image: null, @@ -981,6 +983,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.configuration.CSSImageFilter = configuration.CSSImageFilter; } + if (typeof configuration.hideEditedObject === 'boolean') { + this.data.configuration.hideEditedObject = configuration.hideEditedObject; + } + this.notify(UpdateReasons.CONFIG_UPDATED); } diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts index b7e9cbb9013..77b674dec05 100644 --- a/cvat-canvas/src/typescript/drawHandler.ts +++ b/cvat-canvas/src/typescript/drawHandler.ts @@ -5,7 +5,7 @@ import * as SVG from 'svg.js'; import 'svg.draw.js'; -import './svg.patch'; +import { CIRCLE_STROKE } from './svg.patch'; import { AutoborderHandler } from './autoborderHandler'; import { @@ -104,6 +104,7 @@ export class DrawHandlerImpl implements DrawHandler { private controlPointsSize: number; private selectedShapeOpacity: number; private outlinedBorders: string; + private isHidden: boolean; // we should use any instead of SVG.Shape because svg plugins cannot change declared interface // so, methods like draw() just undefined for SVG.Shape, but nevertheless they exist @@ -1276,6 +1277,7 @@ export class DrawHandlerImpl implements DrawHandler { this.selectedShapeOpacity = configuration.selectedShapeOpacity; this.outlinedBorders = configuration.outlinedBorders || 'black'; this.autobordersEnabled = false; + this.isHidden = false; this.startTimestamp = Date.now(); this.onDrawDoneDefault = onDrawDone; this.canvas = canvas; @@ -1301,10 +1303,28 @@ export class DrawHandlerImpl implements DrawHandler { }); } + private strokePoint(point: SVG.Element): void { + point.attr('stroke', this.isHidden ? 'none' : CIRCLE_STROKE); + point.fill({ opacity: this.isHidden ? 0 : 1 }); + } + + private updateHidden(value: boolean) { + this.isHidden = value; + + if (value) { + this.canvas.attr('pointer-events', 'none'); + } else { + this.canvas.attr('pointer-events', 'all'); + } + } + public configurate(configuration: Configuration): void { this.controlPointsSize = configuration.controlPointsSize; this.selectedShapeOpacity = configuration.selectedShapeOpacity; this.outlinedBorders = configuration.outlinedBorders || 'black'; + if (this.isHidden !== configuration.hideEditedObject) { + this.updateHidden(configuration.hideEditedObject); + } const isFillableRect = this.drawData && this.drawData.shapeType === 'rectangle' && @@ -1315,15 +1335,26 @@ export class DrawHandlerImpl implements DrawHandler { const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon'; if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) { - this.drawInstance.fill({ opacity: configuration.selectedShapeOpacity }); + this.drawInstance.fill({ + opacity: configuration.hideEditedObject ? 0 : configuration.selectedShapeOpacity, + }); + } + + if (this.drawInstance && (isFilalblePolygon)) { + const paintHandler = this.drawInstance.remember('_paintHandler'); + if (paintHandler) { + for (const point of (paintHandler as any).set.members) { + this.strokePoint(point); + } + } } if (this.drawInstance && this.drawInstance.attr('stroke')) { - this.drawInstance.attr('stroke', this.outlinedBorders); + this.drawInstance.attr('stroke', configuration.hideEditedObject ? 'none' : this.outlinedBorders); } if (this.pointsGroup && this.pointsGroup.attr('stroke')) { - this.pointsGroup.attr('stroke', this.outlinedBorders); + this.pointsGroup.attr('stroke', configuration.hideEditedObject ? 'none' : this.outlinedBorders); } this.autobordersEnabled = configuration.autoborders; @@ -1369,6 +1400,7 @@ export class DrawHandlerImpl implements DrawHandler { const paintHandler = this.drawInstance.remember('_paintHandler'); for (const point of (paintHandler as any).set.members) { + this.strokePoint(point); point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`); point.attr('r', `${this.controlPointsSize / geometry.scale}`); } diff --git a/cvat-canvas/src/typescript/editHandler.ts b/cvat-canvas/src/typescript/editHandler.ts index 567eea29c7d..84ecb1684ad 100644 --- a/cvat-canvas/src/typescript/editHandler.ts +++ b/cvat-canvas/src/typescript/editHandler.ts @@ -472,7 +472,7 @@ export class EditHandlerImpl implements EditHandler { const paintHandler = this.editLine.remember('_paintHandler'); - for (const point of (paintHandler as any).set.members) { + for (const point of paintHandler.set.members) { point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`); point.attr('r', `${this.controlPointsSize / geometry.scale}`); } diff --git a/cvat-canvas/src/typescript/masksHandler.ts b/cvat-canvas/src/typescript/masksHandler.ts index cdaa4d86d2f..ca6e5e469a6 100644 --- a/cvat-canvas/src/typescript/masksHandler.ts +++ b/cvat-canvas/src/typescript/masksHandler.ts @@ -6,7 +6,7 @@ import { fabric } from 'fabric'; import debounce from 'lodash/debounce'; import { - DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy, + DrawData, MasksEditData, Geometry, Configuration, BrushTool, ColorBy, Position, } from './canvasModel'; import consts from './consts'; import { DrawHandler } from './drawHandler'; @@ -61,10 +61,11 @@ export class MasksHandlerImpl implements MasksHandler { private editData: MasksEditData | null; private colorBy: ColorBy; - private latestMousePos: { x: number; y: number; }; + private latestMousePos: Position; private startTimestamp: number; private geometry: Geometry; private drawingOpacity: number; + private isHidden: boolean; private keepDrawnPolygon(): void { const canvasWrapper = this.canvas.getElement().parentElement; @@ -217,12 +218,29 @@ export class MasksHandlerImpl implements MasksHandler { private imageDataFromCanvas(wrappingBBox: WrappingBBox): Uint8ClampedArray { const imageData = this.canvas.toCanvasElement() .getContext('2d').getImageData( - wrappingBBox.left, wrappingBBox.top, - wrappingBBox.right - wrappingBBox.left + 1, wrappingBBox.bottom - wrappingBBox.top + 1, + wrappingBBox.left, + wrappingBBox.top, + wrappingBBox.right - wrappingBBox.left + 1, + wrappingBBox.bottom - wrappingBBox.top + 1, ).data; return imageData; } + private updateHidden(value: boolean) { + this.isHidden = value; + + // Need to update style of upper canvas explicitly because update of default cursor is not applied immediately + // https://github.com/fabricjs/fabric.js/issues/1456 + const newOpacity = value ? '0' : ''; + const newCursor = value ? 'inherit' : 'none'; + this.canvas.getElement().parentElement.style.opacity = newOpacity; + const upperCanvas = this.canvas.getElement().parentElement.querySelector('.upper-canvas') as HTMLElement; + if (upperCanvas) { + upperCanvas.style.cursor = newCursor; + } + this.canvas.defaultCursor = newCursor; + } + private updateBrushTools(brushTool?: BrushTool, opts: Partial = {}): void { if (this.isPolygonDrawing) { // tool was switched from polygon to brush for example @@ -350,6 +368,7 @@ export class MasksHandlerImpl implements MasksHandler { this.editData = null; this.drawingOpacity = 0.5; this.brushMarker = null; + this.isHidden = false; this.colorBy = ColorBy.LABEL; this.onDrawDone = onDrawDone; this.onDrawRepeat = onDrawRepeat; @@ -452,7 +471,7 @@ export class MasksHandlerImpl implements MasksHandler { this.canvas.renderAll(); } - if (isMouseDown && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) { + if (isMouseDown && !this.isHidden && !isBrushSizeChanging && ['brush', 'eraser'].includes(tool?.type)) { const color = fabric.Color.fromHex(tool.color); color.setAlpha(tool.type === 'eraser' ? 1 : 0.5); @@ -530,6 +549,10 @@ export class MasksHandlerImpl implements MasksHandler { public configurate(configuration: Configuration): void { this.colorBy = configuration.colorBy; + + if (this.isHidden !== configuration.hideEditedObject) { + this.updateHidden(configuration.hideEditedObject); + } } public transform(geometry: Geometry): void { @@ -563,7 +586,10 @@ export class MasksHandlerImpl implements MasksHandler { const color = fabric.Color.fromHex(this.getStateColor(drawData.initialState)).getSource(); const [left, top, right, bottom] = points.slice(-4); const imageBitmap = expandChannels(color[0], color[1], color[2], points); - imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, + imageDataToDataURL( + imageBitmap, + right - left + 1, + bottom - top + 1, (dataURL: string) => new Promise((resolve) => { fabric.Image.fromURL(dataURL, (image: fabric.Image) => { try { @@ -654,7 +680,10 @@ export class MasksHandlerImpl implements MasksHandler { const color = fabric.Color.fromHex(this.getStateColor(editData.state)).getSource(); const [left, top, right, bottom] = points.slice(-4); const imageBitmap = expandChannels(color[0], color[1], color[2], points); - imageDataToDataURL(imageBitmap, right - left + 1, bottom - top + 1, + imageDataToDataURL( + imageBitmap, + right - left + 1, + bottom - top + 1, (dataURL: string) => new Promise((resolve) => { fabric.Image.fromURL(dataURL, (image: fabric.Image) => { try { diff --git a/cvat-canvas/src/typescript/svg.patch.ts b/cvat-canvas/src/typescript/svg.patch.ts index 40af155a956..7b728b27433 100644 --- a/cvat-canvas/src/typescript/svg.patch.ts +++ b/cvat-canvas/src/typescript/svg.patch.ts @@ -86,6 +86,7 @@ SVG.Element.prototype.draw.extend( }), ); +export const CIRCLE_STROKE = '#000'; // Fix method drawCircles function drawCircles(): void { const array = this.el.array().valueOf(); @@ -109,6 +110,7 @@ function drawCircles(): void { .circle(5) .stroke({ width: 1, + color: CIRCLE_STROKE, }) .fill('#ccc') .center(p.x, p.y), diff --git a/cvat-cli/src/cvat_cli/__main__.py b/cvat-cli/src/cvat_cli/__main__.py index 2448587245f..b18c8d8bb75 100755 --- a/cvat-cli/src/cvat_cli/__main__.py +++ b/cvat-cli/src/cvat_cli/__main__.py @@ -9,6 +9,7 @@ from types import SimpleNamespace from typing import List +import urllib3.exceptions from cvat_sdk import exceptions from cvat_sdk.core.client import Client, Config @@ -70,7 +71,7 @@ def main(args: List[str] = None): try: cli = CLI(client=client, credentials=parsed_args.auth) actions[parsed_args.action](cli, **vars(action_args)) - except exceptions.ApiException as e: + except (exceptions.ApiException, urllib3.exceptions.HTTPError) as e: logger.critical(e) return 1 diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 58b950c670b..53438ed65d2 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -47,7 +47,7 @@ export interface SerializedUser { first_name: string; last_name: string; email?: string; - groups?: ('user' | 'business' | 'admin')[]; + groups?: ('user' | 'admin')[]; is_staff?: boolean; is_superuser?: boolean; is_active?: boolean; diff --git a/cvat-core/src/user.ts b/cvat-core/src/user.ts index 6d7366151fb..ef28f3633f0 100644 --- a/cvat-core/src/user.ts +++ b/cvat-core/src/user.ts @@ -11,7 +11,7 @@ export default class User { public readonly email: string; public readonly firstName: string; public readonly lastName: string; - public readonly groups: ('user' | 'business' | 'admin')[]; + public readonly groups: ('user' | 'admin')[]; public readonly lastLogin: string; public readonly dateJoined: string; public readonly isStaff: boolean; diff --git a/cvat-sdk/cvat_sdk/core/client.py b/cvat-sdk/cvat_sdk/core/client.py index add7ccb5f3d..0ae0b88ecad 100644 --- a/cvat-sdk/cvat_sdk/core/client.py +++ b/cvat-sdk/cvat_sdk/core/client.py @@ -10,7 +10,7 @@ from contextlib import contextmanager, suppress from pathlib import Path from time import sleep -from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, TypeVar +from typing import Any, Dict, Generator, Optional, Sequence, Tuple, TypeVar import attrs import packaging.specifiers as specifiers @@ -121,7 +121,7 @@ def organization_slug(self, org_slug: Optional[str]): self.api_client.default_headers[self._ORG_SLUG_HEADER] = org_slug @contextmanager - def organization_context(self, slug: str) -> Iterator[None]: + def organization_context(self, slug: str) -> Generator[None, None, None]: prev_slug = self.organization_slug self.organization_slug = slug try: diff --git a/cvat-sdk/cvat_sdk/core/progress.py b/cvat-sdk/cvat_sdk/core/progress.py index 7fd2d13a2cd..fd844de722a 100644 --- a/cvat-sdk/cvat_sdk/core/progress.py +++ b/cvat-sdk/cvat_sdk/core/progress.py @@ -6,7 +6,7 @@ from __future__ import annotations import contextlib -from typing import ContextManager, Iterable, Optional, TypeVar +from typing import Generator, Iterable, Optional, TypeVar T = TypeVar("T") @@ -26,7 +26,7 @@ class ProgressReporter: """ @contextlib.contextmanager - def task(self, **kwargs) -> ContextManager[None]: + def task(self, **kwargs) -> Generator[None, None, None]: """ Returns a context manager that represents a long-running task for which progress can be reported. diff --git a/cvat-sdk/cvat_sdk/core/utils.py b/cvat-sdk/cvat_sdk/core/utils.py index 0706a2eec61..1ef434e3ad5 100644 --- a/cvat-sdk/cvat_sdk/core/utils.py +++ b/cvat-sdk/cvat_sdk/core/utils.py @@ -13,7 +13,7 @@ BinaryIO, ContextManager, Dict, - Iterator, + Generator, Literal, Sequence, TextIO, @@ -43,7 +43,7 @@ def atomic_writer( @contextlib.contextmanager def atomic_writer( path: Union[os.PathLike, str], mode: Literal["w", "wb"], encoding: str = "UTF-8" -) -> Iterator[IO]: +) -> Generator[IO, None, None]: """ Returns a context manager that, when entered, returns a handle to a temporary file opened with the specified `mode` and `encoding`. If the context manager diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 2c43904a3fb..fe2e65f809b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.66.2", + "version": "1.66.3", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 670ace099e5..68df5e9eb73 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -126,6 +126,8 @@ export enum AnnotationActionTypes { COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE', COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS', ACTIVATE_OBJECT = 'ACTIVATE_OBJECT', + UPDATE_EDITED_STATE = 'UPDATE_EDITED_STATE', + HIDE_ACTIVE_OBJECT = 'HIDE_ACTIVE_OBJECT', REMOVE_OBJECT = 'REMOVE_OBJECT', REMOVE_OBJECT_SUCCESS = 'REMOVE_OBJECT_SUCCESS', REMOVE_OBJECT_FAILED = 'REMOVE_OBJECT_FAILED', @@ -1320,7 +1322,7 @@ export function searchAnnotationsAsync( }; } -const ShapeTypeToControl: Record = { +export const ShapeTypeToControl: Record = { [ShapeType.RECTANGLE]: ActiveControl.DRAW_RECTANGLE, [ShapeType.POLYLINE]: ActiveControl.DRAW_POLYLINE, [ShapeType.POLYGON]: ActiveControl.DRAW_POLYGON, @@ -1608,3 +1610,50 @@ export function restoreFrameAsync(frame: number): ThunkAction { } }; } + +export function changeHideActiveObjectAsync(hide: boolean): ThunkAction { + return async (dispatch: ThunkDispatch, getState): Promise => { + const state = getState(); + const { instance: canvas } = state.annotation.canvas; + if (canvas) { + (canvas as Canvas).configure({ + hideEditedObject: hide, + }); + + const { objectState } = state.annotation.editing; + if (objectState) { + objectState.hidden = hide; + await dispatch(updateAnnotationsAsync([objectState])); + } + + dispatch({ + type: AnnotationActionTypes.HIDE_ACTIVE_OBJECT, + payload: { + hide, + }, + }); + } + }; +} + +export function updateEditedStateAsync(objectState: ObjectState | null): ThunkAction { + return async (dispatch: ThunkDispatch, getState): Promise => { + let newActiveObjectHidden = false; + if (objectState) { + newActiveObjectHidden = objectState.hidden; + } + + dispatch({ + type: AnnotationActionTypes.UPDATE_EDITED_STATE, + payload: { + objectState, + }, + }); + + const state = getState(); + const { activeObjectHidden } = state.annotation.canvas; + if (activeObjectHidden !== newActiveObjectHidden) { + dispatch(changeHideActiveObjectAsync(newActiveObjectHidden)); + } + }; +} diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx index 6c140438c20..b6a43ce20cf 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/brush-tools.tsx @@ -6,9 +6,9 @@ import './brush-toolbox-styles.scss'; import React, { useCallback, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; -import { useDispatch, useSelector } from 'react-redux'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import Button from 'antd/lib/button'; -import Icon, { VerticalAlignBottomOutlined } from '@ant-design/icons'; +import Icon, { EyeInvisibleFilled, EyeOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons'; import InputNumber from 'antd/lib/input-number'; import Select from 'antd/lib/select'; import notification from 'antd/lib/notification'; @@ -23,7 +23,7 @@ import { import CVATTooltip from 'components/common/cvat-tooltip'; import { CombinedState, ObjectType, ShapeType } from 'reducers'; import LabelSelector from 'components/label-selector/label-selector'; -import { rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions'; +import { changeHideActiveObjectAsync, rememberObject, updateCanvasBrushTools } from 'actions/annotation-actions'; import { ShortcutScope } from 'utils/enums'; import GlobalHotKeys from 'utils/mousetrap-react'; import { subKeyMap } from 'utils/component-subkeymap'; @@ -71,12 +71,17 @@ registerComponentShortcuts(componentShortcuts); const MIN_BRUSH_SIZE = 1; function BrushTools(): React.ReactPortal | null { const dispatch = useDispatch(); - const defaultLabelID = useSelector((state: CombinedState) => state.annotation.drawing.activeLabelID); - const config = useSelector((state: CombinedState) => state.annotation.canvas.brushTools); - const canvasInstance = useSelector((state: CombinedState) => state.annotation.canvas.instance); - const labels = useSelector((state: CombinedState) => state.annotation.job.labels); - const { keyMap, normalizedKeyMap } = useSelector((state: CombinedState) => state.shortcuts); - const { visible } = config; + const { + defaultLabelID, visible, canvasInstance, labels, activeObjectHidden, keyMap, normalizedKeyMap, + } = useSelector((state: CombinedState) => ({ + defaultLabelID: state.annotation.drawing.activeLabelID, + visible: state.annotation.canvas.brushTools.visible, + canvasInstance: state.annotation.canvas.instance, + labels: state.annotation.job.labels, + activeObjectHidden: state.annotation.canvas.activeObjectHidden, + keyMap: state.shortcuts.keyMap, + normalizedKeyMap: state.shortcuts.normalizedKeyMap, + }), shallowEqual); const [editableState, setEditableState] = useState(null); const [currentTool, setCurrentTool] = useState<'brush' | 'eraser' | 'polygon-plus' | 'polygon-minus'>('brush'); @@ -103,6 +108,10 @@ function BrushTools(): React.ReactPortal | null { } }, [setCurrentTool, blockedTools['polygon-minus']]); + const hideMask = useCallback((hide: boolean) => { + dispatch(changeHideActiveObjectAsync(hide)); + }, []); + const handlers: Record void> = { ACTIVATE_BRUSH_TOOL_STANDARD_CONTROLS: setBrushTool, ACTIVATE_ERASER_TOOL_STANDARD_CONTROLS: setEraserTool, @@ -365,6 +374,14 @@ function BrushTools(): React.ReactPortal | null { icon={} onClick={() => setRemoveUnderlyingPixels(!removeUnderlyingPixels)} /> + +