Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
31de067
Implement multi-object selection
senketsukamui Aug 10, 2026
2d54f3d
Extend multi-object selection controls
senketsukamui Aug 11, 2026
f68f345
Support moving and refining object selections
senketsukamui Aug 13, 2026
717ba8d
Fix undo after deleting object selection
senketsukamui Aug 13, 2026
273133f
Improve multi-object selection interactions
senketsukamui Aug 14, 2026
573a945
Improve multi-object selection UX
senketsukamui Aug 14, 2026
10f2e26
Keep selection control highlighted
senketsukamui Aug 18, 2026
412f4c2
Refine canvas multi-selection feedback
senketsukamui Aug 18, 2026
c26e912
Align selected object sidebar styling
senketsukamui Aug 18, 2026
c798108
Allow partial movement of object selections
senketsukamui Aug 18, 2026
27b9852
Preserve selection after partial group move
senketsukamui Aug 19, 2026
2344ca3
Reduce group selection border width
senketsukamui Aug 19, 2026
e4500d3
Merge branch 'develop' of github.com:cvat-ai/cvat into gp/multiselection
senketsukamui Aug 21, 2026
a3b9876
Use Ctrl for object selection toggles
senketsukamui Aug 24, 2026
5ee62cc
Use black border for selection box
senketsukamui Aug 24, 2026
82b836c
Add selection lock and pin actions
senketsukamui Aug 24, 2026
fc3332a
Restore object selection through history
senketsukamui Aug 24, 2026
08a337d
Increase opacity of selected objects
senketsukamui Aug 24, 2026
569e84a
Add range and select-all object selection
senketsukamui Aug 24, 2026
5a87b1d
Apply label shortcuts to object selections
senketsukamui Aug 24, 2026
bc2125d
Add selection lock and pin shortcuts
senketsukamui Aug 25, 2026
0e8ace9
Require clicks to activate objects during selection
senketsukamui Aug 25, 2026
c2a6d0d
Improve multi-selection correctness
senketsukamui Aug 25, 2026
e220e7e
Restore selections after frame navigation
senketsukamui Aug 26, 2026
608a8a8
Refactor multi-selection helpers
senketsukamui Aug 26, 2026
196dd29
Add multi-selection changelog
senketsukamui Aug 26, 2026
166bf8d
Merge branch 'develop' into gp/multiselection
senketsukamui Aug 26, 2026
ba45fc5
Merge branch 'develop' into gp/multiselection
senketsukamui Aug 26, 2026
65f329c
Merge branch 'develop' of github.com:cvat-ai/cvat into gp/multiselection
senketsukamui Aug 26, 2026
0a0a88c
Merge remote-tracking branch 'origin/gp/multiselection' into gp/multi…
senketsukamui Aug 26, 2026
003486f
Avoid conflicts with canvas modifier actions
senketsukamui Aug 26, 2026
2d49e67
Preserve selection when using object controls
senketsukamui Aug 26, 2026
10b7df7
Merge branch 'develop' into gp/multiselection
senketsukamui Aug 27, 2026
e6f9192
Add grouping for object selections
senketsukamui Aug 28, 2026
794abc1
Merge remote-tracking branch 'origin/gp/multiselection' into gp/multi…
senketsukamui Aug 28, 2026
d02acf5
Add attribute editing for object selections
senketsukamui Aug 28, 2026
ce5e91c
Preserve object selections during canvas updates
senketsukamui Aug 31, 2026
e0d6515
Keep multi-selection moves within frame bounds
senketsukamui Aug 31, 2026
af643b6
Reflect multi-selection drag availability
senketsukamui Aug 31, 2026
eb75a67
Align multi-selection controls with object controls
senketsukamui Aug 31, 2026
cace677
Reuse object shortcuts for multi-selection
senketsukamui Aug 31, 2026
5fd89d4
Unify copy placement for objects and selections
senketsukamui Aug 31, 2026
2a31ad0
Address remaining multi-selection review feedback
senketsukamui Sep 1, 2026
af3d67a
Refine multi-selection interaction behavior
senketsukamui Sep 1, 2026
347449a
Merge branch 'develop' into gp/multiselection
senketsukamui Sep 2, 2026
0b861b8
Merge branch 'develop' into gp/multiselection
senketsukamui Sep 2, 2026
606982f
Address multi-selection interaction feedback
senketsukamui Sep 2, 2026
9bf1b39
Indicate selected line-based objects
senketsukamui Sep 3, 2026
08ff436
Merge branch 'develop' of github.com:cvat-ai/cvat into gp/multiselection
senketsukamui Sep 3, 2026
79baba4
Add annotation and layer actions for selections
senketsukamui Sep 3, 2026
b499432
Preserve pin state only when copying objects
senketsukamui Sep 4, 2026
9bce9b1
Allow point context menu with selection modifier
senketsukamui Sep 4, 2026
21fa1b7
Preserve 3D object copy behavior
senketsukamui Sep 4, 2026
1fe25c1
Restore single object copy flow
senketsukamui Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Added

