Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacques Lucke <jacques@blender.org>2022-06-30 16:09:13 +0300
committerJacques Lucke <jacques@blender.org>2022-06-30 16:09:13 +0300
commit416aef4e13ccc30e82ecaa691f26af54dbd5ee7e (patch)
treea4f8e3b96e01031e5f8049b48043ed3208bfe168 /source/blender
parent0f22b5599a03d5ce61450528c74ad2f2e47cf913 (diff)
Curves: New tools for curves sculpt mode.
This commit contains various new features for curves sculpt mode that have been developed in parallel. * Selection: * Operator to select points/curves randomly. * Operator to select endpoints of curves. * Operator to grow/shrink an existing selection. * New Brushes: * Pinch: Moves points towards the brush center. * Smooth: Makes individual curves straight without changing the root or tip position. * Puff: Makes curves stand up, aligning them with the surface normal. * Density: Add or remove curves to achieve a certain density defined by a minimum distance value. * Slide: Move root points on the surface. Differential Revision: https://developer.blender.org/D15134
Diffstat (limited to 'source/blender')
-rw-r--r--source/blender/blenkernel/intern/brush.c1
-rw-r--r--source/blender/blenloader/intern/versioning_300.c8
-rw-r--r--source/blender/editors/curves/CMakeLists.txt13
-rw-r--r--source/blender/editors/curves/intern/curves_ops.cc27
-rw-r--r--source/blender/editors/include/ED_curves.h8
-rw-r--r--source/blender/editors/sculpt_paint/CMakeLists.txt7
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_density.cc834
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_intern.hh2
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_ops.cc964
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc307
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_puff.cc393
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_slide.cc310
-rw-r--r--source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc259
-rw-r--r--source/blender/editors/sculpt_paint/paint_stroke.c2
-rw-r--r--source/blender/makesdna/DNA_brush_enums.h11
-rw-r--r--source/blender/makesdna/DNA_brush_types.h7
-rw-r--r--source/blender/makesrna/intern/rna_brush.c42
17 files changed, 3181 insertions, 14 deletions
diff --git a/source/blender/blenkernel/intern/brush.c b/source/blender/blenkernel/intern/brush.c
index 1cda0e8a4bb..dd38af126fe 100644
--- a/source/blender/blenkernel/intern/brush.c
+++ b/source/blender/blenkernel/intern/brush.c
@@ -1562,6 +1562,7 @@ void BKE_brush_init_curves_sculpt_settings(Brush *brush)
settings->points_per_curve = 8;
settings->minimum_length = 0.01f;
settings->curve_length = 0.3f;
+ settings->density_add_attempts = 100;
}
struct Brush *BKE_brush_first_search(struct Main *bmain, const eObjectMode ob_mode)
diff --git a/source/blender/blenloader/intern/versioning_300.c b/source/blender/blenloader/intern/versioning_300.c
index 68df560a389..35b1367ca1e 100644
--- a/source/blender/blenloader/intern/versioning_300.c
+++ b/source/blender/blenloader/intern/versioning_300.c
@@ -3243,5 +3243,13 @@ void blo_do_versions_300(FileData *fd, Library *UNUSED(lib), Main *bmain)
*/
{
/* Keep this block, even when empty. */
+
+ /* Initialize brush curves sculpt settings. */
+ LISTBASE_FOREACH (Brush *, brush, &bmain->brushes) {
+ if (brush->ob_mode != OB_MODE_SCULPT_CURVES) {
+ continue;
+ }
+ brush->curves_sculpt_settings->density_add_attempts = 100;
+ }
}
}
diff --git a/source/blender/editors/curves/CMakeLists.txt b/source/blender/editors/curves/CMakeLists.txt
index 3c31e8014ff..303d2fb71dc 100644
--- a/source/blender/editors/curves/CMakeLists.txt
+++ b/source/blender/editors/curves/CMakeLists.txt
@@ -8,6 +8,7 @@ set(INC
../../depsgraph
../../functions
../../geometry
+ ../../gpu
../../makesdna
../../makesrna
../../windowmanager
@@ -27,5 +28,17 @@ set(LIB
bf_blenlib
)
+if(WITH_TBB)
+ list(APPEND INC_SYS
+ ${TBB_INCLUDE_DIRS}
+ )
+ add_definitions(-DWITH_TBB)
+ if(WIN32)
+ # TBB includes Windows.h which will define min/max macros
+ # that will collide with the stl versions.
+ add_definitions(-DNOMINMAX)
+ endif()
+endif()
+
blender_add_lib(bf_editor_curves "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
add_dependencies(bf_editor_curves bf_rna)
diff --git a/source/blender/editors/curves/intern/curves_ops.cc b/source/blender/editors/curves/intern/curves_ops.cc
index 25bcba6cfb3..dd7edf66920 100644
--- a/source/blender/editors/curves/intern/curves_ops.cc
+++ b/source/blender/editors/curves/intern/curves_ops.cc
@@ -72,7 +72,7 @@ static bool object_has_editable_curves(const Main &bmain, const Object &object)
return true;
}
-static VectorSet<Curves *> get_unique_editable_curves(const bContext &C)
+VectorSet<Curves *> get_unique_editable_curves(const bContext &C)
{
VectorSet<Curves *> unique_curves;
@@ -715,7 +715,7 @@ 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)
+bool selection_operator_poll(bContext *C)
{
const Object *object = CTX_data_active_object(C);
if (object == nullptr) {
@@ -784,7 +784,7 @@ static void CURVES_OT_set_selection_domain(wmOperatorType *ot)
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->poll = selection_operator_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
@@ -820,13 +820,11 @@ static void CURVES_OT_disable_selection(wmOperatorType *ot)
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->poll = selection_operator_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
-namespace select_all {
-
static bool varray_contains_nonzero(const VArray<float> &data)
{
bool contains_nonzero = false;
@@ -841,6 +839,19 @@ static bool varray_contains_nonzero(const VArray<float> &data)
return contains_nonzero;
}
+bool has_anything_selected(const Curves &curves_id)
+{
+ const CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
+ switch (curves_id.selection_domain) {
+ case ATTR_DOMAIN_POINT:
+ return varray_contains_nonzero(curves.selection_point_float());
+ case ATTR_DOMAIN_CURVE:
+ return varray_contains_nonzero(curves.selection_curve_float());
+ }
+ BLI_assert_unreachable();
+ return false;
+}
+
static bool any_point_selected(const CurvesGeometry &curves)
{
return varray_contains_nonzero(curves.selection_point_float());
@@ -856,6 +867,8 @@ static bool any_point_selected(const Span<Curves *> curves_ids)
return false;
}
+namespace select_all {
+
static void invert_selection(MutableSpan<float> selection)
{
threading::parallel_for(selection.index_range(), 2048, [&](IndexRange range) {
@@ -924,7 +937,7 @@ static void SCULPT_CURVES_OT_select_all(wmOperatorType *ot)
ot->description = "(De)select all control points";
ot->exec = select_all::select_all_exec;
- ot->poll = selection_poll;
+ ot->poll = selection_operator_poll;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
diff --git a/source/blender/editors/include/ED_curves.h b/source/blender/editors/include/ED_curves.h
index 9233b65b2ce..68e09fd1b12 100644
--- a/source/blender/editors/include/ED_curves.h
+++ b/source/blender/editors/include/ED_curves.h
@@ -6,6 +6,8 @@
#pragma once
+struct bContext;
+
#ifdef __cplusplus
extern "C" {
#endif
@@ -19,10 +21,14 @@ void ED_operatortypes_curves(void);
#ifdef __cplusplus
# include "BKE_curves.hh"
+# include "BLI_vector_set.hh"
namespace blender::ed::curves {
bke::CurvesGeometry primitive_random_sphere(int curves_size, int points_per_curve);
+bool selection_operator_poll(bContext *C);
+bool has_anything_selected(const Curves &curves_id);
+VectorSet<Curves *> get_unique_editable_curves(const bContext &C);
-}
+} // namespace blender::ed::curves
#endif
diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt
index 34247b4ef75..edb0f1cda4d 100644
--- a/source/blender/editors/sculpt_paint/CMakeLists.txt
+++ b/source/blender/editors/sculpt_paint/CMakeLists.txt
@@ -32,10 +32,15 @@ set(SRC
curves_sculpt_brush.cc
curves_sculpt_comb.cc
curves_sculpt_delete.cc
+ curves_sculpt_density.cc
curves_sculpt_grow_shrink.cc
curves_sculpt_ops.cc
- curves_sculpt_selection.cc
+ curves_sculpt_pinch.cc
+ curves_sculpt_puff.cc
curves_sculpt_selection_paint.cc
+ curves_sculpt_selection.cc
+ curves_sculpt_slide.cc
+ curves_sculpt_smooth.cc
curves_sculpt_snake_hook.cc
paint_canvas.cc
paint_cursor.c
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_density.cc b/source/blender/editors/sculpt_paint/curves_sculpt_density.cc
new file mode 100644
index 00000000000..536c44ddd44
--- /dev/null
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_density.cc
@@ -0,0 +1,834 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <numeric>
+
+#include "BKE_brush.h"
+#include "BKE_bvhutils.h"
+#include "BKE_context.h"
+#include "BKE_geometry_set.hh"
+#include "BKE_mesh.h"
+#include "BKE_mesh_runtime.h"
+#include "BKE_mesh_sample.hh"
+
+#include "ED_screen.h"
+#include "ED_view3d.h"
+
+#include "DEG_depsgraph.h"
+
+#include "BLI_index_mask_ops.hh"
+#include "BLI_kdtree.h"
+#include "BLI_rand.hh"
+#include "BLI_task.hh"
+
+#include "PIL_time.h"
+
+#include "GEO_add_curves_on_mesh.hh"
+
+#include "DNA_brush_types.h"
+#include "DNA_mesh_types.h"
+
+#include "WM_api.h"
+
+#include "curves_sculpt_intern.hh"
+
+namespace blender::ed::sculpt_paint {
+
+class DensityAddOperation : public CurvesSculptStrokeOperation {
+ private:
+ /** Used when some data should be interpolated from existing curves. */
+ KDTree_3d *curve_roots_kdtree_ = nullptr;
+ int original_curve_num_ = 0;
+
+ friend struct DensityAddOperationExecutor;
+
+ public:
+ ~DensityAddOperation() override
+ {
+ if (curve_roots_kdtree_ != nullptr) {
+ BLI_kdtree_3d_free(curve_roots_kdtree_);
+ }
+ }
+
+ void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
+};
+
+struct DensityAddOperationExecutor {
+ DensityAddOperation *self_ = nullptr;
+ CurvesSculptCommonContext ctx_;
+
+ Object *object_ = nullptr;
+ Curves *curves_id_ = nullptr;
+ CurvesGeometry *curves_ = nullptr;
+
+ Object *surface_ob_ = nullptr;
+ Mesh *surface_ = nullptr;
+ Span<MLoopTri> surface_looptris_;
+ Span<float3> corner_normals_su_;
+ VArray_Span<float2> surface_uv_map_;
+
+ const CurvesSculpt *curves_sculpt_ = nullptr;
+ const Brush *brush_ = nullptr;
+ const BrushCurvesSculptSettings *brush_settings_ = nullptr;
+
+ float brush_strength_;
+ float brush_radius_re_;
+ float2 brush_pos_re_;
+
+ CurvesSculptTransforms transforms_;
+
+ BVHTreeFromMesh surface_bvh_;
+
+ DensityAddOperationExecutor(const bContext &C) : ctx_(C)
+ {
+ }
+
+ void execute(DensityAddOperation &self,
+ const bContext &C,
+ const StrokeExtension &stroke_extension)
+ {
+ self_ = &self;
+ object_ = CTX_data_active_object(&C);
+ curves_id_ = static_cast<Curves *>(object_->data);
+ curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
+
+ if (stroke_extension.is_first) {
+ self_->original_curve_num_ = curves_->curves_num();
+ }
+
+ if (curves_id_->surface == nullptr || curves_id_->surface->type != OB_MESH) {
+ return;
+ }
+
+ surface_ob_ = curves_id_->surface;
+ surface_ = static_cast<Mesh *>(surface_ob_->data);
+
+ surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_),
+ BKE_mesh_runtime_looptri_len(surface_)};
+
+ transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface);
+
+ if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) {
+ BKE_mesh_calc_normals_split(surface_);
+ }
+ corner_normals_su_ = {
+ reinterpret_cast<const float3 *>(CustomData_get_layer(&surface_->ldata, CD_NORMAL)),
+ surface_->totloop};
+
+ curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
+ brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
+ brush_settings_ = brush_->curves_sculpt_settings;
+ brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension);
+ brush_radius_re_ = brush_radius_get(*ctx_.scene, *brush_, stroke_extension);
+ brush_pos_re_ = stroke_extension.mouse_position;
+
+ const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
+ brush_->falloff_shape);
+
+ BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2);
+ BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); });
+
+ Vector<float3> new_bary_coords;
+ Vector<int> new_looptri_indices;
+ Vector<float3> new_positions_cu;
+ const double time = PIL_check_seconds_timer() * 1000000.0;
+ RandomNumberGenerator rng{*(uint32_t *)(&time)};
+
+ /* Find potential new curve root points. */
+ if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
+ this->sample_projected_with_symmetry(
+ rng, new_bary_coords, new_looptri_indices, new_positions_cu);
+ }
+ else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
+ this->sample_spherical_with_symmetry(
+ rng, new_bary_coords, new_looptri_indices, new_positions_cu);
+ }
+ else {
+ BLI_assert_unreachable();
+ }
+ for (float3 &pos : new_positions_cu) {
+ pos = transforms_.surface_to_curves * pos;
+ }
+
+ this->ensure_curve_roots_kdtree();
+
+ const int already_added_curves = curves_->curves_num() - self_->original_curve_num_;
+ KDTree_3d *new_roots_kdtree = BLI_kdtree_3d_new(already_added_curves +
+ new_positions_cu.size());
+ BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(new_roots_kdtree); });
+
+ /* Used to tag all curves that are too close to existing curves or too close to other new
+ * curves. */
+ Array<bool> new_curve_skipped(new_positions_cu.size(), false);
+ threading::parallel_invoke(
+ /* Build kdtree from root points created by the current stroke. */
+ [&]() {
+ const Span<float3> positions_cu = curves_->positions();
+ for (const int curve_i : curves_->curves_range().take_back(already_added_curves)) {
+ const float3 &root_pos_cu = positions_cu[curves_->offsets()[curve_i]];
+ BLI_kdtree_3d_insert(new_roots_kdtree, curve_i, root_pos_cu);
+ }
+ for (const int new_i : new_positions_cu.index_range()) {
+ const int index_in_kdtree = curves_->curves_num() + new_i;
+ const float3 &root_pos_cu = new_positions_cu[new_i];
+ BLI_kdtree_3d_insert(new_roots_kdtree, index_in_kdtree, root_pos_cu);
+ }
+ BLI_kdtree_3d_balance(new_roots_kdtree);
+ },
+ /* Check which new root points are close to roots that existed before the current stroke
+ * started. */
+ [&]() {
+ threading::parallel_for(
+ new_positions_cu.index_range(), 128, [&](const IndexRange range) {
+ for (const int new_i : range) {
+ const float3 &new_root_pos_cu = new_positions_cu[new_i];
+ KDTreeNearest_3d nearest;
+ nearest.dist = FLT_MAX;
+ BLI_kdtree_3d_find_nearest(
+ self_->curve_roots_kdtree_, new_root_pos_cu, &nearest);
+ if (nearest.dist < brush_settings_->minimum_distance) {
+ new_curve_skipped[new_i] = true;
+ }
+ }
+ });
+ });
+
+ /* Find new points that are too close too other new points. */
+ for (const int new_i : new_positions_cu.index_range()) {
+ if (new_curve_skipped[new_i]) {
+ continue;
+ }
+ const float3 &root_pos_cu = new_positions_cu[new_i];
+ BLI_kdtree_3d_range_search_cb_cpp(
+ new_roots_kdtree,
+ root_pos_cu,
+ brush_settings_->minimum_distance,
+ [&](const int other_i, const float *UNUSED(co), float UNUSED(dist_sq)) {
+ if (other_i < curves_->curves_num()) {
+ new_curve_skipped[new_i] = true;
+ return false;
+ }
+ const int other_new_i = other_i - curves_->curves_num();
+ if (new_i == other_new_i) {
+ return true;
+ }
+ new_curve_skipped[other_new_i] = true;
+ return true;
+ });
+ }
+
+ /* Remove points that are too close to others. */
+ for (int64_t i = new_positions_cu.size() - 1; i >= 0; i--) {
+ if (new_curve_skipped[i]) {
+ new_positions_cu.remove_and_reorder(i);
+ new_bary_coords.remove_and_reorder(i);
+ new_looptri_indices.remove_and_reorder(i);
+ }
+ }
+
+ /* Find UV map. */
+ VArray_Span<float2> surface_uv_map;
+ if (curves_id_->surface_uv_map != nullptr) {
+ MeshComponent surface_component;
+ surface_component.replace(surface_, GeometryOwnershipType::ReadOnly);
+ surface_uv_map = surface_component
+ .attribute_try_get_for_read(curves_id_->surface_uv_map,
+ ATTR_DOMAIN_CORNER)
+ .typed<float2>();
+ }
+
+ /* Find normals. */
+ if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) {
+ BKE_mesh_calc_normals_split(surface_);
+ }
+ const Span<float3> corner_normals_su = {
+ reinterpret_cast<const float3 *>(CustomData_get_layer(&surface_->ldata, CD_NORMAL)),
+ surface_->totloop};
+
+ geometry::AddCurvesOnMeshInputs add_inputs;
+ add_inputs.root_positions_cu = new_positions_cu;
+ add_inputs.bary_coords = new_bary_coords;
+ add_inputs.looptri_indices = new_looptri_indices;
+ add_inputs.interpolate_length = brush_settings_->flag &
+ BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_LENGTH;
+ add_inputs.interpolate_shape = brush_settings_->flag &
+ BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_SHAPE;
+ add_inputs.interpolate_point_count = brush_settings_->flag &
+ BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_POINT_COUNT;
+ add_inputs.fallback_curve_length = brush_settings_->curve_length;
+ add_inputs.fallback_point_count = std::max(2, brush_settings_->points_per_curve);
+ add_inputs.surface = surface_;
+ add_inputs.surface_bvh = &surface_bvh_;
+ add_inputs.surface_looptris = surface_looptris_;
+ add_inputs.surface_uv_map = surface_uv_map;
+ add_inputs.corner_normals_su = corner_normals_su;
+ add_inputs.curves_to_surface_mat = transforms_.curves_to_surface;
+ add_inputs.surface_to_curves_normal_mat = transforms_.surface_to_curves_normal;
+ add_inputs.old_roots_kdtree = self_->curve_roots_kdtree_;
+
+ geometry::add_curves_on_mesh(*curves_, add_inputs);
+
+ 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(ctx_.region);
+ }
+
+ void ensure_curve_roots_kdtree()
+ {
+ if (self_->curve_roots_kdtree_ == nullptr) {
+ self_->curve_roots_kdtree_ = BLI_kdtree_3d_new(curves_->curves_num());
+ for (const int curve_i : curves_->curves_range()) {
+ const int root_point_i = curves_->offsets()[curve_i];
+ const float3 &root_pos_cu = curves_->positions()[root_point_i];
+ BLI_kdtree_3d_insert(self_->curve_roots_kdtree_, curve_i, root_pos_cu);
+ }
+ BLI_kdtree_3d_balance(self_->curve_roots_kdtree_);
+ }
+ }
+
+ void sample_projected_with_symmetry(RandomNumberGenerator &rng,
+ Vector<float3> &r_bary_coords,
+ Vector<int> &r_looptri_indices,
+ Vector<float3> &r_positions_su)
+ {
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+ const float4x4 transform = transforms_.curves_to_surface * brush_transform *
+ transforms_.world_to_curves;
+ const int new_points = bke::mesh_surface_sample::sample_surface_points_projected(
+ rng,
+ *surface_,
+ surface_bvh_,
+ brush_pos_re_,
+ brush_radius_re_,
+ [&](const float2 &pos_re, float3 &r_start_su, float3 &r_end_su) {
+ float3 start_wo, end_wo;
+ ED_view3d_win_to_segment_clipped(
+ ctx_.depsgraph, ctx_.region, ctx_.v3d, pos_re, start_wo, end_wo, true);
+ r_start_su = transform * start_wo;
+ r_end_su = transform * end_wo;
+ },
+ true,
+ brush_settings_->density_add_attempts,
+ brush_settings_->density_add_attempts,
+ r_bary_coords,
+ r_looptri_indices,
+ r_positions_su);
+
+ /* Remove some sampled points randomly based on the brush falloff and strength. */
+ const int old_points = r_bary_coords.size() - new_points;
+ for (int i = r_bary_coords.size() - 1; i >= old_points; i--) {
+ const float3 pos_su = r_positions_su[i];
+ const float3 pos_cu = brush_transform_inv * transforms_.surface_to_curves * pos_su;
+ float2 pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values);
+ const float dist_to_brush_re = math::distance(brush_pos_re_, pos_re);
+ const float radius_falloff = BKE_brush_curve_strength(
+ brush_, dist_to_brush_re, brush_radius_re_);
+ const float weight = brush_strength_ * radius_falloff;
+ if (rng.get_float() > weight) {
+ r_bary_coords.remove_and_reorder(i);
+ r_looptri_indices.remove_and_reorder(i);
+ r_positions_su.remove_and_reorder(i);
+ }
+ }
+ }
+ }
+
+ void sample_spherical_with_symmetry(RandomNumberGenerator &rng,
+ Vector<float3> &r_bary_coords,
+ Vector<int> &r_looptri_indices,
+ Vector<float3> &r_positions_su)
+ {
+ const std::optional<CurvesBrush3D> brush_3d = sample_curves_surface_3d_brush(*ctx_.depsgraph,
+ *ctx_.region,
+ *ctx_.v3d,
+ transforms_,
+ surface_bvh_,
+ brush_pos_re_,
+ brush_radius_re_);
+ if (!brush_3d.has_value()) {
+ return;
+ }
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ const float3 brush_pos_cu = brush_transform * brush_3d->position_cu;
+ const float3 brush_pos_su = transforms_.curves_to_surface * brush_pos_cu;
+ const float brush_radius_su = transform_brush_radius(
+ transforms_.curves_to_surface, brush_pos_cu, brush_3d->radius_cu);
+ const float brush_radius_sq_su = pow2f(brush_radius_su);
+
+ Vector<int> looptri_indices;
+ BLI_bvhtree_range_query_cpp(
+ *surface_bvh_.tree,
+ brush_pos_su,
+ brush_radius_su,
+ [&](const int index, const float3 &UNUSED(co), const float UNUSED(dist_sq)) {
+ looptri_indices.append(index);
+ });
+
+ const float brush_plane_area_su = M_PI * brush_radius_sq_su;
+ const float approximate_density_su = brush_settings_->density_add_attempts /
+ brush_plane_area_su;
+
+ const int new_points = bke::mesh_surface_sample::sample_surface_points_spherical(
+ rng,
+ *surface_,
+ looptri_indices,
+ brush_pos_su,
+ brush_radius_su,
+ approximate_density_su,
+ r_bary_coords,
+ r_looptri_indices,
+ r_positions_su);
+
+ /* Remove some sampled points randomly based on the brush falloff and strength. */
+ const int old_points = r_bary_coords.size() - new_points;
+ for (int i = r_bary_coords.size() - 1; i >= old_points; i--) {
+ const float3 pos_su = r_positions_su[i];
+ const float3 pos_cu = transforms_.surface_to_curves * pos_su;
+ const float dist_to_brush_cu = math::distance(pos_cu, brush_pos_cu);
+ const float radius_falloff = BKE_brush_curve_strength(
+ brush_, dist_to_brush_cu, brush_3d->radius_cu);
+ const float weight = brush_strength_ * radius_falloff;
+ if (rng.get_float() > weight) {
+ r_bary_coords.remove_and_reorder(i);
+ r_looptri_indices.remove_and_reorder(i);
+ r_positions_su.remove_and_reorder(i);
+ }
+ }
+ }
+ }
+};
+
+void DensityAddOperation::on_stroke_extended(const bContext &C,
+ const StrokeExtension &stroke_extension)
+{
+ DensityAddOperationExecutor executor{C};
+ executor.execute(*this, C, stroke_extension);
+}
+
+class DensitySubtractOperation : public CurvesSculptStrokeOperation {
+ private:
+ /** Only used when a 3D brush is used. */
+ CurvesBrush3D brush_3d_;
+
+ friend struct DensitySubtractOperationExecutor;
+
+ public:
+ void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
+};
+
+/**
+ * Utility class that actually executes the update when the stroke is updated. That's useful
+ * because it avoids passing a very large number of parameters between functions.
+ */
+struct DensitySubtractOperationExecutor {
+ DensitySubtractOperation *self_ = nullptr;
+ CurvesSculptCommonContext ctx_;
+
+ Object *object_ = nullptr;
+ Curves *curves_id_ = nullptr;
+ CurvesGeometry *curves_ = nullptr;
+
+ Vector<int64_t> selected_curve_indices_;
+ IndexMask curve_selection_;
+
+ Object *surface_ob_ = nullptr;
+ Mesh *surface_ = nullptr;
+
+ const CurvesSculpt *curves_sculpt_ = nullptr;
+ const Brush *brush_ = nullptr;
+ float brush_radius_base_re_;
+ float brush_radius_factor_;
+ float brush_strength_;
+ float2 brush_pos_re_;
+
+ float minimum_distance_;
+
+ CurvesSculptTransforms transforms_;
+ BVHTreeFromMesh surface_bvh_;
+
+ KDTree_3d *root_points_kdtree_;
+
+ DensitySubtractOperationExecutor(const bContext &C) : ctx_(C)
+ {
+ }
+
+ void execute(DensitySubtractOperation &self,
+ const bContext &C,
+ const StrokeExtension &stroke_extension)
+ {
+ self_ = &self;
+
+ object_ = CTX_data_active_object(&C);
+
+ curves_id_ = static_cast<Curves *>(object_->data);
+ curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
+ if (curves_->curves_num() == 0) {
+ return;
+ }
+
+ surface_ob_ = curves_id_->surface;
+ if (surface_ob_ == nullptr) {
+ return;
+ }
+ surface_ = static_cast<Mesh *>(surface_ob_->data);
+
+ curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
+ brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
+ brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_);
+ brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension);
+ brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension);
+ brush_pos_re_ = stroke_extension.mouse_position;
+
+ minimum_distance_ = brush_->curves_sculpt_settings->minimum_distance;
+
+ curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
+
+ transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface);
+ const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
+ brush_->falloff_shape);
+ BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2);
+ BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); });
+
+ const Span<float3> positions_cu = curves_->positions();
+
+ root_points_kdtree_ = BLI_kdtree_3d_new(curve_selection_.size());
+ BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(root_points_kdtree_); });
+ for (const int curve_i : curve_selection_) {
+ const int first_point_i = curves_->offsets()[curve_i];
+ const float3 &pos_cu = positions_cu[first_point_i];
+ BLI_kdtree_3d_insert(root_points_kdtree_, curve_i, pos_cu);
+ }
+ BLI_kdtree_3d_balance(root_points_kdtree_);
+
+ /* Find all curves that should be deleted. */
+ Array<bool> curves_to_delete(curves_->curves_num(), false);
+ if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
+ this->reduce_density_projected_with_symmetry(curves_to_delete);
+ }
+ else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
+ this->reduce_density_spherical_with_symmetry(curves_to_delete);
+ }
+ else {
+ BLI_assert_unreachable();
+ }
+
+ Vector<int64_t> indices;
+ const IndexMask mask = index_mask_ops::find_indices_based_on_predicate(
+ curves_->curves_range(), 4096, indices, [&](const int curve_i) {
+ return curves_to_delete[curve_i];
+ });
+
+ curves_->remove_curves(mask);
+
+ 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(ctx_.region);
+ }
+
+ void reduce_density_projected_with_symmetry(MutableSpan<bool> curves_to_delete)
+ {
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->reduce_density_projected(brush_transform, curves_to_delete);
+ }
+ }
+
+ void reduce_density_projected(const float4x4 &brush_transform,
+ MutableSpan<bool> curves_to_delete)
+ {
+ const 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);
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ const Span<int> offsets = curves_->offsets();
+
+ /* Randomly select the curves that are allowed to be removed, based on the brush radius and
+ * strength. */
+ Array<bool> allow_remove_curve(curves_->curves_num(), false);
+ threading::parallel_for(curves_->curves_range(), 512, [&](const IndexRange range) {
+ RandomNumberGenerator rng((int)(PIL_check_seconds_timer() * 1000000.0));
+
+ for (const int curve_i : range) {
+ if (curves_to_delete[curve_i]) {
+ allow_remove_curve[curve_i] = true;
+ continue;
+ }
+ const int first_point_i = offsets[curve_i];
+ const float3 pos_cu = brush_transform * positions_cu[first_point_i];
+
+ float2 pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values);
+ const float dist_to_brush_sq_re = math::distance_squared(brush_pos_re_, pos_re);
+ if (dist_to_brush_sq_re > brush_radius_sq_re) {
+ continue;
+ }
+ 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;
+ if (rng.get_float() < weight) {
+ allow_remove_curve[curve_i] = true;
+ }
+ }
+ });
+
+ /* Detect curves that are too close to other existing curves. */
+ for (const int curve_i : curve_selection_) {
+ if (curves_to_delete[curve_i]) {
+ continue;
+ }
+ if (!allow_remove_curve[curve_i]) {
+ continue;
+ }
+ const int first_point_i = offsets[curve_i];
+ const float3 orig_pos_cu = positions_cu[first_point_i];
+ const float3 pos_cu = brush_transform * orig_pos_cu;
+ float2 pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values);
+ const float dist_to_brush_sq_re = math::distance_squared(brush_pos_re_, pos_re);
+ if (dist_to_brush_sq_re > brush_radius_sq_re) {
+ continue;
+ }
+ BLI_kdtree_3d_range_search_cb_cpp(
+ root_points_kdtree_,
+ orig_pos_cu,
+ minimum_distance_,
+ [&](const int other_curve_i, const float *UNUSED(co), float UNUSED(dist_sq)) {
+ if (other_curve_i == curve_i) {
+ return true;
+ }
+ if (allow_remove_curve[other_curve_i]) {
+ curves_to_delete[other_curve_i] = true;
+ }
+ return true;
+ });
+ }
+ }
+
+ void reduce_density_spherical_with_symmetry(MutableSpan<bool> curves_to_delete)
+ {
+ const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
+ const std::optional<CurvesBrush3D> brush_3d = sample_curves_surface_3d_brush(*ctx_.depsgraph,
+ *ctx_.region,
+ *ctx_.v3d,
+ transforms_,
+ surface_bvh_,
+ brush_pos_re_,
+ brush_radius_re);
+ if (!brush_3d.has_value()) {
+ return;
+ }
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ const float3 brush_pos_cu = brush_transform * brush_3d->position_cu;
+ this->reduce_density_spherical(brush_pos_cu, brush_3d->radius_cu, curves_to_delete);
+ }
+ }
+
+ void reduce_density_spherical(const float3 &brush_pos_cu,
+ const float brush_radius_cu,
+ MutableSpan<bool> curves_to_delete)
+ {
+ const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+ const Span<float3> positions_cu = curves_->positions();
+ const Span<int> offsets = curves_->offsets();
+
+ /* Randomly select the curves that are allowed to be removed, based on the brush radius and
+ * strength. */
+ Array<bool> allow_remove_curve(curves_->curves_num(), false);
+ threading::parallel_for(curves_->curves_range(), 512, [&](const IndexRange range) {
+ RandomNumberGenerator rng((int)(PIL_check_seconds_timer() * 1000000.0));
+
+ for (const int curve_i : range) {
+ if (curves_to_delete[curve_i]) {
+ allow_remove_curve[curve_i] = true;
+ continue;
+ }
+ const int first_point_i = offsets[curve_i];
+ const float3 pos_cu = positions_cu[first_point_i];
+
+ const float dist_to_brush_sq_cu = math::distance_squared(brush_pos_cu, pos_cu);
+ if (dist_to_brush_sq_cu > brush_radius_sq_cu) {
+ continue;
+ }
+ 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;
+ if (rng.get_float() < weight) {
+ allow_remove_curve[curve_i] = true;
+ }
+ }
+ });
+
+ /* Detect curves that are too close to other existing curves. */
+ for (const int curve_i : curve_selection_) {
+ if (curves_to_delete[curve_i]) {
+ continue;
+ }
+ if (!allow_remove_curve[curve_i]) {
+ continue;
+ }
+ const int first_point_i = offsets[curve_i];
+ const float3 &pos_cu = positions_cu[first_point_i];
+ const float dist_to_brush_sq_cu = math::distance_squared(pos_cu, brush_pos_cu);
+ if (dist_to_brush_sq_cu > brush_radius_sq_cu) {
+ continue;
+ }
+
+ BLI_kdtree_3d_range_search_cb_cpp(
+ root_points_kdtree_,
+ pos_cu,
+ minimum_distance_,
+ [&](const int other_curve_i, const float *UNUSED(co), float UNUSED(dist_sq)) {
+ if (other_curve_i == curve_i) {
+ return true;
+ }
+ if (allow_remove_curve[other_curve_i]) {
+ curves_to_delete[other_curve_i] = true;
+ }
+ return true;
+ });
+ }
+ }
+};
+
+void DensitySubtractOperation::on_stroke_extended(const bContext &C,
+ const StrokeExtension &stroke_extension)
+{
+ DensitySubtractOperationExecutor executor{C};
+ executor.execute(*this, C, stroke_extension);
+}
+
+/**
+ * Detects whether the brush should be in Add or Subtract mode.
+ */
+static bool use_add_density_mode(const BrushStrokeMode brush_mode,
+ const bContext &C,
+ const StrokeExtension &stroke_start)
+{
+ const Scene &scene = *CTX_data_scene(&C);
+ const Brush &brush = *BKE_paint_brush_for_read(&scene.toolsettings->curves_sculpt->paint);
+ const eBrushCurvesSculptDensityMode density_mode = static_cast<eBrushCurvesSculptDensityMode>(
+ brush.curves_sculpt_settings->density_mode);
+ const bool use_invert = brush_mode == BRUSH_STROKE_INVERT;
+
+ if (density_mode == BRUSH_CURVES_SCULPT_DENSITY_MODE_ADD) {
+ return !use_invert;
+ }
+ if (density_mode == BRUSH_CURVES_SCULPT_DENSITY_MODE_REMOVE) {
+ return use_invert;
+ }
+
+ const Object &curves_ob = *CTX_data_active_object(&C);
+ const Curves &curves_id = *static_cast<Curves *>(curves_ob.data);
+ const CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
+ if (curves_id.surface == nullptr) {
+ /* The brush won't do anything in this case anyway. */
+ return true;
+ }
+ if (curves.curves_num() <= 1) {
+ return true;
+ }
+
+ const CurvesSculptTransforms transforms(curves_ob, curves_id.surface);
+ BVHTreeFromMesh surface_bvh;
+ BKE_bvhtree_from_mesh_get(
+ &surface_bvh, static_cast<const Mesh *>(curves_id.surface->data), BVHTREE_FROM_LOOPTRI, 2);
+ BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh); });
+
+ const Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(&C);
+ const ARegion &region = *CTX_wm_region(&C);
+ const View3D &v3d = *CTX_wm_view3d(&C);
+
+ const float2 brush_pos_re = stroke_start.mouse_position;
+ /* Reduce radius so that only an inner circle is used to determine the existing density. */
+ const float brush_radius_re = BKE_brush_size_get(&scene, &brush) * 0.5f;
+
+ /* Find the surface point under the brush. */
+ const std::optional<CurvesBrush3D> brush_3d = sample_curves_surface_3d_brush(
+ depsgraph, region, v3d, transforms, surface_bvh, brush_pos_re, brush_radius_re);
+ if (!brush_3d.has_value()) {
+ return true;
+ }
+
+ const float3 brush_pos_cu = brush_3d->position_cu;
+ const float brush_radius_cu = brush_3d->radius_cu;
+ const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+
+ const Span<int> offsets = curves.offsets();
+ const Span<float3> positions_cu = curves.positions();
+
+ /* Compute distance from brush to curve roots. */
+ Array<std::pair<float, int>> distances_sq_to_brush(curves.curves_num());
+ threading::EnumerableThreadSpecific<int> valid_curve_count_by_thread;
+ threading::parallel_for(curves.curves_range(), 512, [&](const IndexRange range) {
+ int &valid_curve_count = valid_curve_count_by_thread.local();
+ for (const int curve_i : range) {
+ const int root_point_i = offsets[curve_i];
+ const float3 &root_pos_cu = positions_cu[root_point_i];
+ const float dist_sq_cu = math::distance_squared(root_pos_cu, brush_pos_cu);
+ if (dist_sq_cu < brush_radius_sq_cu) {
+ distances_sq_to_brush[curve_i] = {math::distance_squared(root_pos_cu, brush_pos_cu),
+ curve_i};
+ valid_curve_count++;
+ }
+ else {
+ distances_sq_to_brush[curve_i] = {FLT_MAX, -1};
+ }
+ }
+ });
+ const int valid_curve_count = std::accumulate(
+ valid_curve_count_by_thread.begin(), valid_curve_count_by_thread.end(), 0);
+
+ /* Find a couple of curves that are closest to the brush center. */
+ const int check_curve_count = std::min<int>(8, valid_curve_count);
+ std::partial_sort(distances_sq_to_brush.begin(),
+ distances_sq_to_brush.begin() + check_curve_count,
+ distances_sq_to_brush.end());
+
+ /* Compute the minimum pair-wise distance between the curve roots that are close to the brush
+ * center. */
+ float min_dist_sq_cu = FLT_MAX;
+ for (const int i : IndexRange(check_curve_count)) {
+ const float3 &pos_i = positions_cu[offsets[distances_sq_to_brush[i].second]];
+ for (int j = i + 1; j < check_curve_count; j++) {
+ const float3 &pos_j = positions_cu[offsets[distances_sq_to_brush[j].second]];
+ const float dist_sq_cu = math::distance_squared(pos_i, pos_j);
+ math::min_inplace(min_dist_sq_cu, dist_sq_cu);
+ }
+ }
+
+ const float min_dist_cu = std::sqrt(min_dist_sq_cu);
+ if (min_dist_cu > brush.curves_sculpt_settings->minimum_distance) {
+ return true;
+ }
+
+ return false;
+}
+
+std::unique_ptr<CurvesSculptStrokeOperation> new_density_operation(
+ const BrushStrokeMode brush_mode, const bContext &C, const StrokeExtension &stroke_start)
+{
+ if (use_add_density_mode(brush_mode, C, stroke_start)) {
+ return std::make_unique<DensityAddOperation>();
+ }
+ return std::make_unique<DensitySubtractOperation>();
+}
+
+} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh b/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh
index ad3871bee45..b26649e746b 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_intern.hh
@@ -66,7 +66,7 @@ std::unique_ptr<CurvesSculptStrokeOperation> new_pinch_operation(const BrushStro
std::unique_ptr<CurvesSculptStrokeOperation> new_smooth_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_puff_operation();
std::unique_ptr<CurvesSculptStrokeOperation> new_density_operation(
- const BrushStrokeMode brush_mode, const bContext &C);
+ const BrushStrokeMode brush_mode, const bContext &C, const StrokeExtension &stroke_start);
std::unique_ptr<CurvesSculptStrokeOperation> new_slide_operation();
struct CurvesBrush3D {
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc b/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc
index e6da2039433..739c39f6196 100644
--- a/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_ops.cc
@@ -1,8 +1,12 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include "BLI_kdtree.h"
+#include "BLI_rand.hh"
#include "BLI_utildefines.h"
+#include "BLI_vector_set.hh"
#include "BKE_brush.h"
+#include "BKE_bvhutils.h"
#include "BKE_context.h"
#include "BKE_curves.hh"
#include "BKE_paint.h"
@@ -11,10 +15,12 @@
#include "WM_message.h"
#include "WM_toolsystem.h"
+#include "ED_curves.h"
#include "ED_curves_sculpt.h"
#include "ED_image.h"
#include "ED_object.h"
#include "ED_screen.h"
+#include "ED_space_api.h"
#include "ED_view3d.h"
#include "DEG_depsgraph.h"
@@ -24,11 +30,21 @@
#include "DNA_screen_types.h"
#include "RNA_access.h"
+#include "RNA_define.h"
+#include "RNA_enum_types.h"
#include "curves_sculpt_intern.h"
#include "curves_sculpt_intern.hh"
#include "paint_intern.h"
+#include "UI_interface.h"
+#include "UI_resources.h"
+
+#include "GPU_immediate.h"
+#include "GPU_immediate_util.h"
+#include "GPU_matrix.h"
+#include "GPU_state.h"
+
/* -------------------------------------------------------------------- */
/** \name Poll Functions
* \{ */
@@ -90,8 +106,8 @@ float brush_strength_get(const Scene &scene,
return BKE_brush_alpha_get(&scene, &brush) * brush_strength_factor(brush, stroke_extension);
}
-static std::unique_ptr<CurvesSculptStrokeOperation> start_brush_operation(bContext &C,
- wmOperator &op)
+static std::unique_ptr<CurvesSculptStrokeOperation> start_brush_operation(
+ bContext &C, wmOperator &op, const StrokeExtension &stroke_start)
{
const BrushStrokeMode mode = static_cast<BrushStrokeMode>(RNA_enum_get(op.ptr, "mode"));
@@ -111,6 +127,16 @@ static std::unique_ptr<CurvesSculptStrokeOperation> start_brush_operation(bConte
return new_grow_shrink_operation(mode, C);
case CURVES_SCULPT_TOOL_SELECTION_PAINT:
return new_selection_paint_operation(mode, C);
+ case CURVES_SCULPT_TOOL_PINCH:
+ return new_pinch_operation(mode, C);
+ case CURVES_SCULPT_TOOL_SMOOTH:
+ return new_smooth_operation();
+ case CURVES_SCULPT_TOOL_PUFF:
+ return new_puff_operation();
+ case CURVES_SCULPT_TOOL_DENSITY:
+ return new_density_operation(mode, C, stroke_start);
+ case CURVES_SCULPT_TOOL_SLIDE:
+ return new_slide_operation();
}
BLI_assert_unreachable();
return {};
@@ -150,7 +176,7 @@ static void stroke_update_step(bContext *C,
if (!op_data->operation) {
stroke_extension.is_first = true;
- op_data->operation = start_brush_operation(*C, *op);
+ op_data->operation = start_brush_operation(*C, *op, stroke_extension);
}
else {
stroke_extension.is_first = false;
@@ -316,6 +342,934 @@ static void CURVES_OT_sculptmode_toggle(wmOperatorType *ot)
/** \} */
+namespace select_random {
+
+static int select_random_exec(bContext *C, wmOperator *op)
+{
+ VectorSet<Curves *> unique_curves = curves::get_unique_editable_curves(*C);
+
+ const int seed = RNA_int_get(op->ptr, "seed");
+ RandomNumberGenerator rng{static_cast<uint32_t>(seed)};
+
+ const bool partial = RNA_boolean_get(op->ptr, "partial");
+ const bool constant_per_curve = RNA_boolean_get(op->ptr, "constant_per_curve");
+ const float probability = RNA_float_get(op->ptr, "probability");
+ const float min_value = RNA_float_get(op->ptr, "min");
+ const auto next_partial_random_value = [&]() {
+ return rng.get_float() * (1.0f - min_value) + min_value;
+ };
+ const auto next_bool_random_value = [&]() { return rng.get_float() <= probability; };
+
+ for (Curves *curves_id : unique_curves) {
+ CurvesGeometry &curves = CurvesGeometry::wrap(curves_id->geometry);
+ const bool was_anything_selected = curves::has_anything_selected(*curves_id);
+ switch (curves_id->selection_domain) {
+ case ATTR_DOMAIN_POINT: {
+ MutableSpan<float> selection = curves.selection_point_float_for_write();
+ if (!was_anything_selected) {
+ selection.fill(1.0f);
+ }
+ if (partial) {
+ if (constant_per_curve) {
+ for (const int curve_i : curves.curves_range()) {
+ const float random_value = next_partial_random_value();
+ const IndexRange points = curves.points_for_curve(curve_i);
+ for (const int point_i : points) {
+ selection[point_i] *= random_value;
+ }
+ }
+ }
+ else {
+ for (const int point_i : selection.index_range()) {
+ const float random_value = next_partial_random_value();
+ selection[point_i] *= random_value;
+ }
+ }
+ }
+ else {
+ if (constant_per_curve) {
+ for (const int curve_i : curves.curves_range()) {
+ const bool random_value = next_bool_random_value();
+ const IndexRange points = curves.points_for_curve(curve_i);
+ if (!random_value) {
+ selection.slice(points).fill(0.0f);
+ }
+ }
+ }
+ else {
+ for (const int point_i : selection.index_range()) {
+ const bool random_value = next_bool_random_value();
+ if (!random_value) {
+ selection[point_i] = 0.0f;
+ }
+ }
+ }
+ }
+ break;
+ }
+ case ATTR_DOMAIN_CURVE: {
+ MutableSpan<float> selection = curves.selection_curve_float_for_write();
+ if (!was_anything_selected) {
+ selection.fill(1.0f);
+ }
+ if (partial) {
+ for (const int curve_i : curves.curves_range()) {
+ const float random_value = next_partial_random_value();
+ selection[curve_i] *= random_value;
+ }
+ }
+ else {
+ for (const int curve_i : curves.curves_range()) {
+ const bool random_value = next_bool_random_value();
+ if (!random_value) {
+ selection[curve_i] = 0.0f;
+ }
+ }
+ }
+ break;
+ }
+ }
+ MutableSpan<float> selection = curves_id->selection_domain == ATTR_DOMAIN_POINT ?
+ curves.selection_point_float_for_write() :
+ curves.selection_curve_float_for_write();
+ const bool was_any_selected = std::any_of(
+ selection.begin(), selection.end(), [](const float v) { return v > 0.0f; });
+ if (was_any_selected) {
+ for (float &v : selection) {
+ v *= rng.get_float();
+ }
+ }
+ else {
+ for (float &v : selection) {
+ v = rng.get_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);
+ }
+ return OPERATOR_FINISHED;
+}
+
+static void select_random_ui(bContext *UNUSED(C), wmOperator *op)
+{
+ uiLayout *layout = op->layout;
+
+ uiItemR(layout, op->ptr, "seed", 0, nullptr, ICON_NONE);
+ uiItemR(layout, op->ptr, "constant_per_curve", 0, nullptr, ICON_NONE);
+ uiItemR(layout, op->ptr, "partial", 0, nullptr, ICON_NONE);
+
+ if (RNA_boolean_get(op->ptr, "partial")) {
+ uiItemR(layout, op->ptr, "min", UI_ITEM_R_SLIDER, "Min", ICON_NONE);
+ }
+ else {
+ uiItemR(layout, op->ptr, "probability", UI_ITEM_R_SLIDER, "Probability", ICON_NONE);
+ }
+}
+
+} // namespace select_random
+
+static void SCULPT_CURVES_OT_select_random(wmOperatorType *ot)
+{
+ ot->name = "Select Random";
+ ot->idname = __func__;
+ ot->description = "Randomizes existing selection or create new random selection";
+
+ ot->exec = select_random::select_random_exec;
+ ot->poll = curves::selection_operator_poll;
+ ot->ui = select_random::select_random_ui;
+
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ RNA_def_int(ot->srna,
+ "seed",
+ 0,
+ INT32_MIN,
+ INT32_MAX,
+ "Seed",
+ "Source of randomness",
+ INT32_MIN,
+ INT32_MAX);
+ RNA_def_boolean(
+ ot->srna, "partial", false, "Partial", "Allow points or curves to be selected partially");
+ RNA_def_float(ot->srna,
+ "probability",
+ 0.5f,
+ 0.0f,
+ 1.0f,
+ "Probability",
+ "Chance of every point or curve being included in the selection",
+ 0.0f,
+ 1.0f);
+ RNA_def_float(ot->srna,
+ "min",
+ 0.0f,
+ 0.0f,
+ 1.0f,
+ "Min",
+ "Minimum value for the random selection",
+ 0.0f,
+ 1.0f);
+ RNA_def_boolean(ot->srna,
+ "constant_per_curve",
+ true,
+ "Constant per Curve",
+ "The generated random number is the same for every control point of a curve");
+}
+
+namespace select_end {
+static bool select_end_poll(bContext *C)
+{
+ if (!curves::selection_operator_poll(C)) {
+ return false;
+ }
+ const Curves *curves_id = static_cast<const Curves *>(CTX_data_active_object(C)->data);
+ if (curves_id->selection_domain != ATTR_DOMAIN_POINT) {
+ CTX_wm_operator_poll_msg_set(C, "Only available in point selection mode");
+ return false;
+ }
+ return true;
+}
+
+static int select_end_exec(bContext *C, wmOperator *op)
+{
+ VectorSet<Curves *> unique_curves = curves::get_unique_editable_curves(*C);
+ const bool end_points = RNA_boolean_get(op->ptr, "end_points");
+ const int amount = RNA_int_get(op->ptr, "amount");
+
+ for (Curves *curves_id : unique_curves) {
+ CurvesGeometry &curves = CurvesGeometry::wrap(curves_id->geometry);
+ const bool was_anything_selected = curves::has_anything_selected(*curves_id);
+ MutableSpan<float> selection = curves.selection_point_float_for_write();
+ if (!was_anything_selected) {
+ selection.fill(1.0f);
+ }
+ threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange range) {
+ for (const int curve_i : range) {
+ const IndexRange points = curves.points_for_curve(curve_i);
+ if (end_points) {
+ selection.slice(points.drop_back(amount)).fill(0.0f);
+ }
+ else {
+ selection.slice(points.drop_front(amount)).fill(0.0f);
+ }
+ }
+ });
+
+ /* 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_end
+
+static void SCULPT_CURVES_OT_select_end(wmOperatorType *ot)
+{
+ ot->name = "Select End";
+ ot->idname = __func__;
+ ot->description = "Select end points of curves";
+
+ ot->exec = select_end::select_end_exec;
+ ot->poll = select_end::select_end_poll;
+
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ RNA_def_boolean(ot->srna,
+ "end_points",
+ true,
+ "End Points",
+ "Select points at the end of the curve as opposed to the beginning");
+ RNA_def_int(
+ ot->srna, "amount", 1, 0, INT32_MAX, "Amount", "Number of points to select", 0, INT32_MAX);
+}
+
+namespace select_grow {
+
+struct GrowOperatorDataPerCurve : NonCopyable, NonMovable {
+ Curves *curves_id;
+ Vector<int> selected_point_indices;
+ Vector<int> unselected_point_indices;
+ Array<float> distances_to_selected;
+ Array<float> distances_to_unselected;
+
+ Array<float> original_selection;
+ float pixel_to_distance_factor;
+};
+
+struct GrowOperatorData {
+ int initial_mouse_x;
+ Vector<std::unique_ptr<GrowOperatorDataPerCurve>> per_curve;
+};
+
+static void update_points_selection(const GrowOperatorDataPerCurve &data,
+ const float distance,
+ MutableSpan<float> points_selection)
+{
+ if (distance > 0.0f) {
+ threading::parallel_for(
+ data.unselected_point_indices.index_range(), 256, [&](const IndexRange range) {
+ for (const int i : range) {
+ const int point_i = data.unselected_point_indices[i];
+ const float distance_to_selected = data.distances_to_selected[i];
+ const float selection = distance_to_selected <= distance ? 1.0f : 0.0f;
+ points_selection[point_i] = selection;
+ }
+ });
+ threading::parallel_for(
+ data.selected_point_indices.index_range(), 512, [&](const IndexRange range) {
+ for (const int point_i : data.selected_point_indices.as_span().slice(range)) {
+ points_selection[point_i] = 1.0f;
+ }
+ });
+ }
+ else {
+ threading::parallel_for(
+ data.selected_point_indices.index_range(), 256, [&](const IndexRange range) {
+ for (const int i : range) {
+ const int point_i = data.selected_point_indices[i];
+ const float distance_to_unselected = data.distances_to_unselected[i];
+ const float selection = distance_to_unselected <= -distance ? 0.0f : 1.0f;
+ points_selection[point_i] = selection;
+ }
+ });
+ threading::parallel_for(
+ data.unselected_point_indices.index_range(), 512, [&](const IndexRange range) {
+ for (const int point_i : data.unselected_point_indices.as_span().slice(range)) {
+ points_selection[point_i] = 0.0f;
+ }
+ });
+ }
+}
+
+static int select_grow_update(bContext *C, wmOperator *op, const float mouse_diff_x)
+{
+ GrowOperatorData &op_data = *static_cast<GrowOperatorData *>(op->customdata);
+
+ for (std::unique_ptr<GrowOperatorDataPerCurve> &curve_op_data : op_data.per_curve) {
+ Curves &curves_id = *curve_op_data->curves_id;
+ CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
+ const float distance = curve_op_data->pixel_to_distance_factor * mouse_diff_x;
+
+ /* Grow or shrink selection based on precomputed distances. */
+ switch (curves_id.selection_domain) {
+ case ATTR_DOMAIN_POINT: {
+ MutableSpan<float> points_selection = curves.selection_point_float_for_write();
+ update_points_selection(*curve_op_data, distance, points_selection);
+ break;
+ }
+ case ATTR_DOMAIN_CURVE: {
+ Array<float> new_points_selection(curves.points_num());
+ update_points_selection(*curve_op_data, distance, new_points_selection);
+ /* Propagate grown point selection to the curve selection. */
+ MutableSpan<float> curves_selection = curves.selection_curve_float_for_write();
+ for (const int curve_i : curves.curves_range()) {
+ const IndexRange points = curves.points_for_curve(curve_i);
+ const Span<float> points_selection = new_points_selection.as_span().slice(points);
+ const float max_selection = *std::max_element(points_selection.begin(),
+ points_selection.end());
+ curves_selection[curve_i] = max_selection;
+ }
+ break;
+ }
+ }
+
+ /* 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;
+}
+
+static void select_grow_invoke_per_curve(Curves &curves_id,
+ Object &curves_ob,
+ const ARegion &region,
+ const View3D &v3d,
+ const RegionView3D &rv3d,
+ GrowOperatorDataPerCurve &curve_op_data)
+{
+ curve_op_data.curves_id = &curves_id;
+ CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
+ const Span<float3> positions = curves.positions();
+
+ /* Find indices of selected and unselected points. */
+ switch (curves_id.selection_domain) {
+ case ATTR_DOMAIN_POINT: {
+ const VArray<float> points_selection = curves.selection_point_float();
+ curve_op_data.original_selection.reinitialize(points_selection.size());
+ points_selection.materialize(curve_op_data.original_selection);
+ for (const int point_i : points_selection.index_range()) {
+ const float point_selection = points_selection[point_i];
+ if (point_selection > 0.0f) {
+ curve_op_data.selected_point_indices.append(point_i);
+ }
+ else {
+ curve_op_data.unselected_point_indices.append(point_i);
+ }
+ }
+
+ break;
+ }
+ case ATTR_DOMAIN_CURVE: {
+ const VArray<float> curves_selection = curves.selection_curve_float();
+ curve_op_data.original_selection.reinitialize(curves_selection.size());
+ curves_selection.materialize(curve_op_data.original_selection);
+ for (const int curve_i : curves_selection.index_range()) {
+ const float curve_selection = curves_selection[curve_i];
+ const IndexRange points = curves.points_for_curve(curve_i);
+ if (curve_selection > 0.0f) {
+ for (const int point_i : points) {
+ curve_op_data.selected_point_indices.append(point_i);
+ }
+ }
+ else {
+ for (const int point_i : points) {
+ curve_op_data.unselected_point_indices.append(point_i);
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ threading::parallel_invoke(
+ [&]() {
+ /* Build kd-tree for the selected points. */
+ KDTree_3d *kdtree = BLI_kdtree_3d_new(curve_op_data.selected_point_indices.size());
+ BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(kdtree); });
+ for (const int point_i : curve_op_data.selected_point_indices) {
+ const float3 &position = positions[point_i];
+ BLI_kdtree_3d_insert(kdtree, point_i, position);
+ }
+ BLI_kdtree_3d_balance(kdtree);
+
+ /* For each unselected point, compute the distance to the closest selected point. */
+ curve_op_data.distances_to_selected.reinitialize(
+ curve_op_data.unselected_point_indices.size());
+ threading::parallel_for(curve_op_data.unselected_point_indices.index_range(),
+ 256,
+ [&](const IndexRange range) {
+ for (const int i : range) {
+ const int point_i = curve_op_data.unselected_point_indices[i];
+ const float3 &position = positions[point_i];
+ KDTreeNearest_3d nearest;
+ BLI_kdtree_3d_find_nearest(kdtree, position, &nearest);
+ curve_op_data.distances_to_selected[i] = nearest.dist;
+ }
+ });
+ },
+ [&]() {
+ /* Build kd-tree for the unselected points. */
+ KDTree_3d *kdtree = BLI_kdtree_3d_new(curve_op_data.unselected_point_indices.size());
+ BLI_SCOPED_DEFER([&]() { BLI_kdtree_3d_free(kdtree); });
+ for (const int point_i : curve_op_data.unselected_point_indices) {
+ const float3 &position = positions[point_i];
+ BLI_kdtree_3d_insert(kdtree, point_i, position);
+ }
+ BLI_kdtree_3d_balance(kdtree);
+
+ /* For each selected point, compute the distance to the closest unselected point. */
+ curve_op_data.distances_to_unselected.reinitialize(
+ curve_op_data.selected_point_indices.size());
+ threading::parallel_for(
+ curve_op_data.selected_point_indices.index_range(), 256, [&](const IndexRange range) {
+ for (const int i : range) {
+ const int point_i = curve_op_data.selected_point_indices[i];
+ const float3 &position = positions[point_i];
+ KDTreeNearest_3d nearest;
+ BLI_kdtree_3d_find_nearest(kdtree, position, &nearest);
+ curve_op_data.distances_to_unselected[i] = nearest.dist;
+ }
+ });
+ });
+
+ float4x4 curves_to_world_mat = curves_ob.obmat;
+ float4x4 world_to_curves_mat = curves_to_world_mat.inverted();
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(&rv3d, &curves_ob, projection.values);
+
+ /* Compute how mouse movements in screen space are converted into grow/shrink distances in
+ * object space. */
+ curve_op_data.pixel_to_distance_factor = threading::parallel_reduce(
+ curve_op_data.selected_point_indices.index_range(),
+ 256,
+ FLT_MAX,
+ [&](const IndexRange range, float pixel_to_distance_factor) {
+ for (const int i : range) {
+ const int point_i = curve_op_data.selected_point_indices[i];
+ const float3 &pos_cu = positions[point_i];
+
+ float2 pos_re;
+ ED_view3d_project_float_v2_m4(&region, pos_cu, pos_re, projection.values);
+ if (pos_re.x < 0 || pos_re.y < 0 || pos_re.x > region.winx || pos_re.y > region.winy) {
+ continue;
+ }
+ /* Compute how far this point moves in curve space when it moves one unit in screen
+ * space. */
+ const float2 pos_offset_re = pos_re + float2(1, 0);
+ float3 pos_offset_wo;
+ ED_view3d_win_to_3d(
+ &v3d, &region, curves_to_world_mat * pos_cu, pos_offset_re, pos_offset_wo);
+ const float3 pos_offset_cu = world_to_curves_mat * pos_offset_wo;
+ const float dist_cu = math::distance(pos_cu, pos_offset_cu);
+ const float dist_re = math::distance(pos_re, pos_offset_re);
+ const float factor = dist_cu / dist_re;
+ math::min_inplace(pixel_to_distance_factor, factor);
+ }
+ return pixel_to_distance_factor;
+ },
+ [](const float a, const float b) { return std::min(a, b); });
+}
+
+static int select_grow_invoke(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ Object *active_ob = CTX_data_active_object(C);
+ ARegion *region = CTX_wm_region(C);
+ View3D *v3d = CTX_wm_view3d(C);
+ RegionView3D *rv3d = CTX_wm_region_view3d(C);
+
+ GrowOperatorData *op_data = MEM_new<GrowOperatorData>(__func__);
+ op->customdata = op_data;
+
+ op_data->initial_mouse_x = event->xy[0];
+
+ Curves &curves_id = *static_cast<Curves *>(active_ob->data);
+ auto curve_op_data = std::make_unique<GrowOperatorDataPerCurve>();
+ select_grow_invoke_per_curve(curves_id, *active_ob, *region, *v3d, *rv3d, *curve_op_data);
+ op_data->per_curve.append(std::move(curve_op_data));
+
+ WM_event_add_modal_handler(C, op);
+ return OPERATOR_RUNNING_MODAL;
+}
+
+static int select_grow_modal(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ GrowOperatorData &op_data = *static_cast<GrowOperatorData *>(op->customdata);
+ const int mouse_x = event->xy[0];
+ const int mouse_diff_x = mouse_x - op_data.initial_mouse_x;
+ switch (event->type) {
+ case MOUSEMOVE: {
+ select_grow_update(C, op, mouse_diff_x);
+ break;
+ }
+ case LEFTMOUSE: {
+ MEM_delete(&op_data);
+ return OPERATOR_FINISHED;
+ }
+ case EVT_ESCKEY:
+ case RIGHTMOUSE: {
+ /* Undo operator by resetting the selection to the original value. */
+ for (std::unique_ptr<GrowOperatorDataPerCurve> &curve_op_data : op_data.per_curve) {
+ Curves &curves_id = *curve_op_data->curves_id;
+ CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
+ switch (curves_id.selection_domain) {
+ case ATTR_DOMAIN_POINT: {
+ MutableSpan<float> points_selection = curves.selection_point_float_for_write();
+ points_selection.copy_from(curve_op_data->original_selection);
+ break;
+ }
+ case ATTR_DOMAIN_CURVE: {
+ MutableSpan<float> curves_seletion = curves.selection_curve_float_for_write();
+ curves_seletion.copy_from(curve_op_data->original_selection);
+ break;
+ }
+ }
+
+ /* 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);
+ }
+ MEM_delete(&op_data);
+ return OPERATOR_CANCELLED;
+ }
+ }
+ return OPERATOR_RUNNING_MODAL;
+}
+
+} // namespace select_grow
+
+static void SCULPT_CURVES_OT_select_grow(wmOperatorType *ot)
+{
+ ot->name = "Select Grow";
+ ot->idname = __func__;
+ ot->description = "Select curves which are close to curves that are selected already";
+
+ ot->invoke = select_grow::select_grow_invoke;
+ ot->modal = select_grow::select_grow_modal;
+ ot->poll = curves::selection_operator_poll;
+
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ PropertyRNA *prop;
+ prop = RNA_def_float(ot->srna,
+ "distance",
+ 0.1f,
+ -FLT_MAX,
+ FLT_MAX,
+ "Distance",
+ "By how much to grow the selection",
+ -10.0f,
+ 10.f);
+ RNA_def_property_subtype(prop, PROP_DISTANCE);
+}
+
+namespace min_distance_edit {
+
+static bool min_distance_edit_poll(bContext *C)
+{
+ Object *ob = CTX_data_active_object(C);
+ if (ob == nullptr) {
+ return false;
+ }
+ if (ob->type != OB_CURVES) {
+ return false;
+ }
+ Curves *curves_id = static_cast<Curves *>(ob->data);
+ if (curves_id->surface == nullptr || curves_id->surface->type != OB_MESH) {
+ CTX_wm_operator_poll_msg_set(C, "Curves must have a mesh surface object set");
+ return false;
+ }
+ Scene *scene = CTX_data_scene(C);
+ const Brush *brush = BKE_paint_brush_for_read(&scene->toolsettings->curves_sculpt->paint);
+ if (brush == nullptr) {
+ return false;
+ }
+ if (brush->curves_sculpt_tool != CURVES_SCULPT_TOOL_DENSITY) {
+ return false;
+ }
+ return true;
+}
+
+struct MinDistanceEditData {
+ /** Brush whose minimum distance is modified. */
+ Brush *brush;
+ float4x4 curves_to_world_mat;
+
+ /** Where the preview is drawn. */
+ float3 pos_cu;
+ float3 normal_cu;
+
+ int2 initial_mouse;
+ float initial_minimum_distance;
+
+ /** The operator uses a new cursor, but the existing cursors should be restored afterwards. */
+ ListBase orig_paintcursors;
+ void *cursor;
+
+ /** Store the viewport region in case the operator was called from the header. */
+ ARegion *region;
+ RegionView3D *rv3d;
+};
+
+static int calculate_points_per_side(bContext *C, MinDistanceEditData &op_data)
+{
+ Scene *scene = CTX_data_scene(C);
+ ARegion *region = op_data.region;
+
+ const float min_distance = op_data.brush->curves_sculpt_settings->minimum_distance;
+ const float brush_radius = BKE_brush_size_get(scene, op_data.brush);
+
+ float3 tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 0, 1});
+ if (math::is_zero(tangent_x_cu)) {
+ tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 1, 0});
+ }
+ tangent_x_cu = math::normalize(tangent_x_cu);
+ const float3 tangent_y_cu = math::normalize(math::cross(op_data.normal_cu, tangent_x_cu));
+
+ /* Sample a few points to get a good estimate of how large the grid has to be. */
+ Vector<float3> points_wo;
+ points_wo.append(op_data.pos_cu + min_distance * tangent_x_cu);
+ points_wo.append(op_data.pos_cu + min_distance * tangent_y_cu);
+ points_wo.append(op_data.pos_cu - min_distance * tangent_x_cu);
+ points_wo.append(op_data.pos_cu - min_distance * tangent_y_cu);
+
+ Vector<float2> points_re;
+ for (const float3 &pos_wo : points_wo) {
+ float2 pos_re;
+ ED_view3d_project_v2(region, pos_wo, pos_re);
+ points_re.append(pos_re);
+ }
+
+ float2 origin_re;
+ ED_view3d_project_v2(region, op_data.pos_cu, origin_re);
+
+ int needed_points = 0;
+ for (const float2 &pos_re : points_re) {
+ const float distance = math::length(pos_re - origin_re);
+ const int needed_points_iter = (brush_radius * 2.0f) / distance;
+
+ if (needed_points_iter > needed_points) {
+ needed_points = needed_points_iter;
+ }
+ }
+
+ /* Limit to a harcoded number since it only adds noise at some point. */
+ return std::min(300, needed_points);
+}
+
+static void min_distance_edit_draw(bContext *C, int UNUSED(x), int UNUSED(y), void *customdata)
+{
+ Scene *scene = CTX_data_scene(C);
+ MinDistanceEditData &op_data = *static_cast<MinDistanceEditData *>(customdata);
+
+ const float min_distance = op_data.brush->curves_sculpt_settings->minimum_distance;
+
+ float3 tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 0, 1});
+ if (math::is_zero(tangent_x_cu)) {
+ tangent_x_cu = math::cross(op_data.normal_cu, float3{0, 1, 0});
+ }
+ tangent_x_cu = math::normalize(tangent_x_cu);
+ const float3 tangent_y_cu = math::normalize(math::cross(op_data.normal_cu, tangent_x_cu));
+
+ const int points_per_side = calculate_points_per_side(C, op_data);
+ const int points_per_axis_num = 2 * points_per_side + 1;
+
+ Vector<float3> points_wo;
+ for (const int x_i : IndexRange(points_per_axis_num)) {
+ for (const int y_i : IndexRange(points_per_axis_num)) {
+ const float x_iter = min_distance * (x_i - (points_per_axis_num - 1) / 2.0f);
+ const float y_iter = min_distance * (y_i - (points_per_axis_num - 1) / 2.0f);
+
+ const float3 point_pos_cu = op_data.pos_cu + op_data.normal_cu * 0.0001f +
+ x_iter * tangent_x_cu + y_iter * tangent_y_cu;
+ const float3 point_pos_wo = op_data.curves_to_world_mat * point_pos_cu;
+ points_wo.append(point_pos_wo);
+ }
+ }
+
+ float4 circle_col = float4(op_data.brush->add_col);
+ float circle_alpha = op_data.brush->cursor_overlay_alpha;
+ float brush_radius_re = BKE_brush_size_get(scene, op_data.brush);
+
+ /* Draw the grid. */
+ GPU_matrix_push();
+ GPU_matrix_push_projection();
+ GPU_blend(GPU_BLEND_ALPHA);
+
+ ARegion *region = op_data.region;
+ RegionView3D *rv3d = op_data.rv3d;
+ wmWindow *win = CTX_wm_window(C);
+
+ /* It does the same as: `view3d_operator_needs_opengl(C);`. */
+ wmViewport(&region->winrct);
+ GPU_matrix_projection_set(rv3d->winmat);
+ GPU_matrix_set(rv3d->viewmat);
+
+ GPUVertFormat *format3d = immVertexFormat();
+
+ const uint pos3d = GPU_vertformat_attr_add(format3d, "pos", GPU_COMP_F32, 3, GPU_FETCH_FLOAT);
+ const uint col3d = GPU_vertformat_attr_add(format3d, "color", GPU_COMP_F32, 4, GPU_FETCH_FLOAT);
+
+ immBindBuiltinProgram(GPU_SHADER_3D_POINT_FIXED_SIZE_VARYING_COLOR);
+
+ GPU_point_size(3.0f);
+ immBegin(GPU_PRIM_POINTS, points_wo.size());
+
+ float3 brush_origin_wo = op_data.curves_to_world_mat * op_data.pos_cu;
+ float2 brush_origin_re;
+ ED_view3d_project_v2(region, brush_origin_wo, brush_origin_re);
+
+ /* Smooth alpha transition until the brush edge. */
+ const int alpha_border_re = 20;
+ const float dist_to_inner_border_re = brush_radius_re - alpha_border_re;
+
+ for (const float3 &pos_wo : points_wo) {
+ float2 pos_re;
+ ED_view3d_project_v2(region, pos_wo, pos_re);
+
+ const float dist_to_point_re = math::distance(pos_re, brush_origin_re);
+ const float alpha = 1.0f - ((dist_to_point_re - dist_to_inner_border_re) / alpha_border_re);
+
+ immAttr4f(col3d, 0.9f, 0.9f, 0.9f, alpha);
+ immVertex3fv(pos3d, pos_wo);
+ }
+ immEnd();
+ immUnbindProgram();
+
+ /* Reset the drawing settings. */
+ GPU_point_size(1.0f);
+ GPU_matrix_pop_projection();
+ GPU_matrix_pop();
+
+ int4 scissor;
+ GPU_scissor_get(scissor);
+ wmWindowViewport(win);
+ GPU_scissor(scissor[0], scissor[1], scissor[2], scissor[3]);
+
+ /* Draw the brush circle. */
+ GPU_matrix_translate_2f((float)op_data.initial_mouse.x, (float)op_data.initial_mouse.y);
+
+ GPUVertFormat *format = immVertexFormat();
+ uint pos2d = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT);
+
+ immBindBuiltinProgram(GPU_SHADER_2D_UNIFORM_COLOR);
+
+ immUniformColor3fvAlpha(circle_col, circle_alpha);
+ imm_draw_circle_wire_2d(pos2d, 0.0f, 0.0f, brush_radius_re, 80);
+
+ immUnbindProgram();
+ GPU_blend(GPU_BLEND_NONE);
+}
+
+static int min_distance_edit_invoke(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
+ ARegion *region = CTX_wm_region(C);
+ View3D *v3d = CTX_wm_view3d(C);
+ Scene *scene = CTX_data_scene(C);
+
+ Object &curves_ob = *CTX_data_active_object(C);
+ Curves &curves_id = *static_cast<Curves *>(curves_ob.data);
+ Object &surface_ob = *curves_id.surface;
+ Mesh &surface_me = *static_cast<Mesh *>(surface_ob.data);
+
+ BVHTreeFromMesh surface_bvh;
+ BKE_bvhtree_from_mesh_get(&surface_bvh, &surface_me, BVHTREE_FROM_LOOPTRI, 2);
+ BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh); });
+
+ const int2 mouse_pos_int_re{event->mval};
+ const float2 mouse_pos_re{mouse_pos_int_re};
+
+ float3 ray_start_wo, ray_end_wo;
+ ED_view3d_win_to_segment_clipped(
+ depsgraph, region, v3d, mouse_pos_re, ray_start_wo, ray_end_wo, true);
+
+ const float4x4 surface_to_world_mat = surface_ob.obmat;
+ const float4x4 world_to_surface_mat = surface_to_world_mat.inverted();
+
+ const float3 ray_start_su = world_to_surface_mat * ray_start_wo;
+ const float3 ray_end_su = world_to_surface_mat * ray_end_wo;
+ const float3 ray_direction_su = math::normalize(ray_end_su - ray_start_su);
+
+ BVHTreeRayHit ray_hit;
+ ray_hit.dist = FLT_MAX;
+ ray_hit.index = -1;
+ BLI_bvhtree_ray_cast(surface_bvh.tree,
+ ray_start_su,
+ ray_direction_su,
+ 0.0f,
+ &ray_hit,
+ surface_bvh.raycast_callback,
+ &surface_bvh);
+ if (ray_hit.index == -1) {
+ WM_report(RPT_ERROR, "Cursor must be over the surface mesh");
+ return OPERATOR_CANCELLED;
+ }
+
+ const float3 hit_pos_su = ray_hit.co;
+ const float3 hit_normal_su = ray_hit.no;
+ const float4x4 curves_to_world_mat = curves_ob.obmat;
+ const float4x4 world_to_curves_mat = curves_to_world_mat.inverted();
+ const float4x4 surface_to_curves_mat = world_to_curves_mat * surface_to_world_mat;
+ const float4x4 surface_to_curves_normal_mat = surface_to_curves_mat.inverted().transposed();
+
+ const float3 hit_pos_cu = surface_to_curves_mat * hit_pos_su;
+ const float3 hit_normal_cu = math::normalize(surface_to_curves_normal_mat * hit_normal_su);
+
+ MinDistanceEditData *op_data = MEM_new<MinDistanceEditData>(__func__);
+ op_data->curves_to_world_mat = curves_to_world_mat;
+ op_data->normal_cu = hit_normal_cu;
+ op_data->pos_cu = hit_pos_cu;
+ op_data->initial_mouse = event->xy;
+ op_data->brush = BKE_paint_brush(&scene->toolsettings->curves_sculpt->paint);
+ op_data->initial_minimum_distance = op_data->brush->curves_sculpt_settings->minimum_distance;
+
+ if (op_data->initial_minimum_distance <= 0.0f) {
+ op_data->initial_minimum_distance = 0.01f;
+ }
+
+ op->customdata = op_data;
+
+ /* Temporarily disable other paint cursors. */
+ wmWindowManager *wm = CTX_wm_manager(C);
+ op_data->orig_paintcursors = wm->paintcursors;
+ BLI_listbase_clear(&wm->paintcursors);
+
+ /* Add minimum distance paint cursor. */
+ op_data->cursor = WM_paint_cursor_activate(
+ SPACE_TYPE_ANY, RGN_TYPE_ANY, op->type->poll, min_distance_edit_draw, op_data);
+
+ op_data->region = CTX_wm_region(C);
+ op_data->rv3d = CTX_wm_region_view3d(C);
+
+ WM_event_add_modal_handler(C, op);
+ ED_region_tag_redraw(region);
+ return OPERATOR_RUNNING_MODAL;
+}
+
+static int min_distance_edit_modal(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ ARegion *region = CTX_wm_region(C);
+ MinDistanceEditData &op_data = *static_cast<MinDistanceEditData *>(op->customdata);
+
+ auto finish = [&]() {
+ wmWindowManager *wm = CTX_wm_manager(C);
+
+ /* Remove own cursor. */
+ WM_paint_cursor_end(static_cast<wmPaintCursor *>(op_data.cursor));
+ /* Restore original paint cursors. */
+ wm->paintcursors = op_data.orig_paintcursors;
+
+ ED_region_tag_redraw(region);
+ MEM_freeN(&op_data);
+ };
+
+ switch (event->type) {
+ case MOUSEMOVE: {
+ const int2 mouse_pos_int_re{event->xy};
+ const float2 mouse_pos_re{mouse_pos_int_re};
+
+ const float mouse_diff_x = mouse_pos_int_re.x - op_data.initial_mouse.x;
+ const float factor = powf(2, mouse_diff_x / UI_UNIT_X / 10.0f);
+ op_data.brush->curves_sculpt_settings->minimum_distance = op_data.initial_minimum_distance *
+ factor;
+
+ ED_region_tag_redraw(region);
+ WM_main_add_notifier(NC_SCENE | ND_TOOLSETTINGS, nullptr);
+ break;
+ }
+ case LEFTMOUSE: {
+ if (event->val == KM_PRESS) {
+ finish();
+ return OPERATOR_FINISHED;
+ }
+ break;
+ }
+ case RIGHTMOUSE:
+ case EVT_ESCKEY: {
+ op_data.brush->curves_sculpt_settings->minimum_distance = op_data.initial_minimum_distance;
+ finish();
+ WM_main_add_notifier(NC_SCENE | ND_TOOLSETTINGS, nullptr);
+ return OPERATOR_CANCELLED;
+ }
+ }
+
+ return OPERATOR_RUNNING_MODAL;
+}
+
+} // namespace min_distance_edit
+
+static void SCULPT_CURVES_OT_min_distance_edit(wmOperatorType *ot)
+{
+ ot->name = "Edit Minimum Distance";
+ ot->idname = __func__;
+ ot->description = "Change the minimum distance used by the density brush";
+
+ ot->poll = min_distance_edit::min_distance_edit_poll;
+ ot->invoke = min_distance_edit::min_distance_edit_invoke;
+ ot->modal = min_distance_edit::min_distance_edit_modal;
+
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_DEPENDS_ON_CURSOR;
+}
+
} // namespace blender::ed::sculpt_paint
/* -------------------------------------------------------------------- */
@@ -327,6 +1281,10 @@ void ED_operatortypes_sculpt_curves()
using namespace blender::ed::sculpt_paint;
WM_operatortype_append(SCULPT_CURVES_OT_brush_stroke);
WM_operatortype_append(CURVES_OT_sculptmode_toggle);
+ WM_operatortype_append(SCULPT_CURVES_OT_select_random);
+ WM_operatortype_append(SCULPT_CURVES_OT_select_end);
+ WM_operatortype_append(SCULPT_CURVES_OT_select_grow);
+ WM_operatortype_append(SCULPT_CURVES_OT_min_distance_edit);
}
/** \} */
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc b/source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc
new file mode 100644
index 00000000000..db890d6a054
--- /dev/null
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_pinch.cc
@@ -0,0 +1,307 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <algorithm>
+
+#include "curves_sculpt_intern.hh"
+
+#include "BLI_float4x4.hh"
+#include "BLI_vector.hh"
+
+#include "PIL_time.h"
+
+#include "DEG_depsgraph.h"
+
+#include "BKE_brush.h"
+#include "BKE_context.h"
+#include "BKE_curves.hh"
+#include "BKE_paint.h"
+
+#include "DNA_brush_enums.h"
+#include "DNA_brush_types.h"
+#include "DNA_curves_types.h"
+#include "DNA_object_types.h"
+#include "DNA_screen_types.h"
+#include "DNA_space_types.h"
+
+#include "ED_screen.h"
+#include "ED_view3d.h"
+
+#include "WM_api.h"
+
+/**
+ * The code below uses a prefix naming convention to indicate the coordinate space:
+ * cu: Local space of the curves object that is being edited.
+ * su: Local space of the surface object.
+ * wo: World space.
+ * re: 2D coordinates within the region.
+ */
+
+namespace blender::ed::sculpt_paint {
+
+class PinchOperation : public CurvesSculptStrokeOperation {
+ private:
+ bool invert_pinch_;
+ Array<float> segment_lengths_cu_;
+
+ /** Only used when a 3D brush is used. */
+ CurvesBrush3D brush_3d_;
+
+ friend struct PinchOperationExecutor;
+
+ public:
+ PinchOperation(const bool invert_pinch) : invert_pinch_(invert_pinch)
+ {
+ }
+
+ void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
+};
+
+struct PinchOperationExecutor {
+ PinchOperation *self_ = nullptr;
+ CurvesSculptCommonContext ctx_;
+
+ Object *object_ = nullptr;
+ Curves *curves_id_ = nullptr;
+ CurvesGeometry *curves_ = nullptr;
+
+ VArray<float> point_factors_;
+ Vector<int64_t> selected_curve_indices_;
+ IndexMask curve_selection_;
+
+ CurvesSculptTransforms transforms_;
+
+ const CurvesSculpt *curves_sculpt_ = nullptr;
+ const Brush *brush_ = nullptr;
+ float brush_radius_base_re_;
+ float brush_radius_factor_;
+ float brush_strength_;
+
+ float invert_factor_;
+
+ float2 brush_pos_re_;
+
+ PinchOperationExecutor(const bContext &C) : ctx_(C)
+ {
+ }
+
+ void execute(PinchOperation &self, const bContext &C, const StrokeExtension &stroke_extension)
+ {
+ self_ = &self;
+
+ object_ = CTX_data_active_object(&C);
+ curves_id_ = static_cast<Curves *>(object_->data);
+ curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
+ if (curves_->curves_num() == 0) {
+ return;
+ }
+
+ curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
+ brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
+ brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_);
+ brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension);
+ brush_strength_ = BKE_brush_alpha_get(ctx_.scene, brush_);
+
+ invert_factor_ = self_->invert_pinch_ ? -1.0f : 1.0f;
+
+ transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface);
+
+ point_factors_ = get_point_selection(*curves_id_);
+ curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
+
+ brush_pos_re_ = stroke_extension.mouse_position;
+ const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
+ brush_->falloff_shape);
+
+ if (stroke_extension.is_first) {
+ this->initialize_segment_lengths();
+
+ if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
+ self_->brush_3d_ = *sample_curves_3d_brush(*ctx_.depsgraph,
+ *ctx_.region,
+ *ctx_.v3d,
+ *ctx_.rv3d,
+ *object_,
+ brush_pos_re_,
+ brush_radius_base_re_);
+ }
+ }
+
+ Array<bool> changed_curves(curves_->curves_num(), false);
+ if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
+ this->pinch_projected_with_symmetry(changed_curves);
+ }
+ else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
+ this->pinch_spherical_with_symmetry(changed_curves);
+ }
+ else {
+ BLI_assert_unreachable();
+ }
+
+ this->restore_segment_lengths(changed_curves);
+ curves_->tag_positions_changed();
+ 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(ctx_.region);
+ }
+
+ void pinch_projected_with_symmetry(MutableSpan<bool> r_changed_curves)
+ {
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->pinch_projected(brush_transform, r_changed_curves);
+ }
+ }
+
+ void pinch_projected(const float4x4 &brush_transform, MutableSpan<bool> r_changed_curves)
+ {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+ 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(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ for (const int curve_i : curve_selection_.slice(range)) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int point_i : points.drop_front(1)) {
+ const float3 old_pos_cu = brush_transform_inv * positions_cu[point_i];
+ float2 old_pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, old_pos_cu, old_pos_re, projection.values);
+
+ const float dist_to_brush_sq_re = math::distance_squared(old_pos_re, brush_pos_re_);
+ if (dist_to_brush_sq_re > brush_radius_sq_re) {
+ continue;
+ }
+
+ const float dist_to_brush_re = std::sqrt(dist_to_brush_sq_re);
+ const float t = safe_divide(dist_to_brush_re, brush_radius_base_re_);
+ const float radius_falloff = t * BKE_brush_curve_strength(brush_, t, 1.0f);
+ const float weight = invert_factor_ * 0.1f * brush_strength_ * radius_falloff *
+ point_factors_[point_i];
+
+ const float2 new_pos_re = math::interpolate(old_pos_re, brush_pos_re_, weight);
+
+ const float3 old_pos_wo = transforms_.curves_to_world * old_pos_cu;
+ float3 new_pos_wo;
+ ED_view3d_win_to_3d(ctx_.v3d, ctx_.region, old_pos_wo, new_pos_re, new_pos_wo);
+
+ const float3 new_pos_cu = transforms_.world_to_curves * new_pos_wo;
+ positions_cu[point_i] = brush_transform * new_pos_cu;
+ r_changed_curves[curve_i] = true;
+ }
+ }
+ });
+ }
+
+ void pinch_spherical_with_symmetry(MutableSpan<bool> r_changed_curves)
+ {
+ float3 brush_pos_wo;
+ ED_view3d_win_to_3d(ctx_.v3d,
+ ctx_.region,
+ transforms_.curves_to_world * self_->brush_3d_.position_cu,
+ brush_pos_re_,
+ brush_pos_wo);
+ const float3 brush_pos_cu = transforms_.world_to_curves * brush_pos_wo;
+ const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_;
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->pinch_spherical(brush_transform * brush_pos_cu, brush_radius_cu, r_changed_curves);
+ }
+ }
+
+ void pinch_spherical(const float3 &brush_pos_cu,
+ const float brush_radius_cu,
+ MutableSpan<bool> r_changed_curves)
+ {
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+ const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+
+ threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ for (const int curve_i : curve_selection_.slice(range)) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int point_i : points.drop_front(1)) {
+ const float3 old_pos_cu = positions_cu[point_i];
+
+ const float dist_to_brush_sq_cu = math::distance_squared(old_pos_cu, brush_pos_cu);
+ if (dist_to_brush_sq_cu > brush_radius_sq_cu) {
+ continue;
+ }
+
+ const float dist_to_brush_cu = std::sqrt(dist_to_brush_sq_cu);
+ const float t = safe_divide(dist_to_brush_cu, brush_radius_cu);
+ const float radius_falloff = t * BKE_brush_curve_strength(brush_, t, 1.0f);
+ const float weight = invert_factor_ * 0.1f * brush_strength_ * radius_falloff *
+ point_factors_[point_i];
+
+ const float3 new_pos_cu = math::interpolate(old_pos_cu, brush_pos_cu, weight);
+ positions_cu[point_i] = new_pos_cu;
+
+ r_changed_curves[curve_i] = true;
+ }
+ }
+ });
+ }
+
+ void initialize_segment_lengths()
+ {
+ const Span<float3> positions_cu = curves_->positions();
+ self_->segment_lengths_cu_.reinitialize(curves_->points_num());
+ threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ for (const int curve_i : curve_selection_.slice(range)) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int point_i : points.drop_back(1)) {
+ const float3 &p1_cu = positions_cu[point_i];
+ const float3 &p2_cu = positions_cu[point_i + 1];
+ const float length_cu = math::distance(p1_cu, p2_cu);
+ self_->segment_lengths_cu_[point_i] = length_cu;
+ }
+ }
+ });
+ }
+
+ void restore_segment_lengths(const Span<bool> changed_curves)
+ {
+ const Span<float> expected_lengths_cu = self_->segment_lengths_cu_;
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+
+ threading::parallel_for(changed_curves.index_range(), 256, [&](const IndexRange range) {
+ for (const int curve_i : range) {
+ if (!changed_curves[curve_i]) {
+ continue;
+ }
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int segment_i : IndexRange(points.size() - 1)) {
+ const float3 &p1_cu = positions_cu[points[segment_i]];
+ float3 &p2_cu = positions_cu[points[segment_i] + 1];
+ const float3 direction = math::normalize(p2_cu - p1_cu);
+ const float expected_length_cu = expected_lengths_cu[points[segment_i]];
+ p2_cu = p1_cu + direction * expected_length_cu;
+ }
+ }
+ });
+ }
+};
+
+void PinchOperation::on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension)
+{
+ PinchOperationExecutor executor{C};
+ executor.execute(*this, C, stroke_extension);
+}
+
+std::unique_ptr<CurvesSculptStrokeOperation> new_pinch_operation(const BrushStrokeMode brush_mode,
+ const bContext &C)
+{
+ const Scene &scene = *CTX_data_scene(&C);
+ const Brush &brush = *BKE_paint_brush_for_read(&scene.toolsettings->curves_sculpt->paint);
+
+ const bool invert_pinch = (brush_mode == BRUSH_STROKE_INVERT) !=
+ ((brush.flag & BRUSH_DIR_IN) != 0);
+ return std::make_unique<PinchOperation>(invert_pinch);
+}
+
+} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_puff.cc b/source/blender/editors/sculpt_paint/curves_sculpt_puff.cc
new file mode 100644
index 00000000000..dc747fd0bce
--- /dev/null
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_puff.cc
@@ -0,0 +1,393 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "BKE_attribute_math.hh"
+#include "BKE_brush.h"
+#include "BKE_bvhutils.h"
+#include "BKE_context.h"
+#include "BKE_mesh.h"
+#include "BKE_mesh_runtime.h"
+
+#include "ED_screen.h"
+#include "ED_view3d.h"
+
+#include "DEG_depsgraph.h"
+
+#include "DNA_brush_types.h"
+#include "DNA_mesh_types.h"
+#include "DNA_meshdata_types.h"
+
+#include "WM_api.h"
+
+#include "BLI_length_parameterize.hh"
+
+#include "curves_sculpt_intern.hh"
+
+namespace blender::ed::sculpt_paint {
+
+class PuffOperation : public CurvesSculptStrokeOperation {
+ private:
+ /** Only used when a 3D brush is used. */
+ CurvesBrush3D brush_3d_;
+
+ /** Length of each segment indexed by the index of the first point in the segment. */
+ Array<float> segment_lengths_cu_;
+
+ friend struct PuffOperationExecutor;
+
+ public:
+ void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
+};
+
+static float3 compute_surface_point_normal(const MLoopTri &looptri,
+ const float3 &bary_coord,
+ const Span<float3> corner_normals)
+{
+ const int l0 = looptri.tri[0];
+ const int l1 = looptri.tri[1];
+ const int l2 = looptri.tri[2];
+
+ const float3 &l0_normal = corner_normals[l0];
+ const float3 &l1_normal = corner_normals[l1];
+ const float3 &l2_normal = corner_normals[l2];
+
+ const float3 normal = math::normalize(
+ attribute_math::mix3(bary_coord, l0_normal, l1_normal, l2_normal));
+ return normal;
+}
+
+/**
+ * Utility class that actually executes the update when the stroke is updated. That's useful
+ * because it avoids passing a very large number of parameters between functions.
+ */
+struct PuffOperationExecutor {
+ PuffOperation *self_ = nullptr;
+ CurvesSculptCommonContext ctx_;
+
+ Object *object_ = nullptr;
+ Curves *curves_id_ = nullptr;
+ CurvesGeometry *curves_ = nullptr;
+
+ VArray<float> point_factors_;
+ Vector<int64_t> selected_curve_indices_;
+ IndexMask curve_selection_;
+
+ const CurvesSculpt *curves_sculpt_ = nullptr;
+ const Brush *brush_ = nullptr;
+ float brush_radius_base_re_;
+ float brush_radius_factor_;
+ float brush_strength_;
+ float2 brush_pos_re_;
+
+ eBrushFalloffShape falloff_shape_;
+
+ CurvesSculptTransforms transforms_;
+
+ Object *surface_ob_ = nullptr;
+ Mesh *surface_ = nullptr;
+ Span<MLoopTri> surface_looptris_;
+ Span<float3> corner_normals_su_;
+ BVHTreeFromMesh surface_bvh_;
+
+ PuffOperationExecutor(const bContext &C) : ctx_(C)
+ {
+ }
+
+ void execute(PuffOperation &self, const bContext &C, const StrokeExtension &stroke_extension)
+ {
+ UNUSED_VARS(C, stroke_extension);
+ self_ = &self;
+
+ object_ = CTX_data_active_object(&C);
+ curves_id_ = static_cast<Curves *>(object_->data);
+ curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
+ if (curves_->curves_num() == 0) {
+ return;
+ }
+ if (curves_id_->surface == nullptr || curves_id_->surface->type != OB_MESH) {
+ return;
+ }
+
+ curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
+ brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
+ brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_);
+ brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension);
+ brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension);
+ brush_pos_re_ = stroke_extension.mouse_position;
+
+ point_factors_ = get_point_selection(*curves_id_);
+ curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
+
+ falloff_shape_ = static_cast<eBrushFalloffShape>(brush_->falloff_shape);
+
+ surface_ob_ = curves_id_->surface;
+ surface_ = static_cast<Mesh *>(surface_ob_->data);
+
+ transforms_ = CurvesSculptTransforms(*object_, surface_ob_);
+
+ if (!CustomData_has_layer(&surface_->ldata, CD_NORMAL)) {
+ BKE_mesh_calc_normals_split(surface_);
+ }
+ corner_normals_su_ = {
+ reinterpret_cast<const float3 *>(CustomData_get_layer(&surface_->ldata, CD_NORMAL)),
+ surface_->totloop};
+
+ BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2);
+ BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); });
+
+ surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_),
+ BKE_mesh_runtime_looptri_len(surface_)};
+
+ if (stroke_extension.is_first) {
+ this->initialize_segment_lengths();
+ if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) {
+ self.brush_3d_ = *sample_curves_3d_brush(*ctx_.depsgraph,
+ *ctx_.region,
+ *ctx_.v3d,
+ *ctx_.rv3d,
+ *object_,
+ brush_pos_re_,
+ brush_radius_base_re_);
+ }
+ }
+
+ Array<float> curve_weights(curve_selection_.size(), 0.0f);
+
+ if (falloff_shape_ == PAINT_FALLOFF_SHAPE_TUBE) {
+ this->find_curve_weights_projected_with_symmetry(curve_weights);
+ }
+ else if (falloff_shape_ == PAINT_FALLOFF_SHAPE_SPHERE) {
+ this->find_curves_weights_spherical_with_symmetry(curve_weights);
+ }
+ else {
+ BLI_assert_unreachable();
+ }
+
+ this->puff(curve_weights);
+ this->restore_segment_lengths();
+
+ curves_->tag_positions_changed();
+ 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(ctx_.region);
+ }
+
+ void find_curve_weights_projected_with_symmetry(MutableSpan<float> r_curve_weights)
+ {
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->find_curve_weights_projected(brush_transform, r_curve_weights);
+ }
+ }
+
+ void find_curve_weights_projected(const float4x4 &brush_transform,
+ MutableSpan<float> r_curve_weights)
+ {
+ Span<float3> positions_cu = curves_->positions();
+
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.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(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ for (const int curve_selection_i : range) {
+ const int curve_i = curve_selection_[curve_selection_i];
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ const float3 first_pos_cu = brush_transform_inv * positions_cu[points[0]];
+ float2 prev_pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, first_pos_cu, prev_pos_re, projection.values);
+ for (const int point_i : points.drop_front(1)) {
+ const float3 pos_cu = brush_transform_inv * positions_cu[point_i];
+ float2 pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values);
+ BLI_SCOPED_DEFER([&]() { prev_pos_re = pos_re; });
+
+ const float dist_to_brush_sq_re = dist_squared_to_line_segment_v2(
+ brush_pos_re_, prev_pos_re, pos_re);
+ if (dist_to_brush_sq_re > brush_radius_sq_re) {
+ continue;
+ }
+
+ 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 = radius_falloff;
+ math::max_inplace(r_curve_weights[curve_selection_i], weight);
+ }
+ }
+ });
+ }
+
+ void find_curves_weights_spherical_with_symmetry(MutableSpan<float> r_curve_weights)
+ {
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ float3 brush_pos_wo;
+ ED_view3d_win_to_3d(ctx_.v3d,
+ ctx_.region,
+ transforms_.curves_to_world * self_->brush_3d_.position_cu,
+ brush_pos_re_,
+ brush_pos_wo);
+ const float3 brush_pos_cu = transforms_.world_to_curves * brush_pos_wo;
+ const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_;
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->find_curves_weights_spherical(
+ brush_transform * brush_pos_cu, brush_radius_cu, r_curve_weights);
+ }
+ }
+
+ void find_curves_weights_spherical(const float3 &brush_pos_cu,
+ const float brush_radius_cu,
+ MutableSpan<float> r_curve_weights)
+ {
+ const Span<float3> positions_cu = curves_->positions();
+ const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+
+ threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ for (const int curve_selection_i : range) {
+ const int curve_i = curve_selection_[curve_selection_i];
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int point_i : points.drop_front(1)) {
+ const float3 &prev_pos_cu = positions_cu[point_i - 1];
+ const float3 &pos_cu = positions_cu[point_i];
+ const float dist_to_brush_sq_cu = dist_squared_to_line_segment_v3(
+ brush_pos_cu, prev_pos_cu, pos_cu);
+ if (dist_to_brush_sq_cu > brush_radius_sq_cu) {
+ continue;
+ }
+
+ 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 = radius_falloff;
+ math::max_inplace(r_curve_weights[curve_selection_i], weight);
+ }
+ }
+ });
+ }
+
+ void puff(const Span<float> curve_weights)
+ {
+ BLI_assert(curve_weights.size() == curve_selection_.size());
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+
+ threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ Vector<float> accumulated_lengths_cu;
+ for (const int curve_selection_i : range) {
+ const int curve_i = curve_selection_[curve_selection_i];
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ const int first_point_i = points[0];
+ const float3 first_pos_cu = positions_cu[first_point_i];
+ const float3 first_pos_su = transforms_.curves_to_surface * first_pos_cu;
+
+ /* Find the nearest position on the surface. The curve will be aligned to the normal of
+ * that point. */
+ BVHTreeNearest nearest;
+ nearest.dist_sq = FLT_MAX;
+ BLI_bvhtree_find_nearest(surface_bvh_.tree,
+ first_pos_su,
+ &nearest,
+ surface_bvh_.nearest_callback,
+ &surface_bvh_);
+
+ const MLoopTri &looptri = surface_looptris_[nearest.index];
+ const float3 closest_pos_su = nearest.co;
+ const float3 &v0_su = surface_->mvert[surface_->mloop[looptri.tri[0]].v].co;
+ const float3 &v1_su = surface_->mvert[surface_->mloop[looptri.tri[1]].v].co;
+ const float3 &v2_su = surface_->mvert[surface_->mloop[looptri.tri[2]].v].co;
+ float3 bary_coords;
+ interp_weights_tri_v3(bary_coords, v0_su, v1_su, v2_su, closest_pos_su);
+ const float3 normal_su = compute_surface_point_normal(
+ looptri, bary_coords, corner_normals_su_);
+ const float3 normal_cu = math::normalize(transforms_.surface_to_curves_normal * normal_su);
+
+ accumulated_lengths_cu.reinitialize(points.size() - 1);
+ length_parameterize::accumulate_lengths<float3>(
+ positions_cu.slice(points), false, accumulated_lengths_cu);
+
+ /* Align curve to the surface normal while making sure that the curve does not fold up much
+ * in the process (e.g. when the curve was pointing in the opposite direction before). */
+ for (const int i : IndexRange(points.size()).drop_front(1)) {
+ const int point_i = points[i];
+ const float3 old_pos_cu = positions_cu[point_i];
+
+ /* Compute final position of the point. */
+ const float length_param_cu = accumulated_lengths_cu[i - 1];
+ const float3 goal_pos_cu = first_pos_cu + length_param_cu * normal_cu;
+
+ const float weight = 0.01f * brush_strength_ * point_factors_[point_i] *
+ curve_weights[curve_selection_i];
+ float3 new_pos_cu = math::interpolate(old_pos_cu, goal_pos_cu, weight);
+
+ /* Make sure the point does not move closer to the root point than it was initially. This
+ * makes the curve kind of "rotate up". */
+ const float old_dist_to_root_cu = math::distance(old_pos_cu, first_pos_cu);
+ const float new_dist_to_root_cu = math::distance(new_pos_cu, first_pos_cu);
+ if (new_dist_to_root_cu < old_dist_to_root_cu) {
+ const float3 offset = math::normalize(new_pos_cu - first_pos_cu);
+ new_pos_cu += (old_dist_to_root_cu - new_dist_to_root_cu) * offset;
+ }
+
+ positions_cu[point_i] = new_pos_cu;
+ }
+ }
+ });
+ }
+
+ void initialize_segment_lengths()
+ {
+ const Span<float3> positions_cu = curves_->positions();
+ self_->segment_lengths_cu_.reinitialize(curves_->points_num());
+ threading::parallel_for(curves_->curves_range(), 128, [&](const IndexRange range) {
+ for (const int curve_i : range) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int point_i : points.drop_back(1)) {
+ const float3 &p1_cu = positions_cu[point_i];
+ const float3 &p2_cu = positions_cu[point_i + 1];
+ const float length_cu = math::distance(p1_cu, p2_cu);
+ self_->segment_lengths_cu_[point_i] = length_cu;
+ }
+ }
+ });
+ }
+
+ void restore_segment_lengths()
+ {
+ const Span<float> expected_lengths_cu = self_->segment_lengths_cu_;
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+
+ threading::parallel_for(curves_->curves_range(), 256, [&](const IndexRange range) {
+ for (const int curve_i : range) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ for (const int segment_i : points.drop_back(1)) {
+ const float3 &p1_cu = positions_cu[segment_i];
+ float3 &p2_cu = positions_cu[segment_i + 1];
+ const float3 direction = math::normalize(p2_cu - p1_cu);
+ const float expected_length_cu = expected_lengths_cu[segment_i];
+ p2_cu = p1_cu + direction * expected_length_cu;
+ }
+ }
+ });
+ }
+};
+
+void PuffOperation::on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension)
+{
+ PuffOperationExecutor executor{C};
+ executor.execute(*this, C, stroke_extension);
+}
+
+std::unique_ptr<CurvesSculptStrokeOperation> new_puff_operation()
+{
+ return std::make_unique<PuffOperation>();
+}
+
+} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_slide.cc b/source/blender/editors/sculpt_paint/curves_sculpt_slide.cc
new file mode 100644
index 00000000000..3e1949cbf34
--- /dev/null
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_slide.cc
@@ -0,0 +1,310 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <algorithm>
+
+#include "curves_sculpt_intern.hh"
+
+#include "BLI_float4x4.hh"
+#include "BLI_vector.hh"
+
+#include "PIL_time.h"
+
+#include "DEG_depsgraph.h"
+
+#include "BKE_attribute_math.hh"
+#include "BKE_brush.h"
+#include "BKE_bvhutils.h"
+#include "BKE_context.h"
+#include "BKE_curves.hh"
+#include "BKE_geometry_set.hh"
+#include "BKE_mesh.h"
+#include "BKE_mesh_runtime.h"
+#include "BKE_mesh_sample.hh"
+#include "BKE_paint.h"
+
+#include "DNA_brush_enums.h"
+#include "DNA_brush_types.h"
+#include "DNA_curves_types.h"
+#include "DNA_mesh_types.h"
+#include "DNA_meshdata_types.h"
+#include "DNA_object_types.h"
+#include "DNA_screen_types.h"
+#include "DNA_space_types.h"
+
+#include "ED_screen.h"
+#include "ED_view3d.h"
+
+#include "UI_interface.h"
+
+#include "WM_api.h"
+
+namespace blender::ed::sculpt_paint {
+
+struct SlideCurveInfo {
+ /** Index of the curve to slide. */
+ int curve_i;
+ /** A weight based on the initial distance to the brush. */
+ float radius_falloff;
+};
+
+struct SlideInfo {
+ /** The transform used for the curves below (e.g. for symmetry). */
+ float4x4 brush_transform;
+ Vector<SlideCurveInfo> curves_to_slide;
+};
+
+class SlideOperation : public CurvesSculptStrokeOperation {
+ private:
+ /** Last mouse position. */
+ float2 brush_pos_last_re_;
+ /** Information about which curves to slide. This is initialized when the brush starts. */
+ Vector<SlideInfo> slide_info_;
+
+ friend struct SlideOperationExecutor;
+
+ public:
+ void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
+};
+
+/**
+ * Utility class that actually executes the update when the stroke is updated. That's useful
+ * because it avoids passing a very large number of parameters between functions.
+ */
+struct SlideOperationExecutor {
+ SlideOperation *self_ = nullptr;
+ CurvesSculptCommonContext ctx_;
+
+ const CurvesSculpt *curves_sculpt_ = nullptr;
+ const Brush *brush_ = nullptr;
+ float brush_radius_base_re_;
+ float brush_radius_factor_;
+ float brush_strength_;
+
+ Object *object_ = nullptr;
+ Curves *curves_id_ = nullptr;
+ CurvesGeometry *curves_ = nullptr;
+
+ Object *surface_ob_ = nullptr;
+ Mesh *surface_ = nullptr;
+ Span<MLoopTri> surface_looptris_;
+ VArray_Span<float2> surface_uv_map_;
+
+ VArray<float> curve_factors_;
+ VArray<float> point_factors_;
+ Vector<int64_t> selected_curve_indices_;
+ IndexMask curve_selection_;
+
+ float2 brush_pos_prev_re_;
+ float2 brush_pos_re_;
+ float2 brush_pos_diff_re_;
+
+ CurvesSculptTransforms transforms_;
+
+ BVHTreeFromMesh surface_bvh_;
+
+ SlideOperationExecutor(const bContext &C) : ctx_(C)
+ {
+ }
+
+ void execute(SlideOperation &self, const bContext &C, const StrokeExtension &stroke_extension)
+ {
+ UNUSED_VARS(C, stroke_extension);
+ self_ = &self;
+
+ object_ = CTX_data_active_object(&C);
+ curves_id_ = static_cast<Curves *>(object_->data);
+ curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
+ if (curves_id_->surface == nullptr || curves_id_->surface->type != OB_MESH) {
+ return;
+ }
+ if (curves_->curves_num() == 0) {
+ return;
+ }
+
+ curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
+ brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
+ brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_);
+ brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension);
+ brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension);
+
+ curve_factors_ = get_curves_selection(*curves_id_);
+ 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_;
+ BLI_SCOPED_DEFER([&]() { self_->brush_pos_last_re_ = brush_pos_re_; });
+
+ transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface);
+
+ surface_ob_ = curves_id_->surface;
+ surface_ = static_cast<Mesh *>(surface_ob_->data);
+
+ BKE_bvhtree_from_mesh_get(&surface_bvh_, surface_, BVHTREE_FROM_LOOPTRI, 2);
+ BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&surface_bvh_); });
+
+ surface_looptris_ = {BKE_mesh_runtime_looptri_ensure(surface_),
+ BKE_mesh_runtime_looptri_len(surface_)};
+
+ if (curves_id_->surface_uv_map != nullptr) {
+ MeshComponent surface_component;
+ surface_component.replace(surface_, GeometryOwnershipType::ReadOnly);
+ surface_uv_map_ = surface_component
+ .attribute_try_get_for_read(curves_id_->surface_uv_map,
+ ATTR_DOMAIN_CORNER)
+ .typed<float2>();
+ }
+
+ if (stroke_extension.is_first) {
+ const Vector<float4x4> brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : brush_transforms) {
+ this->detect_curves_to_slide(brush_transform);
+ }
+ return;
+ }
+ this->slide_projected();
+
+ curves_->tag_positions_changed();
+ 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(ctx_.region);
+ }
+
+ void detect_curves_to_slide(const float4x4 &brush_transform)
+ {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
+ const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
+ const float brush_radius_sq_re = pow2f(brush_radius_re);
+
+ const Span<float3> positions_cu = curves_->positions();
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ self_->slide_info_.append({brush_transform});
+ Vector<SlideCurveInfo> &curves_to_slide = self_->slide_info_.last().curves_to_slide;
+
+ /* Find curves in brush radius that should be moved. */
+ for (const int curve_i : curve_selection_) {
+ const int first_point_i = curves_->offsets()[curve_i];
+ const float3 &first_pos_cu = brush_transform_inv * positions_cu[first_point_i];
+
+ float2 first_pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, first_pos_cu, first_pos_re, projection.values);
+
+ const float dist_to_brush_sq_re = math::distance_squared(first_pos_re, brush_pos_re_);
+ if (dist_to_brush_sq_re > brush_radius_sq_re) {
+ continue;
+ }
+ 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);
+ curves_to_slide.append({curve_i, radius_falloff});
+ }
+ }
+
+ void slide_projected()
+ {
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+
+ MutableSpan<float2> surface_uv_coords;
+ if (!surface_uv_map_.is_empty()) {
+ surface_uv_coords = curves_->surface_uv_coords_for_write();
+ }
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ for (const SlideInfo &slide_info : self_->slide_info_) {
+ const float4x4 &brush_transform = slide_info.brush_transform;
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+ const Span<SlideCurveInfo> curves_to_slide = slide_info.curves_to_slide;
+
+ threading::parallel_for(curves_to_slide.index_range(), 256, [&](const IndexRange range) {
+ for (const SlideCurveInfo &curve_slide_info : curves_to_slide.slice(range)) {
+ const int curve_i = curve_slide_info.curve_i;
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ const int first_point_i = points.first();
+ const float3 old_first_pos_cu = brush_transform_inv * positions_cu[first_point_i];
+
+ float2 old_first_pos_re;
+ ED_view3d_project_float_v2_m4(
+ ctx_.region, old_first_pos_cu, old_first_pos_re, projection.values);
+ const float first_point_weight = brush_strength_ * curve_slide_info.radius_falloff;
+
+ /* Slide root position in region space and then project it back onto the surface. */
+ const float2 new_first_pos_re = old_first_pos_re +
+ first_point_weight * brush_pos_diff_re_;
+
+ float3 ray_start_wo, ray_end_wo;
+ ED_view3d_win_to_segment_clipped(ctx_.depsgraph,
+ ctx_.region,
+ ctx_.v3d,
+ new_first_pos_re,
+ ray_start_wo,
+ ray_end_wo,
+ true);
+ const float3 ray_start_su = transforms_.world_to_surface * ray_start_wo;
+ const float3 ray_end_su = transforms_.world_to_surface * ray_end_wo;
+
+ const float3 ray_direction_su = math::normalize(ray_end_su - ray_start_su);
+ BVHTreeRayHit hit;
+ hit.dist = FLT_MAX;
+ hit.index = -1;
+ BLI_bvhtree_ray_cast(surface_bvh_.tree,
+ ray_start_su,
+ ray_direction_su,
+ 0.0f,
+ &hit,
+ surface_bvh_.raycast_callback,
+ &surface_bvh_);
+ if (hit.index == -1) {
+ continue;
+ }
+
+ const int looptri_index = hit.index;
+ const float3 attached_pos_su = hit.co;
+
+ const float3 attached_pos_cu = transforms_.surface_to_curves * attached_pos_su;
+ const float3 pos_offset_cu = brush_transform * (attached_pos_cu - old_first_pos_cu);
+
+ /* Update positions. The first point doesn't have an additional weight here, because then
+ * it wouldn't be attached to the surface anymore. */
+ positions_cu[first_point_i] += pos_offset_cu;
+ for (const int point_i : points.drop_front(1)) {
+ const float weight = point_factors_[point_i];
+ positions_cu[point_i] += weight * pos_offset_cu;
+ }
+
+ /* Update surface attachment information if necessary. */
+ if (!surface_uv_map_.is_empty()) {
+ const MLoopTri &looptri = surface_looptris_[looptri_index];
+ const float3 bary_coord = bke::mesh_surface_sample::compute_bary_coord_in_triangle(
+ *surface_, looptri, attached_pos_su);
+ const float2 &uv0 = surface_uv_map_[looptri.tri[0]];
+ const float2 &uv1 = surface_uv_map_[looptri.tri[1]];
+ const float2 &uv2 = surface_uv_map_[looptri.tri[2]];
+ const float2 uv = attribute_math::mix3(bary_coord, uv0, uv1, uv2);
+ surface_uv_coords[curve_i] = uv;
+ }
+ }
+ });
+ }
+ }
+};
+
+void SlideOperation::on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension)
+{
+ SlideOperationExecutor executor{C};
+ executor.execute(*this, C, stroke_extension);
+}
+
+std::unique_ptr<CurvesSculptStrokeOperation> new_slide_operation()
+{
+ return std::make_unique<SlideOperation>();
+}
+
+} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc b/source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc
new file mode 100644
index 00000000000..e72b17d448b
--- /dev/null
+++ b/source/blender/editors/sculpt_paint/curves_sculpt_smooth.cc
@@ -0,0 +1,259 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "BKE_brush.h"
+#include "BKE_context.h"
+
+#include "ED_screen.h"
+#include "ED_view3d.h"
+
+#include "DEG_depsgraph.h"
+
+#include "DNA_brush_types.h"
+
+#include "WM_api.h"
+
+#include "BLI_enumerable_thread_specific.hh"
+
+#include "curves_sculpt_intern.hh"
+
+namespace blender::ed::sculpt_paint {
+
+class SmoothOperation : public CurvesSculptStrokeOperation {
+ private:
+ /** Only used when a 3D brush is used. */
+ CurvesBrush3D brush_3d_;
+
+ friend struct SmoothOperationExecutor;
+
+ public:
+ void on_stroke_extended(const bContext &C, const StrokeExtension &stroke_extension) override;
+};
+
+/**
+ * Utility class that actually executes the update when the stroke is updated. That's useful
+ * because it avoids passing a very large number of parameters between functions.
+ */
+struct SmoothOperationExecutor {
+ SmoothOperation *self_ = nullptr;
+ CurvesSculptCommonContext ctx_;
+
+ Object *object_ = nullptr;
+ Curves *curves_id_ = nullptr;
+ CurvesGeometry *curves_ = nullptr;
+
+ VArray<float> point_factors_;
+ Vector<int64_t> selected_curve_indices_;
+ IndexMask curve_selection_;
+
+ const CurvesSculpt *curves_sculpt_ = nullptr;
+ const Brush *brush_ = nullptr;
+ float brush_radius_base_re_;
+ float brush_radius_factor_;
+ float brush_strength_;
+ float2 brush_pos_re_;
+
+ CurvesSculptTransforms transforms_;
+
+ SmoothOperationExecutor(const bContext &C) : ctx_(C)
+ {
+ }
+
+ void execute(SmoothOperation &self, const bContext &C, const StrokeExtension &stroke_extension)
+ {
+ UNUSED_VARS(C, stroke_extension);
+ self_ = &self;
+
+ object_ = CTX_data_active_object(&C);
+ curves_id_ = static_cast<Curves *>(object_->data);
+ curves_ = &CurvesGeometry::wrap(curves_id_->geometry);
+ if (curves_->curves_num() == 0) {
+ return;
+ }
+
+ curves_sculpt_ = ctx_.scene->toolsettings->curves_sculpt;
+ brush_ = BKE_paint_brush_for_read(&curves_sculpt_->paint);
+ brush_radius_base_re_ = BKE_brush_size_get(ctx_.scene, brush_);
+ brush_radius_factor_ = brush_radius_factor(*brush_, stroke_extension);
+ brush_strength_ = brush_strength_get(*ctx_.scene, *brush_, stroke_extension);
+ brush_pos_re_ = stroke_extension.mouse_position;
+
+ point_factors_ = get_point_selection(*curves_id_);
+ curve_selection_ = retrieve_selected_curves(*curves_id_, selected_curve_indices_);
+ transforms_ = CurvesSculptTransforms(*object_, curves_id_->surface);
+
+ const eBrushFalloffShape falloff_shape = static_cast<eBrushFalloffShape>(
+ brush_->falloff_shape);
+ if (stroke_extension.is_first) {
+ if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
+ self.brush_3d_ = *sample_curves_3d_brush(*ctx_.depsgraph,
+ *ctx_.region,
+ *ctx_.v3d,
+ *ctx_.rv3d,
+ *object_,
+ brush_pos_re_,
+ brush_radius_base_re_);
+ }
+ }
+
+ if (falloff_shape == PAINT_FALLOFF_SHAPE_TUBE) {
+ this->smooth_projected_with_symmetry();
+ }
+ else if (falloff_shape == PAINT_FALLOFF_SHAPE_SPHERE) {
+ this->smooth_spherical_with_symmetry();
+ }
+ else {
+ BLI_assert_unreachable();
+ }
+
+ curves_->tag_positions_changed();
+ 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(ctx_.region);
+ }
+
+ void smooth_projected_with_symmetry()
+ {
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->smooth_projected(brush_transform);
+ }
+ }
+
+ void smooth_projected(const float4x4 &brush_transform)
+ {
+ const float4x4 brush_transform_inv = brush_transform.inverted();
+
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+ const float brush_radius_re = brush_radius_base_re_ * brush_radius_factor_;
+ const float brush_radius_sq_re = pow2f(brush_radius_re);
+
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ Vector<float2> old_curve_positions_re;
+ for (const int curve_i : curve_selection_.slice(range)) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+
+ /* Find position of control points in screen space. */
+ old_curve_positions_re.clear();
+ old_curve_positions_re.reserve(points.size());
+ for (const int point_i : points) {
+ const float3 &pos_cu = brush_transform_inv * positions_cu[point_i];
+ float2 pos_re;
+ ED_view3d_project_float_v2_m4(ctx_.region, pos_cu, pos_re, projection.values);
+ old_curve_positions_re.append_unchecked(pos_re);
+ }
+ for (const int i : IndexRange(points.size()).drop_front(1).drop_back(1)) {
+ const int point_i = points[i];
+ const float2 &old_pos_re = old_curve_positions_re[i];
+ const float dist_to_brush_sq_re = math::distance_squared(old_pos_re, brush_pos_re_);
+ if (dist_to_brush_sq_re > brush_radius_sq_re) {
+ continue;
+ }
+
+ 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);
+ /* Used to make the brush easier to use. Otherwise a strength of 1 would be way too
+ * large. */
+ const float weight_factor = 0.1f;
+ const float weight = weight_factor * brush_strength_ * radius_falloff *
+ point_factors_[point_i];
+
+ /* Move points towards the middle of their neighbors. */
+ const float2 &old_pos_prev_re = old_curve_positions_re[i - 1];
+ const float2 &old_pos_next_re = old_curve_positions_re[i + 1];
+ const float2 goal_pos_re = math::midpoint(old_pos_prev_re, old_pos_next_re);
+ const float2 new_pos_re = math::interpolate(old_pos_re, goal_pos_re, weight);
+ const float3 old_pos_cu = brush_transform_inv * positions_cu[point_i];
+ float3 new_pos_wo;
+ ED_view3d_win_to_3d(ctx_.v3d,
+ ctx_.region,
+ transforms_.curves_to_world * old_pos_cu,
+ new_pos_re,
+ new_pos_wo);
+ const float3 new_pos_cu = brush_transform * (transforms_.world_to_curves * new_pos_wo);
+ positions_cu[point_i] = new_pos_cu;
+ }
+ }
+ });
+ }
+
+ void smooth_spherical_with_symmetry()
+ {
+ float4x4 projection;
+ ED_view3d_ob_project_mat_get(ctx_.rv3d, object_, projection.values);
+
+ float3 brush_pos_wo;
+ ED_view3d_win_to_3d(ctx_.v3d,
+ ctx_.region,
+ transforms_.curves_to_world * self_->brush_3d_.position_cu,
+ brush_pos_re_,
+ brush_pos_wo);
+ const float3 brush_pos_cu = transforms_.world_to_curves * brush_pos_wo;
+ const float brush_radius_cu = self_->brush_3d_.radius_cu * brush_radius_factor_;
+
+ const Vector<float4x4> symmetry_brush_transforms = get_symmetry_brush_transforms(
+ eCurvesSymmetryType(curves_id_->symmetry));
+ for (const float4x4 &brush_transform : symmetry_brush_transforms) {
+ this->smooth_spherical(brush_transform * brush_pos_cu, brush_radius_cu);
+ }
+ }
+
+ void smooth_spherical(const float3 &brush_pos_cu, const float brush_radius_cu)
+ {
+ MutableSpan<float3> positions_cu = curves_->positions_for_write();
+ const float brush_radius_sq_cu = pow2f(brush_radius_cu);
+
+ threading::parallel_for(curve_selection_.index_range(), 256, [&](const IndexRange range) {
+ Vector<float3> old_curve_positions_cu;
+ for (const int curve_i : curve_selection_.slice(range)) {
+ const IndexRange points = curves_->points_for_curve(curve_i);
+ /* Remember original positions so that we don't smooth based on already smoothed points
+ * below. */
+ old_curve_positions_cu.clear();
+ old_curve_positions_cu.extend(positions_cu.slice(points));
+ for (const int i : IndexRange(points.size()).drop_front(1).drop_back(1)) {
+ const int point_i = points[i];
+ const float3 &old_pos_cu = old_curve_positions_cu[i];
+ const float dist_to_brush_sq_cu = math::distance_squared(old_pos_cu, brush_pos_cu);
+ if (dist_to_brush_sq_cu > brush_radius_sq_cu) {
+ continue;
+ }
+
+ 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);
+ /* Used to make the brush easier to use. Otherwise a strength of 1 would be way too
+ * large. */
+ const float weight_factor = 0.1f;
+ const float weight = weight_factor * brush_strength_ * radius_falloff *
+ point_factors_[point_i];
+
+ /* Move points towards the middle of their neighbors. */
+ const float3 &old_pos_prev_cu = old_curve_positions_cu[i - 1];
+ const float3 &old_pos_next_cu = old_curve_positions_cu[i + 1];
+ const float3 goal_pos_cu = math::midpoint(old_pos_prev_cu, old_pos_next_cu);
+ const float3 new_pos_cu = math::interpolate(old_pos_cu, goal_pos_cu, weight);
+ positions_cu[point_i] = new_pos_cu;
+ }
+ }
+ });
+ }
+};
+
+void SmoothOperation::on_stroke_extended(const bContext &C,
+ const StrokeExtension &stroke_extension)
+{
+ SmoothOperationExecutor executor{C};
+ executor.execute(*this, C, stroke_extension);
+}
+
+std::unique_ptr<CurvesSculptStrokeOperation> new_smooth_operation()
+{
+ return std::make_unique<SmoothOperation>();
+}
+
+} // namespace blender::ed::sculpt_paint
diff --git a/source/blender/editors/sculpt_paint/paint_stroke.c b/source/blender/editors/sculpt_paint/paint_stroke.c
index 63e6dc7e965..1ee26935dc9 100644
--- a/source/blender/editors/sculpt_paint/paint_stroke.c
+++ b/source/blender/editors/sculpt_paint/paint_stroke.c
@@ -995,7 +995,7 @@ static void stroke_done(bContext *C, wmOperator *op, PaintStroke *stroke)
static bool curves_sculpt_brush_uses_spacing(const eBrushCurvesSculptTool tool)
{
- return ELEM(tool, CURVES_SCULPT_TOOL_ADD);
+ return ELEM(tool, CURVES_SCULPT_TOOL_ADD, CURVES_SCULPT_TOOL_DENSITY);
}
bool paint_space_stroke_enabled(Brush *br, ePaintMode mode)
diff --git a/source/blender/makesdna/DNA_brush_enums.h b/source/blender/makesdna/DNA_brush_enums.h
index f2cd49b6dea..adda23c26f2 100644
--- a/source/blender/makesdna/DNA_brush_enums.h
+++ b/source/blender/makesdna/DNA_brush_enums.h
@@ -469,6 +469,11 @@ typedef enum eBrushCurvesSculptTool {
CURVES_SCULPT_TOOL_ADD = 3,
CURVES_SCULPT_TOOL_GROW_SHRINK = 4,
CURVES_SCULPT_TOOL_SELECTION_PAINT = 5,
+ CURVES_SCULPT_TOOL_PINCH = 6,
+ CURVES_SCULPT_TOOL_SMOOTH = 7,
+ CURVES_SCULPT_TOOL_PUFF = 8,
+ CURVES_SCULPT_TOOL_DENSITY = 9,
+ CURVES_SCULPT_TOOL_SLIDE = 10,
} eBrushCurvesSculptTool;
/** When #BRUSH_ACCUMULATE is used */
@@ -622,6 +627,12 @@ typedef enum eBrushCurvesSculptFlag {
BRUSH_CURVES_SCULPT_FLAG_INTERPOLATE_POINT_COUNT = (1 << 4),
} eBrushCurvesSculptFlag;
+typedef enum eBrushCurvesSculptDensityMode {
+ BRUSH_CURVES_SCULPT_DENSITY_MODE_AUTO = 0,
+ BRUSH_CURVES_SCULPT_DENSITY_MODE_ADD = 1,
+ BRUSH_CURVES_SCULPT_DENSITY_MODE_REMOVE = 2,
+} eBrushCurvesSculptDensityMode;
+
#define MAX_BRUSH_PIXEL_RADIUS 500
#define GP_MAX_BRUSH_PIXEL_RADIUS 1000
diff --git a/source/blender/makesdna/DNA_brush_types.h b/source/blender/makesdna/DNA_brush_types.h
index d13496b21f7..b24bb786593 100644
--- a/source/blender/makesdna/DNA_brush_types.h
+++ b/source/blender/makesdna/DNA_brush_types.h
@@ -148,6 +148,13 @@ typedef struct BrushCurvesSculptSettings {
float minimum_length;
/** Length of newly added curves when it is not interpolated from other curves. */
float curve_length;
+ /** Minimum distance between curve root points used by the Density brush. */
+ float minimum_distance;
+ /** How often the Density brush tries to add a new curve. */
+ int density_add_attempts;
+ /** #eBrushCurvesSculptDensityMode. */
+ uint8_t density_mode;
+ char _pad[7];
} BrushCurvesSculptSettings;
typedef struct Brush {
diff --git a/source/blender/makesrna/intern/rna_brush.c b/source/blender/makesrna/intern/rna_brush.c
index 80a61543b5a..e0d55050c63 100644
--- a/source/blender/makesrna/intern/rna_brush.c
+++ b/source/blender/makesrna/intern/rna_brush.c
@@ -251,6 +251,11 @@ const EnumPropertyItem rna_enum_brush_curves_sculpt_tool_items[] = {
{CURVES_SCULPT_TOOL_ADD, "ADD", ICON_BRUSH_CURVES_ADD, "Add Curves", ""},
{CURVES_SCULPT_TOOL_GROW_SHRINK, "GROW_SHRINK", ICON_BRUSH_CURVES_GROW_SHRINK, "Grow / Shrink Curves", ""},
{CURVES_SCULPT_TOOL_SELECTION_PAINT, "SELECTION_PAINT", ICON_BRUSH_PAINT_SELECT, "Paint Selection", ""},
+ {CURVES_SCULPT_TOOL_PINCH, "PINCH", ICON_BRUSH_CURVES_PINCH, "Pinch Curves", ""},
+ {CURVES_SCULPT_TOOL_SMOOTH, "SMOOTH", ICON_BRUSH_CURVES_SMOOTH, "Smooth Curves", ""},
+ {CURVES_SCULPT_TOOL_PUFF, "PUFF", ICON_BRUSH_CURVES_PUFF, "Puff Curves", ""},
+ {CURVES_SCULPT_TOOL_DENSITY, "DENSITY", ICON_BRUSH_CURVES_DENSITY, "Density Curves", ""},
+ {CURVES_SCULPT_TOOL_SLIDE, "SLIDE", ICON_BRUSH_CURVES_SLIDE, "Slide Curves", ""},
{0, NULL, 0, NULL, NULL},
};
/* clang-format on */
@@ -889,6 +894,7 @@ static const EnumPropertyItem *rna_Brush_direction_itemf(bContext *C,
switch (me->curves_sculpt_tool) {
case CURVES_SCULPT_TOOL_GROW_SHRINK:
case CURVES_SCULPT_TOOL_SELECTION_PAINT:
+ case CURVES_SCULPT_TOOL_PINCH:
return prop_direction_items;
default:
return DummyRNA_DEFAULT_items;
@@ -1949,6 +1955,26 @@ static void rna_def_curves_sculpt_options(BlenderRNA *brna)
StructRNA *srna;
PropertyRNA *prop;
+ static const EnumPropertyItem density_mode_items[] = {
+ {BRUSH_CURVES_SCULPT_DENSITY_MODE_AUTO,
+ "AUTO",
+ ICON_AUTO,
+ "Auto",
+ "Either add or remove curves depending on the minimum distance of the curves under the "
+ "cursor"},
+ {BRUSH_CURVES_SCULPT_DENSITY_MODE_ADD,
+ "ADD",
+ ICON_ADD,
+ "Add",
+ "Add new curves between existing curves, taking the minimum distance into account"},
+ {BRUSH_CURVES_SCULPT_DENSITY_MODE_REMOVE,
+ "REMOVE",
+ ICON_REMOVE,
+ "Remove",
+ "Remove curves whose root points are too close"},
+ {0, NULL, 0, NULL, NULL},
+ };
+
srna = RNA_def_struct(brna, "BrushCurvesSculptSettings", NULL);
RNA_def_struct_sdna(srna, "BrushCurvesSculptSettings");
RNA_def_struct_ui_text(srna, "Curves Sculpt Brush Settings", "");
@@ -1997,6 +2023,22 @@ static void rna_def_curves_sculpt_options(BlenderRNA *brna)
prop,
"Curve Length",
"Length of newly added curves when it is not interpolated from other curves");
+
+ prop = RNA_def_property(srna, "minimum_distance", PROP_FLOAT, PROP_DISTANCE);
+ RNA_def_property_range(prop, 0.0f, FLT_MAX);
+ RNA_def_property_ui_range(prop, 0.0, 1000.0f, 0.001, 2);
+ RNA_def_property_ui_text(
+ prop, "Minimum Distance", "Goal distance between curve roots for the Density brush");
+
+ prop = RNA_def_property(srna, "density_add_attempts", PROP_INT, PROP_NONE);
+ RNA_def_property_range(prop, 0, INT32_MAX);
+ RNA_def_property_ui_text(
+ prop, "Density Add Attempts", "How many times the Density brush tries to add a new curve");
+
+ prop = RNA_def_property(srna, "density_mode", PROP_ENUM, PROP_NONE);
+ RNA_def_property_enum_items(prop, density_mode_items);
+ RNA_def_property_ui_text(
+ prop, "Density Mode", "Determines whether the brush adds or removes curves");
}
static void rna_def_brush(BlenderRNA *brna)