Curves: Add soft selection in sculpt mode

This commit adds a float selection to curve control points or curves,
a sculpt tool to paint the selection, and uses the selection influence
in the existing sculpt brushes.

The selection is the inverse of the "mask" from mesh sculpt mode
currently. That change is described in more detail here: T97903

Since some sculpt tools are really "per curve" tools, they use the
average point selection of all of their points. The delete brush
considers a curve selected if any of its points have a non-zero
selection.

There is a new option to choose the selection domain, which affects how
painting the selection works. You can also turn the selection off by
clicking on the active domain.

Sculpt brushes can be faster when the selection is small, because
finding selected curves or points is generally faster than the
existing brush intersection and distance checks.

The main limitation currently is that we can't see the selection in the
viewport by default. For now, to see the selection one has to add a
simple material to the curves object as shown in the differential
revision. And one has to switch to Material Preview in the 3d view.

Differential Revision: https://developer.blender.org/D14934
This commit is contained in:
Hans Goudey 2022-05-31 19:00:24 +02:00
parent 96f20ddc1e
commit a1830859fa
Notes: blender-bot 2023-02-14 08:06:35 +01:00
Referenced by issue #100406, Empty Hair object in sculpt mode, set select mode control points has wrong tooltip, doesn't show hotkey, adds new hotkey to wrong section
Referenced by issue #96848, Store a float mask attribute on curves
23 changed files with 1023 additions and 26 deletions

View File

@ -5594,7 +5594,14 @@ def km_sculpt_curves(params):
{"properties": [("mode", 'NORMAL')]}),
("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'INVERT')]}),
("sculpt_curves.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("mode", 'SMOOTH')]}),
("curves.set_selection_domain", {"type": 'ONE', "value": 'PRESS'}, {"properties": [("domain", 'POINT')]}),
("curves.set_selection_domain", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("domain", 'CURVE')]}),
("curves.disable_selection", {"type": 'ONE', "value": 'PRESS', "alt": True}, None),
("curves.disable_selection", {"type": 'TWO', "value": 'PRESS', "alt": True}, None),
*_template_paint_radial_control("curves_sculpt"),
*_template_items_select_actions(params, "sculpt_curves.select_all"),
])
return keymap

View File

@ -859,7 +859,7 @@ def brush_shared_settings(layout, context, brush, popover=False):
if mode == 'SCULPT_CURVES':
size = True
strength = True
direction = brush.curves_sculpt_tool == 'GROW_SHRINK'
direction = brush.curves_sculpt_tool in {'GROW_SHRINK', 'SELECTION_PAINT'}
### Draw settings. ###
ups = context.scene.tool_settings.unified_paint_settings

View File

@ -2316,14 +2316,58 @@ class _defs_gpencil_weight:
class _defs_curves_sculpt:
@staticmethod
def generate_from_brushes(context):
return generate_from_enum_ex(
context,
idname_prefix="builtin_brush.",
icon_prefix="ops.curves.sculpt_",
type=bpy.types.Brush,
attr="curves_sculpt_tool",
@ToolDef.from_fn
def selection_paint():
return dict(
idname="builtin_brush.selection_paint",
label="Selection Paint",
icon="ops.generic.select_paint",
data_block="SELECTION_PAINT"
)
@ToolDef.from_fn
def comb():
return dict(
idname="builtin_brush.comb",
label="Comb",
icon="ops.curves.sculpt_comb",
data_block='COMB'
)
@ToolDef.from_fn
def add():
return dict(
idname="builtin_brush.add",
label="Add",
icon="ops.curves.sculpt_add",
data_block='ADD'
)
@ToolDef.from_fn
def delete():
return dict(
idname="builtin_brush.delete",
label="Delete",
icon="ops.curves.sculpt_delete",
data_block='DELETE'
)
@ToolDef.from_fn
def snake_hook():
return dict(
idname="builtin_brush.snake_hook",
label="Snake Hook",
icon="ops.curves.sculpt_snake_hook",
data_block='SNAKE_HOOK'
)
@ToolDef.from_fn
def grow_shrink():
return dict(
idname="builtin_brush.grow_shrink",
label="Grow/Shrink",
icon="ops.curves.sculpt_grow_shrink",
data_block='GROW_SHRINK'
)
@ -3076,7 +3120,21 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
),
],
'SCULPT_CURVES': [
_defs_curves_sculpt.generate_from_brushes,
lambda context: (
(
_defs_curves_sculpt.selection_paint,
None,
)
if context is None or context.preferences.experimental.use_new_curves_tools
else ()
),
_defs_curves_sculpt.comb,
_defs_curves_sculpt.add,
_defs_curves_sculpt.delete,
_defs_curves_sculpt.snake_hook,
_defs_curves_sculpt.grow_shrink,
None,
*_tools_annotate,
],
}

View File