- Added multi-object selection in the 2D annotation workspace, including
bounding-box selection, batch operations, and undo and redo support
(<https://github.com/cvat-ai/cvat/pull/11084>)
72 changes: 71 additions & 1 deletion cvat-canvas/src/scss/canvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,84 @@ polyline.cvat_shape_drawing_opacity {
fill: #fcfbfc;
}

.cvat_canvas_selected_objects_box {
fill: none;
stroke: #1f1f1f;
stroke-opacity: 1;
pointer-events: all;
cursor: move;
}

.cvat_canvas_selected_objects_box_dragging {
opacity: 0;
}

.cvat_canvas_shape_selected_object {
fill-opacity: var(--cvat-selection-opacity) !important;
}

image.cvat_canvas_shape_selected_object {
opacity: var(--cvat-selection-mask-opacity) !important;
}

polyline.cvat_canvas_shape_selected_object {
fill-opacity: 0 !important;
}

.cvat_canvas_selected_objects_label {
position: absolute;
z-index: 5;
display: flex;
align-items: center;
min-height: 24px;
padding: 2px 4px 2px 8px;
color: white;
background: rgba(31, 31, 31, 88%);
border: 1px solid rgba(255, 255, 255, 35%);
border-radius: 2px;
font-size: 14px;
font-weight: 600;
line-height: 20px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 45%));
pointer-events: none;
}

.cvat_canvas_selected_objects_label_title {
white-space: nowrap;
}

.cvat_canvas_selected_objects_label_dragging {
visibility: hidden;
}

.cvat_canvas_selected_objects_menu_button {
width: 20px;
height: 20px;
margin-left: 6px;
padding: 0 0 4px;
color: white;
background: transparent;
border: 0;
cursor: pointer;
font-size: 18px;
font-weight: bold;
line-height: 20px;
pointer-events: auto;

&:hover,
&:focus-visible {
color: #40a9ff;
}
}

image.cvat_canvas_shape_selection {
visibility: hidden;
}

.cvat_canvas_selection_box {
fill: white;
fill-opacity: 0.1;
stroke: white;
stroke: #1f1f1f;
}

