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:
Diffstat (limited to 'release/scripts/startup/bl_operators/uvcalc_transform.py')
-rw-r--r--release/scripts/startup/bl_operators/uvcalc_transform.py436
1 files changed, 436 insertions, 0 deletions
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,
+)