diff --git a/changelog.d/20250224_114044_sekachev.bs_display_dimensions_support.md b/changelog.d/20250224_114044_sekachev.bs_display_dimensions_support.md new file mode 100644 index 000000000000..a4a4e0ae4043 --- /dev/null +++ b/changelog.d/20250224_114044_sekachev.bs_display_dimensions_support.md @@ -0,0 +1,4 @@ +### Added + +- A setting to display rectangles and ellipses dimensions and rotation + () diff --git a/cvat-canvas/src/scss/canvas.scss b/cvat-canvas/src/scss/canvas.scss index 28404e914464..29262d137e1f 100644 --- a/cvat-canvas/src/scss/canvas.scss +++ b/cvat-canvas/src/scss/canvas.scss @@ -48,12 +48,15 @@ polyline.cvat_shape_drawing_opacity { font-weight: bold; fill: white; cursor: default; - font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif; - text-shadow: 0 0 4px black; + filter: drop-shadow(1px 1px 1px black) drop-shadow(-1px -1px 1px black); user-select: none; pointer-events: none; } +.cvat_canvas_text_dimensions { + fill: lightskyblue; +} + .cvat_canvas_text_description { fill: yellow; font-style: oblique 40deg; diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 7fd7e3aa4361..57c452ea9ec8 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -939,7 +939,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { if (typeof configuration.textContent === 'string') { const splitted = configuration.textContent.split(',').filter((entry: string) => !!entry); - if (splitted.every((entry: string) => ['id', 'label', 'attributes', 'source', 'descriptions'].includes(entry))) { + if (splitted.every((entry: string) => ['id', 'label', 'attributes', 'source', 'descriptions', 'dimensions'].includes(entry))) { this.data.configuration.textContent = configuration.textContent; } } diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 1bf30e34ad9b..fd346c99b7f2 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -32,6 +32,7 @@ import { vectorLength, ShapeSizeElement, DrawnState, rotate2DPoints, readPointsFromShape, setupSkeletonEdges, makeSVGFromTemplate, imageDataToDataURL, expandChannels, stringifyPoints, zipChannels, + composeShapeDimensions, } from './shared'; import { CanvasModel, Geometry, UpdateReasons, FrameZoom, ActiveElement, @@ -2444,10 +2445,10 @@ export class CanvasViewImpl implements CanvasView, Listener { shape.untransform(); } - if ( - state.points.length !== drawnState.points.length || - state.points.some((p: number, id: number): boolean => p !== drawnState.points[id]) - ) { + const pointsUpdated = state.points.length !== drawnState.points.length || + state.points.some((p: number, id: number): boolean => p !== drawnState.points[id]); + + if (pointsUpdated) { if (state.shapeType === 'mask') { // if masks points were updated, draw from scratch this.deleteObjects([this.drawnStates[+clientID]]); @@ -2493,9 +2494,12 @@ export class CanvasViewImpl implements CanvasView, Listener { const stateDescriptions = state.descriptions; const drawnStateDescriptions = drawnState.descriptions; + const rotationUpdated = drawnState.rotation !== state.rotation; if ( drawnState.label.id !== state.label.id || + pointsUpdated || + rotationUpdated || drawnStateDescriptions.length !== stateDescriptions.length || drawnStateDescriptions.some((desc: string, id: number): boolean => desc !== stateDescriptions[id]) ) { @@ -3090,6 +3094,7 @@ export class CanvasViewImpl implements CanvasView, Listener { const withLabel = content.includes('label'); const withSource = content.includes('source'); const withDescriptions = content.includes('descriptions'); + const withDimensions = content.includes('dimensions'); const textFontSize = this.configuration.textFontSize || 12; const { label, clientID, attributes, source, descriptions, @@ -3116,22 +3121,42 @@ export class CanvasViewImpl implements CanvasView, Listener { `${withSource ? `(${source})` : ''}`).style({ 'text-transform': 'uppercase', }); + + if (withDimensions && ['rectangle', 'ellipse'].includes(state.shapeType)) { + let width = state.points[2] - state.points[0]; + let height = state.points[3] - state.points[1]; + + if (state.shapeType === 'ellipse') { + width *= 2; + height *= -2; + } + + block + .tspan(composeShapeDimensions(width, height, state.rotation)) + .attr({ + dy: '1.25em', + x: 0, + }) + .addClass('cvat_canvas_text_dimensions'); + } if (withDescriptions) { - for (const desc of descriptions) { + descriptions.forEach((desc: string, idx: number) => { block .tspan(`${desc}`) .attr({ - dy: '1em', + dy: idx === 0 ? '1.25em' : '1em', x: 0, }) .addClass('cvat_canvas_text_description'); - } + }); } if (withAttr) { - for (const attrID of Object.keys(attributes)) { - const values = `${attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]}`.split('\n'); + Object.keys(attributes).forEach((attrID: string, idx: number) => { + const values = `${attributes[attrID] === undefinedAttrValue ? + '' : attributes[attrID]}`.split('\n'); const parent = block.tspan(`${attrNames[attrID]}: `) - .attr({ attrID, dy: '1em', x: 0 }).addClass('cvat_canvas_text_attribute'); + .attr({ attrID, dy: idx === 0 ? '1.25em' : '1em', x: 0 }) + .addClass('cvat_canvas_text_attribute'); values.forEach((attrLine: string, index: number) => { parent .tspan(attrLine) @@ -3139,7 +3164,7 @@ export class CanvasViewImpl implements CanvasView, Listener { dy: index === 0 ? 0 : '1em', }); }); - } + }); } }) .move(0, 0) diff --git a/cvat-canvas/src/typescript/shared.ts b/cvat-canvas/src/typescript/shared.ts index bde8cdbb8671..b4699be7d34d 100644 --- a/cvat-canvas/src/typescript/shared.ts +++ b/cvat-canvas/src/typescript/shared.ts @@ -90,6 +90,21 @@ export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] { return output; } +export function composeShapeDimensions(width: number, height: number, rotation: number | null): string { + const text = `${width.toFixed(1)}x${height.toFixed(1)}px`; + let adjustableRotation = rotation; + if (adjustableRotation) { + // make sure, that rotation is in range [0; 360] + while (adjustableRotation < 0) { + adjustableRotation += 360; + } + adjustableRotation %= 360; + return `${text} ${adjustableRotation.toFixed(1)}\u00B0`; + } + + return text; +} + export function displayShapeSize(shapesContainer: SVG.Container, textContainer: SVG.Container): ShapeSizeElement { const shapeSize: ShapeSizeElement = { sizeElement: textContainer @@ -100,16 +115,9 @@ export function displayShapeSize(shapesContainer: SVG.Container, textContainer: .fill('white') .addClass('cvat_canvas_text'), update(shape: SVG.Shape): void { - let text = `${Math.floor(shape.width())}x${Math.floor(shape.height())}px`; - if (shape.type === 'rect' || shape.type === 'ellipse') { - let rotation = shape.transform().rotation || 0; - // be sure, that rotation in range [0; 360] - while (rotation < 0) rotation += 360; - rotation %= 360; - if (rotation) { - text = `${text} ${rotation.toFixed(1)}\u00B0`; - } - } + const rotation = shape.type === 'rect' || shape.type === 'ellipse' ? + shape.transform().rotation ?? 0 : null; + const text = composeShapeDimensions(shape.width(), shape.height(), rotation); const [x, y, cx, cy]: number[] = translateToSVG( (textContainer.node as any) as SVGSVGElement, translateFromSVG((shapesContainer.node as any) as SVGSVGElement, [ diff --git a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx index c659daa8ac0e..f1c19d3db654 100644 --- a/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx +++ b/cvat-ui/src/components/header/settings-modal/workspace-settings.tsx @@ -168,6 +168,7 @@ function WorkspaceSettingsComponent(props: Props): JSX.Element { Attributes Source Descriptions + Dimensions diff --git a/cvat-ui/src/reducers/settings-reducer.ts b/cvat-ui/src/reducers/settings-reducer.ts index 2ed2b39753bf..0d74faea4f2d 100644 --- a/cvat-ui/src/reducers/settings-reducer.ts +++ b/cvat-ui/src/reducers/settings-reducer.ts @@ -35,7 +35,7 @@ const defaultState: SettingsState = { textFontSize: 14, controlPointsSize: 5, textPosition: 'auto', - textContent: 'id,source,label,attributes,descriptions', + textContent: 'id,source,label,attributes,descriptions,dimensions', toolsBlockerState: { algorithmsLocked: false, buttonVisible: false, diff --git a/site/content/en/docs/manual/basics/settings.md b/site/content/en/docs/manual/basics/settings.md index 449e99a33e33..71ed70b20453 100644 --- a/site/content/en/docs/manual/basics/settings.md +++ b/site/content/en/docs/manual/basics/settings.md @@ -43,6 +43,8 @@ In tab `Workspace` you can: - `Label` - object label. - `Source`- source of creating of objects `MANUAL`, `AUTO` or `SEMI-AUTO`. - `Descriptions` - description of attributes. + - `Dimensions` - width, height and rotation for rectangles and ellipses. + - `Position of a text` - text positioning mode selection: - `Auto` - the object details will be automatically placed where free space is.