mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
286 lines
8.1 KiB
Python
286 lines
8.1 KiB
Python
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.")
|