Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 92 additions & 40 deletions BRM_UVTranslate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import bpy
import bmesh
import math
import sys
from mathutils import Vector
from . import BRM_Utils

Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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))
Expand Down
73 changes: 72 additions & 1 deletion BRM_Utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -74,7 +75,7 @@ def draw(self, context):

layout.separator()
layout.prop(addon_prefs, "pixel_snap")



class BRM_UVMenu(bpy.types.Menu):
Expand Down