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
This commit is contained in:
Eitan Traurig
2025-09-01 13:57:55 -04:00
committed by Campbell Barton
parent 6d5da83e63
commit 919acbbf7f
4 changed files with 192 additions and 189 deletions

View File

@@ -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,
)

View File

@@ -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()

View File

@@ -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);

View File

@@ -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<float3, BMVert *> mirror_gt, mirror_lt;
Map<BMVert *, BMVert *> 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<Array<BMVert *>, BMFace *> sorted_verts_to_face;
/* Maps faces to their corresponding mirrored face. */
Map<BMFace *, BMFace *> face_map;
BMFace *f;
BMIter iter_face;
BM_ITER_MESH (f, &iter_face, bm, BM_FACES_OF_MESH) {
Array<BMVert *> 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<BMVert *> 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<Object *> 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<ID *>(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()