UV: Edge selection support

This patch adds edge selection support for UV editing (refer T76545).
Developed as a part of GSoC 2021 project - UV Editor Improvements.

Previously, selections in the UV editor always flushed down to vertices
and this caused multiple issues such as T76343, T78757 and T26676.
This patch fixes that by adding edge selection support for all UV
operators and adding support for flushing selections between vertices
and edges. Updating UV select modes is now done using a separate
operator, which also handles select mode flushing and undo for UV
select modes. Drawing edges (in UV edge mode) is also updated to match
the edit-mesh display in the 3D viewport.

Notes on technical changes made with this patch:
* MLOOPUV_EDGESEL flag is restored (was removed in rB9fa29fe7652a).
* Support for flushing selection between vertices and edges.
* Restored the BMLoopUV.select_edge boolean in the Python API.
* New operator to update UV select modes and flushing.
* UV select mode is now part of editmesh undo.

TODOs added with this patch:
* Edge support for shortest path operator (currently uses vertex path logic).
* Change default theme color instead of reducing contrast with edge-select.
* Proper UV element selections for Reveal Hidden operator.

Reviewed By: campbellbarton

Differential Revision: https://developer.blender.org/D12028
This commit is contained in:
Siddhartha Jejurkar 2022-03-03 17:32:07 +05:30
parent 76879e3702
commit ffaaa0bcbf
Notes: blender-bot 2023-02-14 06:55:40 +01:00
Referenced by commit 7725740543, UV: Edge support for select shortest path operator
Referenced by commit c38187393a, Fix T98214: UV selection crash with wire edges
Referenced by issue #99344, Pick Shortest Path does not work as expected with edge select in some cases in UV editor
Referenced by issue #98214, Crash selecting UV's with wire edges
23 changed files with 1350 additions and 307 deletions

View File

@ -385,9 +385,9 @@ def _template_items_uv_select_mode(params):
*_template_items_editmode_mesh_select_mode(params),
# Hack to prevent fall-through, when sync select isn't enabled (and the island button isn't visible).
("mesh.select_mode", {"type": 'FOUR', "value": 'PRESS'}, None),
*(("wm.context_set_enum", {"type": NUMBERS_1[i], "value": 'PRESS'},
{"properties": [("data_path", 'tool_settings.uv_select_mode'), ("value", ty)]})
for i, ty in enumerate(('VERTEX', 'EDGE', 'FACE', 'ISLAND')))
*(("uv.select_mode", {"type": k, "value": 'PRESS'},
{"properties": [("type", e)]})
for k, e in (('ONE', 'VERTEX'), ('TWO', 'EDGE'), ('THREE', 'FACE'), ('FOUR', 'ISLAND')))
]

View File

@ -554,14 +554,14 @@ def km_uv_editor(params):
("wm.search_menu", {"type": 'TAB', "value": 'PRESS'}, None),
# Selection modes.
*_template_items_editmode_mesh_select_mode(params),
("wm.context_set_enum", {"type": 'ONE', "value": 'PRESS'},
{"properties": [("data_path", 'tool_settings.uv_select_mode'), ("value", 'VERTEX')]}),
("wm.context_set_enum", {"type": 'TWO', "value": 'PRESS'},
{"properties": [("data_path", 'tool_settings.uv_select_mode'), ("value", 'EDGE')]}),
("wm.context_set_enum", {"type": 'THREE', "value": 'PRESS'},
{"properties": [("data_path", 'tool_settings.uv_select_mode'), ("value", 'FACE')]}),
("wm.context_set_enum", {"type": 'FOUR', "value": 'PRESS'},
{"properties": [("data_path", 'tool_settings.uv_select_mode'), ("value", 'ISLAND')]}),
("uv.select_mode", {"type": 'ONE', "value": 'PRESS'},
{"properties": [("type", 'VERTEX')]}),
("uv.select_mode", {"type": 'TWO', "value": 'PRESS'},
{"properties": [("type", 'EDGE')]}),
("uv.select_mode", {"type": 'THREE', "value": 'PRESS'},
{"properties": [("type", 'FACE')]}),
("uv.select_mode", {"type": 'FOUR', "value": 'PRESS'},
{"properties": [("type", 'ISLAND')]}),
("uv.select", {"type": 'LEFTMOUSE', "value": 'CLICK'},
{"properties": [("extend", False), ("deselect_all", True)]}),

View File

@ -779,7 +779,17 @@ class IMAGE_HT_header(Header):
if tool_settings.use_uv_select_sync:
layout.template_edit_mode_selection()
else:
layout.prop(tool_settings, "uv_select_mode", text="", expand=True)
row = layout.row(align=True)
uv_select_mode = tool_settings.uv_select_mode[:]
row.operator("uv.select_mode", text="", icon='UV_VERTEXSEL',
depress=(uv_select_mode == 'VERTEX')).type = 'VERTEX'
row.operator("uv.select_mode", text="", icon='UV_EDGESEL',
depress=(uv_select_mode == 'EDGE')).type = 'EDGE'
row.operator("uv.select_mode", text="", icon='UV_FACESEL',
depress=(uv_select_mode == 'FACE')).type = 'FACE'
row.operator("uv.select_mode", text="", icon='UV_ISLANDSEL',
depress=(uv_select_mode == 'ISLAND')).type = 'ISLAND'
layout.prop(tool_settings, "uv_sticky_select_mode", icon_only=True)
IMAGE_MT_editor_menus.draw_collapsible(context, layout)

View File

