Merge branch 'main' into PoseMode

This commit is contained in:
Onan Chew
2024-09-10 20:47:06 -04:00
committed by GitHub
10 changed files with 606 additions and 7 deletions
+22
View File
@@ -262,3 +262,25 @@ def update_progress(self, context, message):
def finish_progress(context): def finish_progress(context):
context.window_manager.progress_end() context.window_manager.progress_end()
context.area.header_text_set(None) context.area.header_text_set(None)
def transfer_vertex_weights(context: Context, obj: bpy.types.Object, source_group: str, target_group: str, delete_source_group: bool = True) -> bool:
modifier: bpy.types.VertexWeightMixModifier = obj.modifiers.new(name="merge_weights",type="VERTEX_WEIGHT_MIX")
modifier.mix_set = 'B'
modifier.vertex_group_a = target_group
modifier.vertex_group_b = source_group
modifier.mask_constant = 1.0
bpy.ops.object.mode_set(mode='OBJECT')
prev_obj: bpy.types.Object = context.view_layer.objects.active
context.view_layer.objects.active = obj
bpy.ops.object.modifier_apply(modifier=modifier.name)
if delete_source_group:
obj.vertex_groups.remove(obj.vertex_groups.get(source_group))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
context.view_layer.objects.active = prev_obj
return True
+155 -1
View File
@@ -1,9 +1,10 @@
import bpy import bpy
from ..core.register import register_wrap from ..core.register import register_wrap
from ..ui.panel import AvatarToolkitPanel from ..ui.panel import AvatarToolkitPanel
from bpy.types import Context, Mesh, Panel, Operator from bpy.types import Context, Mesh, Panel, Operator, Armature, EditBone
from ..functions.translations import t from ..functions.translations import t
from ..core.common import get_selected_armature, get_all_meshes from ..core.common import get_selected_armature, get_all_meshes
from ..core import common
@register_wrap @register_wrap
class AvatarToolkit_OT_StartPoseMode(Operator): class AvatarToolkit_OT_StartPoseMode(Operator):
@@ -188,3 +189,156 @@ class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
bpy.ops.pose.armature_apply(selected=False) bpy.ops.pose.armature_apply(selected=False)
return {'FINISHED'} return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_RemoveZeroWeightBones(Operator):
bl_idname = "avatar_toolkit.remove_zero_weight_bones"
bl_label = t("Tools.remove_zero_weight_bones.label")
bl_description = t("Tools.remove_zero_weight_bones.desc")
bl_options = {'REGISTER', 'UNDO'}
threshold: bpy.props.FloatProperty(
default=0.01,
name=t("Tools.remove_zero_weight_bones.threshold.label"),
description=t("Tools.remove_zero_weight_bones.threshold.desc"),
min=0.0000001,
max=0.9999999)
@classmethod
def poll(cls, context: Context) -> bool:
return common.get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = common.get_selected_armature(context)
if not common.is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
return {'CANCELLED'}
weighted_bones: list[str] = []
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = common.get_all_meshes(context)
for mesh in meshes:
mesh_data: Mesh = mesh.data
for vertex in mesh_data.vertices:
for group in vertex.groups:
if group.weight > self.threshold:
weighted_bones.append(mesh.vertex_groups[group.group].name) #add bone name to list of bones that are greater than the weight threshold
bpy.ops.object.mode_set(mode='EDIT')
amature_data: Armature = armature.data
unweighted_bones: list[str] = []
#doing 2 loops to prevent modification of array during iteration
for bone in amature_data.edit_bones:
if bone.name not in weighted_bones:
unweighted_bones.append(bone.name) #add bones that arent in the list of bones that have weight into the list of bones that don't
for bone_name in unweighted_bones:
for edit_bone in amature_data.edit_bones[bone_name].children:
edit_bone.use_connect = False #to fix randomly moving bones
edit_bone.parent = amature_data.edit_bones[bone_name].parent #to fix unparented bones.
amature_data.edit_bones.remove(amature_data.edit_bones[bone_name]) #delete list of unweighted bones from the armature
self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success"))
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_MergeBonesToActive(Operator):
bl_idname = "avatar_toolkit.merge_bones_to_active"
bl_label = t("Tools.merge_bones_to_active.label")
bl_description = t("Tools.merge_bones_to_active.desc")
bl_options = {'REGISTER', 'UNDO'}
delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_active.delete_old.label"), description=t("Tools.merge_bones_to_active.delete_old.desc"), default=False)
@classmethod
def poll(cls, context: Context) -> bool:
if common.get_selected_armature(context) is not None:
if common.get_selected_armature(context) == context.view_layer.objects.active:
if context.mode == "POSE":
return len(context.selected_pose_bones) > 1
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_bones) > 1
return False
def execute(cls, context: Context) -> set[str]:
prev_mode: str = "EDIT"
if context.mode == "POSE":
prev_mode = "POSE"
#get active bone and a list of all other selected bones
bpy.ops.object.mode_set(mode='EDIT')
target_bone: str = context.active_bone.name
armature_data: Armature = context.view_layer.objects.active.data
bones: list[str] = [i.name for i in context.selected_bones]
bones.remove(target_bone)
for obj in common.get_all_meshes(context):
for bone in bones:
bone_name: str = armature_data.edit_bones[bone].name
common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[target_bone].name)
bpy.ops.object.mode_set(mode='EDIT')
for bone in bones:
if cls.delete_old:
for bone_child in armature_data.edit_bones[bone].children:
bone_child.parent = armature_data.edit_bones[bone].parent
armature_data.edit_bones.remove(armature_data.edit_bones[bone])
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_MergeBonesToParents(Operator):
bl_idname = "avatar_toolkit.merge_bones_to_parents"
bl_label = t("Tools.merge_bones_to_parents.label")
bl_description = t("Tools.merge_bones_to_parents.desc")
bl_options = {'REGISTER', 'UNDO'}
delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_parents.delete_old.label"), description=t("Tools.merge_bones_to_parents.delete_old.desc"), default=False)
@classmethod
def poll(cls, context: Context) -> bool:
if common.get_selected_armature(context) is not None:
if common.get_selected_armature(context) == context.view_layer.objects.active:
if context.mode == "POSE":
return len(context.selected_pose_bones) > 0
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_bones) > 0
return False
def execute(cls, context: Context) -> set[str]:
prev_mode: str = "EDIT"
if context.mode == "POSE":
prev_mode = "POSE"
#get active bone and a list of all other selected bones
bpy.ops.object.mode_set(mode='EDIT')
armature_data: Armature = context.view_layer.objects.active.data
for obj in common.get_all_meshes(context):
for bone in [i.name for i in context.selected_bones]:
if armature_data.edit_bones[bone].parent != None:
bone_name: str = armature_data.edit_bones[bone].name
common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[bone].parent.name)
bpy.ops.object.mode_set(mode='EDIT')
for bone in [i.name for i in context.selected_bones]:
if cls.delete_old:
for bone_child in armature_data.edit_bones[bone].children:
bone_child.parent = armature_data.edit_bones[bone].parent
armature_data.edit_bones.remove(armature_data.edit_bones[bone])
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
+56
View File
@@ -0,0 +1,56 @@
import numpy as np
import bpy
from bpy.types import Context
from ..core.common import get_selected_armature, get_all_meshes, is_valid_armature
from ..functions.translations import t
from ..core.register import register_wrap
@register_wrap
class AvatarToolkit_OT_RemoveUnusedShapekeys(bpy.types.Operators):
tolerance: bpy.props.FloatProperty(name=t("Tools.remove_unused_shapekeys.tolerance.label"), default=0.001, description=t("Tools.remove_unused_shapekeys.tolerance.desc"))
bl_idname = "avatar_toolkit.remove_unused_shapekeys"
bl_label = t("Tools.remove_unused_shapekeys.label")
bl_description = t("Tools.remove_unused_shapekeys.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT")
def execute(self, context: Context) -> set[str]:
#Shamefully taken from: https://blender.stackexchange.com/a/237611
#at least I am crediting them - @989onan
for ob in get_all_meshes(context):
if not ob.data.shape_keys: continue
if not ob.data.shape_keys.use_relative: continue
kbs = ob.data.shape_keys.key_blocks
nverts = len(ob.data.vertices)
to_delete = []
# Cache locs for rel keys since many keys have the same rel key
cache = {}
locs = np.empty(3*nverts, dtype=np.float32)
for kb in kbs:
if kb == kb.relative_key: continue
kb.data.foreach_get("co", locs)
if kb.relative_key.name not in cache:
rel_locs = np.empty(3*nverts, dtype=np.float32)
kb.relative_key.data.foreach_get("co", rel_locs)
cache[kb.relative_key.name] = rel_locs
rel_locs = cache[kb.relative_key.name]
locs -= rel_locs
if (np.abs(locs) < self.tolerance).all():
to_delete.append(kb.name)
for kb_name in to_delete:
if ("-" in kb_name) or ("=" in kb_name) or ("~" in kb_name): #don't delete category names. - @989onan
continue
ob.shape_key_remove(ob.data.shape_keys.key_blocks[kb_name])
+296
View File
@@ -0,0 +1,296 @@
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'}
+21
View File
@@ -126,6 +126,10 @@
"Tools.digitigrade_legs.success": "Digitigrade legs created successfully", "Tools.digitigrade_legs.success": "Digitigrade legs created successfully",
"Tools.import_any_model.desc": "Import any supported model, FBX, SMD, DMX, GLTF, PMD, PMX and more.", "Tools.import_any_model.desc": "Import any supported model, FBX, SMD, DMX, GLTF, PMD, PMX and more.",
"Tools.import_any_model.label": "Import Model", "Tools.import_any_model.label": "Import Model",
"UVTools.align_uv_to_target.warning.too_much": "Error! You have way to much stuff selected. Are you sure you're selecting two edges?",
"UVTools.align_uv_to_target.warning.need_a_line": "You need one line of selected uv points per selected object. Object \"{obj}\" does not meet this requirement!",
"avatar_toolkit.align_uv_edges_to_target.label":"Align UV Edges to Target",
"avatar_toolkit.align_uv_edges_to_target.desc":"Aligns a selected line of UV points on each selected mesh\nto the line of selected uv points on the active mesh.\nUseful for kitbashing textures of one model onto another.\nUses distance from the 2D cursor to identify the start of the line of uv points on each mesh.",
"Tools.label": "Tools", "Tools.label": "Tools",
"Tools.no_armature_selected": "No armature selected", "Tools.no_armature_selected": "No armature selected",
"Tools.select_armature": "Please select an armature", "Tools.select_armature": "Please select an armature",
@@ -141,6 +145,23 @@
"Tools.apply_transforms.desc": "Apply position, rotation, and scale to the armature and its meshes", "Tools.apply_transforms.desc": "Apply position, rotation, and scale to the armature and its meshes",
"Tools.apply_transforms.invalid_armature": "Invalid armature selected", "Tools.apply_transforms.invalid_armature": "Invalid armature selected",
"Tools.apply_transforms.success": "Transforms applied successfully to armature and meshes", "Tools.apply_transforms.success": "Transforms applied successfully to armature and meshes",
"Tools.remove_unused_shapekeys.label": "Remove Unused Shapekeys",
"Tools.remove_unused_shapekeys.tolerance.desc": "Min movement for position on any coordinate\n for any vertex for a shapekey to be kept.",
"Tools.remove_unused_shapekeys.desc": "Remove shapekeys that don't move anything.\nDoesn't get rid of category shapekeys.\n(ex: has \"~\", \"-\", or \"=\" in the name.)",
"Tools.remove_unused_shapekeys.tolerance.label": "Position Tolerance",
"Tools.remove_zero_weight_bones.success": "Zero weight bones removed successfully",
"Tools.remove_zero_weight_bones.label": "Remove Zero Weight Bones",
"Tools.remove_zero_weight_bones.desc": "Remove bones from the armature that have weights less than threshold.",
"Tools.merge_bones_to_active.delete_old.desc": "Remove old bones when merging.",
"Tools.merge_bones_to_active.delete_old.label": "Remove Old Bones",
"Tools.merge_bones_to_active.desc": "Merge selected bones to active bone (selected in bright blue or orange).",
"Tools.merge_bones_to_active.label": "Merge Bones to Active",
"Tools.merge_bones_to_parents.delete_old.desc": "Remove old bones when merging.",
"Tools.merge_bones_to_parents.delete_old.label": "Remove Old Bones",
"Tools.merge_bones_to_parents.desc": "Merges every bone in the selection to each of their parents.",
"Tools.merge_bones_to_parents.label": "Merge Bones to Individual Parents",
"Tools.remove_zero_weight_bones.threshold.label": "Weight Threshold",
"Tools.remove_zero_weight_bones.threshold.desc": "If a bone is not weighted to any part of any mesh under the armature with a threshold greater than this, it is removed",
"VisemePanel.create_visemes": "Create Visemes", "VisemePanel.create_visemes": "Create Visemes",
"VisemePanel.creating_viseme": "Creating viseme: {viseme_name}", "VisemePanel.creating_viseme": "Creating viseme: {viseme_name}",
"VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}", "VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}",
+1 -1
View File
@@ -17,7 +17,7 @@ class AvatarToolkit_PT_OptimizationPanel(bpy.types.Panel):
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 2 bl_order = 2
def draw(self, context): def draw(self: bpy.types.Panel, context: bpy.types.Context):
layout = self.layout layout = self.layout
armature = get_selected_armature(context) armature = get_selected_armature(context)
+8 -5
View File
@@ -2,6 +2,12 @@ import bpy
from ..core.register import register_wrap from ..core.register import register_wrap
from ..functions.translations import t from ..functions.translations import t
def draw_title(self: bpy.types.Panel):
layout = self.layout
layout.label(text=t("AvatarToolkit.welcome"))
layout.label(text=t("AvatarToolkit.description"))
layout.label(text=t("AvatarToolkit.alpha_warning"))
CATEGORY_NAME = "Avatar Toolkit" CATEGORY_NAME = "Avatar Toolkit"
@register_wrap @register_wrap
@@ -12,10 +18,7 @@ class AvatarToolKit_PT_AvatarToolkitPanel(bpy.types.Panel):
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = CATEGORY_NAME bl_category = CATEGORY_NAME
def draw(self, context): def draw(self: bpy.types.Panel, context: bpy.types.Context):
layout = self.layout draw_title(self)
layout.label(text=t("AvatarToolkit.welcome"))
layout.label(text=t("AvatarToolkit.description"))
layout.label(text=t("AvatarToolkit.alpha_warning"))
+8
View File
@@ -6,8 +6,10 @@ from ..functions.digitigrade_legs import AvatarToolKit_OT_CreateDigitigradeLegs
from ..functions.resonite_functions import AvatarToolKit_OT_ConvertToResonite from ..functions.resonite_functions import AvatarToolKit_OT_ConvertToResonite
from ..functions.translations import t from ..functions.translations import t
from ..core.common import get_selected_armature from ..core.common import get_selected_armature
from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys
from ..functions.seperate_by import AvatarToolKit_OT_SeparateByMaterials, AvatarToolKit_OT_SeparateByLooseParts from ..functions.seperate_by import AvatarToolKit_OT_SeparateByMaterials, AvatarToolKit_OT_SeparateByLooseParts
from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms
from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToActive, AvatarToolkit_OT_MergeBonesToParents
@register_wrap @register_wrap
class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel): class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel):
@@ -39,5 +41,11 @@ class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel):
row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH') row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH')
row = layout.row(align=True) row = layout.row(align=True)
row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms.label"), icon='OBJECT_ORIGIN') row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms.label"), icon='OBJECT_ORIGIN')
row.operator(AvatarToolkit_OT_RemoveUnusedShapekeys.bl_idname, text=t("Tools.remove_unused_shapekeys.label"), icon='SHAPEKEY_DATA')
row = layout.row(align=True)
row.operator(AvatarToolkit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.remove_zero_weight_bones.label"), icon='BONE_DATA')
row = layout.row(align=True)
row.operator(AvatarToolkit_OT_MergeBonesToActive.bl_idname, text=t("Tools.merge_bones_to_active.label"), icon='BONE_DATA')
row.operator(AvatarToolkit_OT_MergeBonesToParents.bl_idname, text=t("Tools.merge_bones_to_parents.label"), icon='BONE_DATA')
else: else:
layout.label(text=t("Tools.select_armature"), icon='ERROR') layout.label(text=t("Tools.select_armature"), icon='ERROR')
+17
View File
@@ -0,0 +1,17 @@
import bpy
from ..core.register import register_wrap
from ..functions.translations import t
from .panel import draw_title
@register_wrap
class UVTools_PT_MainPanel(bpy.types.Panel):
bl_label = t("AvatarToolkit.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
def draw(self: bpy.types.Panel, context: bpy.types.Context):
layout = self.layout
draw_title(self)
+22
View File
@@ -0,0 +1,22 @@
import bpy
from ..core.register import register_wrap
from ..functions.translations import t
from ..functions.uv_tools import AvatarToolkit_OT_AlignUVEdgesToTarget
from .panel import draw_title
@register_wrap
class UVTools_PT_Tools(bpy.types.Panel):
bl_label = t("Tools.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
bl_parent_id = "OBJECT_PT_avatar_toolkit_uv"
bl_order = 3
def draw(self, context: bpy.types.Context):
layout = self.layout
row = layout.row(align=True)
row.operator(AvatarToolkit_OT_AlignUVEdgesToTarget.bl_idname, text=t("avatar_toolkit.align_uv_edges_to_target.label"), icon='GP_MULTIFRAME_EDITING')