import os import traceback import importlib import contextlib from typing import Dict, List, Union import bpy import addon_utils from openpype.lib import Logger from . import pipeline log = Logger.get_logger(__name__) def load_scripts(paths): """Copy of `load_scripts` from Blender's implementation. It is possible that this function will be changed in future and usage will be based on Blender version. """ import bpy_types loaded_modules = set() previous_classes = [ cls for cls in bpy.types.bpy_struct.__subclasses__() ] def register_module_call(mod): register = getattr(mod, "register", None) if register: try: register() except: traceback.print_exc() else: print("\nWarning! '%s' has no register function, " "this is now a requirement for registerable scripts" % mod.__file__) def unregister_module_call(mod): unregister = getattr(mod, "unregister", None) if unregister: try: unregister() except: traceback.print_exc() def test_reload(mod): # reloading this causes internal errors # because the classes from this module are stored internally # possibly to refresh internal references too but for now, best not to. if mod == bpy_types: return mod try: return importlib.reload(mod) except: traceback.print_exc() def test_register(mod): if mod: register_module_call(mod) bpy.utils._global_loaded_modules.append(mod.__name__) from bpy_restrict_state import RestrictBlend with RestrictBlend(): for base_path in paths: for path_subdir in bpy.utils._script_module_dirs: path = os.path.join(base_path, path_subdir) if not os.path.isdir(path): continue bpy.utils._sys_path_ensure_prepend(path) # Only add to 'sys.modules' unless this is 'startup'. if path_subdir != "startup": continue for mod in bpy.utils.modules_from_path(path, loaded_modules): test_register(mod) addons_paths = [] for base_path in paths: addons_path = os.path.join(base_path, "addons") if not os.path.exists(addons_path): continue addons_paths.append(addons_path) addons_module_path = os.path.join(addons_path, "modules") if os.path.exists(addons_module_path): bpy.utils._sys_path_ensure_prepend(addons_module_path) if addons_paths: # Fake addons origin_paths = addon_utils.paths def new_paths(): paths = origin_paths() + addons_paths return paths addon_utils.paths = new_paths addon_utils.modules_refresh() # load template (if set) if any(bpy.utils.app_template_paths()): import bl_app_template_utils bl_app_template_utils.reset(reload_scripts=False) del bl_app_template_utils for cls in bpy.types.bpy_struct.__subclasses__(): if cls in previous_classes: continue if not getattr(cls, "is_registered", False): continue for subcls in cls.__subclasses__(): if not subcls.is_registered: print( "Warning, unregistered class: %s(%s)" % (subcls.__name__, cls.__name__) ) def append_user_scripts(): user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS") if not user_scripts: return try: load_scripts(user_scripts.split(os.pathsep)) except Exception: print("Couldn't load user scripts \"{}\"".format(user_scripts)) traceback.print_exc() def imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict): r"""Write `data` to `node` as userDefined attributes Arguments: node: Long name of node data: Dictionary of key/value pairs Example: >>> import bpy >>> def compute(): ... return 6 ... >>> bpy.ops.mesh.primitive_cube_add() >>> cube = bpy.context.view_layer.objects.active >>> imprint(cube, { ... "regularString": "myFamily", ... "computedValue": lambda: compute() ... }) ... >>> cube['avalon']['computedValue'] 6 """ imprint_data = dict() for key, value in data.items(): if value is None: continue if callable(value): # Support values evaluated at imprint value = value() if not isinstance(value, (int, float, bool, str, list)): raise TypeError(f"Unsupported type: {type(value)}") imprint_data[key] = value pipeline.metadata_update(node, imprint_data) def lsattr(attr: str, value: Union[str, int, bool, List, Dict, None] = None) -> List: r"""Return nodes matching `attr` and `value` Arguments: attr: Name of Blender property value: Value of attribute. If none is provided, return all nodes with this attribute. Example: >>> lsattr("id", "myId") ... [bpy.data.objects["myNode"] >>> lsattr("id") ... [bpy.data.objects["myNode"], bpy.data.objects["myOtherNode"]] Returns: list """ return lsattrs({attr: value}) def lsattrs(attrs: Dict) -> List: r"""Return nodes with the given attribute(s). Arguments: attrs: Name and value pairs of expected matches Example: >>> lsattrs({"age": 5}) # Return nodes with an `age` of 5 # Return nodes with both `age` and `color` of 5 and blue >>> lsattrs({"age": 5, "color": "blue"}) Returns a list. """ # For now return all objects, not filtered by scene/collection/view_layer. matches = set() for coll in dir(bpy.data): if not isinstance( getattr(bpy.data, coll), bpy.types.bpy_prop_collection, ): continue for node in getattr(bpy.data, coll): for attr, value in attrs.items(): avalon_prop = node.get(pipeline.AVALON_PROPERTY) if not avalon_prop: continue if (avalon_prop.get(attr) and (value is None or avalon_prop.get(attr) == value)): matches.add(node) return list(matches) def read(node: bpy.types.bpy_struct_meta_idprop): """Return user-defined attributes from `node`""" data = dict(node.get(pipeline.AVALON_PROPERTY, {})) # Ignore hidden/internal data data = { key: value for key, value in data.items() if not key.startswith("_") } return data def get_selection() -> List[bpy.types.Object]: """Return the selected objects from the current scene.""" return [obj for obj in bpy.context.scene.objects if obj.select_get()] @contextlib.contextmanager def maintained_selection(): r"""Maintain selection during context Example: >>> with maintained_selection(): ... # Modify selection ... bpy.ops.object.select_all(action='DESELECT') >>> # Selection restored """ previous_selection = get_selection() previous_active = bpy.context.view_layer.objects.active try: yield finally: # Clear the selection for node in get_selection(): node.select_set(state=False) if previous_selection: for node in previous_selection: try: node.select_set(state=True) except ReferenceError: # This could happen if a selected node was deleted during # the context. log.exception("Failed to reselect") continue try: bpy.context.view_layer.objects.active = previous_active except ReferenceError: # This could happen if the active node was deleted during the # context. log.exception("Failed to set active object.") @contextlib.contextmanager def maintained_time(): """Maintain current frame during context.""" current_time = bpy.context.scene.frame_current try: yield finally: bpy.context.scene.frame_current = current_time