diff --git a/BRM_UVTranslate.py b/BRM_UVTranslate.py index 309e7d6..ce3e931 100755 --- a/BRM_UVTranslate.py +++ b/BRM_UVTranslate.py @@ -1,6 +1,7 @@ import bpy import bmesh import math +import sys from mathutils import Vector from . import BRM_Utils @@ -32,7 +33,15 @@ class UVTranslate(bpy.types.Operator): pixel_steps = None do_pixel_snap = False + face_axis = None + uv_axis_default = None + def invoke(self, context, event): + + if context.object is None: + self.report({'WARNING'}, "No active object") + return {'CANCELLED'} + self.shiftreset = False self.xlock = False self.ylock = False @@ -43,50 +52,93 @@ def invoke(self, context, event): self.pixel_steps = None self.do_pixel_snap = False + self.face_axis = None + # object->edit switch seems to "lock" the data. Ugly but hey it works bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='EDIT') - if context.object: - print("UV Translate") - self.first_mouse_x = event.mouse_x - self.first_mouse_y = event.mouse_y - - self.mesh = bpy.context.object.data - self.bm = bmesh.from_edit_mesh(self.mesh) - - # save original for reference - self.bm2 = bmesh.new() - self.bm2.from_mesh(self.mesh) - self.bm_orig = bmesh.new() - self.bm_orig.from_mesh(self.mesh) - - # have to do this for some reason - self.bm.faces.ensure_lookup_table() - self.bm2.faces.ensure_lookup_table() - self.bm_orig.faces.ensure_lookup_table() - - # Get refrerence to addon preference to get snap setting - module_name = __name__.split('.')[0] - addon_prefs = context.user_preferences.addons[module_name].preferences - self.do_pixel_snap = addon_prefs.pixel_snap - # Precalculate data before going into modal - self.pixel_steps = {} - for i, face in enumerate(self.bm.faces): - if face.select is False: - continue - # Find pixel steps per face here to look up in future translations - if self.do_pixel_snap: - pixel_step = BRM_Utils.get_face_pixel_step(context, face) - if pixel_step is not None: - self.pixel_steps[face.index] = pixel_step - - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} - else: - self.report({'WARNING'}, "No active object") + print("UV Translate") + + self.first_mouse_x = event.mouse_x + self.first_mouse_y = event.mouse_y + + self.mesh = bpy.context.object.data + self.bm = bmesh.from_edit_mesh(self.mesh) + + # save original for reference + self.bm2 = bmesh.new() + self.bm2.from_mesh(self.mesh) + self.bm_orig = bmesh.new() + self.bm_orig.from_mesh(self.mesh) + + # have to do this for some reason + self.bm.faces.ensure_lookup_table() + self.bm2.faces.ensure_lookup_table() + self.bm_orig.faces.ensure_lookup_table() + + # Get reference to addon preference to get snap setting + module_name = __name__.split('.')[0] + addon_prefs = context.user_preferences.addons[module_name].preferences + self.do_pixel_snap = addon_prefs.pixel_snap + + # Precalculate data before going into modal + self.pixel_steps = {} + self.uv_axis_default = ( + Vector((1.0, 0.0)), + Vector((0.0, 1.0)) + ) + + # Won't translate if didn't pass any selected faces + will_translate = False + + # Variables for calculating UV axis + world_matrix = context.object.matrix_world + rv3d = context.region_data + view_up_vector = rv3d.view_rotation * Vector((0.0, 1.0, 0.0)) + view_right_vector = rv3d.view_rotation * Vector((1.0, 0.0, 0.0)) + face_view_dot_max = sys.float_info.min + face_view_dist = sys.float_info.max + uv_layer = self.bm.loops.layers.uv.active + for i, face in enumerate(self.bm.faces): + # Don't process unselected faces + if face.select is False: + continue + will_translate = True + # Find pixel steps per face to look up for pixel snap. + # Doing this per face in case faces have different textures applied + if self.do_pixel_snap: + pixel_step = BRM_Utils.get_face_pixel_step(context, face) + if pixel_step is not None: + self.pixel_steps[face.index] = pixel_step + + # Find the UV directions for this face in relation to viewport + face_axis = BRM_Utils.get_face_uv_axis( + face, uv_layer, world_matrix, + view_up_vector, view_right_vector + ) + # Able to find UV directions for this face... + if face_axis is not None: + face_uv_axis = face_axis[0:2] + + # Find the face closest to view direction + normalized_dot = face_axis[2] + face_pos = world_matrix * face.calc_center_bounds() + dist_to_view = (face_pos - Vector(rv3d.view_location)).magnitude + if normalized_dot > face_view_dot_max and dist_to_view < face_view_dist: + face_view_dot_max = normalized_dot + face_view_dist = dist_to_view + # Make this face UV axis the default + self.uv_axis_default = face_uv_axis + + # No faces selected while looping + if will_translate is False: + self.report({'WARNING'}, "No selected faces to transform") return {'CANCELLED'} + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + def modal(self, context, event): context.area.header_text_set( "BRM UVTranslate: X/Y - contrain along X/Y Axis, MMB drag - alternative axis contrain method, SHIFT - precision mode, CTRL - stepped mode, CTRL + SHIFT - stepped with smaller increments") @@ -194,8 +246,8 @@ def modal(self, context, event): local_delta.x *= pixel_step.x local_delta.y *= pixel_step.y - uv_x_axis = Vector((1.0, 0.0)) - uv_y_axis = Vector((0.0, 1.0)) + uv_x_axis = self.uv_axis_default[0] + uv_y_axis = self.uv_axis_default[1] if self.xlock: uv_x_axis = Vector((0, 0)) diff --git a/BRM_Utils.py b/BRM_Utils.py index beac41d..74a57a5 100644 --- a/BRM_Utils.py +++ b/BRM_Utils.py @@ -1,7 +1,78 @@ -import bpy +import sys from mathutils import Vector +def get_face_uv_axis(face, uv_layer, world_matrix, view_up_vector, view_right_vector): + """ + Find the UV translation axis by matching the face edges to view up and right vectors + :param face: + :param uv_layer: + :param world_matrix: + :param view_up_vector: + :param view_right_vector: + :return: + """ + uv_x = None + uv_y = None + closest_up = sys.float_info.max + closest_right = sys.float_info.max + closest_dot_up = None + closest_dot_right = None + + # Start from index 1, so we can can just + # go to previous loop to get an edge + for o, loop in enumerate(face.loops, 1): + prev_loop = loop.link_loop_prev + + # Calculate the edge in world space, so matches view up and right vectors + edge_vec = (world_matrix * loop.vert.co) - (world_matrix * prev_loop.vert.co) + edge_vec.normalize() + # Calculate the UV vector back to previous vertex + uv_vec = loop[uv_layer].uv - prev_loop[uv_layer].uv + # Calculate dot against view vectors, to get similarity + dot_up = edge_vec.dot(view_up_vector) + dot_right = edge_vec.dot(view_right_vector) + # Restrict dot to 0-1 space for convenience + flat_up = 1.0 - abs(dot_up) + flat_right = 1.0 - abs(dot_right) + + if flat_up < closest_up: + # If dot up is negative, reverse UV vector + # if dot_up < 0: + # uv_vec *= -1 + # Bigger axis is chosen as the vector + if abs(uv_vec.y) > abs(uv_vec.x): + # Lazy normalization, basically + uv_y = Vector((0, 1 if uv_vec.y > 0 else -1)) + else: + uv_y = Vector((1 if uv_vec.x > 0 else -1, 0)) + + closest_up = flat_up + closest_dot_up = dot_up + + if flat_right < closest_right: + # if dot_right < 0: + # uv_vec *= -1 + # Bigger axis is chosen as the vector + if abs(uv_vec.y) > abs(uv_vec.x): + uv_x = Vector((0, 1 if uv_vec.y > 0 else -1)) + else: + uv_x = Vector((1 if uv_vec.x > 0 else -1, 0)) + + closest_right = flat_right + closest_dot_right = dot_right + + if uv_x is None or uv_y is None: + return None + # Fail safe, to make sure a weird parallel axis wasn't constructed + if abs(uv_x.dot(uv_y)) > 0.7: + return None + # Normalize the dots, so value of 1 is the + # closest axis to both view up and view right + normalized_dot = abs(closest_dot_right + closest_dot_up) / 2 + return uv_x, uv_y, normalized_dot + + def get_face_pixel_step(context, face): """ Finds the UV space amount for one pixel of a face, if it is textured diff --git a/__init__.py b/__init__.py index 97c68d5..107b01c 100755 --- a/__init__.py +++ b/__init__.py @@ -49,6 +49,7 @@ def draw(self, context): if self.adduvmenu: row.prop(self, "individualorsubmenu", expand=True) + column.separator() column.prop(self, "show_panel_tools") column.prop(self, "pixel_snap") @@ -74,7 +75,7 @@ def draw(self, context): layout.separator() layout.prop(addon_prefs, "pixel_snap") - + class BRM_UVMenu(bpy.types.Menu):