Sculpt: Add stroke stabilization to lasso tools

This PR adds stroke stabilization settings for the Sculpt mode lasso
tools:
* Mask
* Hide
* Trim
* Face Set

Only Sculpt tools have a user facing change, even though this was
implemented in `WM_gesture_lasso_modal` and related methods. Other
modes may choose to add these settings and toggles.

## Implementation
The implemented functionality is similar to the Annotate tool in both
interpolation of the new point and drawing the UI hint that
stabilization is happening.

The `radius` and `factor` properties have similar bounds as the same
Brush properties. All values are stored on a per-operator level, not on
a scene or otherwise global tool level.

Based off of [1].

[1] - https://blender.community/c/rightclickselect/ZWG5/

Pull Request: https://projects.blender.org/blender/blender/pulls/122062
This commit is contained in:
Sean Kim 2024-06-27 01:32:09 +02:00 committed by Sean Kim
parent 8da3b74ee2
commit 9d4d1aea98
5 changed files with 181 additions and 31 deletions

@ -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",

@ -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

@ -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<short int *>(
MEM_mallocN(sizeof(short[2]) * gesture->points_alloc, "lasso points"));
gesture->customdata = lasso = static_cast<float *>(
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<int2> 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<float(*)[2]>(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 &gt, const uint shdr_pos)

@ -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<wmGesture *>(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<wmGesture *>(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<const short int *>(gesture->customdata);
const float *lasso = static_cast<const float *>(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<wmGesture *>(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<short int(*)[2]>(gesture->customdata);
float(*lasso)[2] = static_cast<float(*)[2]>(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++;
}
}

@ -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)