diff --git a/CHANGELOG.md b/CHANGELOG.md index fe14ba0ff0b9..e32ad1bb70bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dialog window with some helpful information about using filters - Ability to display a bitmap in the new UI - Button to reset colors settings (brightness, saturation, contrast) in the new UI +- Added option to display shape text always ### Changed - diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md index dbf85b8cd129..59806746eb59 100644 --- a/cvat-canvas/README.md +++ b/cvat-canvas/README.md @@ -50,6 +50,11 @@ Canvas itself handles: ZOOM_CANVAS = 'zoom_canvas', } + interface Configuration { + displayAllText?: boolean; + undefinedAttrValue?: string; + } + interface DrawData { enabled: boolean; shapeType?: string; @@ -83,7 +88,6 @@ Canvas itself handles: } interface Canvas { - mode(): Mode; html(): HTMLDivElement; setZLayer(zLayer: number | null): void; setup(frameData: any, objectStates: any[]): void; @@ -104,7 +108,9 @@ Canvas itself handles: dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; + mode(): Mode; cancel(): void; + configure(configuration: Configuration): void; } ``` @@ -190,5 +196,6 @@ Standard JS events are used. | dragCanvas() | + | - | - | - | - | - | + | - | | zoomCanvas() | + | - | - | - | - | - | - | + | | cancel() | - | + | + | + | + | + | + | + | +| configure() | + | - | - | - | - | - | - | - | | bitmap() | + | + | + | + | + | + | + | + | | setZLayer() | + | + | + | + | + | + | + | + | diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts index bb169e388022..04a99d5b1925 100644 --- a/cvat-canvas/src/typescript/canvas.ts +++ b/cvat-canvas/src/typescript/canvas.ts @@ -11,6 +11,7 @@ import { CanvasModel, CanvasModelImpl, RectDrawingMethod, + Configuration, } from './canvasModel'; import { @@ -53,8 +54,9 @@ interface Canvas { dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; - mode(): void; + mode(): Mode; cancel(): void; + configure(configuration: Configuration): void; } class CanvasImpl implements Canvas { @@ -146,11 +148,16 @@ class CanvasImpl implements Canvas { public cancel(): void { this.model.cancel(); } + + public configure(configuration: Configuration): void { + this.model.configure(configuration); + } } export { CanvasImpl as Canvas, CanvasVersion, + Configuration, RectDrawingMethod, Mode as CanvasMode, }; diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts index 7fc555c64690..179f9b32e869 100644 --- a/cvat-canvas/src/typescript/canvasController.ts +++ b/cvat-canvas/src/typescript/canvasController.ts @@ -36,7 +36,6 @@ export interface CanvasController { enableDrag(x: number, y: number): void; drag(x: number, y: number): void; disableDrag(): void; - fit(): void; } diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 87e560927e4e..13f99be1b08a 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -46,6 +46,11 @@ export enum RectDrawingMethod { EXTREME_POINTS = 'By 4 points' } +export interface Configuration { + displayAllText?: boolean; + undefinedAttrValue?: string; +} + export interface DrawData { enabled: boolean; shapeType?: string; @@ -101,6 +106,7 @@ export enum UpdateReasons { BITMAP = 'bitmap', DRAG_CANVAS = 'drag_canvas', ZOOM_CANVAS = 'zoom_canvas', + CONFIG_UPDATED = 'config_updated', } export enum Mode { @@ -128,6 +134,7 @@ export interface CanvasModel { readonly mergeData: MergeData; readonly splitData: SplitData; readonly groupData: GroupData; + readonly configuration: Configuration; readonly selected: any; geometry: Geometry; mode: Mode; @@ -154,6 +161,7 @@ export interface CanvasModel { dragCanvas(enable: boolean): void; zoomCanvas(enable: boolean): void; + configure(configuration: Configuration): void; cancel(): void; } @@ -162,6 +170,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { activeElement: ActiveElement; angle: number; canvasSize: Size; + configuration: Configuration; imageBitmap: boolean; image: Image | null; imageID: number | null; @@ -195,6 +204,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { height: 0, width: 0, }, + configuration: { + displayAllText: false, + undefinedAttrValue: '', + }, imageBitmap: false, image: null, imageID: null, @@ -495,10 +508,30 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { this.data.selected = null; } + public configure(configuration: Configuration): void { + if (this.data.mode !== Mode.IDLE) { + throw Error(`Canvas is busy. Action: ${this.data.mode}`); + } + + if (typeof (configuration.displayAllText) !== 'undefined') { + this.data.configuration.displayAllText = configuration.displayAllText; + } + + if (typeof (configuration.undefinedAttrValue) !== 'undefined') { + this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue; + } + + this.notify(UpdateReasons.CONFIG_UPDATED); + } + public cancel(): void { this.notify(UpdateReasons.CANCEL); } + public get configuration(): Configuration { + return { ...this.data.configuration }; + } + public get geometry(): Geometry { return { angle: this.data.angle, diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 5d6117c29b55..27160436bf5d 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -36,6 +36,7 @@ import { GroupData, Mode, Size, + Configuration, } from './canvasModel'; export interface CanvasView { @@ -66,6 +67,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private groupHandler: GroupHandler; private zoomHandler: ZoomHandler; private activeElement: ActiveElement; + private configuration: Configuration; private set mode(value: Mode) { this.controller.mode = value; @@ -539,6 +541,7 @@ export class CanvasViewImpl implements CanvasView, Listener { clientID: null, attributeID: null, }; + this.configuration = model.configuration; this.mode = Mode.IDLE; // Create HTML elements @@ -707,8 +710,11 @@ export class CanvasViewImpl implements CanvasView, Listener { public notify(model: CanvasModel & Master, reason: UpdateReasons): void { this.geometry = this.controller.geometry; - - if (reason === UpdateReasons.BITMAP) { + if (reason === UpdateReasons.CONFIG_UPDATED) { + this.configuration = model.configuration; + this.setupObjects([]); + this.setupObjects(model.objects); + } else if (reason === UpdateReasons.BITMAP) { const { imageBitmap } = model; if (imageBitmap) { this.bitmap.style.display = ''; @@ -961,31 +967,44 @@ export class CanvasViewImpl implements CanvasView, Listener { for (const state of states) { const { clientID } = state; const drawnState = this.drawnStates[clientID]; + const shape = this.svgShapes[state.clientID]; + const text = this.svgTexts[state.clientID]; if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) { - const none = state.hidden || state.outside; - if (state.shapeType === 'points') { - this.svgShapes[clientID].remember('_selectHandler').nested - .style('display', none ? 'none' : ''); + const isInvisible = state.hidden || state.outside; + if (isInvisible) { + (state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape) + .style('display', 'none'); + if (text) { + text.addClass('cvat_canvas_hidden'); + } } else { - this.svgShapes[clientID].style('display', none ? 'none' : ''); + (state.shapeType === 'points' ? shape.remember('_selectHandler').nested : shape) + .style('display', ''); + if (text) { + text.removeClass('cvat_canvas_hidden'); + this.updateTextPosition( + text, + shape, + ); + } } } if (drawnState.zOrder !== state.zOrder) { if (state.shapeType === 'points') { - this.svgShapes[clientID].remember('_selectHandler').nested + shape.remember('_selectHandler').nested .attr('data-z-order', state.zOrder); } else { - this.svgShapes[clientID].attr('data-z-order', state.zOrder); + shape.attr('data-z-order', state.zOrder); } } if (drawnState.occluded !== state.occluded) { if (state.occluded) { - this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded'); + shape.addClass('cvat_canvas_shape_occluded'); } else { - this.svgShapes[clientID].removeClass('cvat_canvas_shape_occluded'); + shape.removeClass('cvat_canvas_shape_occluded'); } } @@ -1003,7 +1022,7 @@ export class CanvasViewImpl implements CanvasView, Listener { if (state.shapeType === 'rectangle') { const [xtl, ytl, xbr, ybr] = translatedPoints; - this.svgShapes[clientID].attr({ + shape.attr({ x: xtl, y: ytl, width: xbr - xtl, @@ -1019,21 +1038,20 @@ export class CanvasViewImpl implements CanvasView, Listener { return `${acc}${val},`; }, '', ); - (this.svgShapes[clientID] as any).clear(); - this.svgShapes[clientID].attr('points', stringified); + (shape as any).clear(); + shape.attr('points', stringified); if (state.shapeType === 'points') { - this.selectize(false, this.svgShapes[clientID]); - this.setupPoints(this.svgShapes[clientID] as SVG.PolyLine, state); + this.selectize(false, shape); + this.setupPoints(shape as SVG.PolyLine, state); } } } for (const attrID of Object.keys(state.attributes)) { if (state.attributes[attrID] !== drawnState.attributes[attrID]) { - const text = this.svgTexts[state.clientID]; if (text) { - const [span] = this.svgTexts[state.clientID].node + const [span] = text.node .querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[]; if (span && span.textContent) { const prefix = span.textContent.split(':').slice(0, -1).join(':'); @@ -1048,6 +1066,8 @@ export class CanvasViewImpl implements CanvasView, Listener { } private addObjects(states: any[], translate: (points: number[]) => number[]): void { + const { displayAllText } = this.configuration; + for (const state of states) { if (state.objectType === 'tag') { this.addTag(state); @@ -1091,6 +1111,14 @@ export class CanvasViewImpl implements CanvasView, Listener { }, })); }); + + if (displayAllText) { + this.svgTexts[state.clientID] = this.addText(state); + this.updateTextPosition( + this.svgTexts[state.clientID], + this.svgShapes[state.clientID], + ); + } } this.saveState(state); @@ -1139,6 +1167,7 @@ export class CanvasViewImpl implements CanvasView, Listener { private deactivateShape(): void { if (this.activeElement.clientID !== null) { + const { displayAllText } = this.configuration; const { clientID } = this.activeElement; const drawnState = this.drawnStates[clientID]; const shape = this.svgShapes[clientID]; @@ -1162,7 +1191,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // TODO: Hide text only if it is hidden by settings const text = this.svgTexts[clientID]; - if (text) { + if (text && !displayAllText) { text.remove(); delete this.svgTexts[clientID]; } @@ -1370,6 +1399,7 @@ export class CanvasViewImpl implements CanvasView, Listener { // Update text position after corresponding box has been moved, resized, etc. private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void { + if (text.node.style.display === 'none') return; // wrong transformation matrix let box = (shape.node as any).getBBox(); // Translate the whole box to the client coordinate system @@ -1408,6 +1438,7 @@ export class CanvasViewImpl implements CanvasView, Listener { } private addText(state: any): SVG.Text { + const { undefinedAttrValue } = this.configuration; const { label, clientID, attributes } = state; const attrNames = label.attributes.reduce((acc: any, val: any): void => { acc[val.id] = val.name; @@ -1417,7 +1448,7 @@ export class CanvasViewImpl implements CanvasView, Listener { return this.adoptedText.text((block): void => { block.tspan(`${label.name} ${clientID}`).style('text-transform', 'uppercase'); for (const attrID of Object.keys(attributes)) { - const value = attributes[attrID] === consts.UNDEFINED_ATTRIBUTE_VALUE + const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]; block.tspan(`${attrNames[attrID]}: ${value}`).attr({ attrID, diff --git a/cvat-ui/src/actions/settings-actions.ts b/cvat-ui/src/actions/settings-actions.ts index 3cb18f56f614..f06b0fb0eb6b 100644 --- a/cvat-ui/src/actions/settings-actions.ts +++ b/cvat-ui/src/actions/settings-actions.ts @@ -29,6 +29,7 @@ export enum SettingsActionTypes { CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL', CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN', SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS', + SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS = 'SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS', } export function changeShapesOpacity(opacity: number): AnyAction { @@ -210,3 +211,12 @@ export function switchShowingInterpolatedTracks(showAllInterpolationTracks: bool }, }; } + +export function switchShowingObjectsTextAlways(showObjectsTextAlways: boolean): AnyAction { + return { + type: SettingsActionTypes.SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS, + payload: { + showObjectsTextAlways, + }, + }; +} diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx index dbd24fee090e..8296f287b1f8 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -21,6 +21,7 @@ import { import { LogType } from 'cvat-logger'; import { Canvas } from 'cvat-canvas'; import getCore from 'cvat-core'; +import consts from 'consts'; const cvat = getCore(); @@ -59,6 +60,7 @@ interface Props { contextVisible: boolean; contextType: ContextMenuType; aamZoomMargin: number; + showObjectsTextAlways: boolean; workspace: Workspace; keyMap: Record; onSetupCanvas: () => void; @@ -92,6 +94,7 @@ interface Props { export default class CanvasWrapperComponent extends React.PureComponent { public componentDidMount(): void { const { + showObjectsTextAlways, canvasInstance, curZLayer, } = this.props; @@ -102,7 +105,12 @@ export default class CanvasWrapperComponent extends React.PureComponent { .getElementsByClassName('cvat-canvas-container'); wrapper.appendChild(canvasInstance.html()); + canvasInstance.configure({ + undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, + displayAllText: showObjectsTextAlways, + }); canvasInstance.setZLayer(curZLayer); + this.initialSetup(); this.updateCanvas(); } @@ -130,8 +138,16 @@ export default class CanvasWrapperComponent extends React.PureComponent { saturationLevel, workspace, frameFetching, + showObjectsTextAlways, } = this.props; + if (prevProps.showObjectsTextAlways !== showObjectsTextAlways) { + canvasInstance.configure({ + undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, + displayAllText: showObjectsTextAlways, + }); + } + if (prevProps.sidebarCollapsed !== sidebarCollapsed) { const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar'); if (sidebar) { diff --git a/cvat-ui/src/components/settings-page/styles.scss b/cvat-ui/src/components/settings-page/styles.scss index 82046ae1c8be..63269c43ecfc 100644 --- a/cvat-ui/src/components/settings-page/styles.scss +++ b/cvat-ui/src/components/settings-page/styles.scss @@ -23,6 +23,8 @@ .cvat-player-settings-grid, .cvat-workspace-settings-auto-save, +.cvat-workspace-settings-show-text-always, +.cvat-workspace-settings-show-text-always-checkbox, .cvat-workspace-settings-show-interpolated-checkbox { margin-bottom: 10px; } @@ -34,6 +36,7 @@ .cvat-player-settings-speed, .cvat-player-settings-reset-zoom, .cvat-player-settings-rotate-all, +.cvat-workspace-settings-show-text-always, .cvat-workspace-settings-show-interpolated, .cvat-workspace-settings-aam-zoom-margin, .cvat-workspace-settings-auto-save-interval { diff --git a/cvat-ui/src/components/settings-page/workspace-settings.tsx b/cvat-ui/src/components/settings-page/workspace-settings.tsx index 52ba4a5062c7..0ee068aedbe6 100644 --- a/cvat-ui/src/components/settings-page/workspace-settings.tsx +++ b/cvat-ui/src/components/settings-page/workspace-settings.tsx @@ -16,10 +16,12 @@ interface Props { autoSaveInterval: number; aamZoomMargin: number; showAllInterpolationTracks: boolean; + showObjectsTextAlways: boolean; onSwitchAutoSave(enabled: boolean): void; onChangeAutoSaveInterval(interval: number): void; onChangeAAMZoomMargin(margin: number): void; onSwitchShowingInterpolatedTracks(enabled: boolean): void; + onSwitchShowingObjectsTextAlways(enabled: boolean): void; } export default function WorkspaceSettingsComponent(props: Props): JSX.Element { @@ -28,10 +30,12 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element { autoSaveInterval, aamZoomMargin, showAllInterpolationTracks, + showObjectsTextAlways, onSwitchAutoSave, onChangeAutoSaveInterval, onChangeAAMZoomMargin, onSwitchShowingInterpolatedTracks, + onSwitchShowingObjectsTextAlways, } = props; const minAutoSaveInterval = 5; @@ -93,6 +97,22 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element { Show hidden interpolated objects in the side panel + + + { + onSwitchShowingObjectsTextAlways(event.target.checked); + }} + > + Always show object details + + + + Show text for an object on the canvas not only when the object is activated + + Attribute annotation mode (AAM) zoom margin diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx index 4d68d9c4f51c..12501c44432e 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/canvas-wrapper.tsx @@ -74,6 +74,7 @@ interface StateToProps { saturationLevel: number; resetZoom: boolean; aamZoomMargin: number; + showObjectsTextAlways: boolean; workspace: Workspace; minZLayer: number; maxZLayer: number; @@ -164,6 +165,7 @@ function mapStateToProps(state: CombinedState): StateToProps { }, workspace: { aamZoomMargin, + showObjectsTextAlways, }, shapes: { opacity, @@ -206,6 +208,7 @@ function mapStateToProps(state: CombinedState): StateToProps { saturationLevel, resetZoom, aamZoomMargin, + showObjectsTextAlways, curZLayer, minZLayer, maxZLayer, diff --git a/cvat-ui/src/containers/settings-page/workspace-settings.tsx b/cvat-ui/src/containers/settings-page/workspace-settings.tsx index 4ab527b3b1a9..db74456435a3 100644 --- a/cvat-ui/src/containers/settings-page/workspace-settings.tsx +++ b/cvat-ui/src/containers/settings-page/workspace-settings.tsx @@ -10,11 +10,10 @@ import { changeAutoSaveInterval, changeAAMZoomMargin, switchShowingInterpolatedTracks, + switchShowingObjectsTextAlways, } from 'actions/settings-actions'; -import { - CombinedState, -} from 'reducers/interfaces'; +import { CombinedState } from 'reducers/interfaces'; import WorkspaceSettingsComponent from 'components/settings-page/workspace-settings'; @@ -23,6 +22,7 @@ interface StateToProps { autoSaveInterval: number; aamZoomMargin: number; showAllInterpolationTracks: boolean; + showObjectsTextAlways: boolean; } interface DispatchToProps { @@ -30,6 +30,7 @@ interface DispatchToProps { onChangeAutoSaveInterval(interval: number): void; onChangeAAMZoomMargin(margin: number): void; onSwitchShowingInterpolatedTracks(enabled: boolean): void; + onSwitchShowingObjectsTextAlways(enabled: boolean): void; } function mapStateToProps(state: CombinedState): StateToProps { @@ -39,6 +40,7 @@ function mapStateToProps(state: CombinedState): StateToProps { autoSaveInterval, aamZoomMargin, showAllInterpolationTracks, + showObjectsTextAlways, } = workspace; return { @@ -46,6 +48,7 @@ function mapStateToProps(state: CombinedState): StateToProps { autoSaveInterval, aamZoomMargin, showAllInterpolationTracks, + showObjectsTextAlways, }; } @@ -63,6 +66,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { onSwitchShowingInterpolatedTracks(enabled: boolean): void { dispatch(switchShowingInterpolatedTracks(enabled)); }, + onSwitchShowingObjectsTextAlways(enabled: boolean): void { + dispatch(switchShowingObjectsTextAlways(enabled)); + }, }; } diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 5df7c6dbc507..d794ffd72387 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -426,6 +426,7 @@ export interface WorkspaceSettingsState { autoSave: boolean; autoSaveInterval: number; // in ms aamZoomMargin: number; + showObjectsTextAlways: boolean; showAllInterpolationTracks: boolean; } diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 1b38bb896677..00cc552767cf 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -28,6 +28,7 @@ const defaultState: SettingsState = { autoSave: false, autoSaveInterval: 15 * 60 * 1000, aamZoomMargin: 100, + showObjectsTextAlways: false, showAllInterpolationTracks: false, }, player: { @@ -227,6 +228,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => { }, }; } + case SettingsActionTypes.SWITCH_SHOWING_OBJECTS_TEXT_ALWAYS: { + return { + ...state, + workspace: { + ...state.workspace, + showObjectsTextAlways: action.payload.showObjectsTextAlways, + }, + }; + } case BoundariesActionTypes.RESET_AFTER_ERROR: case AnnotationActionTypes.GET_JOB_SUCCESS: { const { job } = action.payload;