Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avatar Toolkit Overhaul - Do not merge yet #81

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 9 additions & 51 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,13 @@
if "bpy" not in locals():
import bpy
from . import ui
from . import core
from . import functions
from .core import register
from .core.register import __bl_ordered_classes
from .core import properties
from .core import addon_preferences
from .core.updater import check_for_update_on_start
else:
import importlib
importlib.reload(ui)
importlib.reload(core)
importlib.reload(functions)
importlib.reload(properties)
importlib.reload(addon_preferences)
modules = None
ordered_classes = None

def register():
print("Registering Avatar Toolkit")
# Register the addon properties
properties.register()

# Load the translations
functions.translations.load_translations()

# Order the classes before registration
core.register.order_classes()
# Register the UI classes
for cls in core.register.__bl_ordered_classes:
print("registering " + str(cls))
bpy.utils.register_class(cls)

#finally register properties that may use some classes.
core.register.register_properties()

bpy.app.handlers.load_post.append(check_for_update_on_start)

from .functions.mesh_tools import AvatarToolkit_OT_ApplyShapeKey

bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.separator()))
bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.operator(AvatarToolkit_OT_ApplyShapeKey.bl_idname, icon="KEY_HLT")))
from .core import auto_load
print("Starting registration")
auto_load.init()
auto_load.register()
print("Registration complete")

def unregister():
print("Unregistering Avatar Toolkit")
# Unregister the UI classes
if check_for_update_on_start in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(check_for_update_on_start)

# Iterate over the classes to unregister in reverse order and unregister them
for cls in reversed(list(__bl_ordered_classes)):
bpy.utils.unregister_class(cls)
print("unregistering " + str(cls))
core.register.unregister_properties()
properties.unregister()
from .core import auto_load
auto_load.unregister()
20 changes: 0 additions & 20 deletions core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +0,0 @@
# core/__init__.py

from .register import register_wrap

#to reload all things in this directory and import them properly - @989onan
if "bpy" not in locals():
import bpy
import glob
import os
from os.path import dirname, basename, isfile, join
modules = glob.glob(join(dirname(__file__), "*.py"))
for module_name in [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]:
exec("from . import "+module_name)
print("importing " +module_name)
else:
import importlib
modules = glob.glob(join(dirname(__file__), "*.py"))
for module_name in [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]:
exec("importlib.reload("+module_name+")")
print("reloading " +module_name)
21 changes: 16 additions & 5 deletions core/addon_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import tomllib
import json
from ..core.logging_setup import logger
from bpy.types import AddonPreferences
from typing import Any, Dict

Expand All @@ -12,22 +13,31 @@
def get_current_version():
main_dir = os.path.dirname(os.path.dirname(__file__))
manifest_path = os.path.join(main_dir, "blender_manifest.toml")
logger.debug(f"Reading version from manifest: {manifest_path}")
with open(manifest_path, 'rb') as f:
manifest_data = tomllib.load(f)
return manifest_data.get('version', 'Unknown')
version = manifest_data.get('version', 'Unknown')
logger.info(f"Current addon version: {version}")
return version

def save_preference(key: str, value: Any) -> None:
"""Save a single preference to the JSON file."""
logger.debug(f"Saving preference: {key} = {value}")
prefs = load_preferences()
prefs[key] = value
with open(PREFERENCES_FILE, 'w') as f:
json.dump(prefs, f, indent=4)
logger.info(f"Preference saved: {key}")

def load_preferences() -> Dict[str, Any]:
"""Load all preferences from the JSON file."""
logger.debug(f"Loading preferences from: {PREFERENCES_FILE}")
if os.path.exists(PREFERENCES_FILE):
with open(PREFERENCES_FILE, 'r') as f:
return json.load(f)
prefs = json.load(f)
logger.debug(f"Loaded preferences: {prefs}")
return prefs
logger.info("No preferences file found, using defaults")
return {}

def get_preference(key: str, default: Any = None) -> Any:
Expand All @@ -40,12 +50,13 @@ class AvatarToolkitPreferences(AddonPreferences):

def draw(self, context):
layout = self.layout
layout.label(text="Preferences are managed internally.")
# You can add more UI elements here if needed
layout.label(text=f"Version: {get_current_version()}")

def get_addon_preferences(context):
return context.preferences.addons[AvatarToolkitPreferences.bl_idname].preferences

# Initialize preferences if the file doesn't exist
if not os.path.exists(PREFERENCES_FILE):
save_preference("language", 0) # Set default language to 0 (auto)
save_preference("language", 0) # Set default language to 0 (auto)
save_preference("validation_mode", "STRICT") # Set default validation mode
save_preference("enable_logging", False) # Set default logging mode
193 changes: 193 additions & 0 deletions core/auto_load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import os
import bpy
import sys
import typing
import inspect
import pkgutil
import tomllib
import importlib
from pathlib import Path
from typing import List, Dict, Set, Optional, Any, Type, Tuple, Generator, TypeVar

__all__ = (
"init",
"register",
"unregister",
)

T = TypeVar('T')
modules: Optional[List[Any]] = None
ordered_classes: Optional[List[Type]] = None

def init() -> None:
"""Initialize the auto-loader by discovering modules and classes"""
global modules
global ordered_classes

# Configure logging first
from .logging_setup import configure_logging
configure_logging(False)

from .addon_preferences import get_preference
configure_logging(get_preference("enable_logging", False))

print("Auto-load init starting")
modules = get_all_submodules(Path(__file__).parent.parent)
ordered_classes = get_ordered_classes_to_register(modules)
print(f"Found modules: {modules}")
print(f"Found classes: {ordered_classes}")

def register() -> None:
"""Register all discovered classes and modules"""
print("Registering classes")
for cls in ordered_classes:
print(f"Registering: {cls}")
try:
bpy.utils.register_class(cls)
except ValueError:
continue

for module in modules:
if module.__name__ == __name__:
continue
if hasattr(module, "register"):
module.register()

def unregister() -> None:
"""Unregister all classes and modules in reverse order"""
for cls in reversed(ordered_classes):
bpy.utils.unregister_class(cls)

for module in modules:
if module.__name__ == __name__:
continue
if hasattr(module, "unregister"):
module.unregister()

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: 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):
if "__pycache__" in root:
continue
path = Path(root)
if path == directory:
package_name = f"bl_ext.user_default.{addon_id}"
else:
relative_path = path.relative_to(directory).as_posix().replace('/', '.')
package_name = f"bl_ext.user_default.{addon_id}.{relative_path}"
for name in sorted(iter_module_names(path)):
modules.append(importlib.import_module(f".{name}", package_name))
return modules

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: 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}")
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: 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: 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: 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: 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: 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: 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: 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: 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() -> 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",
"Node", "NodeSocket", "NodeTree",
"UIList", "RenderEngine"
])

def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
"""Topologically sort classes based on their dependencies"""
sorted_list = []
sorted_values = set()

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')]

for value, deps in base_panels:
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)

while len(deps_dict) > len(sorted_values):
unsorted = []
for value, deps in deps_dict.items():
if value not in sorted_values:
if len(deps - sorted_values) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)

return sorted_list
Loading