Start of the Major Overhaul
I decided to go through each function and UI section one by one, improving and overhauling things. Each function and section is going to be fully tested and not rushed out. This is the best way to catch things, but also include the code base as much as possible.
This commit is contained in:
+40
-27
@@ -7,6 +7,7 @@ import pkgutil
|
||||
import tomllib
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Set, Optional, Any, Type, Tuple, Generator, TypeVar
|
||||
|
||||
__all__ = (
|
||||
"init",
|
||||
@@ -14,10 +15,12 @@ __all__ = (
|
||||
"unregister",
|
||||
)
|
||||
|
||||
modules = None
|
||||
ordered_classes = None
|
||||
T = TypeVar('T')
|
||||
modules: Optional[List[Any]] = None
|
||||
ordered_classes: Optional[List[Type]] = None
|
||||
|
||||
def init():
|
||||
def init() -> None:
|
||||
"""Initialize the auto-loader by discovering modules and classes"""
|
||||
global modules
|
||||
global ordered_classes
|
||||
print("Auto-load init starting")
|
||||
@@ -26,7 +29,8 @@ def init():
|
||||
print(f"Found modules: {modules}")
|
||||
print(f"Found classes: {ordered_classes}")
|
||||
|
||||
def register():
|
||||
def register() -> None:
|
||||
"""Register all discovered classes and modules"""
|
||||
print("Registering classes")
|
||||
for cls in ordered_classes:
|
||||
print(f"Registering: {cls}")
|
||||
@@ -41,7 +45,8 @@ def register():
|
||||
if hasattr(module, "register"):
|
||||
module.register()
|
||||
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
"""Unregister all classes and modules in reverse order"""
|
||||
for cls in reversed(ordered_classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
@@ -51,13 +56,15 @@ def unregister():
|
||||
if hasattr(module, "unregister"):
|
||||
module.unregister()
|
||||
|
||||
def get_manifest_id():
|
||||
def get_manifest_id() -> str:
|
||||
"""Get the addon ID from the manifest file"""
|
||||
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
|
||||
with open(manifest_path, "rb") as f:
|
||||
manifest = tomllib.load(f)
|
||||
return manifest["id"]
|
||||
|
||||
def get_all_submodules(directory):
|
||||
def get_all_submodules(directory: Path) -> List[Any]:
|
||||
"""Discover and import all submodules in the given directory"""
|
||||
modules = []
|
||||
addon_id = get_manifest_id()
|
||||
for root, dirs, files in os.walk(directory):
|
||||
@@ -73,66 +80,75 @@ def get_all_submodules(directory):
|
||||
modules.append(importlib.import_module(f".{name}", package_name))
|
||||
return modules
|
||||
|
||||
def iter_submodules(path, package_name):
|
||||
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
|
||||
"""Iterate through submodules in a package"""
|
||||
for name in sorted(iter_module_names(path)):
|
||||
yield importlib.import_module("." + name, package_name)
|
||||
|
||||
def iter_module_names(path):
|
||||
print(f"Scanning path: {path}") # Debug path
|
||||
def iter_module_names(path: Path) -> Generator[str, None, None]:
|
||||
"""Iterate through module names in a directory"""
|
||||
print(f"Scanning path: {path}")
|
||||
modules_list = list(pkgutil.iter_modules([str(path)]))
|
||||
print(f"Found these modules: {modules_list}") # Debug modules
|
||||
print(f"Found these modules: {modules_list}")
|
||||
for _, module_name, is_pkg in modules_list:
|
||||
if not is_pkg:
|
||||
print(f"Found module: {module_name}")
|
||||
yield module_name
|
||||
|
||||
|
||||
|
||||
def get_ordered_classes_to_register(modules):
|
||||
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
|
||||
"""Get a topologically sorted list of classes to register"""
|
||||
return toposort(get_register_deps_dict(modules))
|
||||
|
||||
def get_register_deps_dict(modules):
|
||||
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
|
||||
"""Get dependencies dictionary for class registration"""
|
||||
deps_dict = {}
|
||||
classes_to_register = set(iter_classes_to_register(modules))
|
||||
for cls in classes_to_register:
|
||||
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
|
||||
return deps_dict
|
||||
|
||||
def iter_own_register_deps(cls, classes_to_register):
|
||||
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
|
||||
"""Iterate through a class's own registration dependencies"""
|
||||
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
|
||||
|
||||
def iter_register_deps(cls):
|
||||
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
|
||||
"""Iterate through all registration dependencies of a class"""
|
||||
for value in typing.get_type_hints(cls, {}, {}).values():
|
||||
dependency = get_dependency_from_annotation(value)
|
||||
if dependency is not None:
|
||||
yield dependency
|
||||
|
||||
def get_dependency_from_annotation(value):
|
||||
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
|
||||
"""Get dependency type from a type annotation"""
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
|
||||
return value[1]["type"]
|
||||
return None
|
||||
|
||||
def iter_classes_to_register(modules):
|
||||
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
|
||||
"""Iterate through classes that need to be registered"""
|
||||
base_types = get_register_base_types()
|
||||
for cls in get_classes_in_modules(modules):
|
||||
if any(base in base_types for base in cls.__bases__):
|
||||
if not getattr(cls, "_is_registered", False):
|
||||
yield cls
|
||||
|
||||
def get_classes_in_modules(modules):
|
||||
def get_classes_in_modules(modules: List[Any]) -> Set[Type]:
|
||||
"""Get all classes defined in the modules"""
|
||||
classes = set()
|
||||
for module in modules:
|
||||
for cls in iter_classes_in_module(module):
|
||||
classes.add(cls)
|
||||
return classes
|
||||
|
||||
def iter_classes_in_module(module):
|
||||
def iter_classes_in_module(module: Any) -> Generator[Type, None, None]:
|
||||
"""Iterate through classes defined in a module"""
|
||||
for value in module.__dict__.values():
|
||||
if inspect.isclass(value):
|
||||
yield value
|
||||
|
||||
def get_register_base_types():
|
||||
def get_register_base_types() -> Set[Type]:
|
||||
"""Get set of base types that need registration"""
|
||||
return set(getattr(bpy.types, name) for name in [
|
||||
"Panel", "Operator", "PropertyGroup",
|
||||
"AddonPreferences", "Header", "Menu",
|
||||
@@ -140,24 +156,22 @@ def get_register_base_types():
|
||||
"UIList", "RenderEngine"
|
||||
])
|
||||
|
||||
def toposort(deps_dict):
|
||||
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
|
||||
"""Topologically sort classes based on their dependencies"""
|
||||
sorted_list = []
|
||||
sorted_values = set()
|
||||
|
||||
# First pass: Register panels without parents
|
||||
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
|
||||
if hasattr(value, 'bl_parent_id')]
|
||||
|
||||
base_panels = [(value, deps) for value, deps in deps_dict.items()
|
||||
if not hasattr(value, 'bl_parent_id')]
|
||||
|
||||
# Add base panels first
|
||||
for value, deps in base_panels:
|
||||
if len(deps) == 0:
|
||||
sorted_list.append(value)
|
||||
sorted_values.add(value)
|
||||
|
||||
# Then add child panels
|
||||
while len(deps_dict) > len(sorted_values):
|
||||
unsorted = []
|
||||
for value, deps in deps_dict.items():
|
||||
@@ -169,4 +183,3 @@ def toposort(deps_dict):
|
||||
unsorted.append(value)
|
||||
|
||||
return sorted_list
|
||||
|
||||
|
||||
+72
-413
@@ -1,268 +1,83 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
from .dictionaries import bone_names
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
import typing
|
||||
from bpy.types import Context, Object
|
||||
from typing import Optional, Tuple, List, Set
|
||||
from ..core.translations import t
|
||||
from ..core.dictionaries import bone_names
|
||||
|
||||
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
|
||||
def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]:
|
||||
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||
armature_name = context.scene.avatar_toolkit.active_armature
|
||||
if armature_name and armature_name != 'NONE':
|
||||
return bpy.data.objects.get(armature_name)
|
||||
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')
|
||||
def set_active_armature(context: bpy.types.Context, armature: bpy.types.Object) -> None:
|
||||
"""Set the active armature for Avatar Toolkit operations"""
|
||||
context.scene.avatar_toolkit.active_armature = armature
|
||||
|
||||
def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tuple[str, str, str]]:
|
||||
"""Get list of all armature objects in the scene"""
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
armatures = [(obj.name, obj.name, "") for obj in context.scene.objects if obj.type == 'ARMATURE']
|
||||
if not armatures:
|
||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||
return armatures
|
||||
|
||||
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, str]:
|
||||
"""
|
||||
Validate if the selected object is a proper armature and has required bones
|
||||
Returns tuple of (is_valid, message)
|
||||
"""
|
||||
if not armature:
|
||||
return False, t("Armature.validation.no_armature")
|
||||
if armature.type != 'ARMATURE':
|
||||
return False, t("Armature.validation.not_armature")
|
||||
if not armature.data.bones:
|
||||
return False, t("Armature.validation.no_bones")
|
||||
|
||||
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
|
||||
found_bones: Set[str] = {bone.name.lower() for bone in armature.data.bones}
|
||||
|
||||
for bone in essential_bones:
|
||||
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
|
||||
return False, t("Armature.validation.missing_bone", bone=bone)
|
||||
|
||||
if source_name:
|
||||
return bpy.data.objects.get(str(source_name))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
return True, t("QuickAccess.valid_armature")
|
||||
|
||||
def set_selected_armature(context: Context, armature: Optional[Object]) -> None:
|
||||
context.scene.avatar_toolkit.selected_armature = armature.name if armature else ""
|
||||
def auto_select_single_armature(context: bpy.types.Context) -> None:
|
||||
"""Automatically select armature if only one exists in scene"""
|
||||
armatures = get_armature_list(context)
|
||||
if len(armatures) == 1:
|
||||
set_active_armature(context, armatures[0])
|
||||
|
||||
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 clear_default_objects() -> None:
|
||||
"""Removes default Blender objects (cube, light, camera)"""
|
||||
default_names: Set[str] = {'Cube', 'Light', 'Camera'}
|
||||
for obj in bpy.data.objects:
|
||||
if obj.name.split('.')[0] in default_names:
|
||||
bpy.data.objects.remove(obj, do_unlink=True)
|
||||
|
||||
def select_current_armature(context: Context) -> bool:
|
||||
armature = get_selected_armature(context)
|
||||
def get_armature_stats(armature: bpy.types.Object) -> dict:
|
||||
"""Get statistics about the armature"""
|
||||
return {
|
||||
'bone_count': len(armature.data.bones),
|
||||
'has_pose': bool(armature.pose),
|
||||
'visible': not armature.hide_viewport,
|
||||
'name': armature.name
|
||||
}
|
||||
|
||||
def get_all_meshes(context: Context) -> List[Object]:
|
||||
armature = get_active_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
|
||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||
return []
|
||||
|
||||
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:
|
||||
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]
|
||||
@@ -274,11 +89,10 @@ def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: list[Obje
|
||||
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:
|
||||
@@ -290,7 +104,7 @@ def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
|
||||
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)
|
||||
|
||||
@@ -298,10 +112,11 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
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 = ''
|
||||
@@ -316,7 +131,7 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
|
||||
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)
|
||||
|
||||
@@ -333,6 +148,7 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
|
||||
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):
|
||||
@@ -341,160 +157,3 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
|
||||
|
||||
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'
|
||||
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 len(obj.modifiers) > 1:
|
||||
obj.modifiers.move(obj.modifiers.find(modifier.name), 0)
|
||||
|
||||
# Apply modifier with correct syntax
|
||||
with context.temp_override(active_object=obj):
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
from typing import List, Optional
|
||||
from ...core.common import get_armature
|
||||
from ...core.common import get_active_armature
|
||||
from bpy.types import Object, ShapeKey, Mesh, Context, Operator
|
||||
from functools import lru_cache
|
||||
from ...core.translations import t
|
||||
@@ -12,10 +12,9 @@ class AvatarToolKit_OT_ExportResonite(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
filepath: bpy.props.StringProperty()
|
||||
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context):
|
||||
if get_armature(context) is None:
|
||||
if get_active_armature(context) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
+119
-44
@@ -1,54 +1,129 @@
|
||||
import bpy
|
||||
|
||||
# Importers which don't need much code should be added here, however if a importer needs alot of code
|
||||
# Like the PMX and PMD importers, they should be added to their own files and referenced in the import_types str->lambda dictionary.
|
||||
|
||||
#See below comments on how the system works. - @989onan
|
||||
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
from typing import Optional, Callable, Dict, List, Union, Set
|
||||
from ..common import clear_default_objects
|
||||
from .import_pmx import import_pmx
|
||||
from .import_pmd import import_pmd
|
||||
|
||||
if importlib.util.find_spec("io_scene_valvesource") is not None:
|
||||
#from .....scripts.addons.io_scene_valvesource.import_smd import SmdImporter #<- use this to check if your IDE is working properly. idfk
|
||||
from io_scene_valvesource.import_smd import SmdImporter #ignore IDE bitching this is fine, trust me, also above comment should be okay to an IDE usually if set up right. ^_^ - @989onan
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
def import_multi_files(method = None, directory: typing.Optional[str] = None, files: list[dict[str,str]] = None, filepath: typing.Optional[str] = ""):
|
||||
if not files:
|
||||
method(directory, filepath)
|
||||
else:
|
||||
for file in files:
|
||||
fullpath = os.path.join(directory,os.path.basename(file["name"]))
|
||||
print("run method!")
|
||||
method(directory, fullpath)
|
||||
#each import should map to a type. even in the case that multiple methods should import together, or have the same import method. Make sure the lambdas match so they get grouped together
|
||||
#In the case of a file importer that takes only one file argument and each one needs individual import, use above method. (example of it in use is ".dae" format)
|
||||
import_types: dict[str, typing.Callable[[str, list[dict[str,str]], str], None]] = {
|
||||
"fbx": (lambda directory, files, filepath : bpy.ops.import_scene.fbx(files=files, directory=directory, filepath=filepath,automatic_bone_orientation=False,use_prepost_rot=False,use_anim=False)),
|
||||
"smd": (lambda directory, files, filepath : eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")),
|
||||
"dmx": (lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")),
|
||||
"gltf": (lambda directory, files, filepath : bpy.ops.import_scene.gltf(files=files, filepath=filepath)),
|
||||
"glb": (lambda directory, files, filepath : bpy.ops.import_scene.gltf(files=files, filepath=filepath)),
|
||||
"qc": (lambda directory, files, filepath : eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)")),
|
||||
"obj": (lambda directory, files, filepath : bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath)),
|
||||
"dae": (lambda directory, files, filepath : import_multi_files(directory=directory, files=files, filepath=filepath, method = (lambda directory, filepath: bpy.ops.wm.collada_import(filepath=filepath, auto_connect = True, find_chains = True, fix_orientation = True)))),
|
||||
"3ds": (lambda directory, files, filepath : bpy.ops.import_scene.max3ds(files=files, directory=directory, filepath=filepath)),
|
||||
"stl": (lambda directory, files, filepath : bpy.ops.import_mesh.stl(files=files, directory=directory, filepath=filepath)),
|
||||
"mtl": (lambda directory, files, filepath : bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath)),
|
||||
"x3d": (lambda directory, files, filepath : bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath)),
|
||||
"wrl": (lambda directory, files, filepath : bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath)),
|
||||
"vmd": (lambda directory, files, filepath : import_multi_files(directory=directory, files=files, filepath=filepath, method = (lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)))),
|
||||
"vrm": (lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath)),
|
||||
"pmx": (lambda directory, files, filepath : import_pmx(filepath)),
|
||||
"pmd": (lambda directory, files, filepath : import_pmd(filepath)),
|
||||
import importlib.util
|
||||
|
||||
if importlib.util.find_spec("io_scene_valvesource") is not None:
|
||||
from io_scene_valvesource.import_smd import SmdImporter
|
||||
|
||||
class ImportProgress:
|
||||
"""Tracks and logs the progress of multi-file imports"""
|
||||
def __init__(self, total_files: int):
|
||||
self.total: int = total_files
|
||||
self.current: int = 0
|
||||
|
||||
def update(self, filename: str) -> None:
|
||||
"""Update import progress and log current file"""
|
||||
self.current += 1
|
||||
logger.info(f"Importing {filename} ({self.current}/{self.total})")
|
||||
|
||||
def validate_file(filepath: str) -> bool:
|
||||
"""
|
||||
Validate if a file exists and is accessible
|
||||
Returns: True if file is valid, False otherwise
|
||||
"""
|
||||
if not os.path.exists(filepath):
|
||||
logger.error(f"File not found: {filepath}")
|
||||
return False
|
||||
if not os.path.isfile(filepath):
|
||||
logger.error(f"Not a file: {filepath}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def import_multi_files(
|
||||
method: Optional[Callable] = None,
|
||||
directory: Optional[str] = None,
|
||||
files: Optional[List[Dict[str, str]]] = None,
|
||||
filepath: str = "",
|
||||
progress_callback: Optional[Callable[[str], None]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Import multiple files using the specified import method
|
||||
|
||||
Args:
|
||||
method: Import method to use
|
||||
directory: Directory containing files
|
||||
files: List of files to import
|
||||
filepath: Single file path to import
|
||||
progress_callback: Callback for progress updates
|
||||
"""
|
||||
try:
|
||||
if not method:
|
||||
raise ValueError("Import method not specified")
|
||||
|
||||
if not files:
|
||||
if not validate_file(filepath):
|
||||
return
|
||||
method(directory, filepath)
|
||||
if progress_callback:
|
||||
progress_callback(filepath)
|
||||
else:
|
||||
progress = ImportProgress(len(files))
|
||||
for file in files:
|
||||
fullpath: str = os.path.join(directory, os.path.basename(file["name"]))
|
||||
if not validate_file(fullpath):
|
||||
continue
|
||||
|
||||
logger.info(f"Importing file: {fullpath}")
|
||||
method(directory, fullpath)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(fullpath)
|
||||
progress.update(file["name"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Import failed: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
ImportMethod = Callable[[str, List[Dict[str, str]], str], None]
|
||||
|
||||
import_types: Dict[str, ImportMethod] = {
|
||||
"fbx": lambda directory, files, filepath: bpy.ops.import_scene.fbx(
|
||||
files=files, directory=directory, filepath=filepath,
|
||||
automatic_bone_orientation=False, use_prepost_rot=False, use_anim=False
|
||||
),
|
||||
"smd": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
|
||||
"dmx": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
|
||||
"gltf": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
|
||||
"glb": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
|
||||
"qc": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
|
||||
"obj": lambda directory, files, filepath: bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath),
|
||||
"dae": lambda directory, files, filepath: import_multi_files(
|
||||
directory=directory,
|
||||
files=files,
|
||||
filepath=filepath,
|
||||
method=lambda directory, filepath: bpy.ops.wm.collada_import(
|
||||
filepath=filepath, auto_connect=True, find_chains=True, fix_orientation=True
|
||||
)
|
||||
),
|
||||
"3ds": lambda directory, files, filepath: bpy.ops.import_scene.max3ds(files=files, directory=directory, filepath=filepath),
|
||||
"stl": lambda directory, files, filepath: bpy.ops.import_mesh.stl(files=files, directory=directory, filepath=filepath),
|
||||
"mtl": lambda directory, files, filepath: bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath),
|
||||
"x3d": lambda directory, files, filepath: bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath),
|
||||
"wrl": lambda directory, files, filepath: bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath),
|
||||
"vmd": lambda directory, files, filepath: import_multi_files(
|
||||
directory=directory,
|
||||
files=files,
|
||||
filepath=filepath,
|
||||
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
|
||||
),
|
||||
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
|
||||
"pmx": lambda directory, files, filepath: import_pmx(filepath),
|
||||
"pmd": lambda directory, files, filepath: import_pmd(filepath),
|
||||
}
|
||||
|
||||
def concat_imports_filter(imports):
|
||||
names = ""
|
||||
for importer in imports.keys():
|
||||
names = names+"*."+importer+";"
|
||||
return names
|
||||
def concat_imports_filter(imports: Dict[str, ImportMethod]) -> str:
|
||||
"""Create a file filter string from import types"""
|
||||
return "".join(f"*.{importer};" for importer in imports.keys())
|
||||
|
||||
imports = concat_imports_filter(import_types)
|
||||
imports: str = concat_imports_filter(import_types)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
# thank you https://stackoverflow.com/a/71432759
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import Optional
|
||||
from bpy.types import Image, Material
|
||||
|
||||
|
||||
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
class Rectangle_Obj:
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
down: Rectangle_Obj = None
|
||||
used: bool = False
|
||||
right: Rectangle_Obj = None
|
||||
|
||||
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.down = down
|
||||
self.used = used
|
||||
self.right = right
|
||||
|
||||
def split(self, w, h) -> Rectangle_Obj:
|
||||
self.used = True
|
||||
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
|
||||
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
|
||||
return self
|
||||
|
||||
def find(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
if self.used:
|
||||
return self.right.find(w, h) or self.down.find(w, h)
|
||||
elif (w <= self.w) and (h <= self.h):
|
||||
return self
|
||||
return None
|
||||
|
||||
class MaterialImageList:
|
||||
albedo: Image
|
||||
normal: Image
|
||||
emission: Image
|
||||
ambient_occlusion: Image
|
||||
height: Image
|
||||
roughness: Image
|
||||
fit: Rectangle_Obj
|
||||
material: Material
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BinPacker(object):
|
||||
root: Rectangle_Obj
|
||||
bin: list[MaterialImageList] = []
|
||||
def __init__(self, structure: list[MaterialImageList]):
|
||||
self.root = None
|
||||
self.bin = structure
|
||||
|
||||
def fit(self):
|
||||
structure = self.bin
|
||||
structure_len = len(self.bin)
|
||||
w: int = 0
|
||||
h: int = 0
|
||||
if structure_len > 0:
|
||||
w = structure[0].w
|
||||
h = structure[0].h
|
||||
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
|
||||
for img in structure:
|
||||
w = img.w
|
||||
h = img.h
|
||||
node = self.root.find(w, h)
|
||||
if node:
|
||||
img.fit = node.split(w, h)
|
||||
else:
|
||||
img.fit = self.grow_node(w, h)
|
||||
return structure
|
||||
|
||||
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
can_grow_right = (h <= self.root.h)
|
||||
can_grow_down = (w <= self.root.w)
|
||||
|
||||
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
|
||||
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
|
||||
|
||||
if should_grow_right:
|
||||
return self.grow_right(w, h)
|
||||
elif should_grow_down:
|
||||
return self.grow_down(w, h)
|
||||
elif can_grow_right:
|
||||
return self.grow_right(w, h)
|
||||
elif can_grow_down:
|
||||
return self.grow_down(w, h)
|
||||
return None
|
||||
|
||||
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
self.root = Rectangle_Obj(
|
||||
used=True,
|
||||
x=0,
|
||||
y=0,
|
||||
w=self.root.w + w,
|
||||
h=self.root.h,
|
||||
down=self.root,
|
||||
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
|
||||
node = self.root.find(w, h)
|
||||
if node:
|
||||
return node.split(w, h)
|
||||
return None
|
||||
|
||||
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
|
||||
self.root = Rectangle_Obj(
|
||||
used=True,
|
||||
x=0,
|
||||
y=0,
|
||||
w=self.root.w,
|
||||
h=self.root.h + h,
|
||||
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
|
||||
right=self.root
|
||||
)
|
||||
node = self.root.find(w, h)
|
||||
if node:
|
||||
return node.split(w, h)
|
||||
return None
|
||||
+23
-173
@@ -1,189 +1,39 @@
|
||||
import bpy
|
||||
from .translations import t, get_languages_list, update_language
|
||||
from typing import List, Tuple, Optional
|
||||
from bpy.types import PropertyGroup, Material, Scene, Object, Context
|
||||
from bpy.props import (StringProperty, BoolProperty, EnumProperty,
|
||||
IntProperty, FloatProperty, CollectionProperty,
|
||||
PointerProperty)
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
EnumProperty,
|
||||
IntProperty,
|
||||
FloatProperty,
|
||||
CollectionProperty,
|
||||
PointerProperty
|
||||
)
|
||||
from .translations import t, get_languages_list, update_language
|
||||
from .addon_preferences import get_preference
|
||||
from .common import SceneMatClass, MaterialListBool, get_armatures, get_mesh_items, get_armatures_that_are_not_selected
|
||||
from .updater import get_version_list
|
||||
from .common import get_armature_list
|
||||
|
||||
class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
language: EnumProperty(
|
||||
name="Language",
|
||||
description="Select the language for the addon",
|
||||
items=get_languages_list,
|
||||
update=update_language
|
||||
)
|
||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||
|
||||
selected_mesh: EnumProperty(
|
||||
items=get_mesh_items,
|
||||
name="Selected Mesh",
|
||||
description="Select mesh to modify"
|
||||
)
|
||||
|
||||
material_search_filter: StringProperty(
|
||||
name="Search Materials",
|
||||
description="Filter materials by name",
|
||||
default=""
|
||||
)
|
||||
|
||||
merge_armature_apply_transforms: BoolProperty(
|
||||
default=False,
|
||||
name="Apply Transforms",
|
||||
description="Apply transforms when merging armatures"
|
||||
)
|
||||
|
||||
merge_armature_align_bones: BoolProperty(
|
||||
default=False,
|
||||
name="Align Bones",
|
||||
description="Align bones when merging armatures"
|
||||
)
|
||||
|
||||
progress_steps: IntProperty(default=0)
|
||||
progress_current: IntProperty(default=0)
|
||||
language_changed: BoolProperty(default=False)
|
||||
|
||||
mouth_a: StringProperty(
|
||||
name="Mouth A",
|
||||
description="Shape key for A sound"
|
||||
)
|
||||
|
||||
mouth_o: StringProperty(
|
||||
name="Mouth O",
|
||||
description="Shape key for O sound"
|
||||
)
|
||||
|
||||
mouth_ch: StringProperty(
|
||||
name="Mouth CH",
|
||||
description="Shape key for CH sound"
|
||||
)
|
||||
|
||||
shape_intensity: FloatProperty(
|
||||
name="Shape Intensity",
|
||||
description="Intensity of shape key modifications",
|
||||
default=1.0,
|
||||
min=0.0,
|
||||
max=2.0
|
||||
)
|
||||
|
||||
merge_twist_bones: BoolProperty(
|
||||
name="Merge Twist Bones",
|
||||
description="Merge twist bones during processing",
|
||||
default=True
|
||||
)
|
||||
|
||||
selected_armature: EnumProperty(
|
||||
items=get_armatures,
|
||||
name="Selected Armature",
|
||||
description="Select the armature to work with"
|
||||
)
|
||||
|
||||
merge_armature_source: EnumProperty(
|
||||
items=get_armatures_that_are_not_selected,
|
||||
name="Source Armature",
|
||||
description="Select the source armature for merging"
|
||||
)
|
||||
|
||||
texture_atlas_material_index: IntProperty(
|
||||
default=-1,
|
||||
get=lambda self: -1,
|
||||
set=lambda self, context: None
|
||||
)
|
||||
|
||||
materials: CollectionProperty(type=SceneMatClass)
|
||||
|
||||
texture_atlas_Has_Mat_List_Shown: BoolProperty(
|
||||
default=False,
|
||||
get=MaterialListBool.get_bool,
|
||||
set=MaterialListBool.set_bool
|
||||
)
|
||||
|
||||
avatar_toolkit_updater_version_list: EnumProperty(
|
||||
items=get_version_list,
|
||||
name="Version List",
|
||||
description="List of available versions"
|
||||
name=t("Scene.avatar_toolkit_updater_version_list.name"),
|
||||
description=t("Scene.avatar_toolkit_updater_version_list.description")
|
||||
)
|
||||
|
||||
|
||||
class AvatarToolkitMaterialProperties(PropertyGroup):
|
||||
material_expanded: BoolProperty(
|
||||
name="Expand Material",
|
||||
description="Show/hide material properties",
|
||||
default=False
|
||||
active_armature: EnumProperty(
|
||||
items=get_armature_list,
|
||||
name=t("QuickAccess.select_armature"),
|
||||
description=t("QuickAccess.select_armature")
|
||||
)
|
||||
|
||||
include_in_atlas: BoolProperty(
|
||||
name="Include in Atlas",
|
||||
description="Include this material in texture atlas",
|
||||
default=True
|
||||
)
|
||||
|
||||
def get_texture_node_list(self, context):
|
||||
# Access the material through the property group's id_data
|
||||
material = self.id_data
|
||||
if material and material.use_nodes:
|
||||
nodes = [(i.image.name if i.image else i.name+"_image",
|
||||
i.image.name if i.image else "node with no image...",
|
||||
i.image.name if i.image else i.name, index+1)
|
||||
for index, i in enumerate(material.node_tree.nodes)
|
||||
if i.bl_idname == "ShaderNodeTexImage"]
|
||||
if not nodes:
|
||||
nodes = [("Error", "No images found", "Error", 0)]
|
||||
else:
|
||||
nodes = [("Error", "No node tree found", "Error", 0)]
|
||||
nodes.append(("None", "None", "None", 0))
|
||||
return nodes
|
||||
|
||||
texture_atlas_albedo: EnumProperty(
|
||||
name="Albedo",
|
||||
description="Albedo texture for atlas",
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
texture_atlas_normal: EnumProperty(
|
||||
name="Normal",
|
||||
description="Normal map for atlas",
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
texture_atlas_emission: EnumProperty(
|
||||
name="Emission",
|
||||
description="Emission texture for atlas",
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
texture_atlas_ambient_occlusion: EnumProperty(
|
||||
name="Ambient Occlusion",
|
||||
description="AO texture for atlas",
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
texture_atlas_height: EnumProperty(
|
||||
name="Height",
|
||||
description="Height map for atlas",
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
texture_atlas_roughness: EnumProperty(
|
||||
name="Roughness",
|
||||
description="Roughness map for atlas",
|
||||
items=get_texture_node_list
|
||||
)
|
||||
|
||||
class AvatarToolkitObjectProperties(PropertyGroup):
|
||||
material_group_expanded: BoolProperty(
|
||||
name="Expand Material Group",
|
||||
description="Show/hide materials for this mesh",
|
||||
default=False
|
||||
)
|
||||
|
||||
def register():
|
||||
def register() -> None:
|
||||
"""Register the Avatar Toolkit property group"""
|
||||
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
|
||||
bpy.types.Material.avatar_toolkit = PointerProperty(type=AvatarToolkitMaterialProperties)
|
||||
bpy.types.Object.avatar_toolkit = PointerProperty(type=AvatarToolkitObjectProperties)
|
||||
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
"""Unregister the Avatar Toolkit property group"""
|
||||
del bpy.types.Scene.avatar_toolkit
|
||||
del bpy.types.Material.avatar_toolkit
|
||||
del bpy.types.Object.avatar_toolkit
|
||||
|
||||
+34
-8
@@ -276,22 +276,48 @@ def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, s
|
||||
return [(v, v, '') for v in version_list.keys()] if version_list else []
|
||||
|
||||
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||
col = layout.column(align=True)
|
||||
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
|
||||
# Header
|
||||
row = col.row()
|
||||
row.scale_y = 1.2
|
||||
row.label(text=t('Updater.label'), icon='DOWNARROW_HLT')
|
||||
|
||||
col.separator()
|
||||
|
||||
# Update check/status section
|
||||
if is_checking_for_update:
|
||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label'))
|
||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
||||
text=t('Updater.CheckForUpdateButton.label'),
|
||||
icon='SORTTIME')
|
||||
elif update_needed:
|
||||
col.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToLatestButton.label', name=latest_version_str))
|
||||
update_row = col.row(align=True)
|
||||
update_row.scale_y = 1.5
|
||||
update_row.alert = True
|
||||
update_row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname,
|
||||
text=t('Updater.UpdateToLatestButton.label', name=latest_version_str),
|
||||
icon='IMPORT')
|
||||
else:
|
||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label_alt'))
|
||||
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
|
||||
text=t('Updater.CheckForUpdateButton.label_alt'),
|
||||
icon='FILE_REFRESH')
|
||||
|
||||
# Version selection section
|
||||
col.separator()
|
||||
row = col.row(align=True)
|
||||
box_inner = col.box()
|
||||
box_inner.label(text=t('Updater.selectVersion'), icon='SETTINGS')
|
||||
row = box_inner.row(align=True)
|
||||
row.prop(context.scene.avatar_toolkit, 'avatar_toolkit_updater_version_list', text='')
|
||||
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToSelectedButton.label'))
|
||||
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname,
|
||||
text=t('Updater.UpdateToSelectedButton.label'),
|
||||
icon='IMPORT')
|
||||
|
||||
# Current version info
|
||||
col.separator()
|
||||
col.label(text=t('Updater.currentVersion').format(name=get_current_version()))
|
||||
curr_ver_row = col.row()
|
||||
curr_ver_row.label(text=t('Updater.currentVersion').format(name=get_current_version()),
|
||||
icon='CHECKMARK')
|
||||
|
||||
def ui_refresh() -> None:
|
||||
for windowManager in bpy.data.window_managers:
|
||||
|
||||
Reference in New Issue
Block a user