296 lines
15 KiB
Python
296 lines
15 KiB
Python
from typing import TypedDict
|
|
import bpy
|
|
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
|
|
import bmesh
|
|
import numpy as np
|
|
import math
|
|
from ..functions.translations import t
|
|
from ..core.register import register_wrap
|
|
|
|
class GenerateLoopTreeResult(TypedDict):
|
|
tree: dict[str, set[str]]
|
|
selected_loops: dict[str,list[int]]
|
|
selected_verts: dict[str,int]
|
|
|
|
@register_wrap
|
|
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
|
|
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
|
|
bl_label = t("avatar_toolkit.align_uv_edges_to_target.label")
|
|
bl_description = t("avatar_toolkit.align_uv_edges_to_target.desc")
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
|
|
|
|
#all selected objects need to be meshes for this to work - @989onan
|
|
@classmethod
|
|
def poll(cls, context: Context):
|
|
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
|
|
return False
|
|
if context.mode != "EDIT_MESH":
|
|
return False
|
|
for obj in context.view_layer.objects.selected:
|
|
if obj.type != "MESH":
|
|
return False
|
|
if not context.space_data:
|
|
return False
|
|
if not context.space_data.show_uvedit:
|
|
return False
|
|
if context.scene.tool_settings.use_uv_select_sync:
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context: Context):
|
|
|
|
|
|
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
|
|
|
|
sources: list[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
|
|
|
|
prev_mode: str = bpy.context.object.mode
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
|
|
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
|
|
print("Finding selected line for: \""+obj_name+"\"!")
|
|
|
|
|
|
vert_target_loops: dict[str,list[int]] = {}
|
|
vert_target_verts: dict[str,int] = {}
|
|
|
|
me: Mesh = bpy.data.objects[obj_name].data
|
|
uv_lay: MeshUVLoopLayer = me.uv_layers.active
|
|
bm: bmesh.types.BMesh = bmesh.new()
|
|
bm.from_mesh(me)
|
|
bm.verts.ensure_lookup_table()
|
|
|
|
|
|
|
|
# To explain:
|
|
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
|
|
#
|
|
# For some preknowledge:
|
|
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
|
|
# the same position on the UV map, then it considers it one point and the user can move it
|
|
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
|
|
#
|
|
# The problem:
|
|
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
|
|
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
|
|
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
|
|
#
|
|
# The solution
|
|
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
|
|
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
|
|
# that two vertices share the same face loop, and therefore are connected.
|
|
|
|
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
|
|
|
|
for k,i in enumerate(uv_lay.vertex_selection): #go through the selected vertices on object.
|
|
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False): #filter out vertices that are hidden from UV port
|
|
key = np.array(uv_lay.uv[k].vector[:])
|
|
key = key.round(decimals=5) #make a key that is the position of a selected vertex
|
|
|
|
if str(key) not in vert_target_loops:
|
|
vert_target_loops[str(key)] = [] #if the vertex's position is not a list yet, add it.
|
|
vert_target_loops[str(key)].append(k) #Basically, group vertices based on their position on a UV map as a list.
|
|
vert_target_verts[str(key)] = me.loops[k].vertex_index #associate the index of the physical vertex in real space with the coordinate of the uv vertices that share a position (Basically associate UV vert with real vert)
|
|
if len(vert_target_loops) > 4000: #This usually indicates that the user has a bunch of crap selected.
|
|
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.too_much"))
|
|
return
|
|
print("Finding connections on line for \""+obj_name+"\"!")
|
|
me.validate()
|
|
|
|
bm = bmesh.new()
|
|
bm.from_mesh(me)
|
|
|
|
|
|
#print(vert_target_loops)
|
|
#print(vert_target_verts)
|
|
tree: dict[str, set[str]] = {}
|
|
selected_verts = np.hstack(list(vert_target_loops.values()))
|
|
#print(selected_verts)
|
|
bm.verts.ensure_lookup_table()
|
|
for uvcoordsstr in vert_target_loops:
|
|
|
|
uv_lay = me.uv_layers.active
|
|
|
|
|
|
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
|
|
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
|
|
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
|
|
# vert_target_loops, we can now identify them
|
|
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
|
|
|
|
# Someone explain this better than me if you can please - @989onan
|
|
extension_loops = []
|
|
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
|
|
loops_indexes = [i.index for i in loops]
|
|
for loop in vert_target_loops[uvcoordsstr]:
|
|
if loop in loops_indexes:
|
|
loop_obj = loops[loops_indexes.index(loop)]
|
|
extension_loops.append(loop_obj.link_loop_next.index)
|
|
extension_loops.append(loop_obj.link_loop_prev.index)
|
|
|
|
|
|
|
|
|
|
|
|
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
|
|
#the order of this dictionary is unknown.
|
|
# Someone explain this better than me if you can please - @989onan
|
|
tree[uvcoordsstr] = set()
|
|
|
|
for i in extension_loops:
|
|
if i in selected_verts:
|
|
key = np.array(uv_lay.uv[i].vector[:])
|
|
key = key.round(decimals=5)
|
|
tree[uvcoordsstr].add(str(key))
|
|
|
|
if uvcoordsstr in tree:
|
|
if len(tree[uvcoordsstr]) > 2:
|
|
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
|
|
return {'FINISHED'}
|
|
|
|
uv_lay = me.uv_layers.active
|
|
for uvcoordstr in vert_target_loops:
|
|
for loop in vert_target_loops[uvcoordstr]:
|
|
uv_lay.vertex_selection[loop].value = True
|
|
|
|
|
|
bm.free()
|
|
me.validate()
|
|
print("found UV line connections for \""+obj_name+"\":")
|
|
#print(tree)
|
|
|
|
return {"tree":tree,"selected_loops":vert_target_loops,"selected_verts":vert_target_verts}
|
|
|
|
|
|
|
|
#This function uses the previous point to find the next point based on connected loops and faces.
|
|
def sort_uv_tree(originaltree: dict[str, set[str]], obj_name: str):
|
|
sortedtree: dict[str, set[str]] = originaltree.copy()
|
|
startpoints: list[str] = []
|
|
for i in sortedtree:
|
|
if len(sortedtree[i]) < 2:
|
|
startpoints.append(i)
|
|
|
|
if len(startpoints) != 2:
|
|
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
|
|
return
|
|
|
|
a_list1 = startpoints[0].replace(", "," ").replace("[","").replace("]","").split()
|
|
map_object1 = map(float, a_list1)
|
|
uvcoords1 = list(map_object1)
|
|
a_list2 = startpoints[1].replace(", "," ").replace("[","").replace("]","").split()
|
|
map_object2 = map(float, a_list2)
|
|
uvcoords2 = list(map_object2)
|
|
|
|
cursor = context.space_data.cursor_location
|
|
|
|
startpoint = None
|
|
if math.sqrt( (((uvcoords1[0]) - (cursor[0])) **2) + (((uvcoords1[1]) - (cursor[1])) **2) ) > math.sqrt( (((uvcoords2[0]) - (cursor[0])) **2) + (((uvcoords2[1]) - (cursor[1])) **2) ):
|
|
startpoint = startpoints[0]
|
|
else:
|
|
startpoint = startpoints[1]
|
|
|
|
#Wew my first actual recursive sort! - @989onan
|
|
def recursive_sort_uv_tree(point: str, sortedfinal: list[str]):
|
|
#print("appending "+point)
|
|
sortedfinal.append(point)
|
|
|
|
new_point: str = ""
|
|
for i in sortedtree:
|
|
if point in sortedtree[i]:
|
|
new_point = i
|
|
removed_value = sortedtree.pop(i)
|
|
#print(removed_value)
|
|
break
|
|
|
|
if new_point == "":
|
|
print("BROKE OUT OF SORTING, FINAL TREE (Should be empty, if not you errored here!):")
|
|
print(sortedtree)
|
|
|
|
return sortedfinal
|
|
|
|
return recursive_sort_uv_tree(new_point, sortedfinal)
|
|
|
|
array = []
|
|
|
|
sortedtree.pop(startpoint)
|
|
return recursive_sort_uv_tree(startpoint, array)
|
|
|
|
def lerp(v0, v1, t):
|
|
return v0 + t * (v1 - v0)
|
|
|
|
|
|
target_data: GenerateLoopTreeResult = generate_loop_tree(target)
|
|
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
|
|
print("sorted target.")
|
|
#print(sorted_target_tree)
|
|
|
|
for source in sources:
|
|
if source == target:
|
|
continue
|
|
|
|
#create our list of points that is a chain. then sort the chain into the correct order based on connections of vertices and the faces that the vertices make up in the UV map.
|
|
try:
|
|
source_data = generate_loop_tree(source)
|
|
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
|
|
print("Sorted source "+source)
|
|
print(sorted_source_tree)
|
|
|
|
vertex_factor = float(len(sorted_target_tree)-1) / (float(len(sorted_source_tree)-1))
|
|
|
|
print(str(vertex_factor)+" = "+str(float(len(sorted_target_tree)-1)) + " / " + str((float(len(sorted_source_tree)-1)))+")")
|
|
except Exception as e:
|
|
print(e)
|
|
return {'FINISHED'}
|
|
|
|
for k,i in enumerate(sorted_source_tree):
|
|
|
|
try:
|
|
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
|
|
progress_along_edge = (float(k)*vertex_factor)
|
|
previous_vertex_index = math.floor(progress_along_edge)
|
|
next_vertex_index = math.ceil(progress_along_edge)
|
|
|
|
|
|
#find the uv coordinates of the previous and next points on the target uv line.
|
|
a_list1 = sorted_target_tree[previous_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
|
|
map_object1 = map(float, a_list1)
|
|
previous_point = list(map_object1)
|
|
a_list2 = sorted_target_tree[next_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
|
|
map_object2 = map(float, a_list2)
|
|
next_point = list(map_object2)
|
|
|
|
|
|
|
|
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
|
|
progress_between_points = progress_along_edge - int(progress_along_edge)
|
|
lerped_point = [lerp(previous_point[0],next_point[0],progress_between_points),lerp(previous_point[1],next_point[1],progress_between_points)]
|
|
|
|
#grab our uv face corners for each uv coord that we saved.
|
|
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
|
|
#basically pretend they are split apart.
|
|
uv_face_corners = source_data["selected_loops"][i]
|
|
#print("doing from vertex "+str(previous_vertex_index)+" to "+str(next_vertex_index)+" total progress: "+str(progress_along_edge))
|
|
|
|
|
|
|
|
me: Mesh = bpy.data.objects[source].data
|
|
me.validate()
|
|
bm: bmesh.types.BMesh = bmesh.new()
|
|
bm.from_mesh(me)
|
|
uv_lay: MeshUVLoopLayer = me.uv_layers.active
|
|
bm.verts.ensure_lookup_table()
|
|
for corner in uv_face_corners:
|
|
uv_lay.uv[corner].vector = lerped_point #put the vertcies at the point we calculated.
|
|
except:
|
|
print("This is probably fine? - @989onan") #TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
|
|
|
|
print("Finished mesh \""+source+"\" for UV's")
|
|
|
|
|
|
|
|
bpy.ops.object.mode_set(mode=prev_mode)
|
|
return {'FINISHED'} |