Fix #109605: Add correct_aspect to uv align rotation operator

The "Align Rotation" operator in the UV editor was producing
unexpected results if an image was displayed with non-square aspect
ratio. This change adds the "Correct Aspect" option to the operator.
This commit is contained in:
Chris Blackbourn
2023-07-06 16:13:06 +12:00
parent 5b3ce7b740
commit 374935fb56

View File

@@ -113,7 +113,7 @@ def island_uv_bounds_center(island, uv_layer):
# ------------------------------------------------------------------------------
# Align UV Rotation Operator
def find_rotation_auto(bm, uv_layer, faces):
def find_rotation_auto(bm, uv_layer, faces, aspect_y):
sum_u = 0.0
sum_v = 0.0
for face in faces:
@@ -122,7 +122,7 @@ def find_rotation_auto(bm, uv_layer, faces):
uv = loop[uv_layer].uv
du = uv[0] - prev_uv[0]
dv = uv[1] - prev_uv[1]
edge_angle = math.atan2(dv, du)
edge_angle = math.atan2(dv, du * aspect_y)
edge_angle *= 4.0 # Wrap 4 times around the circle
sum_u += math.cos(edge_angle)
sum_v += math.sin(edge_angle)
@@ -132,7 +132,7 @@ def find_rotation_auto(bm, uv_layer, faces):
return -math.atan2(sum_v, sum_u) / 4.0
def find_rotation_edge(bm, uv_layer, faces):
def find_rotation_edge(bm, uv_layer, faces, aspect_y):
sum_u = 0.0
sum_v = 0.0
for face in faces:
@@ -143,7 +143,7 @@ def find_rotation_edge(bm, uv_layer, faces):
if prev_select:
du = uv[0] - prev_uv[0]
dv = uv[1] - prev_uv[1]
edge_angle = math.atan2(dv, du)
edge_angle = math.atan2(dv, du * aspect_y)
edge_angle *= 2.0 # Wrap 2 times around the circle
sum_u += math.cos(edge_angle)
sum_v += math.sin(edge_angle)
@@ -159,7 +159,7 @@ def find_rotation_edge(bm, uv_layer, faces):
return -math.atan2(sum_v, sum_u) / 2.0
def find_rotation_geometry(bm, uv_layer, faces, method, axis):
def find_rotation_geometry(bm, uv_layer, faces, method, axis, aspect_y):
sum_u_co = Vector((0.0, 0.0, 0.0))
sum_v_co = Vector((0.0, 0.0, 0.0))
for face in faces:
@@ -168,6 +168,9 @@ def find_rotation_geometry(bm, uv_layer, faces, method, axis):
delta_uv0 = face.loops[fan - 1][uv_layer].uv - face.loops[0][uv_layer].uv
delta_uv1 = face.loops[fan][uv_layer].uv - face.loops[0][uv_layer].uv
delta_uv0[0] *= aspect_y
delta_uv1[0] *= aspect_y
mat = Matrix((delta_uv0, delta_uv1))
mat.invert_safe()
@@ -190,14 +193,14 @@ def find_rotation_geometry(bm, uv_layer, faces, method, axis):
return math.atan2(sum_u_co[axis_index], sum_v_co[axis_index])
def align_uv_rotation_island(bm, uv_layer, faces, method, axis):
def align_uv_rotation_island(bm, uv_layer, faces, method, axis, aspect_y):
angle = 0.0
if method == 'AUTO':
angle = find_rotation_auto(bm, uv_layer, faces)
angle = find_rotation_auto(bm, uv_layer, faces, aspect_y)
elif method == 'EDGE':
angle = find_rotation_edge(bm, uv_layer, faces)
angle = find_rotation_edge(bm, uv_layer, faces, aspect_y)
elif method == 'GEOMETRY':
angle = find_rotation_geometry(bm, uv_layer, faces, method, axis)
angle = find_rotation_geometry(bm, uv_layer, faces, method, axis, aspect_y)
if angle == 0.0:
return False # No change.
@@ -208,21 +211,21 @@ def align_uv_rotation_island(bm, uv_layer, faces, method, axis):
cos_angle = math.cos(angle)
sin_angle = math.sin(angle)
delta_u = mid_u - cos_angle * mid_u + sin_angle * mid_v
delta_v = mid_v - sin_angle * mid_u - cos_angle * mid_v
delta_u = mid_u - cos_angle * mid_u + sin_angle / aspect_y * mid_v
delta_v = mid_v - sin_angle * aspect_y * mid_u - cos_angle * mid_v
# Apply transform.
for face in faces:
for loop in face.loops:
pre_uv = loop[uv_layer].uv
u = cos_angle * pre_uv[0] - sin_angle * pre_uv[1] + delta_u
v = sin_angle * pre_uv[0] + cos_angle * pre_uv[1] + delta_v
u = cos_angle * pre_uv[0] - sin_angle / aspect_y * pre_uv[1] + delta_u
v = sin_angle * aspect_y * pre_uv[0] + cos_angle * pre_uv[1] + delta_v
loop[uv_layer].uv = u, v
return True
def align_uv_rotation_bmesh(mesh, bm, method, axis):
def align_uv_rotation_bmesh(mesh, bm, method, axis, aspect_y):
import bpy_extras.bmesh_utils
uv_layer = bm.loops.layers.uv.active
@@ -233,18 +236,39 @@ def align_uv_rotation_bmesh(mesh, bm, method, axis):
changed = False
for island in islands:
if is_island_uv_selected(island, uv_layer, method == 'EDGE'):
if align_uv_rotation_island(bm, uv_layer, island, method, axis):
if align_uv_rotation_island(bm, uv_layer, island, method, axis, aspect_y):
changed = True
return changed
def align_uv_rotation(context, method, axis):
def get_aspect_y(context):
area = context.area
if not area:
return 1.0
space_data = context.area.spaces.active
if not space_data:
return 1.0
if not space_data.image:
return 1.0
image_width = space_data.image.size[0]
image_height = space_data.image.size[1]
if image_height:
return image_width / image_height
return 1.0
def align_uv_rotation(context, method, axis, correct_aspect):
import bmesh
aspect_y = 1.0
if correct_aspect:
aspect_y = get_aspect_y(context)
ob_list = context.objects_in_mode_unique_data
for ob in ob_list:
bm = bmesh.from_edit_mesh(ob.data)
if bm.loops.layers.uv:
if align_uv_rotation_bmesh(ob.data, bm, method, axis):
if align_uv_rotation_bmesh(ob.data, bm, method, axis, aspect_y):
bmesh.update_edit_mesh(ob.data)
return {'FINISHED'}
@@ -274,14 +298,21 @@ class AlignUVRotation(Operator):
),
)
correct_aspect: BoolProperty(
name="Correct Aspect",
description="Take image aspect ratio into account",
default=False,
)
def execute(self, context):
return align_uv_rotation(context, self.method, self.axis)
return align_uv_rotation(context, self.method, self.axis, self.correct_aspect)
def draw(self, _context):
layout = self.layout
layout.prop(self, "method")
if self.method == 'GEOMETRY':
layout.prop(self, "axis")
layout.prop(self, "correct_aspect")
@classmethod
def poll(cls, context):