diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 12424e474fa..d299941a5ed 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4510,6 +4510,12 @@ def km_face_mask(params): {"properties": [("deselect", True)]}), ("paint.face_select_more", {"type": 'NUMPAD_PLUS', "value": 'PRESS', "ctrl": True}, None), ("paint.face_select_less", {"type": 'NUMPAD_MINUS', "value": 'PRESS', "ctrl": True}, None), + ("paint.face_select_loop", {"type": params.select_mouse, "value": 'PRESS', "alt": True}, + {"properties": [('extend', False), ('select', True)]}), + ("paint.face_select_loop", {"type": params.select_mouse, "value": 'PRESS', "alt": True, "shift": True}, + {"properties": [('extend', True), ('select', True)]}), + ("paint.face_select_loop", {"type": params.select_mouse, "value": 'PRESS', "alt": True, "shift": True, "ctrl": True}, + {"properties": [('extend', True), ('select', False)]}), ]) return keymap @@ -5110,8 +5116,6 @@ def km_weight_paint(params): ("paint.weight_paint", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), ("paint.weight_sample", {"type": params.action_mouse, "value": 'PRESS', "ctrl": True}, None), ("paint.weight_sample_group", {"type": params.action_mouse, "value": 'PRESS', "shift": True}, None), - ("paint.weight_gradient", {"type": 'LEFTMOUSE', "value": 'PRESS', "alt": True}, - {"properties": [("type", 'LINEAR')]}), ("paint.weight_gradient", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True, "alt": True}, {"properties": [("type", 'RADIAL')]}), ("paint.weight_set", {"type": 'K', "value": 'PRESS', "shift": True}, None), diff --git a/source/blender/editors/include/ED_mesh.h b/source/blender/editors/include/ED_mesh.h index d57536da855..709b9897d5a 100644 --- a/source/blender/editors/include/ED_mesh.h +++ b/source/blender/editors/include/ED_mesh.h @@ -427,6 +427,8 @@ void paintface_select_linked(struct bContext *C, struct Object *ob, const int mval[2], bool select); + +void paintface_select_loop(struct bContext *C, struct Object *ob, const int mval[2], bool select); /** * Grow the selection of faces. * \param face_step: If true will also select faces that only touch on the corner. diff --git a/source/blender/editors/mesh/editface.cc b/source/blender/editors/mesh/editface.cc index cdf50c08995..6b0d972e9e4 100644 --- a/source/blender/editors/mesh/editface.cc +++ b/source/blender/editors/mesh/editface.cc @@ -12,7 +12,9 @@ #include "BLI_bitmap.h" #include "BLI_blenlib.h" #include "BLI_math.h" +#include "BLI_math_vector.hh" #include "BLI_task.hh" +#include "BLI_vector_set.hh" #include "IMB_imbuf.h" #include "IMB_imbuf_types.h" @@ -350,6 +352,177 @@ void paintface_select_linked(bContext *C, Object *ob, const int mval[2], const b paintface_flush_flags(C, ob, true, false); } +static int find_closest_edge_in_poly(ARegion *region, + blender::Span edges, + blender::Span poly_edges, + blender::Span vert_positions, + const int mval[2]) +{ + using namespace blender; + int closest_edge_index; + + const float2 mval_f = {float(mval[0]), float(mval[1])}; + float min_distance = FLT_MAX; + for (const int i : poly_edges) { + float2 screen_coordinate; + const int2 edge = edges[i]; + const float3 edge_vert_average = math::midpoint(vert_positions[edge[0]], + vert_positions[edge[1]]); + eV3DProjStatus status = ED_view3d_project_float_object( + region, edge_vert_average, screen_coordinate, V3D_PROJ_TEST_CLIP_DEFAULT); + if (status != V3D_PROJ_RET_OK) { + continue; + } + const float distance = math::distance_squared(mval_f, screen_coordinate); + if (distance < min_distance) { + min_distance = distance; + closest_edge_index = i; + } + } + return closest_edge_index; +} + +static int get_opposing_edge_index(const blender::IndexRange poly, + const blender::Span corner_edges, + const int current_edge_index) +{ + const int index_in_poly = corner_edges.slice(poly).first_index(current_edge_index); + /* Assumes that edge index of opposing face edge is always off by 2 on quads. */ + if (index_in_poly >= 2) { + return corner_edges[poly[index_in_poly - 2]]; + } + /* Cannot be out of bounds because of the preceding if statement: if i < 2 then i+2 < 4. */ + return corner_edges[poly[index_in_poly + 2]]; +} + +/** + * Follow quads around the mesh by finding opposing edges. + * \return True if the search has looped back on itself, finding the same index twice. + */ +static bool follow_face_loop(const int poly_start_index, + const int edge_start_index, + const blender::OffsetIndices polys, + const blender::VArray &hide_poly, + const blender::Span corner_edges, + const blender::GroupedSpan edge_to_poly_map, + blender::VectorSet &r_loop_polys) +{ + using namespace blender; + int current_poly_index = poly_start_index; + int current_edge_index = edge_start_index; + + while (current_edge_index > 0) { + int next_poly_index = -1; + + for (const int poly_index : edge_to_poly_map[current_edge_index]) { + if (poly_index != current_poly_index) { + next_poly_index = poly_index; + break; + } + } + + /* Edge might only have 1 poly connected. */ + if (next_poly_index == -1) { + return false; + } + + /* Only works on quads. */ + if (polys[next_poly_index].size() != 4) { + return false; + } + + /* Happens if we looped around the mesh. */ + if (r_loop_polys.contains(next_poly_index)) { + return true; + } + + /* Hidden polygons stop selection. */ + if (hide_poly[next_poly_index]) { + return false; + } + + r_loop_polys.add(next_poly_index); + + const IndexRange next_poly = polys[next_poly_index]; + current_edge_index = get_opposing_edge_index(next_poly, corner_edges, current_edge_index); + current_poly_index = next_poly_index; + } + return false; +} + +void paintface_select_loop(bContext *C, Object *ob, const int mval[2], const bool select) +{ + using namespace blender; + + Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + ViewContext vc; + ED_view3d_viewcontext_init(C, &vc, depsgraph); + ED_view3d_select_id_validate(&vc); + + Object *ob_eval = DEG_get_evaluated_object(depsgraph, ob); + if (!ob_eval) { + return; + } + + uint poly_pick_index = uint(-1); + if (!ED_mesh_pick_face(C, ob, mval, ED_MESH_PICK_DEFAULT_FACE_DIST, &poly_pick_index)) { + return; + } + + ARegion *region = CTX_wm_region(C); + RegionView3D *rv3d = static_cast(region->regiondata); + ED_view3d_init_mats_rv3d(ob_eval, rv3d); + + Mesh *mesh = BKE_mesh_from_object(ob); + const Span corner_edges = mesh->corner_edges(); + const Span verts = mesh->vert_positions(); + const OffsetIndices polys = mesh->polys(); + const Span edges = mesh->edges(); + + const IndexRange poly = polys[poly_pick_index]; + const int closest_edge_index = find_closest_edge_in_poly( + region, edges, corner_edges.slice(poly), verts, mval); + + Array edge_to_poly_offsets; + Array edge_to_poly_indices; + const GroupedSpan edge_to_poly_map = bke::mesh::build_edge_to_poly_map( + polys, corner_edges, mesh->totedge, edge_to_poly_offsets, edge_to_poly_indices); + + VectorSet polys_to_select; + + bke::MutableAttributeAccessor attributes = mesh->attributes_for_write(); + const VArray hide_poly = *attributes.lookup_or_default( + ".hide_poly", ATTR_DOMAIN_FACE, false); + + const Span polys_to_closest_edge = edge_to_poly_map[closest_edge_index]; + const bool traced_full_loop = follow_face_loop(polys_to_closest_edge[0], + closest_edge_index, + polys, + hide_poly, + corner_edges, + edge_to_poly_map, + polys_to_select); + + if (!traced_full_loop) { + /* Trace the other way. */ + follow_face_loop(polys_to_closest_edge[1], + closest_edge_index, + polys, + hide_poly, + corner_edges, + edge_to_poly_map, + polys_to_select); + } + + bke::SpanAttributeWriter select_poly = attributes.lookup_or_add_for_write_span( + ".select_poly", ATTR_DOMAIN_FACE); + + select_poly.span.fill_indices(polys_to_select.as_span(), select); + + select_poly.finish(); + paintface_flush_flags(C, ob, true, false); +} + static bool poly_has_selected_neighbor(blender::Span poly_edges, blender::Span edges, blender::Span select_vert, diff --git a/source/blender/editors/sculpt_paint/paint_intern.hh b/source/blender/editors/sculpt_paint/paint_intern.hh index 276318e1a85..872d2076fb8 100644 --- a/source/blender/editors/sculpt_paint/paint_intern.hh +++ b/source/blender/editors/sculpt_paint/paint_intern.hh @@ -369,6 +369,7 @@ void PAINT_OT_face_select_all(wmOperatorType *ot); void PAINT_OT_face_select_more(wmOperatorType *ot); void PAINT_OT_face_select_less(wmOperatorType *ot); void PAINT_OT_face_select_hide(wmOperatorType *ot); +void PAINT_OT_face_select_loop(wmOperatorType *ot); void PAINT_OT_face_vert_reveal(wmOperatorType *ot); diff --git a/source/blender/editors/sculpt_paint/paint_ops.cc b/source/blender/editors/sculpt_paint/paint_ops.cc index 5d1fdd92acc..3d67c0bb01d 100644 --- a/source/blender/editors/sculpt_paint/paint_ops.cc +++ b/source/blender/editors/sculpt_paint/paint_ops.cc @@ -389,7 +389,8 @@ static int palette_color_add_exec(bContext *C, wmOperator * /*op*/) PAINT_MODE_TEXTURE_3D, PAINT_MODE_TEXTURE_2D, PAINT_MODE_VERTEX, - PAINT_MODE_SCULPT)) { + PAINT_MODE_SCULPT)) + { copy_v3_v3(color->rgb, BKE_brush_color_get(scene, brush)); color->value = 0.0; } @@ -1540,6 +1541,7 @@ void ED_operatortypes_paint(void) WM_operatortype_append(PAINT_OT_face_select_more); WM_operatortype_append(PAINT_OT_face_select_less); WM_operatortype_append(PAINT_OT_face_select_hide); + WM_operatortype_append(PAINT_OT_face_select_loop); WM_operatortype_append(PAINT_OT_face_vert_reveal); diff --git a/source/blender/editors/sculpt_paint/paint_utils.cc b/source/blender/editors/sculpt_paint/paint_utils.cc index bf9e354aa23..076aa8360ce 100644 --- a/source/blender/editors/sculpt_paint/paint_utils.cc +++ b/source/blender/editors/sculpt_paint/paint_utils.cc @@ -767,6 +767,34 @@ void PAINT_OT_face_select_less(wmOperatorType *ot) ot->srna, "face_step", true, "Face Step", "Also deselect faces that only touch on a corner"); } +static int paintface_select_loop_invoke(bContext *C, wmOperator *op, const wmEvent *event) +{ + const bool select = RNA_boolean_get(op->ptr, "select"); + const bool extend = RNA_boolean_get(op->ptr, "extend"); + if (!extend) { + paintface_deselect_all_visible(C, CTX_data_active_object(C), SEL_DESELECT, false); + } + view3d_operator_needs_opengl(C); + paintface_select_loop(C, CTX_data_active_object(C), event->mval, select); + ED_region_tag_redraw(CTX_wm_region(C)); + return OPERATOR_FINISHED; +} + +void PAINT_OT_face_select_loop(wmOperatorType *ot) +{ + ot->name = "Select Loop"; + ot->description = "Select face loop under the cursor"; + ot->idname = "PAINT_OT_face_select_loop"; + + ot->invoke = paintface_select_loop_invoke; + ot->poll = facemask_paint_poll; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_boolean(ot->srna, "select", true, "Select", "If false, faces will be deselected"); + RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend the selection"); +} + static int vert_select_all_exec(bContext *C, wmOperator *op) { Object *ob = CTX_data_active_object(C);