7e584e3648
This fixes is to get everything working on the new auto load and properties system. Also some other small fixes.
498 lines
18 KiB
Python
498 lines
18 KiB
Python
import bpy
|
|
import numpy as np
|
|
from .dictionaries import bone_names
|
|
import threading
|
|
import time
|
|
import webbrowser
|
|
import typing
|
|
|
|
from typing import List, Optional, Tuple
|
|
from bpy.types import Object, ShapeKey, Mesh, Context, Material, PropertyGroup
|
|
from functools import lru_cache
|
|
from bpy.props import PointerProperty, IntProperty, StringProperty
|
|
from bpy.utils import register_class
|
|
|
|
|
|
|
|
|
|
class SceneMatClass(PropertyGroup):
|
|
mat: PointerProperty(type=Material)
|
|
|
|
register_class(SceneMatClass)
|
|
|
|
class MaterialListBool:
|
|
#For the love that is holy do not ever touch these. If this was java I would make these private
|
|
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
|
|
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
|
|
|
|
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
|
|
old_list: dict[str,list[Material]] = {}
|
|
bool_material_list_expand: dict[str,bool] = {}
|
|
|
|
def set_bool(self, value: bool) -> None:
|
|
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = value
|
|
if value == False:
|
|
MaterialListBool.old_list[bpy.context.scene.name] = []
|
|
|
|
def get_bool(self) -> bool:
|
|
newlist: list[Material] = []
|
|
for obj in bpy.context.scene.objects:
|
|
if len(obj.material_slots)>0:
|
|
for mat_slot in obj.material_slots:
|
|
if mat_slot.material:
|
|
if mat_slot.material not in newlist:
|
|
newlist.append(mat_slot.material)
|
|
|
|
still_the_same: bool = True
|
|
if bpy.context.scene.name in MaterialListBool.old_list:
|
|
for item in newlist:
|
|
if item not in MaterialListBool.old_list[bpy.context.scene.name]:
|
|
still_the_same = False
|
|
break
|
|
for item in MaterialListBool.old_list[bpy.context.scene.name]:
|
|
if item not in newlist:
|
|
still_the_same = False
|
|
break
|
|
else:
|
|
still_the_same = False
|
|
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
|
|
|
|
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
|
|
|
|
|
|
### Clean up material names in the given mesh by removing the '.001' suffix.
|
|
def clean_material_names(mesh: Mesh) -> None:
|
|
for j, mat in enumerate(mesh.material_slots):
|
|
if mat.name.endswith(('.0+', ' 0+')):
|
|
mesh.active_material_index = j
|
|
mesh.active_material.name = mat.name[:-len(mat.name.rstrip('0')) - 1]
|
|
|
|
# This will fix faulty uv coordinates, cats did this a other way which can have unintended consequences,
|
|
# this is the best way i could of think of doing this for the time being, however may need improvements.
|
|
|
|
def fix_uv_coordinates(context: Context) -> None:
|
|
obj = context.object
|
|
|
|
# Store current mode and selection
|
|
current_mode = context.mode
|
|
current_active = context.view_layer.objects.active
|
|
current_selected = context.selected_objects.copy()
|
|
|
|
# Ensure we're in object mode and select the object
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
obj.select_set(True)
|
|
context.view_layer.objects.active = obj
|
|
|
|
# Check if the object has any mesh data
|
|
if obj.type == 'MESH' and obj.data:
|
|
|
|
# Switch to Edit Mode
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
# Select all UVs
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
|
|
# Try to find UV Editor area, fall back to 3D View if not found
|
|
area = next((area for area in context.screen.areas if area.type == 'UV_EDITOR'), None)
|
|
if not area:
|
|
area = next((area for area in context.screen.areas if area.type == 'VIEW_3D'), None)
|
|
|
|
# Get the region and space data
|
|
region = next((region for region in area.regions if region.type == 'WINDOW'), None)
|
|
space_data = area.spaces.active
|
|
|
|
# Create a context override
|
|
override = {
|
|
'area': area,
|
|
'region': region,
|
|
'space_data': space_data,
|
|
'edit_object': obj,
|
|
'active_object': obj,
|
|
'selected_objects': [obj],
|
|
'mode': 'EDIT_MESH',
|
|
}
|
|
|
|
try:
|
|
# Ensure UVs are selected
|
|
bpy.ops.uv.select_all(override, action='SELECT')
|
|
# Average UV island scales
|
|
bpy.ops.uv.average_islands_scale(override)
|
|
except Exception as e:
|
|
print(f"UV Fix - Error during UV scaling: {str(e)}")
|
|
|
|
# Switch back to Object Mode
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
print("UV Fix - Switched back to Object Mode")
|
|
|
|
# Restore previous selection and active object
|
|
for sel_obj in current_selected:
|
|
sel_obj.select_set(True)
|
|
context.view_layer.objects.active = current_active
|
|
else:
|
|
print("UV Fix - Object is not a valid mesh with UV data")
|
|
|
|
def has_shapekeys(mesh_obj: Object) -> bool:
|
|
return mesh_obj.data.shape_keys is not None
|
|
|
|
@lru_cache(maxsize=None)
|
|
def _get_shape_key_co(shape_key: ShapeKey) -> np.ndarray:
|
|
return np.array([v.co for v in shape_key.data])
|
|
|
|
def simplify_bonename(n: str) -> str:
|
|
return n.lower().translate(dict.fromkeys(map(ord, u" _.")))
|
|
|
|
def get_armature(context: Context, armature_name: Optional[str] = None) -> Optional[Object]:
|
|
if armature_name:
|
|
obj = bpy.data.objects[armature_name]
|
|
if obj.type == "ARMATURE":
|
|
return obj
|
|
else:
|
|
return None
|
|
if context.view_layer.objects.active:
|
|
obj = context.view_layer.objects.active
|
|
if obj.type == "ARMATURE":
|
|
return obj
|
|
return next((obj for obj in context.view_layer.objects if obj.type == 'ARMATURE'), None)
|
|
|
|
def get_armatures(self, context: Context) -> List[Tuple[str, str, str]]:
|
|
armatures = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'ARMATURE']
|
|
if not armatures:
|
|
return [('NONE', 'No Armature', '')]
|
|
return armatures
|
|
|
|
def get_armatures_that_are_not_selected(self, context: Context) -> List[Tuple[str, str, str]]:
|
|
armatures = [(obj.name, obj.name, "") for obj in bpy.data.objects if ((obj.type == 'ARMATURE') and (obj.name != context.scene.avatar_toolkit.selected_armature))]
|
|
if not armatures:
|
|
return [('NONE', 'No Other Armature', '')]
|
|
return armatures
|
|
|
|
def get_selected_armature(context: Context) -> Optional[Object]:
|
|
try:
|
|
if hasattr(context.scene, 'avatar_toolkit'):
|
|
armature_name = context.scene.avatar_toolkit.selected_armature
|
|
if isinstance(armature_name, bytes):
|
|
try:
|
|
armature_name = armature_name.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
try:
|
|
armature_name = armature_name.decode('gbk') # For Chinese characters
|
|
except UnicodeDecodeError:
|
|
try:
|
|
armature_name = armature_name.decode('shift-jis')
|
|
except UnicodeDecodeError:
|
|
armature_name = armature_name.decode('latin1')
|
|
|
|
if armature_name:
|
|
armature = bpy.data.objects.get(str(armature_name))
|
|
if is_valid_armature(armature):
|
|
return armature
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def get_merge_armature_source(context: Context) -> Optional[Object]:
|
|
try:
|
|
if hasattr(context.scene, 'merge_armature_source'):
|
|
source_name = context.scene.merge_armature_source
|
|
if isinstance(source_name, bytes):
|
|
try:
|
|
source_name = source_name.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
try:
|
|
source_name = source_name.decode('shift-jis')
|
|
except UnicodeDecodeError:
|
|
source_name = source_name.decode('latin1', errors='ignore')
|
|
|
|
if source_name:
|
|
return bpy.data.objects.get(str(source_name))
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def set_selected_armature(context: Context, armature: Optional[Object]) -> None:
|
|
context.scene.avatar_toolkit.selected_armature = armature.name if armature else ""
|
|
|
|
def is_valid_armature(armature: Object) -> bool:
|
|
if not armature or armature.type != 'ARMATURE':
|
|
return False
|
|
if not armature.data or not armature.data.bones:
|
|
return False
|
|
return True
|
|
|
|
def select_current_armature(context: Context) -> bool:
|
|
armature = get_selected_armature(context)
|
|
if armature:
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
armature.select_set(True)
|
|
context.view_layer.objects.active = armature
|
|
return True
|
|
return False
|
|
|
|
def apply_shapekey_to_basis(context: bpy.types.Context, obj: bpy.types.Object, shape_key_name: str, delete_old: bool = False) -> bool:
|
|
if shape_key_name not in obj.data.shape_keys.key_blocks:
|
|
return False
|
|
shapekeynum = obj.data.shape_keys.key_blocks.find(shape_key_name)
|
|
|
|
bpy.ops.object.mode_set(mode="EDIT")
|
|
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
|
|
|
|
obj.active_shape_key_index = 0
|
|
bpy.ops.mesh.blend_from_shape(shape = shape_key_name, add=True, blend=1)
|
|
obj.active_shape_key_index = shapekeynum
|
|
bpy.ops.mesh.select_all(action='SELECT')
|
|
bpy.ops.mesh.blend_from_shape(shape = shape_key_name, add=True, blend=-2)
|
|
|
|
|
|
bpy.ops.mesh.select_all(action='DESELECT')
|
|
|
|
bpy.ops.object.mode_set(mode="OBJECT")
|
|
print("blended!")
|
|
|
|
if delete_old:
|
|
obj.active_shape_key_index = shapekeynum
|
|
bpy.ops.object.shape_key_remove(all=False)
|
|
else:
|
|
mesh: bpy.types.Mesh = obj.data
|
|
mesh.shape_keys.key_blocks[shape_key_name].name = shape_key_name + "_reversed"
|
|
return True
|
|
|
|
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: list[Object]) -> bool:
|
|
for mesh_obj in meshes:
|
|
if not mesh_obj.data:
|
|
continue
|
|
|
|
if mesh_obj.data.shape_keys and mesh_obj.data.shape_keys.key_blocks:
|
|
if len(mesh_obj.data.shape_keys.key_blocks) == 1:
|
|
basis = mesh_obj.data.shape_keys.key_blocks[0]
|
|
basis_name = basis.name
|
|
mesh_obj.shape_key_remove(basis)
|
|
apply_armature_to_mesh(armature_obj, mesh_obj)
|
|
mesh_obj.shape_key_add(name=basis_name)
|
|
else:
|
|
apply_armature_to_mesh_with_shapekeys(armature_obj, mesh_obj, context)
|
|
else:
|
|
apply_armature_to_mesh(armature_obj, mesh_obj)
|
|
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
bpy.ops.pose.armature_apply(selected=False)
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
return True
|
|
|
|
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
|
armature_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
|
|
armature_mod.object = armature_obj
|
|
|
|
if bpy.app.version >= (3, 5):
|
|
mesh_obj.modifiers.move(mesh_obj.modifiers.find(armature_mod.name), 0)
|
|
else:
|
|
for _ in range(len(mesh_obj.modifiers) - 1):
|
|
bpy.ops.object.modifier_move_up(modifier=armature_mod.name)
|
|
|
|
with bpy.context.temp_override(object=mesh_obj):
|
|
bpy.ops.object.modifier_apply(modifier=armature_mod.name)
|
|
|
|
def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object, context: Context) -> None:
|
|
old_active_index = mesh_obj.active_shape_key_index
|
|
old_show_only = mesh_obj.show_only_shape_key
|
|
mesh_obj.show_only_shape_key = True
|
|
|
|
shape_keys = mesh_obj.data.shape_keys.key_blocks
|
|
vertex_groups = []
|
|
mutes = []
|
|
for sk in shape_keys:
|
|
vertex_groups.append(sk.vertex_group)
|
|
sk.vertex_group = ''
|
|
mutes.append(sk.mute)
|
|
sk.mute = False
|
|
|
|
disabled_mods = []
|
|
for mod in mesh_obj.modifiers:
|
|
if mod.show_viewport:
|
|
mod.show_viewport = False
|
|
disabled_mods.append(mod)
|
|
|
|
arm_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
|
|
arm_mod.object = armature_obj
|
|
|
|
co_length = len(mesh_obj.data.vertices) * 3
|
|
eval_cos = np.empty(co_length, dtype=np.single)
|
|
|
|
for i, shape_key in enumerate(shape_keys):
|
|
mesh_obj.active_shape_key_index = i
|
|
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
eval_mesh = mesh_obj.evaluated_get(depsgraph)
|
|
eval_mesh.data.vertices.foreach_get('co', eval_cos)
|
|
|
|
shape_key.data.foreach_set('co', eval_cos)
|
|
if i == 0:
|
|
mesh_obj.data.vertices.foreach_set('co', eval_cos)
|
|
|
|
for mod in disabled_mods:
|
|
mod.show_viewport = True
|
|
mesh_obj.modifiers.remove(arm_mod)
|
|
|
|
for sk, vg, mute in zip(shape_keys, vertex_groups, mutes):
|
|
sk.vertex_group = vg
|
|
sk.mute = mute
|
|
|
|
mesh_obj.active_shape_key_index = old_active_index
|
|
mesh_obj.show_only_shape_key = old_show_only
|
|
|
|
def get_all_meshes(context: Context) -> List[Object]:
|
|
armature = get_selected_armature(context)
|
|
if armature and is_valid_armature(armature):
|
|
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
|
return []
|
|
|
|
def get_mesh_items(self, context):
|
|
return [(obj.name, obj.name, "") for obj in get_all_meshes(context)]
|
|
|
|
def open_web_after_delay_multi_threaded(delay: typing.Optional[float] = 1.0, url: typing.Union[str, typing.Any] = ""):
|
|
thread = threading.Thread(target=open_web_after_delay,args=[delay,url],name="open_browser_thread")
|
|
thread.start()
|
|
|
|
def open_web_after_delay(delay, url):
|
|
print("opening browser in "+str(delay)+" seconds.")
|
|
time.sleep(delay)
|
|
|
|
webbrowser.open_new_tab(url)
|
|
|
|
def duplicatebone(b: bpy.types.EditBone) -> bpy.types.EditBone:
|
|
arm = bpy.context.object.data
|
|
cb = arm.edit_bones.new(b.name)
|
|
|
|
cb.head = b.head
|
|
cb.tail = b.tail
|
|
cb.matrix = b.matrix
|
|
cb.parent = b.parent
|
|
return cb
|
|
|
|
def has_shapekeys(mesh_obj: Object) -> bool:
|
|
return mesh_obj.data.shape_keys is not None
|
|
|
|
def sort_shape_keys(mesh: Object) -> None:
|
|
print("Starting shape key sorting...")
|
|
if not has_shapekeys(mesh):
|
|
print("No shape keys found. Exiting sort function.")
|
|
return
|
|
|
|
# Set the mesh as the active object
|
|
bpy.context.view_layer.objects.active = mesh
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
order = [
|
|
'Basis',
|
|
'vrc.blink_left',
|
|
'vrc.blink_right',
|
|
'vrc.lowerlid_left',
|
|
'vrc.lowerlid_right',
|
|
'vrc.v_aa',
|
|
'vrc.v_ch',
|
|
'vrc.v_dd',
|
|
'vrc.v_e',
|
|
'vrc.v_ff',
|
|
'vrc.v_ih',
|
|
'vrc.v_kk',
|
|
'vrc.v_nn',
|
|
'vrc.v_oh',
|
|
'vrc.v_ou',
|
|
'vrc.v_pp',
|
|
'vrc.v_rr',
|
|
'vrc.v_sil',
|
|
'vrc.v_ss',
|
|
'vrc.v_th',
|
|
]
|
|
|
|
shape_keys = mesh.data.shape_keys.key_blocks
|
|
print(f"Total shape keys: {len(shape_keys)}")
|
|
|
|
# Create a list of shape key names in their current order
|
|
current_order = [key.name for key in shape_keys]
|
|
|
|
# Create a new order list
|
|
new_order = []
|
|
|
|
# First, add all the keys that are in the predefined order
|
|
for name in order:
|
|
if name in current_order:
|
|
new_order.append(name)
|
|
current_order.remove(name)
|
|
|
|
# Then add any remaining keys that weren't in the predefined order
|
|
new_order.extend(current_order)
|
|
|
|
print("New order:", new_order)
|
|
|
|
# Now, rearrange the shape keys based on the new order
|
|
for i, name in enumerate(new_order):
|
|
index = shape_keys.find(name)
|
|
if index != i:
|
|
print(f"Moving {name} from index {index} to {i}")
|
|
mesh.active_shape_key_index = index
|
|
while mesh.active_shape_key_index > i:
|
|
bpy.ops.object.shape_key_move(type='UP')
|
|
|
|
print("Shape key sorting completed.")
|
|
|
|
def get_shapekeys(mesh: Object, prefix: str = '') -> List[tuple]:
|
|
if not has_shapekeys(mesh):
|
|
return []
|
|
return [(key.name, key.name, key.name) for key in mesh.data.shape_keys.key_blocks if key.name != 'Basis' and key.name.startswith(prefix)]
|
|
|
|
def remove_default_objects():
|
|
for obj in bpy.data.objects:
|
|
if obj.name in ["Camera", "Light", "Cube"]:
|
|
bpy.data.objects.remove(obj, do_unlink=True)
|
|
|
|
def init_progress(context, steps):
|
|
context.window_manager.progress_begin(0, 100)
|
|
context.scene.avatar_toolkit.progress_steps = steps
|
|
context.scene.avatar_toolkit.progress_current = 0
|
|
|
|
def update_progress(self, context, message):
|
|
context.scene.avatar_toolkit.progress_current += 1
|
|
progress = (context.scene.avatar_toolkit.progress_current / context.scene.avatar_toolkit.progress_steps) * 100
|
|
context.window_manager.progress_update(progress)
|
|
context.area.header_text_set(message)
|
|
self.report({'INFO'}, message)
|
|
|
|
def finish_progress(context):
|
|
context.window_manager.progress_end()
|
|
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:
|
|
# Create and configure the Vertex Weight Mix modifier
|
|
modifier = obj.modifiers.new(name="merge_weights", type="VERTEX_WEIGHT_MIX")
|
|
modifier.show_viewport = True
|
|
modifier.show_render = True
|
|
modifier.mix_set = 'B' # Replace weights in A with weights from B
|
|
modifier.vertex_group_a = target_group
|
|
modifier.vertex_group_b = source_group
|
|
modifier.mask_constant = 1.0
|
|
|
|
# Ensure we're in Object Mode
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
# Deselect all objects and select only our target object
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
obj.select_set(True)
|
|
context.view_layer.objects.active = obj
|
|
|
|
# Move modifier to the top of the stack if necessary
|
|
if len(obj.modifiers) > 1:
|
|
obj.modifiers.move(obj.modifiers.find(modifier.name), 0)
|
|
|
|
# Apply modifier
|
|
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
|
|
|
# Clean up
|
|
if delete_source_group and source_group in obj.vertex_groups:
|
|
obj.vertex_groups.remove(obj.vertex_groups[source_group])
|
|
|
|
return True
|
|
|