.cvat_canvas_shape_region_selection {
Expand Down
12 changes: 11 additions & 1 deletion cvat-canvas/src/typescript/canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// SPDX-License-Identifier: MIT

import {
DrawData, MergeData, SplitData, GroupData,
DrawData, MergeData, SplitData, GroupData, SelectData,
JoinData, SliceData, MasksEditData,
InteractionData as _InteractionData,
InteractionResult as _InteractionResult,
Expand Down Expand Up @@ -41,6 +41,8 @@ interface Canvas {
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
selectObjects(selectData: SelectData): void;
setSelectedObjects(clientIDs: number[]): void;

fitCanvas(): void;
bitmap(enable: boolean): void;
Expand Down Expand Up @@ -170,6 +172,14 @@ class CanvasImpl implements Canvas {
this.model.select(objectState);
}

public selectObjects(selectData: SelectData): void {
this.model.selectObjects(selectData);
}

public setSelectedObjects(clientIDs: number[]): void {
this.model.setSelectedObjects(clientIDs);
}

public mode(): Mode {
return this.model.mode;
}
Expand Down
16 changes: 16 additions & 0 deletions cvat-canvas/src/typescript/canvasController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MergeData,
SplitData,
GroupData,
SelectData,
JoinData,
SliceData,
Mode,
Expand All @@ -37,6 +38,8 @@ export interface CanvasController {
readonly mergeData: MergeData;
readonly splitData: SplitData;
readonly groupData: GroupData;
readonly selectData: SelectData;
readonly selectedObjects: number[];
readonly joinData: JoinData;
readonly sliceData: SliceData;
readonly selected: any;
Expand All @@ -47,6 +50,7 @@ export interface CanvasController {
zoom(x: number, y: number, deltaY: number): void;
draw(drawData: DrawData): void;
edit(editData: MasksEditData | PolyEditData): void;
selectObjects(selectData: SelectData): void;
enableDrag(x: number, y: number): void;
drag(x: number, y: number): void;
disableDrag(): void;
Expand Down Expand Up @@ -103,6 +107,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.edit(editData);
}

public selectObjects(selectData: SelectData): void {
this.model.selectObjects(selectData);
}

public focus(clientID: number, padding: number): void {
this.model.focus(clientID, padding);
}
Expand Down Expand Up @@ -163,6 +171,14 @@ export class CanvasControllerImpl implements CanvasController {
return this.model.groupData;
}

public get selectData(): SelectData {
return this.model.selectData;
}

public get selectedObjects(): number[] {
return this.model.selectedObjects;
}

public get joinData(): JoinData {
return this.model.joinData;
}
Expand Down
59 changes: 59 additions & 0 deletions cvat-canvas/src/typescript/canvasModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,13 @@ export enum ColorBy {
LABEL = 'Label',
}

export type MultiSelectModifier = 'shift' | 'ctrl' | 'alt' | 'meta';

export interface Configuration {
smoothImage?: boolean;
autoborders?: boolean;
multiSelectModifier?: MultiSelectModifier;
multiSelectObjectModifier?: MultiSelectModifier;
snapToPoint?: boolean;
adaptiveZoom?: boolean;
displayAllText?: boolean;
Expand Down Expand Up @@ -174,6 +178,11 @@ export interface GroupData {
enabled: boolean;
}

export interface SelectData {
enabled: boolean;
once?: boolean;
}

export interface MergeData {
enabled: boolean;
}
Expand Down Expand Up @@ -222,6 +231,8 @@ export enum UpdateReasons {
JOIN = 'join',
SLICE = 'slice',
SELECT = 'select',
SELECT_OBJECTS = 'select_objects',
SELECTED_OBJECTS_UPDATED = 'selected_objects_updated',
CANCEL = 'cancel',
BITMAP = 'bitmap',
SELECT_REGION = 'select_region',
Expand All @@ -244,6 +255,7 @@ export enum Mode {
JOIN = 'join',
SLICE = 'slice',
INTERACT = 'interact',
SELECT = 'select',
SELECT_REGION = 'select_region',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
Expand All @@ -268,6 +280,8 @@ export interface CanvasModel {
readonly groupData: GroupData;
readonly joinData: JoinData;
readonly sliceData: SliceData;
readonly selectData: SelectData;
readonly selectedObjects: number[];
readonly configuration: Configuration;
readonly selected: any;
geometry: Geometry;
Expand All @@ -294,6 +308,8 @@ export interface CanvasModel {
split(splitData: SplitData): void;
merge(mergeData: MergeData): void;
select(objectState: any): void;
selectObjects(selectData: SelectData): void;
setSelectedObjects(clientIDs: number[]): void;
interact(interactionData: InteractionData): void;

fitCanvas(width: number, height: number): void;
Expand Down Expand Up @@ -324,6 +340,9 @@ const defaultData = {
groupData: {
enabled: false,
},
selectData: {
enabled: false,
},
splitData: {
enabled: false,
},
Expand Down Expand Up @@ -385,10 +404,12 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
interactionData: InteractionData;
mergeData: MergeData;
groupData: GroupData;
selectData: SelectData;
joinData: JoinData;
sliceData: SliceData;
splitData: SplitData;
selected: any;
selectedObjects: number[];
mode: Mode;
exception: Error | null;
};
Expand Down Expand Up @@ -435,6 +456,8 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
undefinedAttrValue: consts.DEFAULT_UNDEFINED_ATTR_VALUE,
hideEditedObject: false,
focusedObjectPadding: 50,
multiSelectModifier: 'shift',
multiSelectObjectModifier: 'ctrl',
},
imageBitmap: false,
image: null,
Expand Down Expand Up @@ -462,6 +485,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
left: 0,
fittedScale: 0,
selected: null,
selectedObjects: [],
mode: Mode.IDLE,
exception: null,
...defaultData,
Expand Down Expand Up @@ -894,6 +918,26 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.notify(UpdateReasons.GROUP);
}

public selectObjects(selectData: SelectData): void {
if (![Mode.IDLE, Mode.SELECT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}

if ((this.data.selectData.enabled && selectData.enabled) || (
!this.data.selectData.enabled && !selectData.enabled
)) {
return;
}

this.data.selectData = { ...selectData };
this.notify(UpdateReasons.SELECT_OBJECTS);
}

public setSelectedObjects(clientIDs: number[]): void {
this.data.selectedObjects = [...clientIDs];
this.notify(UpdateReasons.SELECTED_OBJECTS_UPDATED);
}

public join(joinData: JoinData): void {
if (![Mode.IDLE, Mode.JOIN].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
Expand Down Expand Up @@ -993,6 +1037,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (typeof configuration.smoothImage === 'boolean') {
this.data.configuration.smoothImage = configuration.smoothImage;
}
if (['shift', 'ctrl', 'alt', 'meta'].includes(configuration.multiSelectModifier)) {
this.data.configuration.multiSelectModifier = configuration.multiSelectModifier;
}
if (['shift', 'ctrl', 'alt', 'meta'].includes(configuration.multiSelectObjectModifier)) {
this.data.configuration.multiSelectObjectModifier = configuration.multiSelectObjectModifier;
}

if (typeof configuration.undefinedAttrValue === 'string') {
this.data.configuration.undefinedAttrValue = configuration.undefinedAttrValue;
}
Expand Down Expand Up @@ -1167,6 +1218,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
return { ...this.data.groupData };
}

public get selectData(): SelectData {
return { ...this.data.selectData };
}

public get selectedObjects(): number[] {
return [...this.data.selectedObjects];
}

public get selected(): any {
return this.data.selected;
}
Expand Down
Loading
Loading