From 919acbbf7f25f79b5a47720705f3a2d0d1dfc950 Mon Sep 17 00:00:00 2001 From: Eitan Traurig Date: Mon, 1 Sep 2025 13:57:55 -0400 Subject: [PATCH] Refactor: Convert Copy Mirror UV Coordinates operator from Python to C++ Migrate Copy Mirror UV Coordinates operator to C++. The ID has changed from `mesh.faces_mirror_uv` to `uv.copy_mirrored_faces` Ref !145531 --- scripts/startup/bl_operators/mesh.py | 188 ----------------- scripts/startup/bl_ui/space_image.py | 2 +- .../blender/editors/uvedit/uvedit_intern.hh | 1 + source/blender/editors/uvedit/uvedit_ops.cc | 190 ++++++++++++++++++ 4 files changed, 192 insertions(+), 189 deletions(-) diff --git a/scripts/startup/bl_operators/mesh.py b/scripts/startup/bl_operators/mesh.py index c27eb5f71cc..3931bd442cb 100644 --- a/scripts/startup/bl_operators/mesh.py +++ b/scripts/startup/bl_operators/mesh.py @@ -5,193 +5,6 @@ import bpy from bpy.types import Operator -from bpy.props import ( - EnumProperty, - IntProperty, -) -from bpy.app.translations import pgettext_rpt as rpt_ - - -class MeshMirrorUV(Operator): - """Copy mirror UV coordinates on the X axis based on a mirrored mesh""" - bl_idname = "mesh.faces_mirror_uv" - bl_label = "Copy Mirrored UV Coords" - bl_options = {'REGISTER', 'UNDO'} - - direction: EnumProperty( - name="Axis Direction", - items=( - ('POSITIVE', "Positive", ""), - ('NEGATIVE', "Negative", ""), - ), - ) - - precision: IntProperty( - name="Precision", - description=("Tolerance for finding vertex duplicates"), - min=1, max=16, - soft_min=1, soft_max=16, - default=3, - ) - - # Returns has_active_UV_layer, double_warn. - def do_mesh_mirror_UV(self, mesh, DIR): - precision = self.precision - double_warn = 0 - - if not mesh.uv_layers.active: - # has_active_UV_layer, double_warn - return False, 0 - - # mirror lookups - mirror_gt = {} - mirror_lt = {} - - vcos = (v.co.to_tuple(precision) for v in mesh.vertices) - - for i, co in enumerate(vcos): - if co[0] >= 0.0: - double_warn += co in mirror_gt - mirror_gt[co] = i - if co[0] <= 0.0: - double_warn += co in mirror_lt - mirror_lt[co] = i - - vmap = {} - for mirror_a, mirror_b in ( - (mirror_gt, mirror_lt), - (mirror_lt, mirror_gt), - ): - for co, i in mirror_a.items(): - nco = (-co[0], co[1], co[2]) - j = mirror_b.get(nco) - if j is not None: - vmap[i] = j - - polys = mesh.polygons - loops = mesh.loops - uv_loops = mesh.uv_layers.active.data - nbr_polys = len(polys) - - mirror_pm = {} - pmap = {} - puvs = [None] * nbr_polys - puvs_cpy = [None] * nbr_polys - puvsel = [None] * nbr_polys - pcents = [None] * nbr_polys - vidxs = [None] * nbr_polys - for i, p in enumerate(polys): - lstart = lend = p.loop_start - lend += p.loop_total - puvs[i] = tuple(uv.uv for uv in uv_loops[lstart:lend]) - puvs_cpy[i] = tuple(uv.copy() for uv in puvs[i]) - puvsel[i] = (False not in - (uv.select for uv in uv_loops[lstart:lend])) - # Vert index of the poly. - vidxs[i] = tuple(l.vertex_index for l in loops[lstart:lend]) - pcents[i] = p.center - # Preparing next step finding matching polys. - mirror_pm[tuple(sorted(vidxs[i]))] = i - - for i in range(nbr_polys): - # Find matching mirror poly. - tvidxs = [vmap.get(j) for j in vidxs[i]] - if None not in tvidxs: - tvidxs.sort() - j = mirror_pm.get(tuple(tvidxs)) - if j is not None: - pmap[i] = j - - for i, j in pmap.items(): - if not puvsel[i] or not puvsel[j]: - continue - if DIR == 0 and pcents[i][0] < 0.0: - continue - if DIR == 1 and pcents[i][0] > 0.0: - continue - - # copy UVs - uv1 = puvs[i] - uv2 = puvs_cpy[j] - - # get the correct rotation - v1 = vidxs[j] - v2 = tuple(vmap[k] for k in vidxs[i]) - - if len(v1) == len(v2): - for k in range(len(v1)): - k_map = v1.index(v2[k]) - uv1[k].xy = - (uv2[k_map].x - 0.5) + 0.5, uv2[k_map].y - - # has_active_UV_layer, double_warn - return True, double_warn - - @classmethod - def poll(cls, context): - obj = context.view_layer.objects.active - return (obj and obj.type == 'MESH') - - def execute(self, context): - DIR = (self.direction == 'NEGATIVE') - - total_no_active_UV = 0 - total_duplicates = 0 - meshes_with_duplicates = 0 - - ob = context.view_layer.objects.active - is_editmode = (ob.mode == 'EDIT') - if is_editmode: - bpy.ops.object.mode_set(mode='OBJECT', toggle=False) - - meshes = [ - ob.data for ob in context.view_layer.objects.selected - if ob.type == 'MESH' and ob.data.is_editable - ] - - for mesh in meshes: - mesh.tag = False - - for mesh in meshes: - if mesh.tag: - continue - - mesh.tag = True - - has_active_UV_layer, double_warn = self.do_mesh_mirror_UV(mesh, DIR) - - if not has_active_UV_layer: - total_no_active_UV = total_no_active_UV + 1 - - elif double_warn: - total_duplicates += double_warn - meshes_with_duplicates = meshes_with_duplicates + 1 - - if is_editmode: - bpy.ops.object.mode_set(mode='EDIT', toggle=False) - - if total_duplicates and total_no_active_UV: - self.report( - {'WARNING'}, - rpt_( - "{:d} mesh(es) with no active UV layer, " - "{:d} duplicates found in {:d} mesh(es), mirror may be incomplete" - ).format(total_no_active_UV, total_duplicates, meshes_with_duplicates), - ) - elif total_no_active_UV: - self.report( - {'WARNING'}, - rpt_("{:d} mesh(es) with no active UV layer").format(total_no_active_UV), - ) - elif total_duplicates: - self.report( - {'WARNING'}, - rpt_( - "{:d} duplicates found in {:d} mesh(es), mirror may be incomplete" - ).format(total_duplicates, meshes_with_duplicates), - ) - - return {'FINISHED'} - class MeshSelectNext(Operator): """Select the next element (using selection order)""" @@ -244,7 +57,6 @@ class MeshSelectPrev(Operator): classes = ( - MeshMirrorUV, MeshSelectNext, MeshSelectPrev, ) diff --git a/scripts/startup/bl_ui/space_image.py b/scripts/startup/bl_ui/space_image.py index ee43ce2e2f5..6c412af09dc 100644 --- a/scripts/startup/bl_ui/space_image.py +++ b/scripts/startup/bl_ui/space_image.py @@ -352,7 +352,7 @@ class IMAGE_MT_uvs_mirror(Menu): def draw(self, _context): layout = self.layout - layout.operator("mesh.faces_mirror_uv") + layout.operator("uv.copy_mirrored_faces") layout.separator() diff --git a/source/blender/editors/uvedit/uvedit_intern.hh b/source/blender/editors/uvedit/uvedit_intern.hh index 1b3f7f25b29..611e670d6c4 100644 --- a/source/blender/editors/uvedit/uvedit_intern.hh +++ b/source/blender/editors/uvedit/uvedit_intern.hh @@ -117,6 +117,7 @@ void UV_OT_unwrap(wmOperatorType *ot); void UV_OT_rip(wmOperatorType *ot); void UV_OT_stitch(wmOperatorType *ot); void UV_OT_smart_project(wmOperatorType *ot); +void UV_OT_copy_mirrored_faces(wmOperatorType *ot); /* uvedit_copy_paste.cc */ void UV_OT_copy(wmOperatorType *ot); diff --git a/source/blender/editors/uvedit/uvedit_ops.cc b/source/blender/editors/uvedit/uvedit_ops.cc index 2df3ee73753..f7e2598e441 100644 --- a/source/blender/editors/uvedit/uvedit_ops.cc +++ b/source/blender/editors/uvedit/uvedit_ops.cc @@ -2254,6 +2254,195 @@ static void UV_OT_mark_seam(wmOperatorType *ot) RNA_def_boolean(ot->srna, "clear", false, "Clear Seams", "Clear instead of marking seams"); } +static bool uv_copy_mirrored_faces(BMesh *bm, int direction, int precision, int *r_double_warn) +{ + *r_double_warn = 0; + const float precision_scale = powf(10.0f, precision); + /* TODO: replace mirror look-ups with #EditMeshSymmetryHelper. */ + Map mirror_gt, mirror_lt; + Map vmap; + + BMVert *v; + BMIter iter; + BM_ITER_MESH (v, &iter, bm, BM_VERTS_OF_MESH) { + float3 pos = math::round(float3(v->co) * precision_scale); + if (pos.x >= 0.0f) { + if (mirror_gt.contains(pos)) { + (*r_double_warn)++; + } + mirror_gt.add(pos, v); + } + if (pos.x <= 0.0f) { + if (mirror_lt.contains(pos)) { + (*r_double_warn)++; + } + mirror_lt.add(pos, v); + } + } + + for (const auto &[pos, v] : mirror_gt.items()) { + float3 mirror_pos = pos; + mirror_pos[0] = -mirror_pos[0]; + BMVert *v_mirror = mirror_lt.lookup_default(mirror_pos, nullptr); + if (v_mirror) { + vmap.add(v, v_mirror); + } + } + for (const auto &[pos, v] : mirror_lt.items()) { + float3 mirror_pos = pos; + mirror_pos[0] = -mirror_pos[0]; + BMVert *v_mirror = mirror_gt.lookup_default(mirror_pos, nullptr); + if (v_mirror) { + vmap.add(v, v_mirror); + } + } + + Map, BMFace *> sorted_verts_to_face; + /* Maps faces to their corresponding mirrored face. */ + Map face_map; + + BMFace *f; + BMIter iter_face; + BM_ITER_MESH (f, &iter_face, bm, BM_FACES_OF_MESH) { + Array sorted_verts(f->len); + bool valid = true; + int loop_index = 0; + BMLoop *l; + BMIter liter; + BM_ITER_ELEM_INDEX (l, &liter, f, BM_LOOPS_OF_FACE, loop_index) { + if (!vmap.contains(l->v)) { + valid = false; + break; + } + sorted_verts[loop_index] = l->v; + } + if (valid) { + std::sort(sorted_verts.begin(), sorted_verts.end()); + sorted_verts_to_face.add(std::move(sorted_verts), f); + } + } + + for (const auto &[sorted_verts, f_dst] : sorted_verts_to_face.items()) { + Array mirror_verts(sorted_verts.size()); + for (int index = 0; index < sorted_verts.size(); index++) { + mirror_verts[index] = vmap.lookup_default(sorted_verts[index], nullptr); + } + std::sort(mirror_verts.begin(), mirror_verts.end()); + BMFace *f_src = sorted_verts_to_face.lookup_default(mirror_verts, nullptr); + if (f_src) { + if (f_src != f_dst) { + face_map.add(f_dst, f_src); + } + } + } + + const int cd_loop_uv_offset = CustomData_get_offset(&bm->ldata, CD_PROP_FLOAT2); + + bool changed = false; + for (const auto &[f_dst, f_src] : face_map.items()) { + + { + float f_dst_center[3]; + BM_face_calc_center_median(f_dst, f_dst_center); + if (direction ? (f_dst_center[0] > 0.0f) : (f_dst_center[0] < 0.0f)) { + continue; + } + } + + BMIter liter; + BMLoop *l_dst; + + BM_ITER_ELEM (l_dst, &liter, f_dst, BM_LOOPS_OF_FACE) { + BMVert *v_src = vmap.lookup_default(l_dst->v, nullptr); + if (!v_src) { + continue; + } + + BMLoop *l_src = BM_face_vert_share_loop(f_src, v_src); + if (!l_src) { + continue; + } + const float *uv_src = BM_ELEM_CD_GET_FLOAT_P(l_src, cd_loop_uv_offset); + float *uv_dst = BM_ELEM_CD_GET_FLOAT_P(l_dst, cd_loop_uv_offset); + + uv_dst[0] = -(uv_src[0] - 0.5f) + 0.5f; + uv_dst[1] = uv_src[1]; + changed = true; + } + } + + return changed; +} + +static wmOperatorStatus uv_copy_mirrored_faces_exec(bContext *C, wmOperator *op) +{ + Scene *scene = CTX_data_scene(C); + ViewLayer *view_layer = CTX_data_view_layer(C); + Vector objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs( + scene, view_layer, nullptr); + const int direction = RNA_enum_get(op->ptr, "direction"); + const int precision = RNA_int_get(op->ptr, "precision"); + + int total_duplicates = 0; + int meshes_with_duplicates = 0; + + for (Object *obedit : objects) { + BMEditMesh *em = BKE_editmesh_from_object(obedit); + + int double_warn = 0; + + bool changed = uv_copy_mirrored_faces(em->bm, direction, precision, &double_warn); + + if (double_warn) { + total_duplicates += double_warn; + meshes_with_duplicates++; + } + + if (changed) { + DEG_id_tag_update(static_cast(obedit->data), 0); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, obedit->data); + } + } + + if (total_duplicates) { + BKE_reportf(op->reports, + RPT_WARNING, + "%d duplicates found in %d mesh(es), mirror may be incomplete", + total_duplicates, + meshes_with_duplicates); + } + + return OPERATOR_FINISHED; +} +void UV_OT_copy_mirrored_faces(wmOperatorType *ot) +{ + static const EnumPropertyItem direction_items[] = { + {0, "POSITIVE", 0, "Positive", ""}, + {1, "NEGATIVE", 0, "Negative", ""}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + ot->name = "Copy Mirrored UV Coords"; + ot->description = "Copy mirror UV coordinates on the X axis based on a mirrored mesh"; + ot->idname = "UV_OT_copy_mirrored_faces"; + + ot->exec = uv_copy_mirrored_faces_exec; + ot->poll = ED_operator_editmesh; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_enum(ot->srna, "direction", direction_items, 0, "Axis Direction", ""); + RNA_def_int(ot->srna, + "precision", + 3, + 1, + 16, + "Precision", + "Tolerance for finding vertex duplicates", + 1, + 16); +} + /** \} */ /* -------------------------------------------------------------------- */ @@ -2314,6 +2503,7 @@ void ED_operatortypes_uvedit() WM_operatortype_append(UV_OT_paste); WM_operatortype_append(UV_OT_cursor_set); + WM_operatortype_append(UV_OT_copy_mirrored_faces); } void ED_operatormacros_uvedit()