@ -185,7 +185,8 @@ struct LinkNode *BM_mesh_calc_path_uv_vert(BMesh *bm,
/** \name BM_mesh_calc_path_uv_edge
* \{ */
/* TODO(campbell): not very urgent, since the operator fakes this using vertex path. */
/* TODO(@sidd017): Setting this as todo, since we now support proper UV edge selection (D12028).
* Till then, continue using vertex path to fake shortest path calculation for edges. */
/** \} */

View File

@ -113,6 +113,11 @@ void OVERLAY_edit_uv_init(OVERLAY_Data *vedata)
const bool do_uv_overlay = is_image_type && is_uv_editor && has_edit_object;
const bool show_modified_uvs = sima->flag & SI_DRAWSHADOW;
const bool is_tiled_image = image && (image->source == IMA_SRC_TILED);
const bool do_edges_only = (ts->uv_flag & UV_SYNC_SELECTION) ?
/* NOTE: Ignore #SCE_SELECT_EDGE because a single selected edge
* on the mesh may cause singe UV vertices to be selected. */
false :
(ts->uv_selectmode == UV_SELECT_EDGE);
const bool do_faces = ((sima->flag & SI_NO_DRAWFACES) == 0);
const bool do_face_dots = (ts->uv_flag & UV_SYNC_SELECTION) ?
(ts->selectmode & SCE_SELECT_FACE) != 0 :
@ -124,6 +129,7 @@ void OVERLAY_edit_uv_init(OVERLAY_Data *vedata)
(brush->imagepaint_tool == PAINT_TOOL_CLONE) &&
brush->clone.image;
pd->edit_uv.do_verts = show_overlays && (!do_edges_only);
pd->edit_uv.do_faces = show_overlays && do_faces && !do_uvstretching_overlay;
pd->edit_uv.do_face_dots = show_overlays && do_faces && do_face_dots;
pd->edit_uv.do_uv_overlay = show_overlays && do_uv_overlay;
@ -183,7 +189,11 @@ void OVERLAY_edit_uv_cache_init(OVERLAY_Data *vedata)
DRW_PASS_CREATE(psl->edit_uv_edges_ps,
DRW_STATE_WRITE_COLOR | DRW_STATE_WRITE_DEPTH | DRW_STATE_DEPTH_LESS_EQUAL |
DRW_STATE_BLEND_ALPHA);
GPUShader *sh = OVERLAY_shader_edit_uv_edges_get();
const bool do_edges_only = (ts->uv_flag & UV_SYNC_SELECTION) ?
false :
(ts->uv_selectmode & UV_SELECT_EDGE);
GPUShader *sh = do_edges_only ? OVERLAY_shader_edit_uv_edges_for_edge_select_get() :
OVERLAY_shader_edit_uv_edges_get();
if (pd->edit_uv.do_uv_shadow_overlay) {
pd->edit_uv_shadow_edges_grp = DRW_shgroup_create(sh, psl->edit_uv_edges_ps);
DRW_shgroup_uniform_block(pd->edit_uv_shadow_edges_grp, "globalsBlock", G_draw.block_ubo);
@ -211,11 +221,14 @@ void OVERLAY_edit_uv_cache_init(OVERLAY_Data *vedata)
}
if (pd->edit_uv.do_uv_overlay) {
/* uv verts */
{
if (pd->edit_uv.do_verts || pd->edit_uv.do_face_dots) {
DRW_PASS_CREATE(psl->edit_uv_verts_ps,
DRW_STATE_WRITE_COLOR | DRW_STATE_WRITE_DEPTH | DRW_STATE_DEPTH_LESS_EQUAL |
DRW_STATE_BLEND_ALPHA);
}
/* uv verts */
if (pd->edit_uv.do_verts) {
GPUShader *sh = OVERLAY_shader_edit_uv_verts_get();
pd->edit_uv_verts_grp = DRW_shgroup_create(sh, psl->edit_uv_verts_ps);
@ -430,9 +443,11 @@ static void overlay_edit_uv_cache_populate(OVERLAY_Data *vedata, Object *ob)
if (geom) {
DRW_shgroup_call_obmat(pd->edit_uv_edges_grp, geom, NULL);
}
geom = DRW_mesh_batch_cache_get_edituv_verts(ob, ob->data);
if (geom) {
DRW_shgroup_call_obmat(pd->edit_uv_verts_grp, geom, NULL);
if (pd->edit_uv.do_verts) {
geom = DRW_mesh_batch_cache_get_edituv_verts(ob, ob->data);
if (geom) {
DRW_shgroup_call_obmat(pd->edit_uv_verts_grp, geom, NULL);
}
}
if (pd->edit_uv.do_faces) {
geom = DRW_mesh_batch_cache_get_edituv_faces(ob, ob->data);

View File

@ -359,6 +359,7 @@ typedef struct OVERLAY_PrivateData {
bool do_stencil_overlay;
bool do_mask_overlay;
bool do_verts;
bool do_faces;
bool do_face_dots;
@ -699,6 +700,7 @@ GPUShader *OVERLAY_shader_edit_mesh_vert(void);
GPUShader *OVERLAY_shader_edit_particle_strand(void);
GPUShader *OVERLAY_shader_edit_particle_point(void);
GPUShader *OVERLAY_shader_edit_uv_edges_get(void);
GPUShader *OVERLAY_shader_edit_uv_edges_for_edge_select_get(void);
GPUShader *OVERLAY_shader_edit_uv_face_get(void);
GPUShader *OVERLAY_shader_edit_uv_face_dots_get(void);
GPUShader *OVERLAY_shader_edit_uv_verts_get(void);

View File

@ -167,6 +167,7 @@ typedef struct OVERLAY_Shaders {
GPUShader *edit_uv_verts;
GPUShader *edit_uv_faces;
GPUShader *edit_uv_edges;
GPUShader *edit_uv_edges_for_edge_select;
GPUShader *edit_uv_face_dots;
GPUShader *edit_uv_stretching_angle;
GPUShader *edit_uv_stretching_area;
@ -1605,6 +1606,20 @@ GPUShader *OVERLAY_shader_edit_uv_edges_get(void)
return sh_data->edit_uv_edges;
}
GPUShader *OVERLAY_shader_edit_uv_edges_for_edge_select_get(void)
{
OVERLAY_Shaders *sh_data = &e_data.sh_data[0];
if (!sh_data->edit_uv_edges_for_edge_select) {
sh_data->edit_uv_edges_for_edge_select = DRW_shader_create_with_shaderlib(
datatoc_edit_uv_edges_vert_glsl,
datatoc_edit_uv_edges_geom_glsl,
datatoc_edit_uv_edges_frag_glsl,
e_data.lib,
"#define USE_EDGE_SELECT\n");
}
return sh_data->edit_uv_edges_for_edge_select;
}
GPUShader *OVERLAY_shader_edit_uv_face_get(void)
{
OVERLAY_Shaders *sh_data = &e_data.sh_data[0];

View File

@ -35,7 +35,13 @@ void main()
float line_distance = distance(stipplePos_f, stippleStart_f) / max(dd.x, dd.y);
if (lineStyle == OVERLAY_UV_LINE_STYLE_OUTLINE) {
#ifdef USE_EDGE_SELECT
/* TODO(@campbellbarton): The current wire-edit color contrast enough against the selection.
* Look into changing the default theme color instead of reducing contrast with edge-select. */
inner_color = (selectionFac_f != 0.0) ? colorEdgeSelect : (colorWireEdit * 0.5);
#else
inner_color = mix(colorWireEdit, colorEdgeSelect, selectionFac_f);
#endif
outer_color = vec4(vec3(0.0), 1.0);
}
else if (lineStyle == OVERLAY_UV_LINE_STYLE_DASH) {

View File

@ -53,11 +53,19 @@ void main()
vec2 line_dir = normalize(line);
vec2 line_perp = vec2(-line_dir.y, line_dir.x);
vec2 edge_ofs = line_perp * sizeViewportInv * ceil(half_size);
#ifdef USE_EDGE_SELECT
/* No blending with edge selection. */
float selectFac0 = selectionFac[0];
float selectFac1 = selectionFac[0];
#else
float selectFac0 = selectionFac[0];
float selectFac1 = selectionFac[1];
#endif
do_vertex(pos0, selectionFac[0], stippleStart[0], stipplePos[0], half_size, edge_ofs.xy);
do_vertex(pos0, selectionFac[0], stippleStart[0], stipplePos[0], -half_size, -edge_ofs.xy);
do_vertex(pos1, selectionFac[1], stippleStart[1], stipplePos[1], half_size, edge_ofs.xy);
do_vertex(pos1, selectionFac[1], stippleStart[1], stipplePos[1], -half_size, -edge_ofs.xy);
do_vertex(pos0, selectFac0, stippleStart[0], stipplePos[0], half_size, edge_ofs.xy);
do_vertex(pos0, selectFac0, stippleStart[0], stipplePos[0], -half_size, -edge_ofs.xy);
do_vertex(pos1, selectFac1, stippleStart[1], stipplePos[1], half_size, edge_ofs.xy);
do_vertex(pos1, selectFac1, stippleStart[1], stipplePos[1], -half_size, -edge_ofs.xy);
EndPrimitive();
}

View File

@ -19,7 +19,11 @@ void main()
gl_Position.xy = floor(gl_Position.xy * half_viewport_res) / half_viewport_res +
half_pixel_offset;
#ifdef USE_EDGE_SELECT
bool is_select = (flag & EDGE_UV_SELECT) != 0;
#else
bool is_select = (flag & VERT_UV_SELECT) != 0;
#endif
selectionFac = is_select ? 1.0 : 0.0;
/* Move selected edges to the top
* Vertices are between 0.0 and 0.2, Edges between 0.2 and 0.4

View File

@ -25,6 +25,7 @@ struct Scene;
struct SpaceImage;
struct ToolSettings;
struct ViewLayer;
struct bContext;
struct bNode;
struct bNodeTree;
struct wmKeyConfig;
@ -80,7 +81,8 @@ void ED_object_assign_active_image(struct Main *bmain,
bool ED_uvedit_test(struct Object *obedit);
/* visibility and selection */
/* Visibility and selection tests. */
bool uvedit_face_visible_test_ex(const struct ToolSettings *ts, struct BMFace *efa);
bool uvedit_face_select_test_ex(const struct ToolSettings *ts,
struct BMFace *efa,
@ -91,24 +93,50 @@ bool uvedit_edge_select_test_ex(const struct ToolSettings *ts,
bool uvedit_uv_select_test_ex(const struct ToolSettings *ts,
struct BMLoop *l,
int cd_loop_uv_offset);
bool uvedit_face_visible_test(const struct Scene *scene, struct BMFace *efa);
bool uvedit_face_select_test(const struct Scene *scene, struct BMFace *efa, int cd_loop_uv_offset);
bool uvedit_edge_select_test(const struct Scene *scene, struct BMLoop *l, int cd_loop_uv_offset);
bool uvedit_uv_select_test(const struct Scene *scene, struct BMLoop *l, int cd_loop_uv_offset);
/* uv face */
void uvedit_face_select_set_with_sticky(const struct Scene *scene,
struct BMEditMesh *em,
struct BMFace *efa,
bool select,
bool do_history,
int cd_loop_uv_offset);
/* Individual UV element selection functions. */
/**
* \brief Select UV Face
*
* Changes selection state of a single UV Face.
*/
void uvedit_face_select_set(const struct Scene *scene,
struct BMEditMesh *em,
struct BMFace *efa,
bool select,
bool do_history,
int cd_loop_uv_offset);
/**
* \brief Select UV Edge
*
* Changes selection state of a single UV Edge.
*/
void uvedit_edge_select_set(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
int cd_loop_uv_offset);
/**
* \brief Select UV Vertex
*
* Changes selection state of a single UV vertex.
*/
void uvedit_uv_select_set(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
int cd_loop_uv_offset);
/* Low level functions for (de)selecting individual UV elements. Ensure UV face visibility before
* use. */
void uvedit_face_select_enable(const struct Scene *scene,
struct BMEditMesh *em,
struct BMFace *efa,
@ -118,19 +146,6 @@ void uvedit_face_select_disable(const struct Scene *scene,
struct BMEditMesh *em,
struct BMFace *efa,
int cd_loop_uv_offset);
/* uv edge */
void uvedit_edge_select_set_with_sticky(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
uint cd_loop_uv_offset);
void uvedit_edge_select_set(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
int cd_loop_uv_offset);
void uvedit_edge_select_enable(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
@ -140,19 +155,6 @@ void uvedit_edge_select_disable(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
int cd_loop_uv_offset);
/* uv vert */
void uvedit_uv_select_set_with_sticky(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
uint cd_loop_uv_offset);
void uvedit_uv_select_set(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
int cd_loop_uv_offset);
void uvedit_uv_select_enable(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
@ -163,6 +165,83 @@ void uvedit_uv_select_disable(const struct Scene *scene,
struct BMLoop *l,
int cd_loop_uv_offset);
/* Sticky mode UV element selection functions. */
void uvedit_face_select_set_with_sticky(const struct Scene *scene,
struct BMEditMesh *em,
struct BMFace *efa,
bool select,
bool do_history,
int cd_loop_uv_offset);
void uvedit_edge_select_set_with_sticky(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
uint cd_loop_uv_offset);
void uvedit_uv_select_set_with_sticky(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
bool select,
bool do_history,
uint cd_loop_uv_offset);
/* Low level functions for sticky element selection (sticky mode independent). Type of sticky
* selection is specified explicitly (using sticky_flag, except for face selection). */
void uvedit_face_select_shared_vert(const struct Scene *scene,
struct BMEditMesh *em,
struct BMFace *efa,
const bool select,
const bool do_history,
const int cd_loop_uv_offset);
void uvedit_edge_select_shared_vert(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
const bool select,
const int sticky_flag,
const bool do_history,
const int cd_loop_uv_offset);
void uvedit_uv_select_shared_vert(const struct Scene *scene,
struct BMEditMesh *em,
struct BMLoop *l,
const bool select,
const int sticky_flag,
const bool do_history,
const int cd_loop_uv_offset);
/* Sets required UV edge flags as specified by the sticky_flag. */
void uvedit_edge_select_set_noflush(const struct Scene *scene,
struct BMLoop *l,
const bool select,
const int sticky_flag,
const int cd_loop_uv_offset);
/**
* \brief UV Select Mode set
*
* Updates selection state for UVs based on the select mode and sticky mode. Similar to
* #EDBM_selectmode_set.
*/
void ED_uvedit_selectmode_clean(struct Scene *scene, struct Object *obedit);
void ED_uvedit_selectmode_clean_multi(struct bContext *C);
/**
* \brief UV Select Mode Flush
*
* Flushes selections upwards as dictated by the UV select mode.
*/
void ED_uvedit_selectmode_flush(struct Scene *scene, struct BMEditMesh *em);
/**
* Mode independent UV de-selection flush.
*/
void uvedit_deselect_flush(struct Scene *scene, struct BMEditMesh *em);
/**
* Mode independent UV selection flush.
*/
void uvedit_select_flush(struct Scene *scene, struct BMEditMesh *em);
bool ED_uvedit_nearest_uv(const struct Scene *scene,
struct Object *obedit,
const float co[2],

View File

@ -88,6 +88,7 @@ typedef struct UndoMesh {
Mesh me;
int selectmode;
char uv_selectmode;
/** \note
* This isn't a perfect solution, if you edit keys and change shapes this works well
@ -769,6 +770,7 @@ static bool mesh_undosys_step_encode(struct bContext *C, struct Main *bmain, Und
/* Important not to use the 3D view when getting objects because all objects
* outside of this list will be moved out of edit-mode when reading back undo steps. */
ViewLayer *view_layer = CTX_data_view_layer(C);
ToolSettings *ts = CTX_data_tool_settings(C);
uint objects_len = 0;
Object **objects = ED_undo_editmode_objects_from_view_layer(view_layer, &objects_len);
@ -792,6 +794,7 @@ static bool mesh_undosys_step_encode(struct bContext *C, struct Main *bmain, Und
&elem->data, me->edit_mesh, me->key, um_references ? um_references[i] : NULL);
em->needs_flush_to_id = 1;
us->step.data_size += elem->data.undo_size;
elem->data.uv_selectmode = ts->uv_selectmode;
#ifdef USE_ARRAY_STORE
/** As this is only data storage it is safe to set the session ID here. */
@ -849,6 +852,7 @@ static void mesh_undosys_step_decode(struct bContext *C,
Scene *scene = CTX_data_scene(C);
scene->toolsettings->selectmode = us->elems[0].data.selectmode;
scene->toolsettings->uv_selectmode = us->elems[0].data.uv_selectmode;
bmain->is_memfile_undo_flush_needed = true;

View File

@ -70,11 +70,13 @@ bool uv_find_nearest_vert_multi(struct Scene *scene,
bool uv_find_nearest_edge(struct Scene *scene,
struct Object *obedit,
const float co[2],
float penalty,
struct UvNearestHit *hit);
bool uv_find_nearest_edge_multi(struct Scene *scene,
struct Object **objects,
uint objects_len,
const float co[2],
float penalty,
struct UvNearestHit *hit);
/**
@ -116,6 +118,16 @@ BMLoop *uv_find_nearest_loop_from_edge(struct Scene *scene,
struct BMEdge *e,
const float co[2]);
bool uvedit_vert_is_edge_select_any_other(const struct Scene *scene,
struct BMLoop *l,
const int cd_loop_uv_offset);
bool uvedit_vert_is_face_select_any_other(const struct Scene *scene,
struct BMLoop *l,
const int cd_loop_uv_offset);
bool uvedit_vert_is_all_other_faces_selected(const struct Scene *scene,
struct BMLoop *l,
const int cd_loop_uv_offset);
/* utility tool functions */
void uvedit_live_unwrap_update(struct SpaceImage *sima,
@ -169,3 +181,5 @@ void UV_OT_select_circle(struct wmOperatorType *ot);
void UV_OT_select_more(struct wmOperatorType *ot);
void UV_OT_select_less(struct wmOperatorType *ot);
void UV_OT_select_overlap(struct wmOperatorType *ot);
/* Used only when UV sync select is disabled. */
void UV_OT_select_mode(struct wmOperatorType *ot);

View File

@ -249,7 +249,7 @@ void ED_uvedit_select_all(BMesh *bm)
BM_ITER_MESH (efa, &iter, bm, BM_FACES_OF_MESH) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
luv->flag |= MLOOPUV_VERTSEL;
luv->flag |= (MLOOPUV_VERTSEL | MLOOPUV_EDGESEL);
}
}
}
@ -1406,12 +1406,15 @@ static void UV_OT_pin(wmOperatorType *ot)
/** \name Hide Operator
* \{ */
/* check if we are selected or unselected based on 'bool_test' arg,
* needed for select swap support */
#define UV_SEL_TEST(luv, bool_test) \
/* Check if vertex/edge is selected or unselected based on #bool_test arg. Needed for select swap
* support */
#define UV_VERT_SEL_TEST(luv, bool_test) \
((((luv)->flag & MLOOPUV_VERTSEL) == MLOOPUV_VERTSEL) == bool_test)
/* is every UV vert selected or unselected depending on bool_test */
#define UV_EDGE_SEL_TEST(luv, bool_test) \
((((luv)->flag & MLOOPUV_EDGESEL) == MLOOPUV_EDGESEL) == bool_test)
/* Is the specified UV face, selected or unselected depending on bool_test. */
static bool bm_face_is_all_uv_sel(BMFace *f, bool select_test, const int cd_loop_uv_offset)
{
BMLoop *l_iter;
@ -1420,7 +1423,7 @@ static bool bm_face_is_all_uv_sel(BMFace *f, bool select_test, const int cd_loop
l_iter = l_first = BM_FACE_FIRST_LOOP(f);
do {
MLoopUV *luv = BM_ELEM_CD_GET_VOID_P(l_iter, cd_loop_uv_offset);
if (!UV_SEL_TEST(luv, select_test)) {
if (!UV_EDGE_SEL_TEST(luv, select_test)) {
return false;
}
} while ((l_iter = l_iter->next) != l_first);
@ -1472,19 +1475,17 @@ static int uv_hide_exec(bContext *C, wmOperator *op)
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
if (UV_SEL_TEST(luv, !swap)) {
if (UV_VERT_SEL_TEST(luv, !swap) || UV_EDGE_SEL_TEST(luv, !swap)) {
hide = 1;
break;
}
}
if (hide) {
/* NOTE: a special case for edges could be used,
* for now edges act like verts and get flushed */
if (use_face_center) {
if (em->selectmode == SCE_SELECT_FACE) {
/* check that every UV is selected */
if (bm_face_is_all_uv_sel(efa, true, cd_loop_uv_offset) == !swap) {
/* Deselect BMesh face if UV face is (de)selected depending on #swap. */
if (bm_face_is_all_uv_sel(efa, !swap, cd_loop_uv_offset)) {
BM_face_select_set(em->bm, efa, false);
}
uvedit_face_select_disable(scene, em, efa, cd_loop_uv_offset);
@ -1493,7 +1494,12 @@ static int uv_hide_exec(bContext *C, wmOperator *op)
if (bm_face_is_all_uv_sel(efa, true, cd_loop_uv_offset) == !swap) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
if (UV_SEL_TEST(luv, !swap)) {
/* For both cases rely on edge sel tests, since all vert sel tests are invalid in
* case of sticky selections. */
if (UV_EDGE_SEL_TEST(luv, !swap) && (em->selectmode == SCE_SELECT_EDGE)) {
BM_edge_select_set(em->bm, l->e, false);
}
else if (UV_EDGE_SEL_TEST(luv, !swap) && (em->selectmode == SCE_SELECT_VERTEX)) {
BM_vert_select_set(em->bm, l->v, false);
}
}
@ -1504,29 +1510,57 @@ static int uv_hide_exec(bContext *C, wmOperator *op)
}
}
else if (em->selectmode == SCE_SELECT_FACE) {
/* check if a UV is de-selected */
if (bm_face_is_all_uv_sel(efa, false, cd_loop_uv_offset) != !swap) {
BM_face_select_set(em->bm, efa, false);
uvedit_face_select_disable(scene, em, efa, cd_loop_uv_offset);
/* Deselect BMesh face depending on the type of UV selectmode and the type of UV element
* being considered. */
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
if (UV_EDGE_SEL_TEST(luv, !swap) && (ts->uv_selectmode == UV_SELECT_EDGE)) {
BM_face_select_set(em->bm, efa, false);
break;
}
else if (UV_VERT_SEL_TEST(luv, !swap) && (ts->uv_selectmode == UV_SELECT_VERTEX)) {
BM_face_select_set(em->bm, efa, false);
break;
}
else if (ts->uv_selectmode == UV_SELECT_ISLAND) {
BM_face_select_set(em->bm, efa, false);
break;
}
}
uvedit_face_select_disable(scene, em, efa, cd_loop_uv_offset);
}
else {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
if (UV_SEL_TEST(luv, !swap)) {
BM_vert_select_set(em->bm, l->v, false);
if (!swap) {
luv->flag &= ~MLOOPUV_VERTSEL;
if (UV_EDGE_SEL_TEST(luv, !swap) && (ts->uv_selectmode == UV_SELECT_EDGE)) {
if (em->selectmode == SCE_SELECT_EDGE)
BM_edge_select_set(em->bm, l->e, false);
else {
BM_vert_select_set(em->bm, l->v, false);
BM_vert_select_set(em->bm, l->next->v, false);
}
}
else if (UV_VERT_SEL_TEST(luv, !swap) && (ts->uv_selectmode != UV_SELECT_EDGE)) {
if (em->selectmode == SCE_SELECT_EDGE)
BM_edge_select_set(em->bm, l->e, false);
else
BM_vert_select_set(em->bm, l->v, false);
}
}
if (!swap) {
uvedit_face_select_disable(scene, em, efa, cd_loop_uv_offset);
}
}
}
}
/* flush vertex selection changes */
/* Flush editmesh selections to ensure valid selection states. */
if (em->selectmode != SCE_SELECT_FACE) {
EDBM_selectmode_flush_ex(em, SCE_SELECT_VERTEX | SCE_SELECT_EDGE);
/* NOTE: Make sure correct flags are used. Previously this was done by passing
* (SCE_SELECT_VERTEX | SCE_SELECT_EDGE), which doesn't work now that we support proper UV
* edge selection. */
BM_mesh_select_mode_flush(em->bm);
}
BM_select_history_validate(em->bm);
@ -1540,7 +1574,8 @@ static int uv_hide_exec(bContext *C, wmOperator *op)
return OPERATOR_FINISHED;
}
#undef UV_SEL_TEST
#undef UV_VERT_SEL_TEST
#undef UV_EDGE_SEL_TEST
static void UV_OT_hide(wmOperatorType *ot)
{
@ -1567,12 +1602,10 @@ static void UV_OT_hide(wmOperatorType *ot)
static int uv_reveal_exec(bContext *C, wmOperator *op)
{
ViewLayer *view_layer = CTX_data_view_layer(C);
SpaceImage *sima = CTX_wm_space_image(C);
Scene *scene = CTX_data_scene(C);
const ToolSettings *ts = scene->toolsettings;
const bool use_face_center = (ts->uv_selectmode == UV_SELECT_FACE);
const bool stickymode = sima ? (ts->uv_sticky != SI_STICKY_DISABLE) : 1;
const bool select = RNA_boolean_get(op->ptr, "select");
uint objects_len = 0;
@ -1589,8 +1622,10 @@ static int uv_reveal_exec(bContext *C, wmOperator *op)
const int cd_loop_uv_offset = CustomData_get_offset(&em->bm->ldata, CD_MLOOPUV);
/* note on tagging, selecting faces needs to be delayed so it doesn't select the verts and
* confuse our checks on selected verts. */
/* NOTE: Selecting faces is delayed so that it doesn't select verts/edges and confuse certain
* UV selection checks.
* This creates a temporary state which breaks certain UV selection functions that do face
* visibilty checks internally. Current implementation handles each case separately. */
/* call the mesh function if we are in mesh sync sel */
if (ts->uv_flag & UV_SYNC_SELECTION) {
@ -1604,6 +1639,11 @@ static int uv_reveal_exec(bContext *C, wmOperator *op)
}
continue;
}
/* NOTE(@sidd017): Supporting selections in all cases is quite difficult considering there are
* at least 12 cases to look into (3 mesh selectmodes + 4 uv selectmodes + sticky modes).
* For now we select all UV faces as sticky disabled to ensure proper UV selection states (vert
* + edge flags) */
if (use_face_center) {
if (em->selectmode == SCE_SELECT_FACE) {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
@ -1611,7 +1651,7 @@ static int uv_reveal_exec(bContext *C, wmOperator *op)
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, MLOOPUV_VERTSEL);
SET_FLAG_FROM_TEST(luv->flag, select, (MLOOPUV_VERTSEL | MLOOPUV_EDGESEL));
}
/* BM_face_select_set(em->bm, efa, true); */
BM_elem_flag_enable(efa, BM_ELEM_TAG);
@ -1619,42 +1659,27 @@ static int uv_reveal_exec(bContext *C, wmOperator *op)
}
}
else {
/* enable adjacent faces to have disconnected UV selections if sticky is disabled */
if (!stickymode) {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) &&
!BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
int totsel = 0;
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
int totsel = 0;
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (em->selectmode == SCE_SELECT_VERTEX) {
totsel += BM_elem_flag_test(l->v, BM_ELEM_SELECT);
}
else if (em->selectmode == SCE_SELECT_EDGE) {
totsel += BM_elem_flag_test(l->e, BM_ELEM_SELECT);
}
}
if (!totsel) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, MLOOPUV_VERTSEL);
}
/* BM_face_select_set(em->bm, efa, true); */
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
}
}
}
else {
BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) {
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) &&
!BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
if (!totsel) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (BM_elem_flag_test(l->v, BM_ELEM_SELECT) == 0) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, MLOOPUV_VERTSEL);
}
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, (MLOOPUV_VERTSEL | MLOOPUV_EDGESEL));
}
/* BM_face_select_set(em->bm, efa, true); */
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
/* BM_face_select_set(em->bm, efa, true); */
BM_elem_flag_enable(efa, BM_ELEM_TAG);
}
}
}
@ -1665,7 +1690,7 @@ static int uv_reveal_exec(bContext *C, wmOperator *op)
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, MLOOPUV_VERTSEL);
SET_FLAG_FROM_TEST(luv->flag, select, (MLOOPUV_VERTSEL | MLOOPUV_EDGESEL));
}
/* BM_face_select_set(em->bm, efa, true); */
BM_elem_flag_enable(efa, BM_ELEM_TAG);
@ -1677,10 +1702,8 @@ static int uv_reveal_exec(bContext *C, wmOperator *op)
BM_elem_flag_disable(efa, BM_ELEM_TAG);
if (!BM_elem_flag_test(efa, BM_ELEM_HIDDEN) && !BM_elem_flag_test(efa, BM_ELEM_SELECT)) {
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
if (BM_elem_flag_test(l->v, BM_ELEM_SELECT) == 0) {
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, MLOOPUV_VERTSEL);
}
luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
SET_FLAG_FROM_TEST(luv->flag, select, (MLOOPUV_VERTSEL | MLOOPUV_EDGESEL));
}
/* BM_face_select_set(em->bm, efa, true); */
BM_elem_flag_enable(efa, BM_ELEM_TAG);
@ -2022,6 +2045,7 @@ void ED_operatortypes_uvedit(void)
WM_operatortype_append(UV_OT_select_more);
WM_operatortype_append(UV_OT_select_less);
WM_operatortype_append(UV_OT_select_overlap);
WM_operatortype_append(UV_OT_select_mode);
WM_operatortype_append(UV_OT_snap_cursor);
WM_operatortype_append(UV_OT_snap_selected);

View File

@ -234,6 +234,9 @@ static int mouse_mesh_uv_shortest_path_vert(Scene *scene,
const int cd_loop_uv_offset)
{
const char uv_selectmode = ED_uvedit_select_mode_get(scene);
/* TODO(@sidd017): Implement logic to calculate shortest path for UV edges, since we now support
* proper edge selection for UVs (D12028).
* Till then continue using vertex path to fake shortest path calculation for edges. */
const bool use_fake_edge_select = (uv_selectmode & UV_SELECT_EDGE);
BMEditMesh *em = BKE_editmesh_from_object(obedit);
BMesh *bm = em->bm;
@ -377,7 +380,7 @@ static bool facetag_test_cb(BMFace *f, void *user_data_v)
BMIter iter;
BMLoop *l_iter;
BM_ITER_ELEM (l_iter, &iter, f, BM_LOOPS_OF_FACE) {
if (!uvedit_uv_select_test(scene, l_iter, cd_loop_uv_offset)) {
if (!uvedit_edge_select_test(scene, l_iter, cd_loop_uv_offset)) {
return false;
}
}
@ -531,8 +534,21 @@ static bool uv_shortest_path_pick_ex(Scene *scene,
* flush the selection from the vertices. */
BM_mesh_select_mode_flush_ex(em->bm, SCE_SELECT_VERTEX, BM_SELECT_LEN_FLUSH_RECALC_ALL);
}
ED_uvedit_select_sync_flush(scene->toolsettings, em, select);
}
else {
if (uv_selectmode & UV_SELECT_EDGE) {
/* TODO(@sidd017): Remove this case when adding proper uv edge support for this operator.
* In the meantime, this case helps ensures proper UV selection states for edge mode. */
if (select) {
uvedit_select_flush(scene, em);
}
else {
uvedit_deselect_flush(scene, em);
}
}
ED_uvedit_selectmode_flush(scene, em);
}
ED_uvedit_select_sync_flush(scene->toolsettings, em, select);
}
if (ts->uv_flag & UV_SYNC_SELECTION) {
@ -603,7 +619,7 @@ static int uv_shortest_path_pick_invoke(bContext *C, wmOperator *op, const wmEve
else if (uv_selectmode & UV_SELECT_EDGE) {
UvNearestHit hit = UV_NEAREST_HIT_INIT_MAX(&region->v2d);
if (!uv_find_nearest_edge(scene, obedit, co, &hit)) {
if (!uv_find_nearest_edge(scene, obedit, co, 0.0f, &hit)) {
return OPERATOR_CANCELLED;
}

View File

@ -52,7 +52,7 @@
/** Unordered loop data, stored in #BMLoop.head.index. */
typedef struct ULData {
/** When this UV is selected as well as the next UV. */
/** When the specified UV edge is selected. */
uint is_select_edge : 1;
/**
* When only this UV is selected and none of the other UV's
@ -762,15 +762,17 @@ static bool uv_rip_object(Scene *scene, Object *obedit, const float co[2], const
const MLoopUV *luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
if (luv->flag & MLOOPUV_VERTSEL) {
const MLoopUV *luv_prev = BM_ELEM_CD_GET_VOID_P(l->prev, cd_loop_uv_offset);
const MLoopUV *luv_next = BM_ELEM_CD_GET_VOID_P(l->next, cd_loop_uv_offset);
if (luv_next->flag & MLOOPUV_VERTSEL) {
if (luv->flag & MLOOPUV_EDGESEL) {
UL(l)->is_select_edge = true;
}
else if ((luv_prev->flag & MLOOPUV_EDGESEL) == 0) {
/* #bm_loop_uv_select_single_vert_validate validates below. */
UL(l)->is_select_vert_single = true;
is_all = false;
}
else {
if ((luv_prev->flag & MLOOPUV_VERTSEL) == 0) {
/* #bm_loop_uv_select_single_vert_validate validates below. */
UL(l)->is_select_vert_single = true;
}
/* Cases where all vertices of a face are selected but not all edges are selected. */
is_all = false;
}
}
else {
@ -797,7 +799,7 @@ static bool uv_rip_object(Scene *scene, Object *obedit, const float co[2], const
}
}
/* Special case: if we have selected faces, isolated them.
/* Special case: if we have selected faces, isolate them.
* This isn't a rip, however it's useful for users as a quick way
* to detach the selection.
*
@ -812,6 +814,10 @@ static bool uv_rip_object(Scene *scene, Object *obedit, const float co[2], const
luv->flag &= ~MLOOPUV_VERTSEL;
changed = true;
}
if (luv->flag & MLOOPUV_EDGESEL) {
luv->flag &= ~MLOOPUV_EDGESEL;
changed = true;
}
}
}
}
@ -871,6 +877,9 @@ static bool uv_rip_object(Scene *scene, Object *obedit, const float co[2], const
}
}
}
if (changed) {
uvedit_deselect_flush(scene, em);
}
return changed;
}

File diff suppressed because it is too large Load Diff

View File

@ -2545,7 +2545,7 @@ static StitchState *stitch_select(bContext *C,
return state;
}
}
else if (uv_find_nearest_edge_multi(scene, ssc->objects, ssc->objects_len, co, &hit)) {
else if (uv_find_nearest_edge_multi(scene, ssc->objects, ssc->objects_len, co, 0.0f, &hit)) {
/* find StitchState from hit->ob */
StitchState *state = NULL;
for (uint ob_index = 0; ob_index < ssc->objects_len; ob_index++) {

View File

@ -119,7 +119,7 @@ static bool ED_uvedit_ensure_uvs(Object *obedit)
BM_ITER_ELEM (l, &liter, efa, BM_LOOPS_OF_FACE) {
MLoopUV *luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset);
luv->flag |= MLOOPUV_VERTSEL;
luv->flag |= (MLOOPUV_VERTSEL | MLOOPUV_EDGESEL);
}
}

View File

@ -327,7 +327,7 @@ typedef struct MLoopUV {
/** #MLoopUV.flag */
enum {
/* MLOOPUV_DEPRECATED = (1 << 0), MLOOPUV_EDGESEL removed */
MLOOPUV_EDGESEL = (1 << 0),
MLOOPUV_VERTSEL = (1 << 1),
MLOOPUV_PINNED = (1 << 2),
};

View File

@ -2148,6 +2148,10 @@ static void rna_def_mloopuv(BlenderRNA *brna)
prop = RNA_def_property(srna, "select", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "flag", MLOOPUV_VERTSEL);
RNA_def_property_ui_text(prop, "UV Select", "");
prop = RNA_def_property(srna, "select_edge", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "flag", MLOOPUV_EDGESEL);
RNA_def_property_ui_text(prop, "UV Edge Select", "");
}
static void rna_def_mloopcol(BlenderRNA *brna)

View File

@ -34,6 +34,7 @@
#include "ED_gpencil.h"
#include "ED_object.h"
#include "ED_uvedit.h"
#include "RNA_define.h"
#include "RNA_enum_types.h"
@ -1835,6 +1836,13 @@ static void rna_Scene_editmesh_select_mode_update(bContext *C, PointerRNA *UNUSE
}
}
static void rna_Scene_uv_select_mode_update(bContext *C, PointerRNA *UNUSED(ptr))
{
/* Makes sure that the UV selection states are consistent with the current UV select mode and
* sticky mode.*/
ED_uvedit_selectmode_clean_multi(C);
}
static void object_simplify_update(Object *ob)
{
ModifierData *md;
@ -3463,7 +3471,8 @@ static void rna_def_tool_settings(BlenderRNA *brna)
RNA_def_property_enum_sdna(prop, NULL, "uv_selectmode");
RNA_def_property_enum_items(prop, rna_enum_mesh_select_mode_uv_items);
RNA_def_property_ui_text(prop, "UV Selection Mode", "UV selection and display mode");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_IMAGE, NULL);
RNA_def_property_flag(prop, PROP_CONTEXT_UPDATE);
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_IMAGE, "rna_Scene_uv_select_mode_update");
prop = RNA_def_property(srna, "uv_sticky_select_mode", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, NULL, "uv_sticky");

View File

@ -56,6 +56,7 @@ static int bpy_bmloopuv_uv_set(BPy_BMLoopUV *self, PyObject *value, void *UNUSED
PyDoc_STRVAR(bpy_bmloopuv_flag__pin_uv_doc, "UV pin state.\n\n:type: boolean");
PyDoc_STRVAR(bpy_bmloopuv_flag__select_doc, "UV select state.\n\n:type: boolean");
PyDoc_STRVAR(bpy_bmloopuv_flag__select_edge_doc, "UV edge select state.\n\n:type: boolean");
static PyObject *bpy_bmloopuv_flag_get(BPy_BMLoopUV *self, void *flag_p)
{
@ -93,6 +94,11 @@ static PyGetSetDef bpy_bmloopuv_getseters[] = {
(setter)bpy_bmloopuv_flag_set,
bpy_bmloopuv_flag__select_doc,
(void *)MLOOPUV_VERTSEL},
{"select_edge",
(getter)bpy_bmloopuv_flag_get,
(setter)bpy_bmloopuv_flag_set,
bpy_bmloopuv_flag__select_edge_doc,
(void *)MLOOPUV_EDGESEL},
{NULL, NULL, NULL, NULL, NULL} /* Sentinel */
};