@ -535,6 +535,11 @@ class _draw_tool_settings_context_mode:
if brush.curves_sculpt_tool == 'DELETE':
layout.prop(brush, "falloff_shape", expand=True)
if brush.curves_sculpt_tool == 'SELECTION_PAINT':
layout.prop(brush, "direction", expand=True, text="")
layout.prop(brush, "falloff_shape", expand=True)
layout.popover("VIEW3D_PT_tools_brush_falloff")
class VIEW3D_HT_header(Header):
bl_space_type = 'VIEW_3D'
@ -687,6 +692,24 @@ class VIEW3D_HT_header(Header):
if object_mode == 'PARTICLE_EDIT':
row = layout.row()
row.prop(tool_settings.particle_edit, "select_mode", text="", expand=True)
elif object_mode == 'SCULPT_CURVES' and obj.type == 'CURVES':
curves = obj.data
row = layout.row(align=True)
experimental = context.preferences.experimental
if experimental.use_new_curves_tools:
# Combine the "use selection" toggle with the "set domain" operators
# to allow turning selection off directly.
domain = curves.selection_domain
if domain == 'POINT':
row.prop(curves, "use_sculpt_selection", text="", icon='CURVE_BEZCIRCLE')
else:
row.operator("curves.set_selection_domain", text="", icon='CURVE_BEZCIRCLE').domain = 'POINT'
if domain == 'CURVE':
row.prop(curves, "use_sculpt_selection", text="", icon='CURVE_PATH')
else:
row.operator("curves.set_selection_domain", text="", icon='CURVE_PATH').domain = 'CURVE'
# Grease Pencil
if obj and obj.type == 'GPENCIL' and context.gpencil_data:
@ -939,6 +962,7 @@ class VIEW3D_MT_editor_menus(Menu):
layout.menu("VIEW3D_MT_mask")
layout.menu("VIEW3D_MT_face_sets")
if mode_string == 'SCULPT_CURVES':
layout.menu("VIEW3D_MT_select_sculpt_curves")
layout.menu("VIEW3D_MT_sculpt_curves")
else:
@ -1976,6 +2000,17 @@ class VIEW3D_MT_select_edit_curves(Menu):
pass
class VIEW3D_MT_select_sculpt_curves(Menu):
bl_label = "Select"
def draw(self, _context):
layout = self.layout
layout.operator("sculpt_curves.select_all", text="All").action = 'SELECT'
layout.operator("sculpt_curves.select_all", text="None").action = 'DESELECT'
layout.operator("sculpt_curves.select_all", text="Invert").action = 'INVERT'
class VIEW3D_MT_angle_control(Menu):
bl_label = "Angle Control"
@ -7703,6 +7738,7 @@ classes = (
VIEW3D_MT_select_paint_mask,
VIEW3D_MT_select_paint_mask_vertex,
VIEW3D_MT_select_edit_curves,
VIEW3D_MT_select_sculpt_curves,
VIEW3D_MT_angle_control,
VIEW3D_MT_mesh_add,
VIEW3D_MT_curve_add,

View File

@ -281,6 +281,11 @@ class CurvesGeometry : public ::CurvesGeometry {
Span<float2> surface_triangle_coords() const;
MutableSpan<float2> surface_triangle_coords_for_write();
VArray<float> selection_point_float() const;
MutableSpan<float> selection_point_float_for_write();
VArray<float> selection_curve_float() const;
MutableSpan<float> selection_curve_float_for_write();
/**
* Calculate the largest and smallest position values, only including control points
* (rather than evaluated points). The existing values of `min` and `max` are taken into account.
@ -406,6 +411,11 @@ class CurvesGeometry : public ::CurvesGeometry {
*/
GVArray adapt_domain(const GVArray &varray, AttributeDomain from, AttributeDomain to) const;
template<typename T>
VArray<T> adapt_domain(const VArray<T> &varray, AttributeDomain from, AttributeDomain to) const
{
return this->adapt_domain(GVArray(varray), from, to).typed<T>();
}
};
namespace curves {

View File

@ -37,6 +37,8 @@ static const std::string ATTR_NURBS_WEIGHT = "nurbs_weight";
static const std::string ATTR_NURBS_KNOTS_MODE = "knots_mode";
static const std::string ATTR_SURFACE_TRIANGLE_INDEX = "surface_triangle_index";
static const std::string ATTR_SURFACE_TRIANGLE_COORDINATE = "surface_triangle_coordinate";
static const std::string ATTR_SELECTION_POINT_FLOAT = ".selection_point_float";
static const std::string ATTR_SELECTION_CURVE_FLOAT = ".selection_curve_float";
/* -------------------------------------------------------------------- */
/** \name Constructors/Destructor
@ -438,6 +440,26 @@ MutableSpan<float2> CurvesGeometry::surface_triangle_coords_for_write()
return get_mutable_attribute<float2>(*this, ATTR_DOMAIN_CURVE, ATTR_SURFACE_TRIANGLE_COORDINATE);
}
VArray<float> CurvesGeometry::selection_point_float() const
{
return get_varray_attribute<float>(*this, ATTR_DOMAIN_POINT, ATTR_SELECTION_POINT_FLOAT, 1.0f);
}
MutableSpan<float> CurvesGeometry::selection_point_float_for_write()
{
return get_mutable_attribute<float>(*this, ATTR_DOMAIN_POINT, ATTR_SELECTION_POINT_FLOAT, 1.0f);
}
VArray<float> CurvesGeometry::selection_curve_float() const
{
return get_varray_attribute<float>(*this, ATTR_DOMAIN_CURVE, ATTR_SELECTION_CURVE_FLOAT, 1.0f);
}
MutableSpan<float> CurvesGeometry::selection_curve_float_for_write()
{
return get_mutable_attribute<float>(*this, ATTR_DOMAIN_CURVE, ATTR_SELECTION_CURVE_FLOAT, 1.0f);
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@ -6,18 +6,23 @@
#include <atomic>
#include "BLI_devirtualize_parameters.hh"
#include "BLI_utildefines.h"
#include "BLI_vector_set.hh"
#include "ED_curves.h"
#include "ED_object.h"
#include "ED_screen.h"
#include "ED_select_utils.h"
#include "WM_api.h"
#include "BKE_bvhutils.h"
#include "BKE_context.h"
#include "BKE_curves.hh"
#include "BKE_geometry_set.hh"
#include "BKE_layer.h"
#include "BKE_lib_id.h"
#include "BKE_mesh.h"
#include "BKE_mesh_runtime.h"
#include "BKE_object.h"
@ -37,6 +42,7 @@
#include "RNA_access.h"
#include "RNA_define.h"
#include "RNA_enum_types.h"
#include "RNA_prototypes.h"
/**
@ -49,6 +55,41 @@
namespace blender::ed::curves {
static bool object_has_editable_curves(const Main &bmain, const Object &object)
{
if (object.type != OB_CURVES) {
return false;
}
if (!ELEM(object.mode, OB_MODE_SCULPT_CURVES, OB_MODE_EDIT)) {
return false;
}
if (!BKE_id_is_editable(&bmain, static_cast<const ID *>(object.data))) {
return false;
}
return true;
}
static VectorSet<Curves *> get_unique_editable_curves(const bContext &C)
{
VectorSet<Curves *> unique_curves;
const Main &bmain = *CTX_data_main(&C);
Object *object = CTX_data_active_object(&C);
if (object && object_has_editable_curves(bmain, *object)) {
unique_curves.add_new(static_cast<Curves *>(object->data));
}
CTX_DATA_BEGIN (&C, Object *, object, selected_objects) {
if (object_has_editable_curves(bmain, *object)) {
unique_curves.add(static_cast<Curves *>(object->data));
}
}
CTX_DATA_END;
return unique_curves;
}
using bke::CurvesGeometry;
namespace convert_to_particle_system {
@ -645,6 +686,211 @@ static void CURVES_OT_snap_curves_to_surface(wmOperatorType *ot)
"How to find the point on the surface to attach to");
}
static bool selection_poll(bContext *C)
{
const Object *object = CTX_data_active_object(C);
if (object == nullptr) {
return false;
}
if (object->type != OB_CURVES) {
return false;
}
if (!BKE_id_is_editable(CTX_data_main(C), static_cast<const ID *>(object->data))) {
return false;
}
return true;
}
namespace set_selection_domain {
static int curves_set_selection_domain_exec(bContext *C, wmOperator *op)
{
const AttributeDomain domain = AttributeDomain(RNA_enum_get(op->ptr, "domain"));
for (Curves *curves_id : get_unique_editable_curves(*C)) {
if (curves_id->selection_domain == domain && (curves_id->flag & CV_SCULPT_SELECTION_ENABLED)) {
continue;
}
const AttributeDomain old_domain = AttributeDomain(curves_id->selection_domain);
curves_id->selection_domain = domain;
curves_id->flag |= CV_SCULPT_SELECTION_ENABLED;
CurveComponent component;
component.replace(curves_id, GeometryOwnershipType::Editable);
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id->geometry);
if (old_domain == ATTR_DOMAIN_POINT && domain == ATTR_DOMAIN_CURVE) {
VArray<float> curve_selection = curves.adapt_domain(
curves.selection_point_float(), ATTR_DOMAIN_POINT, ATTR_DOMAIN_CURVE);
curve_selection.materialize(curves.selection_curve_float_for_write());
component.attribute_try_delete(".selection_point_float");
}
else if (old_domain == ATTR_DOMAIN_CURVE && domain == ATTR_DOMAIN_POINT) {
VArray<float> point_selection = curves.adapt_domain(
curves.selection_curve_float(), ATTR_DOMAIN_CURVE, ATTR_DOMAIN_POINT);
point_selection.materialize(curves.selection_point_float_for_write());
component.attribute_try_delete(".selection_curve_float");
}
/* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic
* attribute for now. */
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id);
}
WM_main_add_notifier(NC_SPACE | ND_SPACE_VIEW3D, nullptr);
return OPERATOR_FINISHED;
}
} // namespace set_selection_domain
static void CURVES_OT_set_selection_domain(wmOperatorType *ot)
{
PropertyRNA *prop;
ot->name = "Set Select Mode";
ot->idname = __func__;
ot->description = "Change the mode used for selection masking in curves sculpt mode";
ot->exec = set_selection_domain::curves_set_selection_domain_exec;
ot->poll = selection_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
ot->prop = prop = RNA_def_enum(
ot->srna, "domain", rna_enum_attribute_curves_domain_items, 0, "Domain", "");
RNA_def_property_flag(prop, (PropertyFlag)(PROP_HIDDEN | PROP_SKIP_SAVE));
}
namespace disable_selection {
static int curves_disable_selection_exec(bContext *C, wmOperator *UNUSED(op))
{
for (Curves *curves_id : get_unique_editable_curves(*C)) {
curves_id->flag &= ~CV_SCULPT_SELECTION_ENABLED;
/* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic
* attribute for now. */
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id);
}
WM_main_add_notifier(NC_SPACE | ND_SPACE_VIEW3D, nullptr);
return OPERATOR_FINISHED;
}
} // namespace disable_selection
static void CURVES_OT_disable_selection(wmOperatorType *ot)
{
ot->name = "Disable Selection";
ot->idname = __func__;
ot->description = "Disable the drawing of influence of selection in sculpt mode";
ot->exec = disable_selection::curves_disable_selection_exec;
ot->poll = selection_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
namespace select_all {
static bool varray_contains_nonzero(const VArray<float> &data)
{
bool contains_nonzero = false;
devirtualize_varray(data, [&](const auto array) {
for (const int i : data.index_range()) {
if (array[i] != 0.0f) {
contains_nonzero = true;
break;
}
}
});
return contains_nonzero;
}
static bool any_point_selected(const CurvesGeometry &curves)
{
return varray_contains_nonzero(curves.selection_point_float());
}
static bool any_point_selected(const Span<Curves *> curves_ids)
{
for (const Curves *curves_id : curves_ids) {
if (any_point_selected(CurvesGeometry::wrap(curves_id->geometry))) {
return true;
}
}
return false;
}
static void invert_selection(MutableSpan<float> selection)
{
threading::parallel_for(selection.index_range(), 2048, [&](IndexRange range) {
for (const int i : range) {
selection[i] = 1.0f - selection[i];
}
});
}
static int select_all_exec(bContext *C, wmOperator *op)
{
int action = RNA_enum_get(op->ptr, "action");
VectorSet<Curves *> unique_curves = get_unique_editable_curves(*C);
if (action == SEL_TOGGLE) {
action = any_point_selected(unique_curves) ? SEL_DESELECT : SEL_SELECT;
}
for (Curves *curves_id : unique_curves) {
if (action == SEL_SELECT) {
CurveComponent component;
component.replace(curves_id, GeometryOwnershipType::Editable);
component.attribute_try_delete(".selection_point_float");
component.attribute_try_delete(".selection_curve_float");
}
else {
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id->geometry);
MutableSpan<float> selection = curves_id->selection_domain == ATTR_DOMAIN_POINT ?
curves.selection_point_float_for_write() :
curves.selection_curve_float_for_write();
if (action == SEL_DESELECT) {
selection.fill(0.0f);
}
else if (action == SEL_INVERT) {
invert_selection(selection);
}
}
/* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because it is handled as a generic
* attribute for now. */
DEG_id_tag_update(&curves_id->id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, curves_id);
}
return OPERATOR_FINISHED;
}
} // namespace select_all
static void SCULPT_CURVES_OT_select_all(wmOperatorType *ot)
{
ot->name = "(De)select All";
ot->idname = __func__;
ot->description = "(De)select all control points";
ot->exec = select_all::select_all_exec;
ot->poll = selection_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
WM_operator_properties_select_all(ot);
}
} // namespace blender::ed::curves
void ED_operatortypes_curves()
@ -653,4 +899,7 @@ void ED_operatortypes_curves()
WM_operatortype_append(CURVES_OT_convert_to_particle_system);
WM_operatortype_append(CURVES_OT_convert_from_particle_system);
WM_operatortype_append(CURVES_OT_snap_curves_to_surface);
WM_operatortype_append(CURVES_OT_set_selection_domain);
WM_operatortype_append(SCULPT_CURVES_OT_select_all);
WM_operatortype_append(CURVES_OT_disable_selection);
}

