diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index a09ff5865aa..42db9fa51c1 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -1407,6 +1407,21 @@ class _defs_sculpt: use_separators=False, ) + @staticmethod + def draw_lasso_stroke_settings(layout, props, draw_inline, draw_popover): + if draw_inline: + layout.prop(props, "use_smooth_stroke", text="Stabilize Stroke") + + layout.use_property_split = True + layout.use_property_decorate = False + col = layout.column() + col.active = props.use_smooth_stroke + col.prop(props, "smooth_stroke_radius", text="Radius", slider=True) + col.prop(props, "smooth_stroke_factor", text="Factor", slider=True) + + if draw_popover: + layout.popover("TOPBAR_PT_tool_settings_extra", text="Stroke") + @ToolDef.from_fn def mask_border(): def draw_settings(_context, layout, tool): @@ -1424,9 +1439,19 @@ class _defs_sculpt: @ToolDef.from_fn def mask_lasso(): - def draw_settings(_context, layout, tool): + def draw_settings(_context, layout, tool, *, extra=False): + draw_popover = False props = tool.operator_properties("paint.mask_lasso_gesture") - layout.prop(props, "use_front_faces_only", expand=False) + + if not extra: + layout.prop(props, "use_front_faces_only", expand=False) + region_is_header = bpy.context.region.type == 'TOOL_HEADER' + if region_is_header: + draw_popover = True + else: + extra = True + + _defs_sculpt.draw_lasso_stroke_settings(layout, props, extra, draw_popover) return dict( idname="builtin.lasso_mask", @@ -1485,9 +1510,19 @@ class _defs_sculpt: @ToolDef.from_fn def hide_lasso(): - def draw_settings(_context, layout, tool): + def draw_settings(_context, layout, tool, *, extra=False): + draw_popover = False props = tool.operator_properties("paint.hide_show_lasso_gesture") - layout.prop(props, "area", expand=False) + + if not extra: + layout.prop(props, "area", expand=False) + region_is_header = bpy.context.region.type == 'TOOL_HEADER' + if region_is_header: + draw_popover = True + else: + extra = True + + _defs_sculpt.draw_lasso_stroke_settings(layout, props, extra, draw_popover) return dict( idname="builtin.lasso_hide", @@ -1545,9 +1580,19 @@ class _defs_sculpt: @ToolDef.from_fn def face_set_lasso(): - def draw_settings(_context, layout, tool): + def draw_settings(_context, layout, tool, *, extra=False): + draw_popover = False props = tool.operator_properties("sculpt.face_set_lasso_gesture") - layout.prop(props, "use_front_faces_only", expand=False) + + if not extra: + layout.prop(props, "use_front_faces_only", expand=False) + region_is_header = bpy.context.region.type == 'TOOL_HEADER' + if region_is_header: + draw_popover = True + else: + extra = True + + _defs_sculpt.draw_lasso_stroke_settings(layout, props, extra, draw_popover) return dict( idname="builtin.lasso_face_set", @@ -1609,13 +1654,24 @@ class _defs_sculpt: @ToolDef.from_fn def trim_lasso(): - def draw_settings(_context, layout, tool): + def draw_settings(_context, layout, tool, *, extra=False): + draw_popover = False props = tool.operator_properties("sculpt.trim_lasso_gesture") - layout.prop(props, "trim_solver", expand=False) - layout.prop(props, "trim_mode", expand=False) - layout.prop(props, "trim_orientation", expand=False) - layout.prop(props, "trim_extrude_mode", expand=False) - layout.prop(props, "use_cursor_depth", expand=False) + + if not extra: + layout.prop(props, "trim_solver", expand=False) + layout.prop(props, "trim_mode", expand=False) + layout.prop(props, "trim_orientation", expand=False) + layout.prop(props, "trim_extrude_mode", expand=False) + layout.prop(props, "use_cursor_depth", expand=False) + region_is_header = bpy.context.region.type == 'TOOL_HEADER' + if region_is_header: + draw_popover = True + else: + extra = True + + _defs_sculpt.draw_lasso_stroke_settings(layout, props, extra, draw_popover) + return dict( idname="builtin.lasso_trim", label="Lasso Trim", diff --git a/source/blender/windowmanager/WM_types.hh b/source/blender/windowmanager/WM_types.hh index 74a22a97343..08c047b8cc0 100644 --- a/source/blender/windowmanager/WM_types.hh +++ b/source/blender/windowmanager/WM_types.hh @@ -593,6 +593,8 @@ struct wmGesture { int modal_state; /** Optional, draw the active side of the straight-line gesture. */ bool draw_active_side; + /** Latest mouse position relative to area. Currently only used by lasso drawing code.*/ + blender::int2 mval; /** * For modal operators which may be running idle, waiting for an event to activate the gesture. @@ -612,6 +614,9 @@ struct wmGesture { /** For gestures that support flip, stores if flip is enabled using the modal keymap * toggle. */ uint use_flip : 1; + /** For gestures that support smoothing, stores if smoothing is enabled using the modal keymap + * toggle. */ + uint use_smooth : 1; /** * customdata diff --git a/source/blender/windowmanager/intern/wm_gesture.cc b/source/blender/windowmanager/intern/wm_gesture.cc index 40bbfdf5bf6..a746ca7285e 100644 --- a/source/blender/windowmanager/intern/wm_gesture.cc +++ b/source/blender/windowmanager/intern/wm_gesture.cc @@ -72,10 +72,10 @@ wmGesture *WM_gesture_new(wmWindow *window, const ARegion *region, const wmEvent } } else if (ELEM(type, WM_GESTURE_LINES, WM_GESTURE_LASSO)) { - short *lasso; + float *lasso; gesture->points_alloc = 1024; - gesture->customdata = lasso = static_cast( - MEM_mallocN(sizeof(short[2]) * gesture->points_alloc, "lasso points")); + gesture->customdata = lasso = static_cast( + MEM_mallocN(sizeof(float[2]) * gesture->points_alloc, "lasso points")); lasso[0] = xy[0] - gesture->winrct.xmin; lasso[1] = xy[1] - gesture->winrct.ymin; gesture->points = 1; @@ -300,7 +300,7 @@ static void draw_filled_lasso_px_cb(int x, int x_end, int y, void *user_data) static void draw_filled_lasso(wmGesture *gt) { - const short *lasso = (short *)gt->customdata; + const float *lasso = (float *)gt->customdata; const int mcoords_len = gt->points; Array mcoords(mcoords_len); int i; @@ -351,9 +351,48 @@ static void draw_filled_lasso(wmGesture *gt) } } +/* TODO: Extract this common functionality so it can be shared between Sculpt brushes, the annotate + * tool, and this common logic. */ +static void draw_lasso_smooth_stroke_indicator(wmGesture *gt, const uint shdr_pos) +{ + float(*lasso)[2] = static_cast(gt->customdata); + float last_x = lasso[gt->points - 1][0]; + float last_y = lasso[gt->points - 1][1]; + + immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); + GPU_line_smooth(true); + GPU_blend(GPU_BLEND_ALPHA); + + GPU_line_width(1.25f); + const float color[3] = {1.0f, 0.39f, 0.39f}; + + const float radius = 4.0f; + + /* Draw Inner Ring */ + immUniformColor4f(color[0], color[1], color[2], 0.8f); + imm_draw_circle_wire_2d(shdr_pos, gt->mval.x, gt->mval.y, radius, 40); + + /* Draw Outer Ring: Dark color for contrast on light backgrounds (e.g. gray on white) */ + float darkcolor[3]; + mul_v3_v3fl(darkcolor, color, 0.40f); + immUniformColor4f(darkcolor[0], darkcolor[1], darkcolor[2], 0.8f); + imm_draw_circle_wire_2d(shdr_pos, gt->mval.x, gt->mval.y, radius + 1, 40); + + /* Draw line from the last saved position to the current mouse position. */ + immUniformColor4f(color[0], color[1], color[2], 0.8f); + immBegin(GPU_PRIM_LINES, 2); + immVertex2f(shdr_pos, gt->mval.x, gt->mval.y); + immVertex2f(shdr_pos, last_x, last_y); + immEnd(); + + GPU_blend(GPU_BLEND_NONE); + GPU_line_smooth(false); + immUnbindProgram(); +} + static void wm_gesture_draw_lasso(wmGesture *gt, bool filled) { - const short *lasso = (short *)gt->customdata; + const float *lasso = (float *)gt->customdata; int i; if (filled) { @@ -385,12 +424,15 @@ static void wm_gesture_draw_lasso(wmGesture *gt, bool filled) immBegin((gt->type == WM_GESTURE_LASSO) ? GPU_PRIM_LINE_LOOP : GPU_PRIM_LINE_STRIP, numverts); for (i = 0; i < gt->points; i++, lasso += 2) { - immVertex2f(shdr_pos, float(lasso[0]), float(lasso[1])); + immVertex2f(shdr_pos, lasso[0], lasso[1]); } immEnd(); - immUnbindProgram(); + + if (gt->use_smooth) { + draw_lasso_smooth_stroke_indicator(gt, shdr_pos); + } } static void draw_start_vertex_circle(const wmGesture >, const uint shdr_pos) diff --git a/source/blender/windowmanager/intern/wm_gesture_ops.cc b/source/blender/windowmanager/intern/wm_gesture_ops.cc index b39a25c28f2..4d1d942ef5c 100644 --- a/source/blender/windowmanager/intern/wm_gesture_ops.cc +++ b/source/blender/windowmanager/intern/wm_gesture_ops.cc @@ -17,8 +17,10 @@ #include "DNA_space_types.h" #include "DNA_windowmanager_types.h" +#include "BLI_math_base.hh" #include "BLI_math_rotation.h" #include "BLI_math_vector.h" +#include "BLI_math_vector.hh" #include "BLI_math_vector_types.hh" #include "BLI_rect.h" @@ -491,6 +493,8 @@ int WM_gesture_lasso_invoke(bContext *C, wmOperator *op, const wmEvent *event) PropertyRNA *prop; op->customdata = WM_gesture_new(win, CTX_wm_region(C), event, WM_GESTURE_LASSO); + wmGesture *gesture = static_cast(op->customdata); + gesture->use_smooth = RNA_boolean_get(op->ptr, "use_smooth_stroke"); /* Add modal handler. */ WM_event_add_modal_handler(C, op); @@ -510,6 +514,8 @@ int WM_gesture_lines_invoke(bContext *C, wmOperator *op, const wmEvent *event) PropertyRNA *prop; op->customdata = WM_gesture_new(win, CTX_wm_region(C), event, WM_GESTURE_LINES); + wmGesture *gesture = static_cast(op->customdata); + gesture->use_smooth = RNA_boolean_get(op->ptr, "use_smooth_stroke"); /* Add modal handler. */ WM_event_add_modal_handler(C, op); @@ -530,7 +536,7 @@ static int gesture_lasso_apply(bContext *C, wmOperator *op) PointerRNA itemptr; float loc[2]; int i; - const short *lasso = static_cast(gesture->customdata); + const float *lasso = static_cast(gesture->customdata); /* Operator storage as path. */ @@ -555,6 +561,8 @@ static int gesture_lasso_apply(bContext *C, wmOperator *op) int WM_gesture_lasso_modal(bContext *C, wmOperator *op, const wmEvent *event) { wmGesture *gesture = static_cast(op->customdata); + const float factor = RNA_float_get(op->ptr, "smooth_stroke_factor"); + const int radius = RNA_int_get(op->ptr, "smooth_stroke_radius"); if (event->type == EVT_MODAL_MAP) { switch (event->val) { @@ -569,31 +577,46 @@ int WM_gesture_lasso_modal(bContext *C, wmOperator *op, const wmEvent *event) case MOUSEMOVE: case INBETWEEN_MOUSEMOVE: { wm_gesture_tag_redraw(CTX_wm_window(C)); + gesture->mval = int2((event->xy[0] - gesture->winrct.xmin), + (event->xy[1] - gesture->winrct.ymin)); if (gesture->points == gesture->points_alloc) { gesture->points_alloc *= 2; gesture->customdata = MEM_reallocN(gesture->customdata, - sizeof(short[2]) * gesture->points_alloc); + sizeof(float[2]) * gesture->points_alloc); } { - short(*lasso)[2] = static_cast(gesture->customdata); + float(*lasso)[2] = static_cast(gesture->customdata); + const float2 current_mouse_position = float2(gesture->mval); + const float2 last_position(lasso[gesture->points - 1][0], lasso[gesture->points - 1][1]); - const int x = ((event->xy[0] - gesture->winrct.xmin) - lasso[gesture->points - 1][0]); - const int y = ((event->xy[1] - gesture->winrct.ymin) - lasso[gesture->points - 1][1]); + const float2 delta = current_mouse_position - last_position; + const float dist_squared = blender::math::length_squared(delta); /* Move the lasso. */ if (gesture->move) { for (int i = 0; i < gesture->points; i++) { - lasso[i][0] += x; - lasso[i][1] += y; + lasso[i][0] += delta.x; + lasso[i][1] += delta.y; } } - /* Make a simple distance check to get a smoother lasso - * add only when at least 2 pixels between this and previous location. */ - else if ((x * x + y * y) > pow2f(2.0f * UI_SCALE_FAC)) { - lasso[gesture->points][0] = event->xy[0] - gesture->winrct.xmin; - lasso[gesture->points][1] = event->xy[1] - gesture->winrct.ymin; + else if (gesture->use_smooth) { + const float radius_squared = square_f(radius); + if (dist_squared > square_f(radius)) { + float2 result = blender::math::interpolate( + current_mouse_position, last_position, factor); + + lasso[gesture->points][0] = result.x; + lasso[gesture->points][1] = result.y; + gesture->points++; + } + } + else if (dist_squared > pow2f(2.0f * UI_SCALE_FAC)) { + /* Make a simple distance check to get a smoother lasso even if smoothing isn't enabled + * add only when at least 2 pixels between this and previous location. */ + lasso[gesture->points][0] = gesture->mval.x; + lasso[gesture->points][1] = gesture->mval.y; gesture->points++; } } diff --git a/source/blender/windowmanager/intern/wm_operator_props.cc b/source/blender/windowmanager/intern/wm_operator_props.cc index 05c0b8a3191..0ee9d8985c1 100644 --- a/source/blender/windowmanager/intern/wm_operator_props.cc +++ b/source/blender/windowmanager/intern/wm_operator_props.cc @@ -526,6 +526,30 @@ void WM_operator_properties_gesture_lasso(wmOperatorType *ot) PropertyRNA *prop; prop = RNA_def_collection_runtime(ot->srna, "path", &RNA_OperatorMousePath, "Path", ""); RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); + prop = RNA_def_boolean(ot->srna, + "use_smooth_stroke", + false, + "Stabilize Stroke", + "Selection lags behind mouse and follows a smoother path"); + prop = RNA_def_float(ot->srna, + "smooth_stroke_factor", + 0.75f, + 0.5f, + 0.99f, + "Smooth Stroke Factor", + "Higher values gives a smoother stroke", + 0.5f, + 0.99f); + prop = RNA_def_int(ot->srna, + "smooth_stroke_radius", + 35, + 10, + 200, + "Smooth Stroke Radius", + "Minimum distance from last point before selection continues", + 10, + 200); + RNA_def_property_subtype(prop, PROP_PIXEL); } void WM_operator_properties_gesture_polyline(wmOperatorType *ot)