VSE: Implement Lasso Select

This feature works like the select lasso in other editors. In preview
the user can draw a region they want to select and when a strips origin
is in this lasso region the strip gets selected.

In timeline the user can do the same and the strip gets selected when
the strip is in the lasso or some part of the lasso is in the strip.

The tool can be accessed through in the toolbar or via shortcut.

Pull Request: https://projects.blender.org/blender/blender/pulls/143391
This commit is contained in:
Ramon Klauck
2025-09-05 17:53:13 +02:00
committed by Richard Antalik
parent d939de98a9
commit b910e04a2a
6 changed files with 241 additions and 0 deletions

View File

@@ -3246,6 +3246,10 @@ def km_sequencer_preview(params):
{"properties": [("unselected", False)]}),
("sequencer.delete", {"type": 'X', "value": 'PRESS'}, None),
("sequencer.delete", {"type": 'DEL', "value": 'PRESS'}, None),
("sequencer.select_lasso", {"type": params.action_mouse, "value": 'CLICK_DRAG', "ctrl": True},
{"properties": [("mode", 'ADD')]}),
("sequencer.select_lasso", {"type": params.action_mouse, "value": 'CLICK_DRAG', "shift": True, "ctrl": True},
{"properties": [("mode", 'SUB')]}),
("sequencer.copy", {"type": 'C', "value": 'PRESS', "ctrl": True}, None),
("sequencer.paste", {"type": 'V', "value": 'PRESS', "ctrl": True}, None),
("sequencer.paste", {"type": 'V', "value": 'PRESS', "ctrl": True, "shift": True},
@@ -8451,6 +8455,19 @@ def km_sequencer_tool_generic_select_box(params, *, fallback):
)
def km_sequencer_tool_generic_select_lasso(params, *, fallback):
return (
_fallback_id("Sequencer Tool: Select Lasso", fallback),
{"space_type": 'SEQUENCE_EDITOR', "region_type": 'WINDOW'},
{"items": [
*([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple(
"sequencer.select_lasso",
**(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else
params.tool_tweak_event))),
]},
)
def km_sequencer_tool_generic_select_circle(params, *, fallback):
return (
_fallback_id("Sequencer Tool: Select Circle", fallback),
@@ -8498,6 +8515,19 @@ def km_sequencer_preview_tool_generic_select_box(params, *, fallback):
)
def km_sequencer_preview_tool_generic_select_lasso(params, *, fallback):
return (
_fallback_id("Preview Tool: Select Lasso", fallback),
{"space_type": 'SEQUENCE_EDITOR', "region_type": 'WINDOW'},
{"items": [
*([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple(
"sequencer.select_lasso",
**(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else
params.tool_tweak_event))),
]},
)
def km_sequencer_preview_tool_generic_select_circle(params, *, fallback):
return (
_fallback_id("Preview Tool: Select Circle", fallback),
@@ -8897,6 +8927,10 @@ def generate_keymaps(params=None):
for fallback in (False, True)),
*(km_sequencer_preview_tool_generic_select_box(params, fallback=fallback)
for fallback in (False, True)),
*(km_sequencer_tool_generic_select_lasso(params, fallback=fallback)
for fallback in (False, True)),
*(km_sequencer_preview_tool_generic_select_lasso(params, fallback=fallback)
for fallback in (False, True)),
*(km_sequencer_preview_tool_generic_select_circle(params, fallback=fallback)
for fallback in (False, True)),
*(km_sequencer_tool_generic_select_circle(params, fallback=fallback)

View File

@@ -3272,6 +3272,38 @@ class _defs_sequencer_select:
draw_settings=draw_settings,
)
@ToolDef.from_fn
def lasso_timeline():
def draw_settings(_context, layout, tool):
props = tool.operator_properties("sequencer.select_lasso")
row = layout.row()
row.use_property_split = False
row.prop(props, "mode", text="", expand=True, icon_only=True)
return dict(
idname="sequencer.select_lasso",
label="Select Lasso",
icon="ops.generic.select_lasso",
widget=None,
keymap="Sequencer Tool: Select Lasso",
draw_settings=draw_settings,
)
@ToolDef.from_fn
def lasso_preview():
def draw_settings(_context, layout, tool):
props = tool.operator_properties("sequencer.select_lasso")
row = layout.row()
row.use_property_split = False
row.prop(props, "mode", text="", expand=True, icon_only=True)
return dict(
idname="sequencer.select_lasso",
label="Select Lasso",
icon="ops.generic.select_lasso",
widget=None,
keymap="Preview Tool: Select Lasso",
draw_settings=draw_settings,
)
@ToolDef.from_fn
def circle_timeline():
def draw_settings(_context, layout, tool):
@@ -4006,6 +4038,7 @@ class SEQUENCER_PT_tools_active(ToolSelectPanelHelper, Panel):
(
_defs_sequencer_select.select_preview,
_defs_sequencer_select.box_preview,
_defs_sequencer_select.lasso_preview,
_defs_sequencer_select.circle_preview,
),
_defs_sequencer_generic.cursor,
@@ -4021,6 +4054,7 @@ class SEQUENCER_PT_tools_active(ToolSelectPanelHelper, Panel):
'SEQUENCER': [
(
_defs_sequencer_select.box_timeline,
_defs_sequencer_select.lasso_timeline,
_defs_sequencer_select.circle_timeline,
),
_defs_sequencer_generic.blade,

View File

@@ -274,6 +274,7 @@ void SEQUENCER_OT_select_linked_pick(wmOperatorType *ot);
void SEQUENCER_OT_select_handles(wmOperatorType *ot);
void SEQUENCER_OT_select_side(wmOperatorType *ot);
void SEQUENCER_OT_select_box(wmOperatorType *ot);
void SEQUENCER_OT_select_lasso(wmOperatorType *ot);
void SEQUENCER_OT_select_circle(wmOperatorType *ot);
void SEQUENCER_OT_select_inverse(wmOperatorType *ot);
void SEQUENCER_OT_select_grouped(wmOperatorType *ot);

View File

@@ -105,6 +105,7 @@ void sequencer_operatortypes()
WM_operatortype_append(SEQUENCER_OT_select_side);
WM_operatortype_append(SEQUENCER_OT_select_side_of_frame);
WM_operatortype_append(SEQUENCER_OT_select_box);
WM_operatortype_append(SEQUENCER_OT_select_lasso);
WM_operatortype_append(SEQUENCER_OT_select_circle);
WM_operatortype_append(SEQUENCER_OT_select_grouped);

View File

@@ -10,6 +10,7 @@
#include <cstdlib>
#include <cstring>
#include "BLI_lasso_2d.hh"
#include "BLI_rect.h"
#include "MEM_guardedalloc.h"
@@ -2325,7 +2326,176 @@ void SEQUENCER_OT_select_box(wmOperatorType *ot)
"Select strips individually whether or not they are connected");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Lasso Select Operator
* \{ */
static bool do_lasso_select_is_origin_inside(const ARegion *region,
const rcti *clip_rect,
const Span<int2> mcoords,
const float co_test[2])
{
int co_screen[2];
if (UI_view2d_view_to_region_clip(
&region->v2d, co_test[0], co_test[1], &co_screen[0], &co_screen[1]) &&
BLI_rcti_isect_pt_v(clip_rect, co_screen) &&
BLI_lasso_is_point_inside(mcoords, co_screen[0], co_screen[1], V2D_IS_CLIPPED))
{
return true;
}
return false;
}
static bool rcti_in_lasso(const rcti rect, const Span<int2> mcoords)
{
rcti lasso_rect;
BLI_lasso_boundbox(&lasso_rect, mcoords);
/* Check if edge of strip is in the lasso. */
if (BLI_lasso_is_edge_inside(
mcoords, rect.xmin, rect.ymin, rect.xmax, rect.ymin, V2D_IS_CLIPPED) ||
BLI_lasso_is_edge_inside(
mcoords, rect.xmax, rect.ymin, rect.xmax, rect.ymax, V2D_IS_CLIPPED) ||
BLI_lasso_is_edge_inside(
mcoords, rect.xmax, rect.ymax, rect.xmin, rect.ymax, V2D_IS_CLIPPED) ||
BLI_lasso_is_edge_inside(
mcoords, rect.xmin, rect.ymax, rect.xmin, rect.ymin, V2D_IS_CLIPPED))
{
return true;
}
/* Check if lasso is in the strip rect. Used when the lasso is only inside one strip. */
if (BLI_rcti_inside_rcti(&rect, &lasso_rect)) {
return true;
}
return false;
}
static bool do_lasso_select_timeline(bContext *C,
const Span<int2> mcoords,
ARegion *region,
const eSelectOp sel_op)
{
Scene *scene = CTX_data_scene(C);
Editing *ed = seq::editing_get(scene);
bool changed = false;
const bool select = (sel_op != SEL_OP_SUB);
LISTBASE_FOREACH (Strip *, strip, &ed->seqbase) {
rctf strip_rct;
rcti region_rct;
strip_rectf(scene, strip, &strip_rct);
UI_view2d_view_to_region_clip(
&region->v2d, strip_rct.xmin, strip_rct.ymin, &region_rct.xmin, &region_rct.ymin);
UI_view2d_view_to_region_clip(
&region->v2d, strip_rct.xmax, strip_rct.ymax, &region_rct.xmax, &region_rct.ymax);
if (rcti_in_lasso(region_rct, mcoords)) {
SET_FLAG_FROM_TEST(strip->flag, select, SELECT);
changed = true;
}
}
return changed;
}
static bool do_lasso_select_preview(bContext *C,
Editing *ed,
const Span<int2> mcoords,
const eSelectOp sel_op)
{
Scene *scene = CTX_data_scene(C);
const ARegion *region = CTX_wm_region(C);
bool changed = false;
rcti rect;
BLI_lasso_boundbox(&rect, mcoords);
ListBase *seqbase = seq::active_seqbase_get(ed);
ListBase *channels = seq::channels_displayed_get(ed);
SpaceSeq *sseq = CTX_wm_space_seq(C);
blender::VectorSet strips = seq::query_rendered_strips(
scene, channels, seqbase, scene->r.cfra, sseq->chanshown);
for (Strip *strip : strips) {
float2 origin = seq::image_transform_origin_offset_pixelspace_get(scene, strip);
if (do_lasso_select_is_origin_inside(region, &rect, mcoords, origin)) {
changed = true;
if (ELEM(sel_op, SEL_OP_ADD, SEL_OP_SET)) {
strip->flag |= SELECT;
}
else {
BLI_assert(sel_op == SEL_OP_SUB);
strip->flag &= ~SELECT;
}
}
}
return changed;
}
static wmOperatorStatus vse_lasso_select_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
ARegion *region = CTX_wm_region(C);
Array<int2> mcoords = WM_gesture_lasso_path_to_array(C, op);
Editing *ed = seq::editing_get(scene);
if (ed == nullptr) {
return OPERATOR_CANCELLED;
}
if (mcoords.is_empty()) {
return OPERATOR_PASS_THROUGH;
}
const eSelectOp sel_op = eSelectOp(RNA_enum_get(op->ptr, "mode"));
const bool use_pre_deselect = SEL_OP_USE_PRE_DESELECT(sel_op);
bool changed = false;
if (use_pre_deselect) {
changed |= deselect_all_strips(scene);
}
if (region->regiontype == RGN_TYPE_PREVIEW) {
changed = do_lasso_select_preview(C, ed, mcoords, sel_op);
}
else {
changed = do_lasso_select_timeline(C, mcoords, region, sel_op);
}
if (changed) {
sequencer_select_do_updates(C, scene);
return OPERATOR_FINISHED;
}
return OPERATOR_CANCELLED;
}
void SEQUENCER_OT_select_lasso(wmOperatorType *ot)
{
ot->name = "Lasso Select";
ot->description = "Select strips using lasso selection";
ot->idname = "SEQUENCER_OT_select_lasso";
ot->invoke = WM_gesture_lasso_invoke;
ot->modal = WM_gesture_lasso_modal;
ot->exec = vse_lasso_select_exec;
ot->poll = ED_operator_sequencer_active;
ot->cancel = WM_gesture_lasso_cancel;
/* flags */
ot->flag = OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR;
/* properties */
WM_operator_properties_gesture_lasso(ot);
WM_operator_properties_select_operation_simple(ot);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Circle Select Operator
* \{ */
static bool strip_circle_select_radius_image_isect(const Scene *scene,
const Strip *strip,
const int *radius,

View File

@@ -4402,6 +4402,7 @@ static void gesture_lasso_modal_keymap(wmKeyConfig *keyconf)
WM_modalkeymap_assign(keymap, "GRAPH_OT_select_lasso");
WM_modalkeymap_assign(keymap, "NODE_OT_select_lasso");
WM_modalkeymap_assign(keymap, "UV_OT_select_lasso");
WM_modalkeymap_assign(keymap, "SEQUENCER_OT_select_lasso");
WM_modalkeymap_assign(keymap, "PAINT_OT_hide_show_lasso_gesture");
WM_modalkeymap_assign(keymap, "GREASE_PENCIL_OT_erase_lasso");
}