View File

@ -33,6 +33,8 @@ set(SRC
curves_sculpt_delete.cc
curves_sculpt_grow_shrink.cc
curves_sculpt_ops.cc
curves_sculpt_selection.cc
curves_sculpt_selection_paint.cc
curves_sculpt_snake_hook.cc
paint_canvas.cc
paint_cursor.c

View File

@ -652,6 +652,8 @@ struct AddOperationExecutor {
Array<float3> new_normals_su = this->compute_normals_for_added_curves_su(added_points);
this->initialize_surface_attachment(added_points);
this->fill_new_selection();
if (interpolate_shape_) {
this->initialize_position_with_interpolation(
added_points, neighbors_per_curve, new_normals_su, new_lengths_cu);
@ -662,6 +664,33 @@ struct AddOperationExecutor {
}
}
/**
* Select newly created points or curves in new curves if necessary.
*/
void fill_new_selection()
{
switch (curves_id_->selection_domain) {
case ATTR_DOMAIN_CURVE: {
const VArray<float> selection = curves_->selection_curve_float();
if (selection.is_single() && selection.get_internal_single() >= 1.0f) {
return;
}
curves_->selection_curve_float_for_write().drop_front(tot_old_curves_).fill(1.0f);
break;
}
case ATTR_DOMAIN_POINT: {
const VArray<float> selection = curves_->selection_point_float();
if (selection.is_single() && selection.get_internal_single() >= 1.0f) {
return;
}
curves_->selection_point_float_for_write().drop_front(tot_old_points_).fill(1.0f);
break;
}
default:
BLI_assert_unreachable();
}
}
Array<NeighborsVector> find_curve_neighbors(const AddedPoints &added_points)
{
const int tot_added_curves = added_points.bary_coords.size();

View File

@ -96,6 +96,10 @@ struct CombOperationExecutor {
Curves *curves_id_ = nullptr;
CurvesGeometry *curves_ = nullptr;
VArray<float> point_factors_;
Vector<int64_t> selected_curve_indices_;
IndexMask curve_selection_;
const Object *surface_ob_ = nullptr;
const Mesh *surface_ = nullptr;
Span<MLoopTri> surface_looptris_;
@ -142,6 +146,9 @@ struct CombOperationExecutor {
return;
}
point_factors_ = get_point_selection(*curves_id_);
curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
brush_pos_prev_re_ = self_->brush_pos_last_re_;
brush_pos_re_ = stroke_extension.mouse_position;
brush_pos_diff_re_ = brush_pos_re_ - brush_pos_prev_re_;
@ -217,9 +224,9 @@ struct CombOperationExecutor {
const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
const float brush_radius_sq_re = pow2f(brush_radius_re);
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
Vector<int> &local_changed_curves = r_changed_curves.local();
for (const int curve_i : curves_range) {
for (const int curve_i : curve_selection_.slice(range)) {
bool curve_changed = false;
const IndexRange points = curves_->points_for_curve(curve_i);
for (const int point_i : points.drop_front(1)) {
@ -241,9 +248,10 @@ struct CombOperationExecutor {
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_re, brush_radius_re);
/* Combine the falloff and brush strength. */
const float weight = brush_strength_ * radius_falloff;
const float weight = brush_strength_ * radius_falloff * point_factors_[point_i];
/* Offset the old point position in screen space and transform it back into 3D space. */
/* Offset the old point position in screen space and transform it back into 3D space.
*/
const float2 new_position_re = old_pos_re + brush_pos_diff_re_ * weight;
float3 new_position_wo;
ED_view3d_win_to_3d(
@ -304,9 +312,9 @@ struct CombOperationExecutor {
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
const float3 brush_diff_cu = brush_end_cu - brush_start_cu;
threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange curves_range) {
threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
Vector<int> &local_changed_curves = r_changed_curves.local();
for (const int curve_i : curves_range) {
for (const int curve_i : curve_selection_.slice(range)) {
bool curve_changed = false;
const IndexRange points = curves_->points_for_curve(curve_i);
for (const int point_i : points.drop_front(1)) {
@ -326,7 +334,7 @@ struct CombOperationExecutor {
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_cu, brush_radius_cu);
/* Combine the falloff and brush strength. */
const float weight = brush_strength_ * radius_falloff;
const float weight = brush_strength_ * radius_falloff * point_factors_[point_i];
/* Update the point position. */
positions_cu[point_i] = pos_old_cu + weight * brush_diff_cu;

View File

@ -70,6 +70,9 @@ struct DeleteOperationExecutor {
Curves *curves_id_ = nullptr;
CurvesGeometry *curves_ = nullptr;
Vector<int64_t> selected_curve_indices_;
IndexMask curve_selection_;
const CurvesSculpt *curves_sculpt_ = nullptr;
const Brush *brush_ = nullptr;
float brush_radius_base_re_;
@ -94,6 +97,9 @@ struct DeleteOperationExecutor {
curves_id_ = static_cast<Curves *>(object_->data);
curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
selected_curve_indices_.clear();
curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
curves_sculpt_ = scene_->toolsettings->curves_sculpt;
brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
brush_radius_base_re_ = BKE_brush_size_get(scene_, brush_);
@ -158,8 +164,8 @@ struct DeleteOperationExecutor {
const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
const float brush_radius_sq_re = pow2f(brush_radius_re);
threading::parallel_for(curves_->curves_range(), 512, [&](IndexRange curve_range) {
for (const int curve_i : curve_range) {
threading::parallel_for(curve_selection_.index_range(), 512, [&](const IndexRange range) {
for (const int curve_i : curve_selection_.slice(range)) {
const IndexRange points = curves_->points_for_curve(curve_i);
if (points.size() == 1) {
const float3 pos_cu = brush_transform_inv * positions_cu[points.first()];
@ -219,8 +225,8 @@ struct DeleteOperationExecutor {
const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_;
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
threading::parallel_for(curves_->curves_range(), 512, [&](IndexRange curve_range) {
for (const int curve_i : curve_range) {
threading::parallel_for(curve_selection_.index_range(), 512, [&](const IndexRange range) {
for (const int curve_i : curve_selection_.slice(range)) {
const IndexRange points = curves_->points_for_curve(curve_i);
if (points.size() == 1) {

View File

@ -280,6 +280,10 @@ struct CurvesEffectOperationExecutor {
Curves *curves_id_ = nullptr;
CurvesGeometry *curves_ = nullptr;
VArray<float> curve_selection_factors_;
Vector<int64_t> selected_curve_indices_;
IndexMask curve_selection_;
const Brush *brush_ = nullptr;
float brush_radius_base_re_;
float brush_radius_factor_;
@ -318,6 +322,9 @@ struct CurvesEffectOperationExecutor {
return;
}
curve_selection_factors_ = get_curves_selection(*curves_id_);
curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
const CurvesSculpt &curves_sculpt = *scene_->toolsettings->curves_sculpt;
brush_ = BKE_paint_brush_for_read(&curves_sculpt.paint);
brush_strength_ = brush_strength_get(*scene_, *brush_, stroke_extension);
@ -396,6 +403,8 @@ struct CurvesEffectOperationExecutor {
for (const int curve_i : curves_range) {
const IndexRange points = curves_->points_for_curve(curve_i);
const float curve_selection_factor = curve_selection_factors_[curve_i];
float max_move_distance_cu = 0.0f;
for (const float4x4 &brush_transform_inv : symmetry_brush_transforms_inv) {
for (const int segment_i : points.drop_back(1)) {
@ -426,7 +435,7 @@ struct CurvesEffectOperationExecutor {
const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re);
const float radius_falloff = BKE_brush_curve_strength(
brush_, dist_to_brush_re, brush_radius_re);
const float weight = brush_strength_ * radius_falloff;
const float weight = brush_strength_ * radius_falloff * curve_selection_factor;
const float3 closest_on_segment_cu = math::interpolate(
p1_cu, p2_cu, lambda_on_segment);
@ -491,6 +500,9 @@ struct CurvesEffectOperationExecutor {
const IndexRange points = curves_->points_for_curve(curve_i);
float max_move_distance_cu = 0.0f;
const float curve_selection_factor = curve_selection_factors_[curve_i];
for (const float4x4 &brush_transform : symmetry_brush_transforms) {
const float3 brush_pos_start_transformed_cu = brush_transform * brush_pos_start_cu;
const float3 brush_pos_end_transformed_cu = brush_transform * brush_pos_end_cu;
@ -517,7 +529,7 @@ struct CurvesEffectOperationExecutor {
const float dist_to_brush_cu = std::sqrt(dist_to_brush_sq_cu);
const float radius_falloff = BKE_brush_curve_strength(
brush_, dist_to_brush_cu, brush_radius_cu);
const float weight = brush_strength_ * radius_falloff;
const float weight = brush_strength_ * radius_falloff * curve_selection_factor;
const float move_distance_cu = weight * brush_pos_diff_length_cu;
max_move_distance_cu = std::max(max_move_distance_cu, move_distance_cu);

View File

@ -8,7 +8,10 @@
#include "paint_intern.h"
#include "BLI_math_vector.hh"
#include "BLI_vector.hh"
#include "BLI_virtual_array.hh"
#include "BKE_attribute.h"
#include "BKE_curves.hh"
struct ARegion;
@ -55,6 +58,8 @@ std::unique_ptr<CurvesSculptStrokeOperation> new_delete_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_snake_hook_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_grow_shrink_operation(
const BrushStrokeMode brush_mode, const bContext &C);
std::unique_ptr<CurvesSculptStrokeOperation> new_selection_paint_operation(
const BrushStrokeMode brush_mode, const bContext &C);
struct CurvesBrush3D {
float3 position_cu;
@ -74,4 +79,20 @@ std::optional<CurvesBrush3D> sample_curves_3d_brush(const Depsgraph &depsgraph,
Vector<float4x4> get_symmetry_brush_transforms(eCurvesSymmetryType symmetry);
/**
* Get the floating point selection on the curve domain, averaged from points if necessary.
*/
VArray<float> get_curves_selection(const Curves &curves_id);
/**
* Get the floating point selection on the curve domain, copied from curves if necessary.
*/
VArray<float> get_point_selection(const Curves &curves_id);
/**
* Find curves that have any point selected (a selection factor greater than zero),
* or curves that have their own selection factor greater than zero.
*/
IndexMask retrieve_selected_curves(const Curves &curves_id, Vector<int64_t> &r_indices);
} // namespace blender::ed::sculpt_paint

View File

@ -123,6 +123,8 @@ static std::unique_ptr<CurvesSculptStrokeOperation> start_brush_operation(bConte
return new_add_operation(C, op.reports);
case CURVES_SCULPT_TOOL_GROW_SHRINK:
return new_grow_shrink_operation(mode, C);
case CURVES_SCULPT_TOOL_SELECTION_PAINT:
return new_selection_paint_operation(mode, C);
}
BLI_assert_unreachable();
return {};

View File

@ -0,0 +1,107 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_mask_ops.hh"
#include "BKE_curves.hh"
#include "curves_sculpt_intern.hh"
namespace blender::ed::sculpt_paint {
static VArray<float> get_curves_selection(const CurvesGeometry &curves,
const AttributeDomain domain)
{
switch (domain) {
case ATTR_DOMAIN_CURVE:
return curves.selection_curve_float();
case ATTR_DOMAIN_POINT:
return curves.adapt_domain(
curves.selection_point_float(), ATTR_DOMAIN_POINT, ATTR_DOMAIN_CURVE);
default:
BLI_assert_unreachable();
return {};
}
}
VArray<float> get_curves_selection(const Curves &curves_id)
{
if (!(curves_id.flag & CV_SCULPT_SELECTION_ENABLED)) {
return VArray<float>::ForSingle(1.0f, CurvesGeometry::wrap(curves_id.geometry).curves_num());
}
return get_curves_selection(CurvesGeometry::wrap(curves_id.geometry),
AttributeDomain(curves_id.selection_domain));
}
static VArray<float> get_point_selection(const CurvesGeometry &curves,
const AttributeDomain domain)
{
switch (domain) {
case ATTR_DOMAIN_CURVE:
return curves.adapt_domain(
curves.selection_curve_float(), ATTR_DOMAIN_CURVE, ATTR_DOMAIN_POINT);
case ATTR_DOMAIN_POINT:
return curves.selection_point_float();
default:
BLI_assert_unreachable();
return {};
}
}
VArray<float> get_point_selection(const Curves &curves_id)
{
if (!(curves_id.flag & CV_SCULPT_SELECTION_ENABLED)) {
return VArray<float>::ForSingle(1.0f, CurvesGeometry::wrap(curves_id.geometry).points_num());
}
return get_point_selection(CurvesGeometry::wrap(curves_id.geometry),
AttributeDomain(curves_id.selection_domain));
}
static IndexMask retrieve_selected_curves(const CurvesGeometry &curves,
const AttributeDomain domain,
Vector<int64_t> &r_indices)
{
switch (domain) {
case ATTR_DOMAIN_POINT: {
const VArray<float> selection = curves.selection_point_float();
if (selection.is_single()) {
return selection.get_internal_single() == 0.0f ? IndexMask(0) :
IndexMask(curves.curves_num());
}
return index_mask_ops::find_indices_based_on_predicate(
curves.curves_range(), 512, r_indices, [&](const int curve_i) {
for (const int i : curves.points_for_curve(curve_i)) {
if (selection[i] > 0.0f) {
return true;
}
}
return false;
});
}
case ATTR_DOMAIN_CURVE: {
const VArray<float> selection = curves.selection_curve_float();
if (selection.is_single()) {
return selection.get_internal_single() == 0.0f ? IndexMask(0) :
IndexMask(curves.curves_num());
}
return index_mask_ops::find_indices_based_on_predicate(
curves.curves_range(), 2048, r_indices, [&](const int i) {
return selection[i] > 0.0f;
});
}
default:
BLI_assert_unreachable();
return {};
}
}
IndexMask retrieve_selected_curves(const Curves &curves_id, Vector<int64_t> &r_indices)
{
if (!(curves_id.flag & CV_SCULPT_SELECTION_ENABLED)) {
return CurvesGeometry::wrap(curves_id.geometry).curves_range();
}
return retrieve_selected_curves(CurvesGeometry::wrap(curves_id.geometry),
AttributeDomain(curves_id.selection_domain),
r_indices);
}
} // namespace blender::ed::sculpt_paint

View File

@ -0,0 +1,394 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#include <numeric>
#include "BLI_memory_utils.hh"
#include "BLI_task.hh"
#include "DNA_brush_types.h"
#include "BKE_brush.h"
#include "BKE_context.h"
#include "BKE_curves.hh"
#include "DEG_depsgraph.h"
#include "ED_screen.h"
#include "ED_view3d.h"
#include "WM_api.h"
#include "curves_sculpt_intern.hh"
/**
* The code below uses a suffix naming convention to indicate the coordinate space:
* cu: Local space of the curves object that is being edited.
* wo: World space.
* re: 2D coordinates within the region.
*/
namespace blender::ed::sculpt_paint {
using bke::CurvesGeometry;
class SelectionPaintOperation : public CurvesSculptStrokeOperation {
private:
bool use_select_;
bool clear_selection_;
CurvesBrush3D brush_3d_;
friend struct SelectionPaintOperationExecutor;
public:
SelectionPaintOperation(const bool use_select, const bool clear_selection)
: use_select_(use_select), clear_selection_(clear_selection)
{
}
void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
};
struct SelectionPaintOperationExecutor {
SelectionPaintOperation *self_ = nullptr;
const bContext *C_ = nullptr;
const Depsgraph *depsgraph_ = nullptr;
const Scene *scene_ = nullptr;
ARegion *region_ = nullptr;
const View3D *v3d_ = nullptr;
const RegionView3D *rv3d_ = nullptr;
Object *object_ = nullptr;
Curves *curves_id_ = nullptr;
CurvesGeometry *curves_ = nullptr;
const Brush *brush_ = nullptr;
float brush_radius_base_re_;
float brush_radius_factor_;
float brush_strength_;
float selection_goal_;
float2 brush_pos_re_;
float4x4 curves_to_world_mat_;
float4x4 world_to_curves_mat_;
void execute(SelectionPaintOperation &self,
const bContext &C,
const StrokeExtension &stroke_extension)
{
self_ = &self;
depsgraph_ = CTX_data_depsgraph_pointer(&C);
scene_ = CTX_data_scene(&C);
object_ = CTX_data_active_object(&C);
region_ = CTX_wm_region(&C);
v3d_ = CTX_wm_view3d(&C);
rv3d_ = CTX_wm_region_view3d(&C);
curves_id_ = static_cast<Curves *>(object_->data);
curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
curves_id_->flag |= CV_SCULPT_SELECTION_ENABLED;
brush_ = BKE_paint_brush_for_read(&scene_->toolsettings->curves_sculpt->paint);
brush_radius_base_re_ = BKE_brush_size_get(scene_, brush_);
brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension);
brush_strength_ = BKE_brush_alpha_get(scene_, brush_);
brush_pos_re_ = stroke_extension.mouse_position;
if (self.clear_selection_) {
if (stroke_extension.is_first) {
if (curves_id_->selection_domain == ATTR_DOMAIN_POINT) {
curves_->selection_point_float_for_write().fill(0.0f);
}
else if (curves_id_->selection_domain == ATTR_DOMAIN_CURVE) {
curves_->selection_curve_float_for_write().fill(0.0f);
}
}
}
curves_to_world_mat_ = object_->obmat;
world_to_curves_mat_ = curves_to_world_mat_.inverted();
const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
brush_->falloff_shape);
selection_goal_ = self_->use_select_ ? 1.0f : 0.0f;
if (stroke_extension.is_first) {
if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
this->initialize_spherical_brush_reference_point();
}
}
if (curves_id_->selection_domain == ATTR_DOMAIN_POINT) {
MutableSpan<float> selection = curves_->selection_point_float_for_write();
if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
this->paint_point_selection_projected_with_symmetry(selection);
}
else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
this->paint_point_selection_spherical_with_symmetry(selection);
}
}
else {
MutableSpan<float> selection = curves_->selection_curve_float_for_write();
if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
this->paint_curve_selection_projected_with_symmetry(selection);
}
else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
this->paint_curve_selection_spherical_with_symmetry(selection);
}
}
/* Use #ID_RECALC_GEOMETRY instead of #ID_RECALC_SELECT because
* selection is handled as a generic attribute for now. */
DEG_id_tag_update(&curves_id_->id, ID_RECALC_GEOMETRY);
WM_main_add_notifier(NC_GEOM | ND_DATA, &curves_id_->id);
ED_region_tag_redraw(region_);
}
void paint_point_selection_projected_with_symmetry(MutableSpan<float> selection)
{
const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
eCurvesSymmetryType(curves_id_->symmetry));
for (const float4x4 &brush_transform : symmetry_brush_transforms) {
this->paint_point_selection_projected(brush_transform, selection);
}
}
void paint_point_selection_projected(const float4x4 &brush_transform,
MutableSpan<float> selection)
{
const float4x4 brush_transform_inv = brush_transform.inverted();
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
Span<float3> positions_cu = curves_->positions();
const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
const float brush_radius_sq_re = pow2f(brush_radius_re);
threading::parallel_for(curves_->points_range(), 1024, [&](const IndexRange point_range) {
for (const int point_i : point_range) {
const float3 pos_cu = brush_transform_inv * positions_cu[point_i];
/* Find the position of the point in screen space. */
float2 pos_re;
ED_view3d_project_float_v2_m4(region_, pos_cu, pos_re, projection.values);
const float distance_to_brush_sq_re = math::distance_squared(pos_re, brush_pos_re_);
if (distance_to_brush_sq_re > brush_radius_sq_re) {
/* Ignore the point because it's too far away. */
continue;
}
const float distance_to_brush_re = std::sqrt(distance_to_brush_sq_re);
/* A falloff that is based on how far away the point is from the stroke. */
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_re, brush_radius_re);
/* Combine the falloff and brush strength. */
const float weight = brush_strength_ * radius_falloff;
selection[point_i] = math::interpolate(selection[point_i], selection_goal_, weight);
}
});
}
void paint_point_selection_spherical_with_symmetry(MutableSpan<float> selection)
{
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
float3 brush_wo;
ED_view3d_win_to_3d(v3d_,
region_,
curves_to_world_mat_ * self_->brush_3d_.position_cu,
brush_pos_re_,
brush_wo);
const float3 brush_cu = world_to_curves_mat_ * brush_wo;
const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
eCurvesSymmetryType(curves_id_->symmetry));
for (const float4x4 &brush_transform : symmetry_brush_transforms) {
this->paint_point_selection_spherical(selection, brush_transform * brush_cu);
}
}
void paint_point_selection_spherical(MutableSpan<float> selection, const float3 &brush_cu)
{
Span<float3> positions_cu = curves_->positions();
const float brush_radius_cu = self_->brush_3d_.radius_cu;
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
threading::parallel_for(curves_->points_range(), 1024, [&](const IndexRange point_range) {
for (const int i : point_range) {
const float3 pos_old_cu = positions_cu[i];
/* Compute distance to the brush. */
const float distance_to_brush_sq_cu = math::distance_squared(pos_old_cu, brush_cu);
if (distance_to_brush_sq_cu > brush_radius_sq_cu) {
/* Ignore the point because it's too far away. */
continue;
}
const float distance_to_brush_cu = std::sqrt(distance_to_brush_sq_cu);
/* A falloff that is based on how far away the point is from the stroke. */
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_cu, brush_radius_cu);
/* Combine the falloff and brush strength. */
const float weight = brush_strength_ * radius_falloff;
selection[i] = math::interpolate(selection[i], selection_goal_, weight);
}
});
}
void paint_curve_selection_projected_with_symmetry(MutableSpan<float> selection)
{
const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
eCurvesSymmetryType(curves_id_->symmetry));
for (const float4x4 &brush_transform : symmetry_brush_transforms) {
this->paint_curve_selection_projected(brush_transform, selection);
}
}
void paint_curve_selection_projected(const float4x4 &brush_transform,
MutableSpan<float> selection)
{
const Span<float3> positions_cu = curves_->positions();
const float4x4 brush_transform_inv = brush_transform.inverted();
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
const float brush_radius_sq_re = pow2f(brush_radius_re);
threading::parallel_for(curves_->curves_range(), 1024, [&](const IndexRange curves_range) {
for (const int curve_i : curves_range) {
const float max_weight = threading::parallel_reduce(
curves_->points_for_curve(curve_i).drop_back(1),
1024,
0.0f,
[&](const IndexRange segment_range, const float init) {
float max_weight = init;
for (const int segment_i : segment_range) {
const float3 pos1_cu = brush_transform_inv * positions_cu[segment_i];
const float3 pos2_cu = brush_transform_inv * positions_cu[segment_i + 1];
float2 pos1_re;
float2 pos2_re;
ED_view3d_project_float_v2_m4(region_, pos1_cu, pos1_re, projection.values);
ED_view3d_project_float_v2_m4(region_, pos2_cu, pos2_re, projection.values);
const float distance_sq_re = dist_squared_to_line_segment_v2(
brush_pos_re_, pos1_re, pos2_re);
if (distance_sq_re > brush_radius_sq_re) {
continue;
}
const float radius_falloff = BKE_brush_curve_strength(
brush_, std::sqrt(distance_sq_re), brush_radius_re);
const float weight = brush_strength_ * radius_falloff;
max_weight = std::max(max_weight, weight);
}
return max_weight;
},
[](float a, float b) { return std::max(a, b); });
selection[curve_i] = math::interpolate(selection[curve_i], selection_goal_, max_weight);
}
});
}
void paint_curve_selection_spherical_with_symmetry(MutableSpan<float> selection)
{
float4x4 projection;
ED_view3d_ob_project_mat_get(rv3d_, object_, projection.values);
float3 brush_wo;
ED_view3d_win_to_3d(v3d_,
region_,
curves_to_world_mat_ * self_->brush_3d_.position_cu,
brush_pos_re_,
brush_wo);
const float3 brush_cu = world_to_curves_mat_ * brush_wo;
const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
eCurvesSymmetryType(curves_id_->symmetry));
for (const float4x4 &brush_transform : symmetry_brush_transforms) {
this->paint_curve_selection_spherical(selection, brush_transform * brush_cu);
}
}
void paint_curve_selection_spherical(MutableSpan<float> selection, const float3 &brush_cu)
{
const Span<float3> positions_cu = curves_->positions();
const float brush_radius_cu = self_->brush_3d_.radius_cu;
const float brush_radius_sq_cu = pow2f(brush_radius_cu);
threading::parallel_for(curves_->curves_range(), 1024, [&](const IndexRange curves_range) {
for (const int curve_i : curves_range) {
const float max_weight = threading::parallel_reduce(
curves_->points_for_curve(curve_i).drop_back(1),
1024,
0.0f,
[&](const IndexRange segment_range, const float init) {
float max_weight = init;
for (const int segment_i : segment_range) {
const float3 &pos1_cu = positions_cu[segment_i];
const float3 &pos2_cu = positions_cu[segment_i + 1];
const float distance_sq_cu = dist_squared_to_line_segment_v3(
brush_cu, pos1_cu, pos2_cu);
if (distance_sq_cu > brush_radius_sq_cu) {
continue;
}
const float radius_falloff = BKE_brush_curve_strength(
brush_, std::sqrt(distance_sq_cu), brush_radius_cu);
const float weight = brush_strength_ * radius_falloff;
max_weight = std::max(max_weight, weight);
}
return max_weight;
},
[](float a, float b) { return std::max(a, b); });
selection[curve_i] = math::interpolate(selection[curve_i], selection_goal_, max_weight);
}
});
}
void initialize_spherical_brush_reference_point()
{
std::optional<CurvesBrush3D> brush_3d = sample_curves_3d_brush(
*depsgraph_, *region_, *v3d_, *rv3d_, *object_, brush_pos_re_, brush_radius_base_re_);
if (brush_3d.has_value()) {
self_->brush_3d_ = *brush_3d;
}
}
};
void SelectionPaintOperation::on_stroke_extended(const bContext &C,
const StrokeExtension &stroke_extension)
{
SelectionPaintOperationExecutor executor;
executor.execute(*this, C, stroke_extension);
}
std::unique_ptr<CurvesSculptStrokeOperation> new_selection_paint_operation(
const BrushStrokeMode brush_mode, const bContext &C)
{
Scene &scene = *CTX_data_scene(&C);
Brush &brush = *BKE_paint_brush(&scene.toolsettings->curves_sculpt->paint);
const bool use_select = ELEM(brush_mode, BRUSH_STROKE_INVERT) ==
((brush.flag & BRUSH_DIR_IN) != 0);
const bool clear_selection = use_select && brush_mode != BRUSH_STROKE_SMOOTH;
return std::make_unique<SelectionPaintOperation>(use_select, clear_selection);
}
} // namespace blender::ed::sculpt_paint

View File

@ -90,6 +90,10 @@ struct SnakeHookOperatorExecutor {
Curves *curves_id_ = nullptr;
CurvesGeometry *curves_ = nullptr;
VArray<float> curve_factors_;
Vector<int64_t> selected_curve_indices_;
IndexMask curve_selection_;
float4x4 curves_to_world_mat_;
float4x4 world_to_curves_mat_;
@ -130,6 +134,9 @@ struct SnakeHookOperatorExecutor {
return;
}
curve_factors_ = get_curves_selection(*curves_id_);
curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
brush_pos_prev_re_ = self.last_mouse_position_re_;
brush_pos_re_ = stroke_extension.mouse_position;
brush_pos_diff_re_ = brush_pos_re_ - brush_pos_prev_re_;
@ -199,7 +206,7 @@ struct SnakeHookOperatorExecutor {
const float radius_falloff = BKE_brush_curve_strength(
brush_, std::sqrt(distance_to_brush_sq_re), brush_radius_re);
const float weight = brush_strength_ * radius_falloff;
const float weight = brush_strength_ * radius_falloff * curve_factors_[curve_i];
const float2 new_position_re = old_pos_re + brush_pos_diff_re_ * weight;
float3 new_position_wo;
@ -265,7 +272,7 @@ struct SnakeHookOperatorExecutor {
const float radius_falloff = BKE_brush_curve_strength(
brush_, distance_to_brush_cu, brush_radius_cu);
const float weight = brush_strength_ * radius_falloff;
const float weight = brush_strength_ * radius_falloff * curve_factors_[curve_i];
const float3 new_pos_cu = old_pos_cu + weight * brush_diff_cu;

View File

@ -468,6 +468,7 @@ typedef enum eBrushCurvesSculptTool {
CURVES_SCULPT_TOOL_SNAKE_HOOK = 2,
CURVES_SCULPT_TOOL_ADD = 3,
CURVES_SCULPT_TOOL_GROW_SHRINK = 4,
CURVES_SCULPT_TOOL_SELECTION_PAINT = 5,
} eBrushCurvesSculptTool;
/** When #BRUSH_ACCUMULATE is used */

View File

@ -141,7 +141,12 @@ typedef struct Curves {
* symmetrical geometry.
*/
char symmetry;
char _pad2[5];
/**
* #AttributeDomain. The active selection mode domain. At most one selection mode can be active
* at a time.
*/
char selection_domain;
char _pad[4];
/**
* Used as base mesh when curves represent e.g. hair or fur. This surface is used in edit modes.
@ -159,6 +164,7 @@ typedef struct Curves {
/** #Curves.flag */
enum {
HA_DS_EXPAND = (1 << 0),
CV_SCULPT_SELECTION_ENABLED = (1 << 1),
};
/** #Curves.symmetry */

View File

@ -209,6 +209,7 @@ DEF_ENUM(rna_enum_attribute_type_items)
DEF_ENUM(rna_enum_color_attribute_type_items)
DEF_ENUM(rna_enum_attribute_type_with_auto_items)
DEF_ENUM(rna_enum_attribute_domain_items)
DEF_ENUM(rna_enum_attribute_curves_domain_items)
DEF_ENUM(rna_enum_color_attribute_domain_items)
DEF_ENUM(rna_enum_attribute_domain_without_corner_items)
DEF_ENUM(rna_enum_attribute_domain_with_auto_items)

View File

@ -106,6 +106,11 @@ const EnumPropertyItem rna_enum_color_attribute_domain_items[] = {
{ATTR_DOMAIN_CORNER, "CORNER", 0, "Face Corner", ""},
{0, NULL, 0, NULL, NULL}};
const EnumPropertyItem rna_enum_attribute_curves_domain_items[] = {
{ATTR_DOMAIN_POINT, "POINT", 0, "Control Point", ""},
{ATTR_DOMAIN_CURVE, "CURVE", 0, "Curve", ""},
{0, NULL, 0, NULL, NULL}};
#ifdef RNA_RUNTIME
# include "BLI_math.h"

View File

@ -249,6 +249,7 @@ const EnumPropertyItem rna_enum_brush_curves_sculpt_tool_items[] = {
{CURVES_SCULPT_TOOL_SNAKE_HOOK, "SNAKE_HOOK", ICON_NONE, "Curves Snake Hook", ""},
{CURVES_SCULPT_TOOL_ADD, "ADD", ICON_NONE, "Add Curves", ""},
{CURVES_SCULPT_TOOL_GROW_SHRINK, "GROW_SHRINK", ICON_NONE, "Grow / Shrink Curves", ""},
{CURVES_SCULPT_TOOL_SELECTION_PAINT, "SELECTION_PAINT", ICON_NONE, "Paint Selection", ""},
{0, NULL, 0, NULL, NULL},
};
@ -885,6 +886,7 @@ static const EnumPropertyItem *rna_Brush_direction_itemf(bContext *C,
case PAINT_MODE_SCULPT_CURVES:
switch (me->curves_sculpt_tool) {
case CURVES_SCULPT_TOOL_GROW_SHRINK:
case CURVES_SCULPT_TOOL_SELECTION_PAINT:
return prop_direction_items;
default:
return DummyRNA_DEFAULT_items;

View File

@ -308,6 +308,18 @@ static void rna_def_curves(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Z", "Enable symmetry in the Z axis");
RNA_def_property_update(prop, 0, "rna_Curves_update_draw");
prop = RNA_def_property(srna, "selection_domain", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, rna_enum_attribute_curves_domain_items);
RNA_def_property_ui_text(prop, "Selection Domain", "");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, 0, "rna_Curves_update_data");
prop = RNA_def_property(srna, "use_sculpt_selection", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "flag", CV_SCULPT_SELECTION_ENABLED);
RNA_def_property_ui_text(prop, "Use Sculpt Selection", "");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, 0, "rna_Curves_update_draw");
/* attributes */
rna_def_attributes_common(srna);