Sculpt: Add increase / decrease visibility operator #120282

Open
Sean Kim wants to merge 23 commits from Sean-Kim/blender:hide-show-grow-shrink into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
7 changed files with 479 additions and 7 deletions

View File

@ -5609,6 +5609,10 @@ def km_sculpt(params):
{"properties": [("mode", 'HIDE_ACTIVE')]}),
("paint.hide_show_all", {"type": 'H', "value": 'PRESS', "alt": True},
{"properties": [("action", "SHOW")]}),
("paint.visibility_filter", {"type": 'PAGE_UP', "value": 'PRESS', "repeat": True},
{"properties": [("action", 'GROW')]}),
("paint.visibility_filter", {"type": 'PAGE_DOWN', "value": 'PRESS', "repeat": True},
{"properties": [("action", 'SHRINK')]}),
("sculpt.face_set_edit", {"type": 'W', "value": 'PRESS', "ctrl": True},
{"properties": [("mode", 'GROW')]}),
("sculpt.face_set_edit", {"type": 'W', "value": 'PRESS', "ctrl": True, "alt": True},

View File

@ -3818,6 +3818,12 @@ class VIEW3D_MT_sculpt(Menu):
props = layout.operator("paint.hide_show_masked", text="Hide Masked")
props.action = 'HIDE'
props = layout.operator("paint.visibility_filter", text="Grow Visibility")
props.action = "GROW"
props = layout.operator("paint.visibility_filter", text="Shrink Visibility")
props.action = "SHRINK"
layout.separator()
props = layout.operator("sculpt.trim_box_gesture", text="Box Trim")

View File

@ -343,6 +343,12 @@ void mesh_sharp_edges_set_from_angle(Mesh &mesh, float angle, bool keep_sharp_ed
* vertices are hidden. */
void mesh_edge_hide_from_vert(Span<int2> edges, Span<bool> hide_vert, MutableSpan<bool> hide_edge);
/* Hide faces when any of their vertices are hidden. */
void mesh_face_hide_from_vert(OffsetIndices<int> faces,
Span<int> corner_verts,
Span<bool> hide_vert,
MutableSpan<bool> hide_poly);
/** Make edge and face visibility consistent with vertices. */
void mesh_hide_vert_flush(Mesh &mesh);
/** Make vertex and edge visibility consistent with faces. */

View File

@ -517,11 +517,10 @@ void mesh_edge_hide_from_vert(const Span<int2> edges,
});
}
/* Hide faces when any of their vertices are hidden. */
static void face_hide_from_vert(const OffsetIndices<int> faces,
const Span<int> corner_verts,
const Span<bool> hide_vert,
MutableSpan<bool> hide_poly)
void mesh_face_hide_from_vert(const OffsetIndices<int> faces,
const Span<int> corner_verts,
const Span<bool> hide_vert,
MutableSpan<bool> hide_poly)
{
using namespace blender;
threading::parallel_for(faces.index_range(), 4096, [&](const IndexRange range) {
@ -552,7 +551,7 @@ void mesh_hide_vert_flush(Mesh &mesh)
".hide_poly", AttrDomain::Face);
mesh_edge_hide_from_vert(mesh.edges(), hide_vert_span, hide_edge.span);
face_hide_from_vert(mesh.faces(), mesh.corner_verts(), hide_vert_span, hide_poly.span);
mesh_face_hide_from_vert(mesh.faces(), mesh.corner_verts(), hide_vert_span, hide_poly.span);
hide_edge.finish();
hide_poly.finish();

View File

@ -192,7 +192,7 @@ enum class VisAction {
Show = 1,
};
static bool action_to_hide(const VisAction action)
constexpr static bool action_to_hide(const VisAction action)
{
return action == VisAction::Hide;
}
@ -253,6 +253,18 @@ static void flush_face_changes_node(Mesh &mesh,
hide_poly.finish();
}
/* Updates a node's face's visibility based on the updated vertex visibility. */
static void flush_face_changes(Mesh &mesh, const Span<bool> hide_vert)
{
bke::MutableAttributeAccessor attributes = mesh.attributes_for_write();
bke::SpanAttributeWriter<bool> hide_poly = attributes.lookup_or_add_for_write_span<bool>(
".hide_poly", bke::AttrDomain::Face);
bke::mesh_face_hide_from_vert(mesh.faces(), mesh.corner_verts(), hide_vert, hide_poly.span);
hide_poly.finish();
}
/* Updates all of a mesh's edge visibility based on vertex visibility. */
static void flush_edge_changes(Mesh &mesh, const Span<bool> hide_vert)
{
@ -787,6 +799,449 @@ void PAINT_OT_visibility_invert(wmOperatorType *ot)
ot->flag = OPTYPE_REGISTER;
}
/* Number of vertices per iteration step size when growing or shrinking visibility. */
static constexpr float VERTEX_ITERATION_THRESHOLD = 50000.0f;
/* For both this and the following #shrinK_face_visibility_mesh methods, extracting the loop
* and comparing against / writing with a constant `false` or `true` instead of using
* #action_to_hide results in a nearly 600ms speedup on a mesh with 1.5m verts. */
static void grow_face_visibility_mesh(const IndexRange &face,
Sean-Kim marked this conversation as resolved Outdated

for (const int i : neighbors.coords.index_range().drop_front(neighbors.num_duplicates))?

`for (const int i : neighbors.coords.index_range().drop_front(neighbors.num_duplicates))`?
const Span<int> corner_verts,
const Span<bool> read_buffer,
MutableSpan<bool> write_buffer)
{
for (const int corner : face) {
int vert = corner_verts[corner];
if (read_buffer[vert]) {
Sean-Kim marked this conversation as resolved Outdated

Should probably use lookup_or_default here in case the hide_poly attribute doesn't exist for some reason.

Should probably use `lookup_or_default` here in case the hide_poly attribute doesn't exist for some reason.
continue;
}
const int prev = bke::mesh::face_corner_prev(face, corner);
const int prev_vert = corner_verts[prev];
write_buffer[prev_vert] = false;
const int next = bke::mesh::face_corner_next(face, corner);
const int next_vert = corner_verts[next];
write_buffer[next_vert] = false;
}
}
static void shrink_face_visibility_mesh(const IndexRange &face,
Review

I think this will allow deduplicating these two functions:

template<bool value>
static void affect_visibility_mesh(const IndexRange &face,
                                   const Span<int> corner_verts,
                                   const Span<bool> read_buffer,
                                   MutableSpan<bool> write_buffer)
{
  for (const int corner : face) {
    int vert = corner_verts[corner];
    if (read_buffer[vert] != value) {
      continue;
    }

    const int prev = bke::mesh::face_corner_prev(face, corner);
    const int prev_vert = corner_verts[prev];
    write_buffer[prev_vert] = value;

    const int next = bke::mesh::face_corner_next(face, corner);
    const int next_vert = corner_verts[next];
    write_buffer[next_vert] = value;
  }
}

Also IndexRange is a small struct and can be passed by value. I usually just pass a sliced corner_verts already though, and call it face_verts

I think this will allow deduplicating these two functions: ```cpp template<bool value> static void affect_visibility_mesh(const IndexRange &face, const Span<int> corner_verts, const Span<bool> read_buffer, MutableSpan<bool> write_buffer) { for (const int corner : face) { int vert = corner_verts[corner]; if (read_buffer[vert] != value) { continue; } const int prev = bke::mesh::face_corner_prev(face, corner); const int prev_vert = corner_verts[prev]; write_buffer[prev_vert] = value; const int next = bke::mesh::face_corner_next(face, corner); const int next_vert = corner_verts[next]; write_buffer[next_vert] = value; } } ``` Also `IndexRange` is a small struct and can be passed by value. I usually just pass a sliced `corner_verts` already though, and call it `face_verts`
Review

Isn't IndexRange face still needed for face_corner_prev & face_corner_next?

Isn't `IndexRange face` still needed for `face_corner_prev` & `face_corner_next`?
const Span<int> corner_verts,
const Span<bool> read_buffer,
Sean-Kim marked this conversation as resolved Outdated

Could iterate over the indices instead of their index range. Same for multires

Could iterate over the indices instead of their index range. Same for multires
MutableSpan<bool> write_buffer)
{
for (const int corner : face) {
int vert = corner_verts[corner];
if (!read_buffer[vert]) {
continue;
}
const int prev = bke::mesh::face_corner_prev(face, corner);
const int prev_vert = corner_verts[prev];
write_buffer[prev_vert] = true;
const int next = bke::mesh::face_corner_next(face, corner);
const int next_vert = corner_verts[next];
write_buffer[next_vert] = true;
}
}
struct DualBuffer {
Array<bool> front;
Array<bool> back;
MutableSpan<bool> write_buffer(int count)
{
return count % 2 == 0 ? back.as_mutable_span() : front.as_mutable_span();
}
Span<bool> read_buffer(int count)
{
return count % 2 == 0 ? front.as_span() : back.as_span();
}
};
static int propagate_vertex_visibility(Mesh &mesh,
DualBuffer &buffers,
const VArraySpan<bool> &hide_poly,
const VisAction action,
const int iterations)
{
const OffsetIndices faces = mesh.faces();
const Span<int> corner_verts = mesh.corner_verts();
int last_changed_iteration = -1;
for (int i = 0; i < iterations; i++) {
Span<bool> read_buffer = buffers.read_buffer(i);
MutableSpan<bool> write_buffer = buffers.write_buffer(i);
threading::parallel_for(faces.index_range(), 1024, [&](const IndexRange range) {
Sean-Kim marked this conversation as resolved Outdated

Some of the same performance concerns apply here from the mesh case, particularly copying the grid visibility every iteration.

Some of the same performance concerns apply here from the mesh case, particularly copying the grid visibility every iteration.
for (const int face_index : range) {
Sean-Kim marked this conversation as resolved Outdated

BitGroupVector should probably have a copy constructor instead of this manual copy of each group individually

`BitGroupVector` should probably have a copy constructor instead of this manual copy of each group individually

I'll pull this out into a separate PR.

I'll pull this out into a separate PR.

PR: #122026

PR: #122026
if (!hide_poly[face_index]) {
continue;
}
const IndexRange face = faces[face_index];
if (action == VisAction::Hide) {
shrink_face_visibility_mesh(face, corner_verts, read_buffer, write_buffer);
}
else {
grow_face_visibility_mesh(face, corner_verts, read_buffer, write_buffer);
}
}
});
const bool any_changed = read_buffer != write_buffer;
Review

How about this?

    if (read_buffer == write_buffer) {
      /* If nothing has changed for a given iteration, nothing will change in future iterations. */
      break;
    }
    flush_face_changes(mesh, write_buffer);
    last_changed_iteration = i;

Also did you find that this was occurring frequently? I wouldn't imagine it would be that common-- maybe it's not worth it.

How about this? ```cpp if (read_buffer == write_buffer) { /* If nothing has changed for a given iteration, nothing will change in future iterations. */ break; } flush_face_changes(mesh, write_buffer); last_changed_iteration = i; ``` Also did you find that this was occurring frequently? I wouldn't imagine it would be that common-- maybe it's not worth it.
Review

Realistically, it only happens when you're hiding a mesh that's already mostly hidden or showing a mesh that's already mostly visible. I didn't measure the impact of removing this and just letting the iteration go further

Realistically, it only happens when you're hiding a mesh that's already mostly hidden or showing a mesh that's already mostly visible. I didn't measure the impact of removing this and just letting the iteration go further
if (any_changed) {
flush_face_changes(mesh, write_buffer);
last_changed_iteration = i;
}
else {
/* If nothing has changed for a given iteration, nothing will change in future iterations. */
break;
}
}
return last_changed_iteration;
}
static void update_undo_state(Object &object,
const Span<PBVHNode *> nodes,
const Span<bool> old_hide_vert,
const Span<bool> new_hide_vert)
{
threading::EnumerableThreadSpecific<Vector<bool>> all_new_hide;
Sean-Kim marked this conversation as resolved Outdated

Could you split this logic to a separate local function called elem_xy_to_index?

Could you split this logic to a separate local function called `elem_xy_to_index`?
Review

I like the idea of reusing the abstractions from elsewhere here, but maybe it's not totally worth it? For example this is quite a bit simpler:

  threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) {
    for (PBVHNode *node : nodes.slice(range)) {
      for (const int vert : bke::pbvh::node_unique_verts(*node)) {
        if (old_hide_vert[vert] != new_hide_vert[vert]) {
          undo::push_node(object, node, undo::Type::HideVert);
          break;
        }
      }
    }
  });

Though actually, I think the only reason not to use vert_hide_update instead is an extra step of vert->face flushing. Maybe if that was resolved we could benefit from reusing that code?

I like the idea of reusing the abstractions from elsewhere here, but maybe it's not totally worth it? For example this is quite a bit simpler: ```cpp threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) { for (PBVHNode *node : nodes.slice(range)) { for (const int vert : bke::pbvh::node_unique_verts(*node)) { if (old_hide_vert[vert] != new_hide_vert[vert]) { undo::push_node(object, node, undo::Type::HideVert); break; } } } }); ``` Though actually, I think the only reason not to use `vert_hide_update` instead is an extra step of vert->face flushing. Maybe if that was resolved we could benefit from reusing that code?
threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) {
Vector<bool> &new_hide = all_new_hide.local();
for (PBVHNode *node : nodes.slice(range)) {
const Span<int> verts = bke::pbvh::node_unique_verts(*node);
new_hide.reinitialize(verts.size());
array_utils::gather(new_hide_vert, verts, new_hide.as_mutable_span());
if (array_utils::indexed_data_equal<bool>(old_hide_vert, verts, new_hide)) {
continue;
}
undo::push_node(object, node, undo::Type::HideVert);
}
});
}
static void update_node_visibility_from_face_changes(const Span<PBVHNode *> nodes,
const Span<int> tri_faces,
const Span<bool> orig_hide_poly,
const Span<bool> new_hide_poly,
const Span<bool> hide_vert)
{
threading::EnumerableThreadSpecific<Vector<int>> all_face_indices;
threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) {
Vector<int> &face_indices = all_face_indices.local();
for (PBVHNode *node : nodes.slice(range)) {
bool any_changed = false;
const Span<int> indices = bke::pbvh::node_face_indices_calc_mesh(
tri_faces, *node, face_indices);
for (const int face_index : indices) {
if (orig_hide_poly[face_index] != new_hide_poly[face_index]) {
any_changed = true;
break;
}
}
if (any_changed) {
BKE_pbvh_node_mark_update_visibility(node);
bke::pbvh::node_update_visibility_mesh(hide_vert, *node);
}
}
});
}
static void grow_shrink_visibility_mesh(Object &object,
const Span<PBVHNode *> nodes,
const VisAction action,
const int iterations)
{
Mesh &mesh = *static_cast<Mesh *>(object.data);
bke::MutableAttributeAccessor attributes = mesh.attributes_for_write();
if (!attributes.contains(".hide_vert")) {
/* If the entire mesh is visible, we can neither grow nor shrink the boundary. */
return;
}
bke::SpanAttributeWriter<bool> hide_vert = attributes.lookup_or_add_for_write_span<bool>(
".hide_vert", bke::AttrDomain::Point);
const VArraySpan hide_poly = *attributes.lookup_or_default<bool>(
".hide_poly", bke::AttrDomain::Face, false);
DualBuffer buffers;
buffers.back.reinitialize(hide_vert.span.size());
buffers.front.reinitialize(hide_vert.span.size());
array_utils::copy(hide_vert.span.as_span(), buffers.back.as_mutable_span());
array_utils::copy(hide_vert.span.as_span(), buffers.front.as_mutable_span());
Vector<bool> orig_hide_poly(hide_poly);
Sean-Kim marked this conversation as resolved
Review

Vector -> Array here since it isn't resized

`Vector` -> `Array` here since it isn't resized
const int last_changed_iteration = propagate_vertex_visibility(
mesh, buffers, hide_poly, action, iterations);
if (last_changed_iteration == -1) {
/* No changes at all happened. */
hide_vert.finish();
return;
}
const Span<bool> last_buffer = buffers.write_buffer(last_changed_iteration);
update_undo_state(object, nodes, hide_vert.span, last_buffer);
/* We can wait until after all iterations are done to flush edge changes as they are
* not used for coarse filtering while iterating.*/
flush_edge_changes(mesh, last_buffer);
update_node_visibility_from_face_changes(
nodes, mesh.corner_tri_faces(), orig_hide_poly, hide_poly, last_buffer);
array_utils::copy(last_buffer, hide_vert.span);
hide_vert.finish();
}
/* TODO: This is probably better off as a member function of a CCGKey?*/
static int elem_xy_to_index(int x, int y, int grid_size)
{
return y * grid_size + x;
}
struct DualBitBuffer {
BitGroupVector<> front;
BitGroupVector<> back;
BitGroupVector<> &write_buffer(int count)
{
return count % 2 == 0 ? back : front;
}
BitGroupVector<> &read_buffer(int count)
{
return count % 2 == 0 ? front : back;
}
};
static void grow_shrink_visibility_grid(Depsgraph &depsgraph,
Object &object,
PBVH &pbvh,
const Span<PBVHNode *> nodes,
const VisAction action,
const int iterations)
{
Mesh &mesh = *static_cast<Mesh *>(object.data);
SubdivCCG &subdiv_ccg = *object.sculpt->subdiv_ccg;
BitGroupVector<> &grid_hidden = BKE_subdiv_ccg_grid_hidden_ensure(subdiv_ccg);
const bool desired_state = action_to_hide(action);
const CCGKey key = *BKE_pbvh_get_grid_key(pbvh);
DualBitBuffer buffers;
Sean-Kim marked this conversation as resolved
Review

Can be written as:

  DualBitBuffer buffers;
  buffers.front = grid_hidden;
  buffers.back = grid_hidden;
Can be written as: ```cpp DualBitBuffer buffers; buffers.front = grid_hidden; buffers.back = grid_hidden;
buffers.front = BitGroupVector<>(grid_hidden);
buffers.back = BitGroupVector<>(grid_hidden);
Array<bool> node_changed(nodes.size());
Sean-Kim marked this conversation as resolved
Review

Should probably be initialized to false here

Should probably be initialized to false here
for (int i = 0; i < iterations; i++) {
Sean-Kim marked this conversation as resolved
Review

for (const int i : IndexRange(iterations)) {

`for (const int i : IndexRange(iterations)) {`
threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) {
for (const int node_index : range) {
PBVHNode *node = nodes[node_index];
const Span<int> grids = bke::pbvh::node_grid_indices(*node);
for (const int grid_index : grids) {
for (const int y : IndexRange(key.grid_size)) {
for (const int x : IndexRange(key.grid_size)) {
const int grid_elem_idx = elem_xy_to_index(x, y, key.grid_size);
if (buffers.read_buffer(i)[grid_index][grid_elem_idx] != desired_state) {
Sean-Kim marked this conversation as resolved
Review

Seems nice to extract the read_buffer and write_buffer calls to before the parallel loop

Seems nice to extract the `read_buffer` and `write_buffer` calls to before the parallel loop
continue;
}
SubdivCCGCoord coord{};
coord.grid_index = grid_index;
coord.x = x;
coord.y = y;
SubdivCCGNeighbors neighbors;
BKE_subdiv_ccg_neighbor_coords_get(subdiv_ccg, coord, true, neighbors);
for (const int j : neighbors.coords.index_range()) {
const SubdivCCGCoord neighbor = neighbors.coords[j];
const int neighbor_grid_elem_idx = elem_xy_to_index(
neighbor.x, neighbor.y, key.grid_size);
buffers.write_buffer(i)[neighbor.grid_index][neighbor_grid_elem_idx].set(
desired_state);
}
}
}
}
node_changed[node_index] = true;
}
});
}
threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) {
for (const int node_index : range) {
if (!node_changed[node_index]) {
Sean-Kim marked this conversation as resolved
Review

In general skipping work inside of a parallel loop can be problematic. The tasks are scheduled assuming each index has the same amount of work. For example, if all the changed nodes happen to be grouped around specific indices, that means one thread can be stuck with a group of changed nodes while other threads happen to take the early out here.

The alternative would be creating an index mask based on node_changed using IndexMask::from_bools and using foreach_index(GrainSize(1) for iteration

In general skipping work inside of a parallel loop can be problematic. The tasks are scheduled assuming each index has the same amount of work. For example, if all the changed nodes happen to be grouped around specific indices, that means one thread can be stuck with a group of changed nodes while other threads happen to take the early out here. The alternative would be creating an index mask based on `node_changed` using `IndexMask::from_bools` and using `foreach_index(GrainSize(1)` for iteration
Review

Oh, good to know. I wasn't aware that that's how TBB operated

Oh, good to know. I wasn't aware that that's how TBB operated
continue;
}
undo::push_node(object, nodes[node_index], undo::Type::HideVert);
}
});
BitGroupVector<> &last_buffer = buffers.write_buffer(iterations - 1);
for (int i = 0; i < grid_hidden.size(); i++) {
Sean-Kim marked this conversation as resolved
Review

The same as grid_hidden = std::move(last_buffer);, no?

The same as `grid_hidden = std::move(last_buffer);`, no?
Review

Yep, makes sense. I'm still not super solid on move semantics but it's good to keep in mind.

Yep, makes sense. I'm still not super solid on move semantics but it's good to keep in mind.
for (int j = 0; j < grid_hidden.group_size(); j++) {
grid_hidden[i][j].set_branchless(last_buffer[i][j]);
}
}
threading::parallel_for(nodes.index_range(), 1, [&](const IndexRange range) {
for (const int node_index : range) {
if (!node_changed[node_index]) {
continue;
}
PBVHNode *node = nodes[node_index];
BKE_pbvh_node_mark_update_visibility(node);
bke::pbvh::node_update_visibility_grids(grid_hidden, *node);
}
});
multires_mark_as_modified(&depsgraph, &object, MULTIRES_HIDDEN_MODIFIED);
BKE_pbvh_sync_visibility_from_verts(pbvh, &mesh);
}
static Array<bool> duplicate_visibility(const Object &object)
Sean-Kim marked this conversation as resolved
Review

duplicate_visibility_bmesh maybe?

`duplicate_visibility_bmesh` maybe?
{
const SculptSession &ss = *object.sculpt;
BMesh &bm = *ss.bm;
Array<bool> result(bm.totvert);
BM_mesh_elem_table_ensure(&bm, BM_VERT);
for (const int i : result.index_range()) {
result[i] = BM_elem_flag_test_bool(BM_vert_at_index(&bm, i), BM_ELEM_HIDDEN);
}
return result;
}
static void grow_shrink_visibility_bmesh(Object &object,
PBVH &pbvh,
const Span<PBVHNode *> nodes,
const VisAction action,
const int iterations)
{
SculptSession &ss = *object.sculpt;
for (int i = 0; i < iterations; i++) {
const Array<bool> prev_visibility = duplicate_visibility(object);
partialvis_update_bmesh_nodes(object, nodes, action, [&](const BMVert *vert) {
int vi = BM_elem_index_get(vert);
PBVHVertRef vref = BKE_pbvh_index_to_vertex(pbvh, vi);
SculptVertexNeighborIter ni;
bool should_change = false;
SCULPT_VERTEX_NEIGHBORS_ITER_BEGIN (ss, vref, ni) {
if (prev_visibility[ni.index] == action_to_hide(action)) {
/* Not returning instantly to avoid leaking memory. */
should_change = true;
break;
}
}
SCULPT_VERTEX_NEIGHBORS_ITER_END(ni);
return should_change;
});
}
}
static int visibility_filter_exec(bContext *C, wmOperator *op)
{
Object &object = *CTX_data_active_object(C);
Depsgraph &depsgraph = *CTX_data_ensure_evaluated_depsgraph(C);
PBVH &pbvh = *BKE_sculpt_object_pbvh_ensure(&depsgraph, &object);
BLI_assert(BKE_object_sculpt_pbvh_get(&object) == &pbvh);
const VisAction mode = VisAction(RNA_enum_get(op->ptr, "action"));
Vector<PBVHNode *> nodes = bke::pbvh::search_gather(pbvh, {});
const SculptSession &ss = *object.sculpt;
int num_verts = SCULPT_vertex_count_get(ss);
int iterations = RNA_int_get(op->ptr, "iterations");
if (RNA_boolean_get(op->ptr, "auto_iteration_count")) {
/* Automatically adjust the number of iterations based on the number
* of vertices in the mesh. */
iterations = int(num_verts / VERTEX_ITERATION_THRESHOLD) + 1;
}
undo::push_begin(object, op);
switch (BKE_pbvh_type(pbvh)) {
case PBVH_FACES:
grow_shrink_visibility_mesh(object, nodes, mode, iterations);
break;
case PBVH_GRIDS:
grow_shrink_visibility_grid(depsgraph, object, pbvh, nodes, mode, iterations);
break;
case PBVH_BMESH:
grow_shrink_visibility_bmesh(object, pbvh, nodes, mode, iterations);
break;
}
undo::push_end(object);
SCULPT_topology_islands_invalidate(*object.sculpt);
tag_update_visibility(*C);
return OPERATOR_FINISHED;
}
void PAINT_OT_visibility_filter(wmOperatorType *ot)
{
static EnumPropertyItem actions[] = {
{int(VisAction::Show),
"GROW",
0,
"Grow Visibility",
"Grows the visibility by one face based on mesh topology"},
Sean-Kim marked this conversation as resolved
Review

It's usually preferred to use the imperative tense for descriptions. For example: Grow the visibility, Shrink the visibility, Edit the visibility

It's usually preferred to use the imperative tense for descriptions. For example: `Grow the visibility`, `Shrink the visibility`, `Edit the visibility`
{int(VisAction::Hide),
"SHRINK",
0,
"Shrink Visibility",
"Shrinks the visibility by one face based on mesh topology"},
{0, nullptr, 0, nullptr, nullptr},
};
ot->name = "Visibility Filter";
ot->idname = "PAINT_OT_visibility_filter";
ot->description = "Edits the visibility of the current mesh";
ot->exec = visibility_filter_exec;
ot->poll = SCULPT_mode_poll_view3d;
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
RNA_def_enum(ot->srna, "action", actions, int(VisAction::Show), "Action", "");
RNA_def_int(ot->srna,
"iterations",
1,
1,
100,
"Iterations",
"Number of times that the filter is going to be applied",
1,
100);
RNA_def_boolean(
ot->srna,
"auto_iteration_count",
true,
"Auto Iteration Count",
"Use an automatic number of iterations based on the number of vertices of the sculpt");
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@ -461,6 +461,7 @@ void PAINT_OT_hide_show_line_gesture(wmOperatorType *ot);
void PAINT_OT_hide_show_polyline_gesture(wmOperatorType *ot);
void PAINT_OT_visibility_invert(wmOperatorType *ot);
void PAINT_OT_visibility_filter(wmOperatorType *ot);
} // namespace blender::ed::sculpt_paint::hide
/* `paint_mask.cc` */

View File

@ -1561,6 +1561,7 @@ void ED_operatortypes_paint()
WM_operatortype_append(hide::PAINT_OT_hide_show_line_gesture);
WM_operatortype_append(hide::PAINT_OT_hide_show_polyline_gesture);
WM_operatortype_append(hide::PAINT_OT_visibility_invert);
WM_operatortype_append(hide::PAINT_OT_visibility_filter);
/* paint masking */
WM_operatortype_append(mask::PAINT_OT_mask_flood_fill);