Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Blackbourn <chrisbblend@gmail.com>2022-09-07 04:22:45 +0300
committerChris Blackbourn <chrisbblend@gmail.com>2022-09-07 07:26:32 +0300
commit20daaeffce4cf9bfe48ab7c84cb9e2b1d71d2c91 (patch)
tree1b57761fdbdfbca25397c0e9229b85986ffaff65
parent18d1ef46f24ff52668d1d1e31cf804c47ca62a0a (diff)
UV: add new operator, uvcalc_align_rotation
Adds a new operator to automatically rotate UV Islands into alignment. Modes: * Auto (All edges) * Geometry (V direction will point in geometry direction) [1] * Edge (Rotate until selected edge is in V direction) Also adds uv_sync_selection support to UV Randomize Transform. Resolves: T78399 Differential Revision: https://developer.blender.org/D15820 [1] Listed as "World" in Task description.
-rw-r--r--release/scripts/startup/bl_operators/__init__.py2
-rw-r--r--release/scripts/startup/bl_operators/uvcalc_randomize_transform.py212
-rw-r--r--release/scripts/startup/bl_operators/uvcalc_transform.py436
-rw-r--r--release/scripts/startup/bl_ui/space_image.py1
4 files changed, 438 insertions, 213 deletions
diff --git a/release/scripts/startup/bl_operators/__init__.py b/release/scripts/startup/bl_operators/__init__.py
index 6f61d7e7129..de0b7798072 100644
--- a/release/scripts/startup/bl_operators/__init__.py
+++ b/release/scripts/startup/bl_operators/__init__.py
@@ -31,7 +31,7 @@ _modules = [
"userpref",
"uvcalc_follow_active",
"uvcalc_lightmap",
- "uvcalc_randomize_transform",
+ "uvcalc_transform",
"vertexpaint_dirt",
"view3d",
"wm",
diff --git a/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py b/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py
deleted file mode 100644
index 2867164a72e..00000000000
--- a/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py
+++ /dev/null
@@ -1,212 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-
-from bpy.types import Operator
-from mathutils import Vector
-
-import math
-
-
-def get_random_transform(transform_params, entropy):
- from random import uniform
- from random import seed as random_seed
-
- (seed, loc, rot, scale, scale_even) = transform_params
-
- # First, seed the RNG.
- random_seed(seed + entropy)
-
- # Next, call uniform a known number of times.
- offset_u = uniform(0, 1)
- offset_v = uniform(0, 1)
- angle = uniform(0, 1)
- scale_u = uniform(0, 1)
- scale_v = uniform(0, 1)
-
- # Apply the transform_params.
- if loc:
- offset_u *= loc[0]
- offset_v *= loc[1]
- else:
- offset_u = 0
- offset_v = 0
-
- if rot:
- angle *= rot
- else:
- angle = 0
-
- if scale:
- scale_u = scale_u * (2 * scale[0] - 2.0) + 2.0 - scale[0]
- scale_v = scale_v * (2 * scale[1] - 2.0) + 2.0 - scale[1]
- else:
- scale_u = 1
- scale_v = 1
-
- if scale_even:
- scale_v = scale_u
-
- # Results in homogenous co-ordinates.
- return [[scale_u * math.cos(angle), -scale_v * math.sin(angle), offset_u],
- [scale_u * math.sin(angle), scale_v * math.cos(angle), offset_v]]
-
-
-def randomize_uv_transform_island(bm, uv_layer, faces, transform_params):
- # Ensure consistent random values for island, regardless of selection etc.
- entropy = min(f.index for f in faces)
-
- transform = get_random_transform(transform_params, entropy)
-
- # Find bounding box.
- minmax = [1e30, 1e30, -1e30, -1e30]
- for face in faces:
- for loop in face.loops:
- u, v = loop[uv_layer].uv
- minmax[0] = min(minmax[0], u)
- minmax[1] = min(minmax[1], v)
- minmax[2] = max(minmax[2], u)
- minmax[3] = max(minmax[3], v)
-
- mid_u = (minmax[0] + minmax[2]) / 2
- mid_v = (minmax[1] + minmax[3]) / 2
-
- del_u = transform[0][2] + mid_u - transform[0][0] * mid_u - transform[0][1] * mid_v
- del_v = transform[1][2] + mid_v - transform[1][0] * mid_u - transform[1][1] * mid_v
-
- # Apply transform.
- for face in faces:
- for loop in face.loops:
- pre_uv = loop[uv_layer].uv
- u = transform[0][0] * pre_uv[0] + transform[0][1] * pre_uv[1] + del_u
- v = transform[1][0] * pre_uv[0] + transform[1][1] * pre_uv[1] + del_v
- loop[uv_layer].uv = (u, v)
-
-
-def is_face_uv_selected(face, uv_layer):
- for loop in face.loops:
- if not loop[uv_layer].select:
- return False
- return True
-
-
-def is_island_uv_selected(bm, island, uv_layer):
- for face in island:
- if is_face_uv_selected(face, uv_layer):
- return True
- return False
-
-
-def randomize_uv_transform_bmesh(mesh, bm, transform_params):
- import bpy_extras.bmesh_utils
- uv_layer = bm.loops.layers.uv.verify()
- islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer)
- for island in islands:
- if is_island_uv_selected(bm, island, uv_layer):
- randomize_uv_transform_island(bm, uv_layer, island, transform_params)
-
-
-def randomize_uv_transform(context, transform_params):
- import bmesh
- ob_list = context.objects_in_mode_unique_data
- for ob in ob_list:
- bm = bmesh.from_edit_mesh(ob.data)
- if not bm.loops.layers.uv:
- continue
-
- # Only needed to access the minimum face index of each island.
- bm.faces.index_update()
- randomize_uv_transform_bmesh(ob.data, bm, transform_params)
-
- for ob in ob_list:
- bmesh.update_edit_mesh(ob.data)
-
- return {'FINISHED'}
-
-
-from bpy.props import (
- BoolProperty,
- FloatProperty,
- FloatVectorProperty,
- IntProperty,
-)
-
-
-class RandomizeUVTransform(Operator):
- """Randomize uv island's location, rotation, and scale"""
- bl_idname = "uv.randomize_uv_transform"
- bl_label = "Randomize"
- bl_options = {'REGISTER', 'UNDO'}
-
- random_seed: IntProperty(
- name="Random Seed",
- description="Seed value for the random generator",
- min=0,
- max=10000,
- default=0,
- )
- use_loc: BoolProperty(
- name="Randomize Location",
- description="Randomize the location values",
- default=True,
- )
- loc: FloatVectorProperty(
- name="Location",
- description=("Maximum distance the objects "
- "can spread over each axis"),
- min=-100.0,
- max=100.0,
- size=2,
- subtype='TRANSLATION',
- default=(0.0, 0.0),
- )
- use_rot: BoolProperty(
- name="Randomize Rotation",
- description="Randomize the rotation value",
- default=True,
- )
- rot: FloatProperty(
- name="Rotation",
- description="Maximum rotation",
- min=-2 * math.pi,
- max=2 * math.pi,
- subtype='ANGLE',
- default=0.0,
- )
- use_scale: BoolProperty(
- name="Randomize Scale",
- description="Randomize the scale values",
- default=True,
- )
- scale_even: BoolProperty(
- name="Scale Even",
- description="Use the same scale value for both axes",
- default=False,
- )
-
- scale: FloatVectorProperty(
- name="Scale",
- description="Maximum scale randomization over each axis",
- min=-100.0,
- max=100.0,
- default=(1.0, 1.0),
- size=2,
- )
-
- @classmethod
- def poll(cls, context):
- return context.mode == 'EDIT_MESH'
-
- def execute(self, context):
- seed = self.random_seed
-
- loc = [0, 0] if not self.use_loc else self.loc
- rot = 0 if not self.use_rot else self.rot
- scale = None if not self.use_scale else self.scale
- scale_even = self.scale_even
-
- transformParams = [seed, loc, rot, scale, scale_even]
- return randomize_uv_transform(context, transformParams)
-
-
-classes = (
- RandomizeUVTransform,
-)
diff --git a/release/scripts/startup/bl_operators/uvcalc_transform.py b/release/scripts/startup/bl_operators/uvcalc_transform.py
new file mode 100644
index 00000000000..093aea4eaa8
--- /dev/null
+++ b/release/scripts/startup/bl_operators/uvcalc_transform.py
@@ -0,0 +1,436 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from bpy.types import Operator
+from mathutils import Matrix,Vector
+
+import math
+
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+ FloatVectorProperty,
+ IntProperty,
+)
+
+
+def is_face_uv_selected(face, uv_layer, any_edge):
+ """
+ Returns True if the face is UV selected.
+
+ :arg face: the face to query.
+ :type bmesh: :class:`BMFace`
+ :arg uv_layer: the UV layer to source UVs from.
+ :type bmesh: :class:`BMLayerItem`
+ :arg any_edge: use edge selection instead of vertex selection.
+ :type any_edge: bool
+ :return: True if the face is UV selected.
+ :rtype: bool
+ """
+
+ if not face.select: # Geometry selection
+ return False
+
+ import bpy
+ if bpy.context.tool_settings.use_uv_select_sync:
+ # In sync selection mode, UV selection comes solely from geometry selection.
+ return True
+
+ if any_edge:
+ for loop in face.loops:
+ if loop[uv_layer].select_edge:
+ return True
+ return False
+
+ for loop in face.loops:
+ if not loop[uv_layer].select:
+ return False
+ return True
+
+
+def is_island_uv_selected(island, uv_layer, any_edge):
+ """
+ Returns True if the island is UV selected.
+
+ :arg island: list of faces to query.
+ :arg uv_layer: the UV layer to source UVs from.
+ :type bmesh: :class:`BMLayerItem`
+ :arg any_edge: use edge selection instead of vertex selection.
+ :type any_edge: bool
+ :return: list of lists containing polygon indices.
+ :rtype: bool
+ """
+ for face in island:
+ if is_face_uv_selected(face, uv_layer, any_edge):
+ return True
+ return False
+
+
+def find_rotation_auto(bm, uv_layer, faces):
+ sum_u = 0.0
+ sum_v = 0.0
+ for face in faces:
+ prev_uv = face.loops[-1][uv_layer].uv
+ for loop in face.loops:
+ 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 *= 4.0 # Wrap 4 times around the circle
+ sum_u += math.cos(edge_angle)
+ sum_v += math.sin(edge_angle)
+ prev_uv = uv
+
+ # Compute angle.
+ return -math.atan2(sum_v, sum_u) / 4.0
+
+
+def find_rotation_edge(bm, uv_layer, faces):
+ sum_u = 0.0
+ sum_v = 0.0
+ for face in faces:
+ prev_uv = face.loops[-1][uv_layer].uv
+ prev_select = face.loops[-1][uv_layer].select_edge
+ for loop in face.loops:
+ uv = loop[uv_layer].uv
+ if prev_select:
+ du = uv[0] - prev_uv[0]
+ dv = uv[1] - prev_uv[1]
+ edge_angle = math.atan2(dv, du)
+ edge_angle *= 2.0 # Wrap 2 times around the circle
+ sum_u += math.cos(edge_angle)
+ sum_v += math.sin(edge_angle)
+
+ prev_uv = uv
+ prev_select = loop[uv_layer].select_edge
+
+ # Add 90 degrees to align along V co-ordinate.
+ # Twice, because we divide by two.
+ sum_u, sum_v = -sum_u, -sum_v
+
+ # Compute angle.
+ return -math.atan2(sum_v, sum_u) / 2.0
+
+
+def find_rotation_geometry(bm, uv_layer, faces, method, axis):
+ sum_u_co = Vector((0.0, 0.0, 0.0))
+ sum_v_co = Vector((0.0, 0.0, 0.0))
+ for face in faces:
+ # Triangulate.
+ for fan in range(2, len(face.loops)):
+ 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
+
+ mat = Matrix((delta_uv0, delta_uv1))
+ mat.invert_safe()
+
+ delta_co0 = face.loops[fan - 1].vert.co - face.loops[0].vert.co
+ delta_co1 = face.loops[fan].vert.co - face.loops[0].vert.co
+ w = delta_co0.cross(delta_co1).length
+ # U direction in geometry co-ordinates.
+ sum_u_co += (delta_co0 * mat[0][0] + delta_co1 * mat[0][1]) * w
+ # V direction in geometry co-ordinates.
+ sum_v_co += (delta_co0 * mat[1][0] + delta_co1 * mat[1][1]) * w
+
+ if axis == 'X':
+ axis_index = 0
+ elif axis == 'Y':
+ axis_index = 1
+ elif axis == 'Z':
+ axis_index = 2
+
+ # Compute angle.
+ return math.atan2(sum_u_co[axis_index], sum_v_co[axis_index])
+
+
+def align_uv_rotation_island(bm, uv_layer, faces, method, axis):
+ angle = 0.0
+ if method == 'AUTO':
+ angle = find_rotation_auto(bm, uv_layer, faces)
+ elif method == 'EDGE':
+ angle = find_rotation_edge(bm, uv_layer, faces)
+ elif method == 'GEOMETRY':
+ angle = find_rotation_geometry(bm, uv_layer, faces, method, axis)
+
+ if angle == 0.0:
+ return False # No change.
+
+ # Find bounding box.
+ minmax = [1e30, 1e30, -1e30, -1e30]
+ for face in faces:
+ for loop in face.loops:
+ u, v = loop[uv_layer].uv
+ minmax[0] = min(minmax[0], u)
+ minmax[1] = min(minmax[1], v)
+ minmax[2] = max(minmax[2], u)
+ minmax[3] = max(minmax[3], v)
+
+ mid_u = (minmax[0] + minmax[2]) / 2.0
+ mid_v = (minmax[1] + minmax[3]) / 2.0
+
+ 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
+
+ # 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
+ loop[uv_layer].uv = u, v
+
+ return True
+
+
+def align_uv_rotation_bmesh(mesh, bm, method, axis):
+ import bpy_extras.bmesh_utils
+
+ uv_layer = bm.loops.layers.uv.active
+ if not uv_layer:
+ return False
+
+ islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer)
+ 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):
+ changed = True
+ return changed
+
+
+def align_uv_rotation(context, method, axis):
+ import bmesh
+ 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):
+ bmesh.update_edit_mesh(ob.data)
+
+ return {'FINISHED'}
+
+
+class AlignUVRotation(Operator):
+ """Align uv island's rotation"""
+ bl_idname = "uv.align_rotation"
+ bl_label = "Align Rotation"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ method: EnumProperty(
+ name="Method", description="Method to calculate rotation angle",
+ items=(
+ ('AUTO', "Auto", "Align from all edges"),
+ ('EDGE', "Edge", "Only selected edges"),
+ ('GEOMETRY', "Geometry", "Align to Geometry axis"),
+ ),
+ )
+
+ axis: EnumProperty(
+ name="Axis", description="Axis to align to",
+ items=(
+ ('X', "X", "X axis"),
+ ('Y', "Y", "Y axis"),
+ ('Z', "Z", "Z axis"),
+ ),
+ )
+
+ def execute(self, context):
+ return align_uv_rotation(context, self.method, self.axis)
+
+ def draw(self, _context):
+ layout = self.layout
+ layout.prop(self, "method")
+ if self.method == 'GEOMETRY':
+ layout.prop(self, "axis")
+
+ @classmethod
+ def poll(cls, context):
+ return context.mode == 'EDIT_MESH'
+
+
+def get_random_transform(transform_params, entropy):
+ from random import uniform
+ from random import seed as random_seed
+
+ (seed, loc, rot, scale, scale_even) = transform_params
+
+ # First, seed the RNG.
+ random_seed(seed + entropy)
+
+ # Next, call uniform a known number of times.
+ offset_u = uniform(0, 1)
+ offset_v = uniform(0, 1)
+ angle = uniform(0, 1)
+ scale_u = uniform(0, 1)
+ scale_v = uniform(0, 1)
+
+ # Apply the transform_params.
+ if loc:
+ offset_u *= loc[0]
+ offset_v *= loc[1]
+ else:
+ offset_u = 0
+ offset_v = 0
+
+ if rot:
+ angle *= rot
+ else:
+ angle = 0
+
+ if scale:
+ scale_u = scale_u * (2 * scale[0] - 2.0) + 2.0 - scale[0]
+ scale_v = scale_v * (2 * scale[1] - 2.0) + 2.0 - scale[1]
+ else:
+ scale_u = 1
+ scale_v = 1
+
+ if scale_even:
+ scale_v = scale_u
+
+ # Results in homogenous co-ordinates.
+ return [[scale_u * math.cos(angle), -scale_v * math.sin(angle), offset_u],
+ [scale_u * math.sin(angle), scale_v * math.cos(angle), offset_v]]
+
+
+def randomize_uv_transform_island(bm, uv_layer, faces, transform_params):
+ # Ensure consistent random values for island, regardless of selection etc.
+ entropy = min(f.index for f in faces)
+
+ transform = get_random_transform(transform_params, entropy)
+
+ # Find bounding box.
+ minmax = [1e30, 1e30, -1e30, -1e30]
+ for face in faces:
+ for loop in face.loops:
+ u, v = loop[uv_layer].uv
+ minmax[0] = min(minmax[0], u)
+ minmax[1] = min(minmax[1], v)
+ minmax[2] = max(minmax[2], u)
+ minmax[3] = max(minmax[3], v)
+
+ mid_u = (minmax[0] + minmax[2]) / 2
+ mid_v = (minmax[1] + minmax[3]) / 2
+
+ del_u = transform[0][2] + mid_u - transform[0][0] * mid_u - transform[0][1] * mid_v
+ del_v = transform[1][2] + mid_v - transform[1][0] * mid_u - transform[1][1] * mid_v
+
+ # Apply transform.
+ for face in faces:
+ for loop in face.loops:
+ pre_uv = loop[uv_layer].uv
+ u = transform[0][0] * pre_uv[0] + transform[0][1] * pre_uv[1] + del_u
+ v = transform[1][0] * pre_uv[0] + transform[1][1] * pre_uv[1] + del_v
+ loop[uv_layer].uv = (u, v)
+
+
+def randomize_uv_transform_bmesh(mesh, bm, transform_params):
+ import bpy_extras.bmesh_utils
+ uv_layer = bm.loops.layers.uv.verify()
+ islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer)
+ for island in islands:
+ if is_island_uv_selected(island, uv_layer, False):
+ randomize_uv_transform_island(bm, uv_layer, island, transform_params)
+
+
+def randomize_uv_transform(context, transform_params):
+ import bmesh
+ ob_list = context.objects_in_mode_unique_data
+ for ob in ob_list:
+ bm = bmesh.from_edit_mesh(ob.data)
+ if not bm.loops.layers.uv:
+ continue
+
+ # Only needed to access the minimum face index of each island.
+ bm.faces.index_update()
+ randomize_uv_transform_bmesh(ob.data, bm, transform_params)
+
+ for ob in ob_list:
+ bmesh.update_edit_mesh(ob.data)
+
+ return {'FINISHED'}
+
+
+class RandomizeUVTransform(Operator):
+ """Randomize uv island's location, rotation, and scale"""
+ bl_idname = "uv.randomize_uv_transform"
+ bl_label = "Randomize"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ random_seed: IntProperty(
+ name="Random Seed",
+ description="Seed value for the random generator",
+ min=0,
+ max=10000,
+ default=0,
+ )
+ use_loc: BoolProperty(
+ name="Randomize Location",
+ description="Randomize the location values",
+ default=True,
+ )
+ loc: FloatVectorProperty(
+ name="Location",
+ description=("Maximum distance the objects "
+ "can spread over each axis"),
+ min=-100.0,
+ max=100.0,
+ size=2,
+ subtype='TRANSLATION',
+ default=(0.0, 0.0),
+ )
+ use_rot: BoolProperty(
+ name="Randomize Rotation",
+ description="Randomize the rotation value",
+ default=True,
+ )
+ rot: FloatProperty(
+ name="Rotation",
+ description="Maximum rotation",
+ min=-2 * math.pi,
+ max=2 * math.pi,
+ subtype='ANGLE',
+ default=0.0,
+ )
+ use_scale: BoolProperty(
+ name="Randomize Scale",
+ description="Randomize the scale values",
+ default=True,
+ )
+ scale_even: BoolProperty(
+ name="Scale Even",
+ description="Use the same scale value for both axes",
+ default=False,
+ )
+
+ scale: FloatVectorProperty(
+ name="Scale",
+ description="Maximum scale randomization over each axis",
+ min=-100.0,
+ max=100.0,
+ default=(1.0, 1.0),
+ size=2,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.mode == 'EDIT_MESH'
+
+ def execute(self, context):
+ seed = self.random_seed
+
+ loc = [0, 0] if not self.use_loc else self.loc
+ rot = 0 if not self.use_rot else self.rot
+ scale = None if not self.use_scale else self.scale
+ scale_even = self.scale_even
+
+ transformParams = [seed, loc, rot, scale, scale_even]
+ return randomize_uv_transform(context, transformParams)
+
+
+classes = (
+ AlignUVRotation,
+ RandomizeUVTransform,
+)
diff --git a/release/scripts/startup/bl_ui/space_image.py b/release/scripts/startup/bl_ui/space_image.py
index 4165f6ab0cf..0b26f0b1203 100644
--- a/release/scripts/startup/bl_ui/space_image.py
+++ b/release/scripts/startup/bl_ui/space_image.py
@@ -436,6 +436,7 @@ class IMAGE_MT_uvs(Menu):
layout.operator("uv.minimize_stretch")
layout.operator("uv.stitch")
layout.menu("IMAGE_MT_uvs_align")
+ layout.operator("uv.align_rotation")
layout.separator()