From b910e04a2a05f8f801c3d3dd82e0af3d93112e5d Mon Sep 17 00:00:00 2001 From: Ramon Klauck Date: Fri, 5 Sep 2025 17:53:13 +0200 Subject: [PATCH] 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 --- .../keyconfig/keymap_data/blender_default.py | 34 ++++ .../startup/bl_ui/space_toolsystem_toolbar.py | 34 ++++ .../space_sequencer/sequencer_intern.hh | 1 + .../editors/space_sequencer/sequencer_ops.cc | 1 + .../space_sequencer/sequencer_select.cc | 170 ++++++++++++++++++ .../windowmanager/intern/wm_operators.cc | 1 + 6 files changed, 241 insertions(+) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 86cd3e5ef46..91b90191a74 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -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) diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index 96d2fbe0328..cbb7d8e9cc2 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -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, diff --git a/source/blender/editors/space_sequencer/sequencer_intern.hh b/source/blender/editors/space_sequencer/sequencer_intern.hh index 0333b5ca664..6b822af2707 100644 --- a/source/blender/editors/space_sequencer/sequencer_intern.hh +++ b/source/blender/editors/space_sequencer/sequencer_intern.hh @@ -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); diff --git a/source/blender/editors/space_sequencer/sequencer_ops.cc b/source/blender/editors/space_sequencer/sequencer_ops.cc index 2daa7d0811a..58b746de6d3 100644 --- a/source/blender/editors/space_sequencer/sequencer_ops.cc +++ b/source/blender/editors/space_sequencer/sequencer_ops.cc @@ -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); diff --git a/source/blender/editors/space_sequencer/sequencer_select.cc b/source/blender/editors/space_sequencer/sequencer_select.cc index c79eb5ba178..cda2671d85f 100644 --- a/source/blender/editors/space_sequencer/sequencer_select.cc +++ b/source/blender/editors/space_sequencer/sequencer_select.cc @@ -10,6 +10,7 @@ #include #include +#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 mcoords, + const float co_test[2]) +{ + int co_screen[2]; + if (UI_view2d_view_to_region_clip( + ®ion->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 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 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( + ®ion->v2d, strip_rct.xmin, strip_rct.ymin, ®ion_rct.xmin, ®ion_rct.ymin); + UI_view2d_view_to_region_clip( + ®ion->v2d, strip_rct.xmax, strip_rct.ymax, ®ion_rct.xmax, ®ion_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 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 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, diff --git a/source/blender/windowmanager/intern/wm_operators.cc b/source/blender/windowmanager/intern/wm_operators.cc index 71878863484..89d9b6cbb68 100644 --- a/source/blender/windowmanager/intern/wm_operators.cc +++ b/source/blender/windowmanager/intern/wm_operators.cc @@ -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"); }