Merge branch 'Alpha-2' into texture-atlas

This commit is contained in:
Yusarina
2025-02-07 18:38:12 +00:00
committed by GitHub
14 changed files with 896 additions and 45 deletions
+52 -19
View File
@@ -54,6 +54,28 @@ def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[in
return merged_vertices
def vertex_moves(mesh_data: bpy.types.Mesh, vertex: int) -> bool:
for shapekey in mesh_data.shape_keys.key_blocks:
data: bpy.types.ShapeKey = shapekey
if data.points[vertex].co.xyz != mesh_data.vertices[vertex].co.xyz:
return True
return False
def merge_vertex_at_index(mesh_data: bpy.types.Mesh, index: int, distance: float):
select_target_vertex = [False]*len(mesh_data.vertices)
select_target_vertex[index] = True
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",select_target_vertex)
bpy.ops.object.mode_set(mode='EDIT')
for _ in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
bpy.ops.mesh.remove_doubles(threshold=distance, use_unselected=True, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
bl_idname = "avatar_toolkit.remove_doubles_advanced"
bl_label = t("Optimization.remove_doubles_advanced")
@@ -168,7 +190,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
except Exception as e:
logger.error(f"Error in modify_mesh: {str(e)}")
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> bool:
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> int:
"""Advanced mesh modification with shape key handling"""
try:
final_merged_vertex_group = []
@@ -179,26 +201,28 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
merge_vertex_at_index(duplicate.data, mesh_entry["cur_vertex_pass"], merge_distance) #merge the vertex at our pass to find vertices that would merge to our vertex at this shapekey.
# Process merging
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"])
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"]) # find what vertices actually merged.
if not initialized_final:
final_merged_vertex_group = merged_vertices.copy()
initialized_final = True
else:
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] # remove vertices that merged from the list if they didn't merge during this shapkey.
bpy.ops.object.delete()
# Apply final merging
if final_merged_vertex_group:
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance)
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance) # merge all vertices that merged on every shapekey no matter the shapekey during the loop.
return not (len(final_merged_vertex_group) > 1)
return len(final_merged_vertex_group)
except Exception as e:
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
return True
return 1
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
"""Apply final vertex merging operations"""
@@ -232,16 +256,14 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
"""Complete the mesh processing by performing final merge operations"""
logger.debug("Finishing mesh processing")
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
if not advanced:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
"""Modal operator execution"""
@@ -266,10 +288,21 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
self.process_simple_mesh(context, mesh, merge_distance)
self.objects_to_do.pop(0)
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
if self.modify_mesh_advanced(context, mesh):
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex
if(mesh["cur_vertex_pass"] < 0): #make sure it doesn't go below 0 and explode when advancing backwards from a previous step
mesh["cur_vertex_pass"] = 0
if vertex_moves(mesh["mesh"].data, mesh["cur_vertex_pass"]): # do not do advanced merging for vertices that don't move
mesh["cur_vertex_pass"] -= self.modify_mesh_advanced(context, mesh)-2 #advance forward or backwards based on how many vertices actually got merged, changing the list size.
#if above returns 1 (no vertices other than this one being merged to ourselves), advance by 1. else don't advance or go backwards. Makes sure all vertices get merged in the end.
else:
mesh["cur_vertex_pass"] += 1
elif (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced and len(mesh['shapekeys']) > 0: #after advanced merging has gone past all the moving vertices, now we need to merge non moving vertices.
shapekeyname = mesh['shapekeys'].pop(0)
mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname)
logger.debug(f"Processing shapekey {shapekeyname}")
self.modify_mesh(context, mesh)
else:
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
self.objects_to_do.pop(0)
+65 -13
View File
@@ -134,17 +134,11 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation"""
# Make sure we are in Object mode first or it will error
bpy.ops.object.mode_set(mode='OBJECT')
armature = get_active_armature(context)
# Select armature and make it active before changing mode
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
constraints_removed = 0
@@ -157,7 +151,6 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
return {'FINISHED'}
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
"""Operator to remove bones with no vertex weights"""
bl_idname = "avatar_toolkit.clean_weights"
@@ -167,10 +160,37 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
"""Check if bone should be preserved based on settings"""
if context.scene.avatar_toolkit.merge_twist_bones:
return "twist" in bone_name.lower()
toolkit = context.scene.avatar_toolkit
bone = context.active_object.data.bones.get(bone_name)
if not bone:
return False
if toolkit.preserve_parent_bones and bone.children:
return True
if toolkit.target_bone_type == 'DEFORM' and not bone.use_deform:
return True
if toolkit.target_bone_type == 'NON_DEFORM' and bone.use_deform:
return True
return False
def populate_bone_list(self, context: Context, zero_weight_bones: List[str]) -> None:
"""Populate the zero weight bones list"""
toolkit = context.scene.avatar_toolkit
toolkit.zero_weight_bones.clear()
armature = get_active_armature(context)
for bone_name in zero_weight_bones:
bone = armature.data.bones.get(bone_name)
if bone:
item = toolkit.zero_weight_bones.add()
item.name = bone_name
item.has_children = len(bone.children) > 0
item.is_deform = bone.use_deform
def execute(self, context: Context) -> set[str]:
"""Execute the zero weight bone removal operation"""
armature = get_active_armature(context)
@@ -192,6 +212,7 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
# Get weighted bones
weighted_bones: List[str] = []
meshes = get_all_meshes(context)
zero_weight_bones: List[str] = []
for mesh in meshes:
mesh_data: Mesh = mesh.data
@@ -209,6 +230,10 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
if (bone.name not in weighted_bones and
not self.should_preserve_bone(bone.name, context)):
if context.scene.avatar_toolkit.list_only_mode:
zero_weight_bones.append(bone.name)
continue
# Store children data
children = bone.children
children_data = {child.name: initial_transforms[child.name] for child in children}
@@ -227,11 +252,38 @@ class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
for child_name, data in children_data.items():
if child_name in armature_data.edit_bones:
child = armature_data.edit_bones[child_name]
child.head = data['head']
child.tail = data['tail']
child.roll = data['roll']
child.matrix = data['matrix']
restore_bone_transforms(child, data)
bpy.ops.object.mode_set(mode='OBJECT')
if context.scene.avatar_toolkit.list_only_mode:
self.populate_bone_list(context, zero_weight_bones)
return {'FINISHED'}
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
return {'FINISHED'}
class AvatarToolKit_OT_RemoveSelectedBones(Operator):
"""Operator to remove selected bones from the zero weight bones list"""
bl_idname = "avatar_toolkit.remove_selected_bones"
bl_label = t("Tools.remove_selected_bones")
bl_description = t("Tools.remove_selected_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
armature = get_active_armature(context)
toolkit = context.scene.avatar_toolkit
selected_bones = [item.name for item in toolkit.zero_weight_bones
if item.selected]
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in selected_bones:
if bone_name in armature.data.edit_bones:
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
bpy.ops.object.mode_set(mode='OBJECT')
toolkit.zero_weight_bones.clear()
self.report({'INFO'}, t("Tools.bones_removed", count=len(selected_bones)))
return {'FINISHED'}
+255
View File
@@ -0,0 +1,255 @@
import bpy
from typing import Dict, List, Set, Optional, Tuple, Any
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint
from ...core.common import get_active_armature, validate_armature
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones
class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
"""Convert Rigify armature to Unity-compatible format"""
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
bl_label = t("Tools.convert_rigify_to_unity")
bl_description = t("Tools.convert_rigify_to_unity_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return ("DEF-spine" in armature.data.bones or
"spine" in armature.data.bones and "metarig" in armature.name.lower())
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting Rigify to Unity conversion")
armature = get_active_armature(context)
if not armature:
logger.error("No armature found")
self.report({'ERROR'}, t("Tools.no_armature"))
return {'CANCELLED'}
logger.debug(f"Converting armature: {armature.name}")
armature.name = "Armature"
armature.data.name = "Armature"
logger.debug("Renamed armature to 'Armature'")
if "DEF-spine" in armature.data.bones:
logger.info("Processing DEF bones")
self.move_def_bones(armature)
self.rename_bones_for_unity(armature)
else:
logger.info("Processing basic bones")
self.cleanup_extra_bones(armature)
self.rename_basic_bones_for_unity(armature)
logger.debug("Cleaning up bone collections")
self.cleanup_bone_collections(armature)
if context.scene.avatar_toolkit.merge_twist_bones:
logger.info("Merging twist bones")
self.handle_twist_bones(armature)
logger.info("Successfully converted Rigify armature to Unity format")
self.report({'INFO'}, t("Tools.rigify_converted"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to convert Rigify: {str(e)}", exc_info=True)
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def cleanup_extra_bones(self, armature: Object) -> None:
"""Remove unnecessary bones and merge neck bones"""
logger.debug("Starting cleanup of extra bones")
# Set armature as active object before mode switch
bpy.context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bones_to_remove: List[str] = []
for bone in armature.data.edit_bones:
if any(pattern in bone.name.lower() for pattern in rigify_unnecessary_bones):
bones_to_remove.append(bone.name)
for bone_name in bones_to_remove:
if bone_name in armature.data.edit_bones:
logger.debug(f"Removing bone: {bone_name}")
armature.data.edit_bones.remove(armature.data.edit_bones[bone_name])
if 'spine.004' in armature.data.edit_bones and 'spine.005' in armature.data.edit_bones:
logger.debug("Merging neck bones")
neck_start = armature.data.edit_bones['spine.004']
neck_end = armature.data.edit_bones['spine.005']
neck_start.tail = neck_end.tail
armature.data.edit_bones.remove(neck_end)
neck_start.name = "Neck"
if 'spine.006' in armature.data.edit_bones:
logger.debug("Renaming head bone")
head_bone = armature.data.edit_bones['spine.006']
head_bone.name = "Head"
def move_def_bones(self, armature: Object) -> None:
"""Move DEF bones to their correct positions"""
logger.debug("Moving DEF bones to correct positions")
# Set armature as active object
bpy.context.view_layer.objects.active = armature
remap: Dict[str, str] = self.get_org_remap(armature)
remap.update(self.get_special_remap())
remove_bones_in_chain: List[str] = [
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
'DEF-thigh.L.001', 'DEF-shin.L.001',
'DEF-thigh.R.001', 'DEF-shin.R.001'
]
transform_copies: List[str] = self.get_transform_copies(armature)
logger.debug("Setting up transform copies")
bpy.ops.object.mode_set(mode='POSE')
for bone_name in transform_copies:
bone = armature.pose.bones[bone_name]
org_name = 'ORG-' + self.get_proto_name(bone_name)
if org_name in armature.pose.bones:
constraint = bone.constraints.new('COPY_TRANSFORMS')
constraint.target = armature
constraint.subtarget = org_name
constr_count = len(bone.constraints)
if constr_count > 1:
bone.constraints.move(constr_count-1, 0)
logger.debug("Remapping bone parents")
bpy.ops.object.mode_set(mode='EDIT')
for remap_key in remap:
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
logger.debug("Processing bone chain removal")
bpy.ops.object.mode_set(mode='OBJECT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
armature.data.bones[bone_name].use_deform = False
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
remove_bone = armature.data.edit_bones[bone_name]
parent_bone = remove_bone.parent
parent_bone.tail = remove_bone.tail
retarget_bones = list(remove_bone.children)
for bone in retarget_bones:
bone.parent = parent_bone
armature.data.edit_bones.remove(remove_bone)
def rename_bones_for_unity(self, armature: Object) -> None:
"""Rename bones to Unity-compatible names"""
logger.debug("Renaming bones to Unity format")
for old_name, new_name in rigify_unity_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
logger.debug(f"Renaming bone: {old_name} -> {new_name}")
bone.name = new_name
def rename_basic_bones_for_unity(self, armature: Object) -> None:
"""Rename basic metarig bones to Unity-compatible names"""
logger.debug("Renaming basic metarig bones")
for old_name, new_name in rigify_basic_unity_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
logger.debug(f"Renaming basic bone: {old_name} -> {new_name}")
bone.name = new_name
def cleanup_bone_collections(self, armature: Object) -> None:
"""Remove all bone collections since they're not needed for Unity"""
logger.debug("Cleaning up bone collections")
if hasattr(armature.data, 'collections') and armature.data.collections:
while len(armature.data.collections) > 0:
collection = armature.data.collections[0]
armature.data.collections.remove(collection)
while len(armature.data.collections) > 1:
collection = armature.data.collections[1]
armature.data.collections.remove(collection)
def handle_twist_bones(self, armature: Object) -> None:
"""Handle twist bones during conversion"""
logger.debug("Processing twist bones")
twist_bones: List[Tuple[str, str]] = [
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
("DEF-forearm_twist.L", "DEF-forearm.L"),
("DEF-forearm_twist.R", "DEF-forearm.R"),
("DEF-thigh_twist.L", "DEF-thigh.L"),
("DEF-thigh_twist.R", "DEF-thigh.R")
]
bpy.ops.object.mode_set(mode='EDIT')
for twist_bone, parent_bone in twist_bones:
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
logger.debug(f"Merging twist bone: {twist_bone} into {parent_bone}")
twist = armature.data.edit_bones[twist_bone]
parent = armature.data.edit_bones[parent_bone]
parent.tail = twist.tail
for child in twist.children:
child.parent = parent
armature.data.edit_bones.remove(twist)
bpy.ops.object.mode_set(mode='OBJECT')
def get_org_remap(self, armature: Object) -> Dict[str, str]:
"""Get original bone remapping"""
logger.debug("Getting original bone remapping")
remap: Dict[str, str] = {}
for bone in armature.data.bones:
if self.is_def_bone(bone.name):
name = self.get_proto_name(bone.name)
parent = bone.parent
while parent:
parent_name = self.get_proto_name(parent.name)
if parent_name != name:
if ('DEF-' + parent_name) in armature.data.bones:
remap[bone.name] = 'DEF-' + parent_name
break
parent = parent.parent
return remap
def get_special_remap(self) -> Dict[str, str]:
"""Get special bone remapping cases"""
logger.debug("Getting special bone remapping")
return {
'DEF-thigh.L': 'DEF-pelvis.L',
'DEF-thigh.R': 'DEF-pelvis.R',
'DEF-upper_arm.L': 'DEF-shoulder.L',
'DEF-upper_arm.R': 'DEF-shoulder.R',
}
def get_transform_copies(self, armature: Object) -> List[str]:
"""Get bones that need transform copies"""
logger.debug("Getting transform copy bones")
result: List[str] = []
for bone in armature.pose.bones:
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
result.append(bone.name)
return result
def has_transform_copies(self, bone: PoseBone) -> bool:
"""Check if bone has transform copy constraints"""
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
def is_def_bone(self, bone_name: str) -> bool:
"""Check if bone is a DEF bone"""
return bone_name.startswith('DEF-')
def is_org_bone(self, bone_name: str) -> bool:
"""Check if bone is an ORG bone"""
return bone_name.startswith('ORG-')
def get_proto_name(self, bone_name: str) -> str:
"""Get the prototype name of a bone"""
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
return bone_name[4:]
return bone_name
+253
View File
@@ -0,0 +1,253 @@
from typing import TypedDict, Set, Dict, List, Optional, Any, Tuple
import bpy
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
import bmesh
import numpy as np
import math
from ...core.translations import t
from ...core.logging_setup import logger
class GenerateLoopTreeResult(TypedDict):
tree: Dict[str, Set[str]]
selected_loops: Dict[str, List[int]]
selected_verts: Dict[str, int]
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
"""Operator to align selected UV edges to target edge"""
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
bl_label = t("UVTools.align_edges")
bl_description = t("UVTools.align_edges_desc")
bl_options = {'REGISTER', 'UNDO'}
#all selected objects need to be meshes for this to work - @989onan
@classmethod
def poll(cls, context: Context) -> bool:
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) -> Set[str]:
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:
logger.debug(f"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):
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False):
key = np.array(uv_lay.uv[k].vector[:])
key = key.round(decimals=5)
if str(key) not in vert_target_loops:
vert_target_loops[str(key)] = []
vert_target_loops[str(key)].append(k)
vert_target_verts[str(key)] = me.loops[k].vertex_index
if len(vert_target_loops) > 4000:
self.report({'WARNING'}, t("UVTools.too_many_vertices"))
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
logger.debug(f"Finding connections on line for {obj_name}")
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
tree: Dict[str, Set[str]] = {}
selected_verts = np.hstack(list(vert_target_loops.values()))
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.need_line", obj=obj_name))
return {"tree": {}, "selected_loops": {}, "selected_verts": {}}
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()
logger.debug(f"Found UV line connections for {obj_name}")
return {"tree": tree, "selected_loops": vert_target_loops, "selected_verts": vert_target_verts}
def sort_uv_tree(originaltree: Dict[str, Set[str]], obj_name: str) -> List[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.need_line", obj=obj_name))
return []
uvcoords1 = [float(x) for x in startpoints[0].replace("[","").replace("]","").split()]
uvcoords2 = [float(x) for x in startpoints[1].replace("[","").replace("]","").split()]
cursor = context.space_data.cursor_location
startpoint = startpoints[0] 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) else startpoints[1]
#Wew my first actual recursive sort! - @989onan
def recursive_sort_uv_tree(point: str, sortedfinal: List[str]) -> 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 == "":
logger.debug("Sorting complete, remaining tree:")
logger.debug(sortedtree)
return sortedfinal
return recursive_sort_uv_tree(new_point, sortedfinal)
sortedtree.pop(startpoint)
return recursive_sort_uv_tree(startpoint, [])
def lerp(v0: float, v1: float, t: float) -> float:
return v0 + t * (v1 - v0)
target_data = generate_loop_tree(target)
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
logger.debug("Sorted target tree")
for source in sources:
if source == target:
continue
try:
source_data = generate_loop_tree(source)
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
logger.debug(f"Sorted source {source}")
vertex_factor = float(len(sorted_target_tree)-1) / float(len(sorted_source_tree)-1)
logger.debug(f"Vertex factor: {vertex_factor}")
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.
previous_point = [float(x) for x in sorted_target_tree[previous_vertex_index].replace("[","").replace("]","").split()]
next_point = [float(x) for x in sorted_target_tree[next_vertex_index].replace("[","").replace("]","").split()]
#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]
me = bpy.data.objects[source].data
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
uv_lay = me.uv_layers.active
bm.verts.ensure_lookup_table()
for corner in uv_face_corners:
uv_lay.uv[corner].vector = lerped_point
except:
#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
pass
logger.info(f"Finished mesh {source} for UV's")
except Exception as e:
logger.error(f"Error processing source {source}: {str(e)}")
return {'CANCELLED'}
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}