From 53bcbf3e6d4ffb3e84bde4bf2aafe079e62cc0f9 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 12 Aug 2024 20:34:48 +0300 Subject: [PATCH] Refactoring: tools blocker state outside of canvas (#8293) --- cvat-canvas/package.json | 2 +- cvat-canvas/src/typescript/canvasModel.ts | 1 - .../src/typescript/interactionHandler.ts | 55 +++++-------------- cvat-core/package.json | 2 +- cvat-core/src/core-types.ts | 3 +- cvat-core/src/ml-model.ts | 12 +--- cvat-ui/package.json | 2 +- .../controls-side-bar/opencv-control.tsx | 29 +++++++--- .../controls-side-bar/tools-control.tsx | 47 +++++++++++++--- .../opencv-wrapper/intelligent-scissors.ts | 7 +-- .../utils/opencv-wrapper/opencv-wrapper.ts | 8 +-- 11 files changed, 83 insertions(+), 85 deletions(-) diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 3db51844734..5dc500eb6ee 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.20.7", + "version": "2.20.8", "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 165ca626ea5..88d3a270118 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -136,7 +136,6 @@ export interface InteractionData { shapeType: string; points: number[]; }; - onChangeToolsBlockerState?: (event: string) => void; } export interface InteractionResult { diff --git a/cvat-canvas/src/typescript/interactionHandler.ts b/cvat-canvas/src/typescript/interactionHandler.ts index 747c3c54537..90e8f57d6d3 100644 --- a/cvat-canvas/src/typescript/interactionHandler.ts +++ b/cvat-canvas/src/typescript/interactionHandler.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -36,7 +36,6 @@ export class InteractionHandlerImpl implements InteractionHandler { private thresholdRectSize: number; private intermediateShape: PropType; private drawnIntermediateShape: SVG.Shape; - private thresholdWasModified: boolean; private controlPointsSize: number; private selectedShapeOpacity: number; private cancelled: boolean; @@ -64,7 +63,7 @@ export class InteractionHandlerImpl implements InteractionHandler { ); } - private shouldRaiseEvent(ctrlKey: boolean): boolean { + private shouldRaiseEvent(): boolean { const { interactionData, interactionShapes, shapesWereUpdated } = this; const { minPosVertices, minNegVertices, enabled } = interactionData; @@ -75,8 +74,9 @@ export class InteractionHandlerImpl implements InteractionHandler { (shape: SVG.Shape): boolean => (shape as any).attr('stroke') !== 'green', ); + const somethingWasDrawn = interactionShapes.some((shape) => shape.type === 'rect') || positiveShapes.length; if (interactionData.shapeType === 'rectangle') { - return enabled && !ctrlKey && !!interactionShapes.length; + return enabled && !!interactionShapes.length; } const minPosVerticesDefined = Number.isInteger(minPosVertices); @@ -84,7 +84,7 @@ export class InteractionHandlerImpl implements InteractionHandler { const minPosVerticesAchieved = !minPosVerticesDefined || minPosVertices <= positiveShapes.length; const minNegVerticesAchieved = !minNegVerticesDefined || minNegVertices <= negativeShapes.length; const minimumVerticesAchieved = minPosVerticesAchieved && minNegVerticesAchieved; - return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated; + return enabled && somethingWasDrawn && minimumVerticesAchieved && shapesWereUpdated; } private addThreshold(): void { @@ -125,7 +125,7 @@ export class InteractionHandlerImpl implements InteractionHandler { this.interactionShapes.push(this.currentInteractionShape); this.shapesWereUpdated = true; - if (this.shouldRaiseEvent(e.ctrlKey)) { + if (this.shouldRaiseEvent()) { this.onInteraction(this.prepareResult(), true, false); } @@ -154,7 +154,7 @@ export class InteractionHandlerImpl implements InteractionHandler { if (this.interactionData.startWithBox && this.interactionShapes.length === 1) { this.interactionShapes[0].style({ visibility: '' }); } - const shouldRaiseEvent = this.shouldRaiseEvent(_e.ctrlKey); + const shouldRaiseEvent = this.shouldRaiseEvent(); if (shouldRaiseEvent) { this.onInteraction(this.prepareResult(), true, false); } @@ -193,7 +193,7 @@ export class InteractionHandlerImpl implements InteractionHandler { this.currentInteractionShape = this.canvas.rect(); this.canvas.on('mousedown.interaction', eventListener); this.currentInteractionShape - .on('drawstop', (e): void => { + .on('drawstop', (): void => { if (this.cancelled) { return; } @@ -204,7 +204,7 @@ export class InteractionHandlerImpl implements InteractionHandler { if (shouldFinish) { this.interact({ enabled: false }); - } else if (this.shouldRaiseEvent(e.ctrlKey)) { + } else if (this.shouldRaiseEvent()) { this.onInteraction(this.prepareResult(), true, false); } @@ -391,7 +391,7 @@ export class InteractionHandlerImpl implements InteractionHandler { } private visualComponentsChanged(interactionData: InteractionData): boolean { - const allowedKeys = ['enabled', 'crosshair', 'enableThreshold', 'onChangeToolsBlockerState']; + const allowedKeys = ['enabled', 'crosshair', 'enableThreshold']; if (Object.keys(interactionData).every((key: string): boolean => allowedKeys.includes(key))) { if (this.interactionData.enableThreshold !== undefined && interactionData.enableThreshold !== undefined && this.interactionData.enableThreshold !== interactionData.enableThreshold) { @@ -405,27 +405,6 @@ export class InteractionHandlerImpl implements InteractionHandler { return false; } - private onKeyUp = (e: KeyboardEvent): void => { - if (this.interactionData.enabled && e.keyCode === 17) { - if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) { - this.interactionData.onChangeToolsBlockerState('keyup'); - } - if (this.shouldRaiseEvent(false)) { - // 17 is ctrl - this.onInteraction(this.prepareResult(), true, false); - } - } - }; - - private onKeyDown = (e: KeyboardEvent): void => { - if (!e.repeat && this.interactionData.enabled && e.keyCode === 17) { - if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) { - this.interactionData.onChangeToolsBlockerState('keydown'); - } - this.thresholdWasModified = false; - } - }; - public constructor( onInteraction: ( shapes: InteractionResult[] | null, @@ -488,11 +467,11 @@ export class InteractionHandlerImpl implements InteractionHandler { }); this.canvas.on('wheel.interaction', (e: WheelEvent): void => { - if (e.ctrlKey) { + if (e.altKey) { + e.stopPropagation(); + e.preventDefault(); if (this.threshold) { - this.thresholdWasModified = true; const { x, y } = this.cursorPosition; - e.preventDefault(); if (e.deltaY > 0) { this.thresholdRectSize *= 6 / 5; } else { @@ -503,9 +482,6 @@ export class InteractionHandlerImpl implements InteractionHandler { } } }); - - window.document.addEventListener('keyup', this.onKeyUp); - window.document.addEventListener('keydown', this.onKeyDown); } public transform(geometry: Geometry): void { @@ -565,7 +541,7 @@ export class InteractionHandlerImpl implements InteractionHandler { (this.currentInteractionShape as any).draw('stop'); } - this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(false), true); + this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(), true); this.release(); this.interactionData = interactionData; } @@ -599,7 +575,6 @@ export class InteractionHandlerImpl implements InteractionHandler { } public destroy(): void { - window.document.removeEventListener('keyup', this.onKeyUp); - window.document.removeEventListener('keydown', this.onKeyDown); + // nothing to release } } diff --git a/cvat-core/package.json b/cvat-core/package.json index 030b2cb0e49..a93411284bc 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "15.1.0", + "version": "15.1.1", "type": "module", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", diff --git a/cvat-core/src/core-types.ts b/cvat-core/src/core-types.ts index fee5bb4caa8..e44a354cb5b 100644 --- a/cvat-core/src/core-types.ts +++ b/cvat-core/src/core-types.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -16,7 +16,6 @@ export interface ModelParams { minNegVertices?: number; startWithBox?: boolean; startWithBoxOptional?: boolean; - onChangeToolsBlockerState?: (event: string) => void; }; } diff --git a/cvat-core/src/ml-model.ts b/cvat-core/src/ml-model.ts index 3767bb71334..787277cf559 100644 --- a/cvat-core/src/ml-model.ts +++ b/cvat-core/src/ml-model.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,7 +13,6 @@ import { export default class MLModel { private serialized: SerializedModel; - private changeToolsBlockerStateCallback?: (event: string) => void; constructor(serialized: SerializedModel) { this.serialized = { ...serialized }; @@ -61,10 +60,6 @@ export default class MLModel { }, }; - if (this.changeToolsBlockerStateCallback) { - result.canvas.onChangeToolsBlockerState = this.changeToolsBlockerStateCallback; - } - return result; } @@ -103,11 +98,6 @@ export default class MLModel { return this.serialized?.return_type; } - // Used to set a callback when the tool is blocked in UI - public set onChangeToolsBlockerState(onChangeToolsBlockerState: (event: string) => void) { - this.changeToolsBlockerStateCallback = onChangeToolsBlockerState; - } - public async preview(): Promise { const result = await PluginRegistry.apiWrapper.call(this, MLModel.prototype.preview); return result; diff --git a/cvat-ui/package.json b/cvat-ui/package.json index af9c203ca98..ffbc6f08b7f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.64.4", + "version": "1.64.5", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx index c2ceab8f24d..df3b2400f1b 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/opencv-control.tsx @@ -161,6 +161,8 @@ class OpenCVControlComponent extends React.PureComponent shape.trackerModel.delete()); @@ -368,21 +372,29 @@ class OpenCVControlComponent extends React.PureComponent { + private onChangeToolsBlockerState = (): void => { const { - isActivated, toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, + toolsBlockerState, canvasInstance, + isActivated, onSwitchToolsBlockerState, } = this.props; - if (isActivated && event === 'keyup') { - onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked }); + + if (isActivated) { + const isLocked = !toolsBlockerState.algorithmsLocked; + onSwitchToolsBlockerState({ algorithmsLocked: isLocked }); canvasInstance.interact({ enabled: true, - crosshair: toolsBlockerState.algorithmsLocked, - enableThreshold: toolsBlockerState.algorithmsLocked, - onChangeToolsBlockerState: this.onChangeToolsBlockerState, + crosshair: !isLocked, + enableThreshold: !isLocked, }); } }; + private onKeyUp = (e: KeyboardEvent): void => { + if (e.key === 'Control') { + this.onChangeToolsBlockerState(); + } + }; + private applyTracking = (imageData: ImageData, shape: TrackedShape, objectState: any): Promise => new Promise((resolve, reject) => { setTimeout(() => { @@ -580,8 +592,7 @@ class OpenCVControlComponent extends React.PureComponent { this.setState({ mode: 'interaction' }); - this.activeTool = openCVWrapper.segmentation - .intelligentScissorsFactory(this.onChangeToolsBlockerState); + this.activeTool = openCVWrapper.segmentation.intelligentScissorsFactory(); canvasInstance.cancel(); const interactorParameters = this.activeTool.params.canvas; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index 30ba6cf8b44..7e7c0eb41d6 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx @@ -219,6 +219,7 @@ export class ToolsControlComponent extends React.PureComponent { points: [number, number][]; bounds?: [number, number, number, number]; }; + latestPostponedEvent: Event | null; lastestApproximatedPoints: number[][]; latestRequest: null | { interactor: MLModel; @@ -251,6 +252,7 @@ export class ToolsControlComponent extends React.PureComponent { this.interaction = { id: null, isAborted: false, + latestPostponedEvent: null, latestResponse: { rle: [], points: [], @@ -270,6 +272,9 @@ export class ToolsControlComponent extends React.PureComponent { this.setState({ portals: this.collectTrackerPortals(), }); + + window.document.addEventListener('keydown', this.onKeyDown); + window.document.addEventListener('keyup', this.onKeyUp); canvasInstance.html().addEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().addEventListener('canvas.canceled', this.cancelListener); } @@ -298,6 +303,7 @@ export class ToolsControlComponent extends React.PureComponent { this.interaction = { id: null, isAborted: false, + latestPostponedEvent: null, latestResponse: { rle: [], points: [] }, lastestApproximatedPoints: [], latestRequest: null, @@ -322,7 +328,6 @@ export class ToolsControlComponent extends React.PureComponent { shapeType: ShapeType.POLYGON, points: this.interaction.lastestApproximatedPoints.flat(), }, - onChangeToolsBlockerState: this.onChangeToolsBlockerState, }); }); } @@ -334,6 +339,8 @@ export class ToolsControlComponent extends React.PureComponent { public componentWillUnmount(): void { const { canvasInstance } = this.props; onRemoveAnnotations(null); + window.document.removeEventListener('keydown', this.onKeyDown); + window.document.removeEventListener('keyup', this.onKeyUp); canvasInstance.html().removeEventListener('canvas.interacted', this.interactionListener); canvasInstance.html().removeEventListener('canvas.canceled', this.cancelListener); } @@ -436,7 +443,6 @@ export class ToolsControlComponent extends React.PureComponent { points: convertMasksToPolygons ? this.interaction.lastestApproximatedPoints.flat() : this.interaction.latestResponse.rle, }, - onChangeToolsBlockerState: this.onChangeToolsBlockerState, }); } @@ -545,9 +551,15 @@ export class ToolsControlComponent extends React.PureComponent { }; private interactionListener = async (e: Event): Promise => { + const { toolsBlockerState } = this.props; const { mode } = this.state; if (mode === 'interaction') { + if (toolsBlockerState.algorithmsLocked) { + this.interaction.latestPostponedEvent = e; + return; + } + await this.onInteraction(e); } @@ -581,10 +593,30 @@ export class ToolsControlComponent extends React.PureComponent { private onChangeToolsBlockerState = (event: string): void => { const { isActivated, onSwitchToolsBlockerState } = this.props; - if (isActivated && event === 'keydown') { - onSwitchToolsBlockerState({ algorithmsLocked: true }); - } else if (isActivated && event === 'keyup') { - onSwitchToolsBlockerState({ algorithmsLocked: false }); + const { mode } = this.state; + + if (isActivated && mode === 'interaction') { + if (event === 'keydown') { + this.interaction.latestPostponedEvent = null; + onSwitchToolsBlockerState({ algorithmsLocked: true }); + } else if (event === 'keyup') { + onSwitchToolsBlockerState({ algorithmsLocked: false }); + if (this.interaction.latestPostponedEvent) { + this.onInteraction(this.interaction.latestPostponedEvent); + } + } + } + }; + + private onKeyUp = (e: KeyboardEvent): void => { + if (e.key === 'Control') { + this.onChangeToolsBlockerState('keyup'); + } + }; + + private onKeyDown = (e: KeyboardEvent): void => { + if (!e.repeat && e.key === 'Control') { + this.onChangeToolsBlockerState('keydown'); } }; @@ -1141,10 +1173,7 @@ export class ToolsControlComponent extends React.PureComponent { onClick={() => { if (activeInteractor && activeLabelID && labels.length) { this.setState({ mode: 'interaction' }); - canvasInstance.cancel(); - activeInteractor.onChangeToolsBlockerState = this.onChangeToolsBlockerState; - const interactorParameters = { ...omit(activeInteractor.params.canvas, 'startWithBoxOptional'), // replace 'optional' with true or false depending on user specified setting diff --git a/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts b/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts index a73f8b38911..cf05ddb72cb 100644 --- a/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts +++ b/cvat-ui/src/utils/opencv-wrapper/intelligent-scissors.ts @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -14,7 +15,6 @@ export interface IntelligentScissorsParams { enableSliding: boolean; allowRemoveOnlyLast: boolean; minPosVertices: number; - onChangeToolsBlockerState: (event:string)=>void; }; } @@ -38,7 +38,6 @@ function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] export default class IntelligentScissorsImplementation implements IntelligentScissors { public kind = 'opencv_intelligent_scissors'; private cv: any; - private onChangeToolsBlockerState: (event:string)=>void; private scissors: { tool: any; state: { @@ -55,9 +54,8 @@ export default class IntelligentScissorsImplementation implements IntelligentSci }; }; - public constructor(cv: any, onChangeToolsBlockerState:(event:string)=>void) { + public constructor(cv: any) { this.cv = cv; - this.onChangeToolsBlockerState = onChangeToolsBlockerState; this.reset(); } @@ -180,7 +178,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci enableSliding: true, allowRemoveOnlyLast: true, minPosVertices: 1, - onChangeToolsBlockerState: this.onChangeToolsBlockerState, }, }; } diff --git a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts index 4e2f3ea57f4..b2d045c3248 100644 --- a/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts +++ b/cvat-ui/src/utils/opencv-wrapper/opencv-wrapper.ts @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -14,7 +14,7 @@ import { OpenCVTracker } from './opencv-interfaces'; const core = getCore(); export interface Segmentation { - intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => IntelligentScissors; + intelligentScissorsFactory: () => IntelligentScissors; } export interface MatSpace { @@ -291,9 +291,7 @@ export class OpenCVWrapper { public get segmentation(): Segmentation { this.checkInitialization(); return { - intelligentScissorsFactory: - (onChangeToolsBlockerState: - (event:string)=>void) => new IntelligentScissorsImplementation(this.cv, onChangeToolsBlockerState), + intelligentScissorsFactory: () => new IntelligentScissorsImplementation(this.cv), }; }