mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
Merge branch 'develop' into enhancement/create_context_typing
This commit is contained in:
commit
63d68b7fef
257 changed files with 2334 additions and 1068 deletions
|
|
@ -15,6 +15,7 @@ from abc import ABCMeta, abstractmethod
|
|||
import six
|
||||
import appdirs
|
||||
import ayon_api
|
||||
from semver import VersionInfo
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.lib import Logger, is_dev_mode_enabled
|
||||
|
|
@ -46,6 +47,11 @@ IGNORED_HOSTS_IN_AYON = {
|
|||
}
|
||||
IGNORED_MODULES_IN_AYON = set()
|
||||
|
||||
# When addon was moved from ayon-core codebase
|
||||
# - this is used to log the missing addon
|
||||
MOVED_ADDON_MILESTONE_VERSIONS = {
|
||||
"applications": VersionInfo(0, 2, 0),
|
||||
}
|
||||
|
||||
# Inherit from `object` for Python 2 hosts
|
||||
class _ModuleClass(object):
|
||||
|
|
@ -192,6 +198,45 @@ def _get_ayon_addons_information(bundle_info):
|
|||
return output
|
||||
|
||||
|
||||
def _handle_moved_addons(addon_name, milestone_version, log):
|
||||
"""Log message that addon version is not compatible with current core.
|
||||
|
||||
The function can return path to addon client code, but that can happen
|
||||
only if ayon-core is used from code (for development), but still
|
||||
logs a warning.
|
||||
|
||||
Args:
|
||||
addon_name (str): Addon name.
|
||||
milestone_version (str): Milestone addon version.
|
||||
log (logging.Logger): Logger object.
|
||||
|
||||
Returns:
|
||||
Union[str, None]: Addon dir or None.
|
||||
"""
|
||||
# Handle addons which were moved out of ayon-core
|
||||
# - Try to fix it by loading it directly from server addons dir in
|
||||
# ayon-core repository. But that will work only if ayon-core is
|
||||
# used from code.
|
||||
addon_dir = os.path.join(
|
||||
os.path.dirname(os.path.dirname(AYON_CORE_ROOT)),
|
||||
"server_addon",
|
||||
addon_name,
|
||||
"client",
|
||||
)
|
||||
if not os.path.exists(addon_dir):
|
||||
log.error((
|
||||
"Addon '{}' is not be available."
|
||||
" Please update applications addon to '{}' or higher."
|
||||
).format(addon_name, milestone_version))
|
||||
return None
|
||||
|
||||
log.warning((
|
||||
"Please update '{}' addon to '{}' or higher."
|
||||
" Using client code from ayon-core repository."
|
||||
).format(addon_name, milestone_version))
|
||||
return addon_dir
|
||||
|
||||
|
||||
def _load_ayon_addons(openpype_modules, modules_key, log):
|
||||
"""Load AYON addons based on information from server.
|
||||
|
||||
|
|
@ -249,6 +294,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log):
|
|||
use_dev_path = dev_addon_info.get("enabled", False)
|
||||
|
||||
addon_dir = None
|
||||
milestone_version = MOVED_ADDON_MILESTONE_VERSIONS.get(addon_name)
|
||||
if use_dev_path:
|
||||
addon_dir = dev_addon_info["path"]
|
||||
if not addon_dir or not os.path.exists(addon_dir):
|
||||
|
|
@ -257,6 +303,16 @@ def _load_ayon_addons(openpype_modules, modules_key, log):
|
|||
).format(addon_name, addon_version, addon_dir))
|
||||
continue
|
||||
|
||||
elif (
|
||||
milestone_version is not None
|
||||
and VersionInfo.parse(addon_version) < milestone_version
|
||||
):
|
||||
addon_dir = _handle_moved_addons(
|
||||
addon_name, milestone_version, log
|
||||
)
|
||||
if not addon_dir:
|
||||
continue
|
||||
|
||||
elif addons_dir_exists:
|
||||
folder_name = "{}_{}".format(addon_name, addon_version)
|
||||
addon_dir = os.path.join(addons_dir, folder_name)
|
||||
|
|
@ -336,66 +392,9 @@ def _load_ayon_addons(openpype_modules, modules_key, log):
|
|||
return addons_to_skip_in_core
|
||||
|
||||
|
||||
def _load_ayon_core_addons_dir(
|
||||
ignore_addon_names, openpype_modules, modules_key, log
|
||||
):
|
||||
addons_dir = os.path.join(AYON_CORE_ROOT, "addons")
|
||||
if not os.path.exists(addons_dir):
|
||||
return
|
||||
|
||||
imported_modules = []
|
||||
|
||||
# Make sure that addons which already have client code are not loaded
|
||||
# from core again, with older code
|
||||
filtered_paths = []
|
||||
for name in os.listdir(addons_dir):
|
||||
if name in ignore_addon_names:
|
||||
continue
|
||||
path = os.path.join(addons_dir, name)
|
||||
if os.path.isdir(path):
|
||||
filtered_paths.append(path)
|
||||
|
||||
for path in filtered_paths:
|
||||
while path in sys.path:
|
||||
sys.path.remove(path)
|
||||
sys.path.insert(0, path)
|
||||
|
||||
for name in os.listdir(path):
|
||||
fullpath = os.path.join(path, name)
|
||||
if os.path.isfile(fullpath):
|
||||
basename, ext = os.path.splitext(name)
|
||||
if ext != ".py":
|
||||
continue
|
||||
else:
|
||||
basename = name
|
||||
try:
|
||||
module = __import__(basename, fromlist=("",))
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if (
|
||||
inspect.isclass(attr)
|
||||
and issubclass(attr, AYONAddon)
|
||||
):
|
||||
new_import_str = "{}.{}".format(modules_key, basename)
|
||||
sys.modules[new_import_str] = module
|
||||
setattr(openpype_modules, basename, module)
|
||||
imported_modules.append(module)
|
||||
break
|
||||
|
||||
except Exception:
|
||||
log.error(
|
||||
"Failed to import addon '{}'.".format(fullpath),
|
||||
exc_info=True
|
||||
)
|
||||
return imported_modules
|
||||
|
||||
|
||||
def _load_addons_in_core(
|
||||
ignore_addon_names, openpype_modules, modules_key, log
|
||||
):
|
||||
_load_ayon_core_addons_dir(
|
||||
ignore_addon_names, openpype_modules, modules_key, log
|
||||
)
|
||||
# Add current directory at first place
|
||||
# - has small differences in import logic
|
||||
hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
import sys
|
||||
import code
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import acre
|
||||
|
|
@ -11,7 +12,7 @@ import acre
|
|||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.settings import get_general_environments
|
||||
from ayon_core.lib import initialize_ayon_connection
|
||||
from ayon_core.lib import initialize_ayon_connection, is_running_from_build
|
||||
|
||||
from .cli_commands import Commands
|
||||
|
||||
|
|
@ -167,16 +168,27 @@ def run(script):
|
|||
|
||||
if not script:
|
||||
print("Error: missing path to script file.")
|
||||
return
|
||||
|
||||
# Remove first argument if it is the same as AYON executable
|
||||
# - Forward compatibility with future AYON versions.
|
||||
# - Current AYON launcher keeps the arguments with first argument but
|
||||
# future versions might remove it.
|
||||
first_arg = sys.argv[0]
|
||||
if is_running_from_build():
|
||||
comp_path = os.getenv("AYON_EXECUTABLE")
|
||||
else:
|
||||
comp_path = os.path.join(os.environ["AYON_ROOT"], "start.py")
|
||||
# Compare paths and remove first argument if it is the same as AYON
|
||||
if Path(first_arg).resolve() == Path(comp_path).resolve():
|
||||
sys.argv.pop(0)
|
||||
|
||||
args = sys.argv
|
||||
args.remove("run")
|
||||
args.remove(script)
|
||||
sys.argv = args
|
||||
# Remove 'run' command from sys.argv
|
||||
sys.argv.remove("run")
|
||||
|
||||
args_string = " ".join(args[1:])
|
||||
print(f"... running: {script} {args_string}")
|
||||
runpy.run_path(script, run_name="__main__", )
|
||||
args_string = " ".join(sys.argv[1:])
|
||||
print(f"... running: {script} {args_string}")
|
||||
runpy.run_path(script, run_name="__main__")
|
||||
|
||||
|
||||
@main_cli.command()
|
||||
|
|
|
|||
|
|
@ -36,23 +36,23 @@ class HostDirmap(object):
|
|||
host_name,
|
||||
project_name,
|
||||
project_settings=None,
|
||||
sync_module=None
|
||||
sitesync_addon=None
|
||||
):
|
||||
self.host_name = host_name
|
||||
self.project_name = project_name
|
||||
self._project_settings = project_settings
|
||||
self._sync_module = sync_module
|
||||
self._sitesync_addon = sitesync_addon
|
||||
# to limit reinit of Modules
|
||||
self._sync_module_discovered = sync_module is not None
|
||||
self._sitesync_addon_discovered = sitesync_addon is not None
|
||||
self._log = None
|
||||
|
||||
@property
|
||||
def sync_module(self):
|
||||
if not self._sync_module_discovered:
|
||||
self._sync_module_discovered = True
|
||||
def sitesync_addon(self):
|
||||
if not self._sitesync_addon_discovered:
|
||||
self._sitesync_addon_discovered = True
|
||||
manager = AddonsManager()
|
||||
self._sync_module = manager.get("sync_server")
|
||||
return self._sync_module
|
||||
self._sitesync_addon = manager.get("sitesync")
|
||||
return self._sitesync_addon
|
||||
|
||||
@property
|
||||
def project_settings(self):
|
||||
|
|
@ -158,25 +158,25 @@ class HostDirmap(object):
|
|||
"""
|
||||
project_name = self.project_name
|
||||
|
||||
sync_module = self.sync_module
|
||||
sitesync_addon = self.sitesync_addon
|
||||
mapping = {}
|
||||
if (
|
||||
sync_module is None
|
||||
or not sync_module.enabled
|
||||
or project_name not in sync_module.get_enabled_projects()
|
||||
sitesync_addon is None
|
||||
or not sitesync_addon.enabled
|
||||
or project_name not in sitesync_addon.get_enabled_projects()
|
||||
):
|
||||
return mapping
|
||||
|
||||
active_site = sync_module.get_local_normalized_site(
|
||||
sync_module.get_active_site(project_name))
|
||||
remote_site = sync_module.get_local_normalized_site(
|
||||
sync_module.get_remote_site(project_name))
|
||||
active_site = sitesync_addon.get_local_normalized_site(
|
||||
sitesync_addon.get_active_site(project_name))
|
||||
remote_site = sitesync_addon.get_local_normalized_site(
|
||||
sitesync_addon.get_remote_site(project_name))
|
||||
self.log.debug(
|
||||
"active {} - remote {}".format(active_site, remote_site)
|
||||
)
|
||||
|
||||
if active_site == "local" and active_site != remote_site:
|
||||
sync_settings = sync_module.get_sync_project_setting(
|
||||
sync_settings = sitesync_addon.get_sync_project_setting(
|
||||
project_name,
|
||||
exclude_locals=False,
|
||||
cached=False)
|
||||
|
|
@ -194,7 +194,7 @@ class HostDirmap(object):
|
|||
self.log.debug("remote overrides {}".format(remote_overrides))
|
||||
|
||||
current_platform = platform.system().lower()
|
||||
remote_provider = sync_module.get_provider_for_site(
|
||||
remote_provider = sitesync_addon.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
# dirmap has sense only with regular disk provider, in the workfile
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class BackgroundLoader(api.AfterEffectsLoader):
|
|||
"""
|
||||
label = "Load JSON Background"
|
||||
product_types = {"background"}
|
||||
representations = ["json"]
|
||||
representations = {"json"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
stub = self.get_stub()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class FileLoader(api.AfterEffectsLoader):
|
|||
"review",
|
||||
"audio",
|
||||
}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
stub = self.get_stub()
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
|
||||
def get_instances(self, context):
|
||||
instances = []
|
||||
instances_to_remove = []
|
||||
|
||||
app_version = CollectAERender.get_stub().get_app_version()
|
||||
app_version = app_version[0:4]
|
||||
|
|
@ -117,7 +116,10 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
fps=fps,
|
||||
app_version=app_version,
|
||||
publish_attributes=inst.data.get("publish_attributes", {}),
|
||||
file_names=[item.file_name for item in render_q]
|
||||
file_names=[item.file_name for item in render_q],
|
||||
|
||||
# The source instance this render instance replaces
|
||||
source_instance=inst
|
||||
)
|
||||
|
||||
comp = compositions_by_id.get(comp_id)
|
||||
|
|
@ -145,10 +147,7 @@ class CollectAERender(publish.AbstractCollectRender):
|
|||
instance.families.remove("review")
|
||||
|
||||
instances.append(instance)
|
||||
instances_to_remove.append(inst)
|
||||
|
||||
for instance in instances_to_remove:
|
||||
context.remove(instance)
|
||||
return instances
|
||||
|
||||
def get_expected_files(self, render_instance):
|
||||
|
|
|
|||
|
|
@ -55,8 +55,7 @@ class BlenderAddon(AYONAddon, IHostAddon):
|
|||
)
|
||||
|
||||
# Define Qt binding if not defined
|
||||
if not env.get("QT_PREFERRED_BINDING"):
|
||||
env["QT_PREFERRED_BINDING"] = "PySide2"
|
||||
env.pop("QT_PREFERRED_BINDING", None)
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
if app.host_name != self.host_name:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
|
||||
def inner_execute(self):
|
||||
# Get blender's python directory
|
||||
version_regex = re.compile(r"^[2-4]\.[0-9]+$")
|
||||
version_regex = re.compile(r"^([2-4])\.[0-9]+$")
|
||||
|
||||
platform = system().lower()
|
||||
executable = self.launch_context.executable.executable_path
|
||||
|
|
@ -42,7 +42,8 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
if os.path.basename(executable).lower() != expected_executable:
|
||||
self.log.info((
|
||||
f"Executable does not lead to {expected_executable} file."
|
||||
"Can't determine blender's python to check/install PySide2."
|
||||
"Can't determine blender's python to check/install"
|
||||
" Qt binding."
|
||||
))
|
||||
return
|
||||
|
||||
|
|
@ -73,6 +74,15 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
return
|
||||
|
||||
version_subfolder = version_subfolders[0]
|
||||
before_blender_4 = False
|
||||
if int(version_regex.match(version_subfolder).group(1)) < 4:
|
||||
before_blender_4 = True
|
||||
# Blender 4 has Python 3.11 which does not support 'PySide2'
|
||||
# QUESTION could we always install PySide6?
|
||||
qt_binding = "PySide2" if before_blender_4 else "PySide6"
|
||||
# Use PySide6 6.6.3 because 6.7.0 had a bug
|
||||
# - 'QTextEdit' can't be added to 'QBoxLayout'
|
||||
qt_binding_version = None if before_blender_4 else "6.6.3"
|
||||
|
||||
python_dir = os.path.join(versions_dir, version_subfolder, "python")
|
||||
python_lib = os.path.join(python_dir, "lib")
|
||||
|
|
@ -116,22 +126,41 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
return
|
||||
|
||||
# Check if PySide2 is installed and skip if yes
|
||||
if self.is_pyside_installed(python_executable):
|
||||
if self.is_pyside_installed(python_executable, qt_binding):
|
||||
self.log.debug("Blender has already installed PySide2.")
|
||||
return
|
||||
|
||||
# Install PySide2 in blender's python
|
||||
if platform == "windows":
|
||||
result = self.install_pyside_windows(python_executable)
|
||||
result = self.install_pyside_windows(
|
||||
python_executable,
|
||||
qt_binding,
|
||||
qt_binding_version,
|
||||
before_blender_4,
|
||||
)
|
||||
else:
|
||||
result = self.install_pyside(python_executable)
|
||||
result = self.install_pyside(
|
||||
python_executable,
|
||||
qt_binding,
|
||||
qt_binding_version,
|
||||
)
|
||||
|
||||
if result:
|
||||
self.log.info("Successfully installed PySide2 module to blender.")
|
||||
self.log.info(
|
||||
f"Successfully installed {qt_binding} module to blender."
|
||||
)
|
||||
else:
|
||||
self.log.warning("Failed to install PySide2 module to blender.")
|
||||
self.log.warning(
|
||||
f"Failed to install {qt_binding} module to blender."
|
||||
)
|
||||
|
||||
def install_pyside_windows(self, python_executable):
|
||||
def install_pyside_windows(
|
||||
self,
|
||||
python_executable,
|
||||
qt_binding,
|
||||
qt_binding_version,
|
||||
before_blender_4,
|
||||
):
|
||||
"""Install PySide2 python module to blender's python.
|
||||
|
||||
Installation requires administration rights that's why it is required
|
||||
|
|
@ -139,7 +168,6 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
administration rights.
|
||||
"""
|
||||
try:
|
||||
import win32api
|
||||
import win32con
|
||||
import win32process
|
||||
import win32event
|
||||
|
|
@ -150,12 +178,37 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
self.log.warning("Couldn't import \"pywin32\" modules")
|
||||
return
|
||||
|
||||
if qt_binding_version:
|
||||
qt_binding = f"{qt_binding}=={qt_binding_version}"
|
||||
|
||||
try:
|
||||
# Parameters
|
||||
# - use "-m pip" as module pip to install PySide2 and argument
|
||||
# "--ignore-installed" is to force install module to blender's
|
||||
# site-packages and make sure it is binary compatible
|
||||
parameters = "-m pip install --ignore-installed PySide2"
|
||||
fake_exe = "fake.exe"
|
||||
site_packages_prefix = os.path.dirname(
|
||||
os.path.dirname(python_executable)
|
||||
)
|
||||
args = [
|
||||
fake_exe,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--ignore-installed",
|
||||
qt_binding,
|
||||
]
|
||||
if not before_blender_4:
|
||||
# Define prefix for site package
|
||||
# Python in blender 4.x is installing packages in AppData and
|
||||
# not in blender's directory.
|
||||
args.extend(["--prefix", site_packages_prefix])
|
||||
|
||||
parameters = (
|
||||
subprocess.list2cmdline(args)
|
||||
.lstrip(fake_exe)
|
||||
.lstrip(" ")
|
||||
)
|
||||
|
||||
# Execute command and ask for administrator's rights
|
||||
process_info = ShellExecuteEx(
|
||||
|
|
@ -173,20 +226,29 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
except pywintypes.error:
|
||||
pass
|
||||
|
||||
def install_pyside(self, python_executable):
|
||||
"""Install PySide2 python module to blender's python."""
|
||||
def install_pyside(
|
||||
self,
|
||||
python_executable,
|
||||
qt_binding,
|
||||
qt_binding_version,
|
||||
):
|
||||
"""Install Qt binding python module to blender's python."""
|
||||
if qt_binding_version:
|
||||
qt_binding = f"{qt_binding}=={qt_binding_version}"
|
||||
try:
|
||||
# Parameters
|
||||
# - use "-m pip" as module pip to install PySide2 and argument
|
||||
# - use "-m pip" as module pip to install qt binding and argument
|
||||
# "--ignore-installed" is to force install module to blender's
|
||||
# site-packages and make sure it is binary compatible
|
||||
# TODO find out if blender 4.x on linux/darwin does install
|
||||
# qt binding to correct place.
|
||||
args = [
|
||||
python_executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--ignore-installed",
|
||||
"PySide2",
|
||||
qt_binding,
|
||||
]
|
||||
process = subprocess.Popen(
|
||||
args, stdout=subprocess.PIPE, universal_newlines=True
|
||||
|
|
@ -203,13 +265,15 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
except subprocess.SubprocessError:
|
||||
pass
|
||||
|
||||
def is_pyside_installed(self, python_executable):
|
||||
def is_pyside_installed(self, python_executable, qt_binding):
|
||||
"""Check if PySide2 module is in blender's pip list.
|
||||
|
||||
Check that PySide2 is installed directly in blender's site-packages.
|
||||
It is possible that it is installed in user's site-packages but that
|
||||
may be incompatible with blender's python.
|
||||
"""
|
||||
|
||||
qt_binding_low = qt_binding.lower()
|
||||
# Get pip list from blender's python executable
|
||||
args = [python_executable, "-m", "pip", "list"]
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
|
|
@ -226,6 +290,6 @@ class InstallPySideToBlender(PreLaunchHook):
|
|||
if not line:
|
||||
continue
|
||||
package_name = line[0:package_len].strip()
|
||||
if package_name.lower() == "pyside2":
|
||||
if package_name.lower() == qt_binding_low:
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class AppendBlendLoader(plugin.AssetLoader):
|
|||
so you could also use it as a new base.
|
||||
"""
|
||||
|
||||
representations = ["blend"]
|
||||
representations = {"blend"}
|
||||
product_types = {"workfile"}
|
||||
|
||||
label = "Append Workfile"
|
||||
|
|
@ -68,7 +68,7 @@ class ImportBlendLoader(plugin.AssetLoader):
|
|||
so you could also use it as a new base.
|
||||
"""
|
||||
|
||||
representations = ["blend"]
|
||||
representations = {"blend"}
|
||||
product_types = {"workfile"}
|
||||
|
||||
label = "Import Workfile"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
At least for now it only supports Alembic files.
|
||||
"""
|
||||
product_types = {"model", "pointcache", "animation"}
|
||||
representations = ["abc"]
|
||||
representations = {"abc"}
|
||||
|
||||
label = "Load Alembic"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class BlendActionLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
product_types = {"action"}
|
||||
representations = ["blend"]
|
||||
representations = {"blend"}
|
||||
|
||||
label = "Link Action"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class BlendAnimationLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
product_types = {"animation"}
|
||||
representations = ["blend"]
|
||||
representations = {"blend"}
|
||||
|
||||
label = "Link Animation"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class AudioLoader(plugin.AssetLoader):
|
|||
"""Load audio in Blender."""
|
||||
|
||||
product_types = {"audio"}
|
||||
representations = ["wav"]
|
||||
representations = {"wav"}
|
||||
|
||||
label = "Load Audio"
|
||||
icon = "volume-up"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class BlendLoader(plugin.AssetLoader):
|
|||
"""Load assets from a .blend file."""
|
||||
|
||||
product_types = {"model", "rig", "layout", "camera"}
|
||||
representations = ["blend"]
|
||||
representations = {"blend"}
|
||||
|
||||
label = "Append Blend"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class BlendSceneLoader(plugin.AssetLoader):
|
|||
"""Load assets from a .blend file."""
|
||||
|
||||
product_types = {"blendScene"}
|
||||
representations = ["blend"]
|
||||
representations = {"blend"}
|
||||
|
||||
label = "Append Blend"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class AbcCameraLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
product_types = {"camera"}
|
||||
representations = ["abc"]
|
||||
representations = {"abc"}
|
||||
|
||||
label = "Load Camera (ABC)"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class FbxCameraLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
product_types = {"camera"}
|
||||
representations = ["fbx"]
|
||||
representations = {"fbx"}
|
||||
|
||||
label = "Load Camera (FBX)"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class FbxModelLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
product_types = {"model", "rig"}
|
||||
representations = ["fbx"]
|
||||
representations = {"fbx"}
|
||||
|
||||
label = "Load FBX"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
"""Load layout published from Unreal."""
|
||||
|
||||
product_types = {"layout"}
|
||||
representations = ["json"]
|
||||
representations = {"json"}
|
||||
|
||||
label = "Load Layout"
|
||||
icon = "code-fork"
|
||||
|
|
@ -167,7 +167,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
self._process(libpath, asset, asset_group, None)
|
||||
self._process(libpath, asset_name, asset_group, None)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class BlendLookLoader(plugin.AssetLoader):
|
|||
"""
|
||||
|
||||
product_types = {"look"}
|
||||
representations = ["json"]
|
||||
representations = {"json"}
|
||||
|
||||
label = "Load Look"
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class LoadClip(opfapi.ClipLoader):
|
|||
"""
|
||||
|
||||
product_types = {"render2d", "source", "plate", "render", "review"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
"""
|
||||
|
||||
product_types = {"render2d", "source", "plate", "render", "review"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import sys
|
|||
import re
|
||||
import contextlib
|
||||
|
||||
from ayon_core.lib import Logger
|
||||
|
||||
from ayon_core.lib import Logger, BoolDef, UILabelDef
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
|
@ -181,7 +181,6 @@ def validate_comp_prefs(comp=None, force_repair=False):
|
|||
|
||||
from . import menu
|
||||
from ayon_core.tools.utils import SimplePopup
|
||||
from ayon_core.style import load_stylesheet
|
||||
dialog = SimplePopup(parent=menu.menu)
|
||||
dialog.setWindowTitle("Fusion comp has invalid configuration")
|
||||
|
||||
|
|
@ -340,9 +339,7 @@ def prompt_reset_context():
|
|||
from ayon_core.tools.attribute_defs.dialog import (
|
||||
AttributeDefinitionsDialog
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.lib import BoolDef, UILabelDef
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from qtpy import QtCore
|
||||
|
||||
definitions = [
|
||||
UILabelDef(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ from ayon_core.pipeline import (
|
|||
AVALON_INSTANCE_ID,
|
||||
AYON_INSTANCE_ID,
|
||||
)
|
||||
from ayon_core.pipeline.workfile import get_workdir
|
||||
from ayon_api import (
|
||||
get_project,
|
||||
get_folder_by_path,
|
||||
get_task_by_name
|
||||
)
|
||||
|
||||
|
||||
class GenericCreateSaver(Creator):
|
||||
|
|
@ -125,6 +131,8 @@ class GenericCreateSaver(Creator):
|
|||
product_name = data["productName"]
|
||||
if (
|
||||
original_product_name != product_name
|
||||
or tool.GetData("openpype.task") != data["task"]
|
||||
or tool.GetData("openpype.folderPath") != data["folderPath"]
|
||||
or original_format != data["creator_attributes"]["image_format"]
|
||||
):
|
||||
self._configure_saver_tool(data, tool, product_name)
|
||||
|
|
@ -145,7 +153,30 @@ class GenericCreateSaver(Creator):
|
|||
folder_path = formatting_data["folderPath"]
|
||||
folder_name = folder_path.rsplit("/", 1)[-1]
|
||||
|
||||
workdir = os.path.normpath(os.getenv("AYON_WORKDIR"))
|
||||
# If the folder path and task do not match the current context then the
|
||||
# workdir is not just the `AYON_WORKDIR`. Hence, we need to actually
|
||||
# compute the resulting workdir
|
||||
if (
|
||||
data["folderPath"] == self.create_context.get_current_folder_path()
|
||||
and data["task"] == self.create_context.get_current_task_name()
|
||||
):
|
||||
workdir = os.path.normpath(os.getenv("AYON_WORKDIR"))
|
||||
else:
|
||||
# TODO: Optimize this logic
|
||||
project_name = self.create_context.get_current_project_name()
|
||||
project_entity = get_project(project_name)
|
||||
folder_entity = get_folder_by_path(project_name,
|
||||
data["folderPath"])
|
||||
task_entity = get_task_by_name(project_name,
|
||||
folder_id=folder_entity["id"],
|
||||
task_name=data["task"])
|
||||
workdir = get_workdir(
|
||||
project_entity=project_entity,
|
||||
folder_entity=folder_entity,
|
||||
task_entity=task_entity,
|
||||
host_name=self.create_context.host_name,
|
||||
)
|
||||
|
||||
formatting_data.update({
|
||||
"workdir": workdir,
|
||||
"frame": "0" * frame_padding,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import os
|
||||
from ayon_applications import PreLaunchHook
|
||||
from ayon_core.hosts.fusion import FUSION_HOST_DIR
|
||||
|
||||
|
||||
class FusionLaunchMenuHook(PreLaunchHook):
|
||||
"""Launch AYON menu on start of Fusion"""
|
||||
app_groups = ["fusion"]
|
||||
order = 9
|
||||
|
||||
def execute(self):
|
||||
# Prelaunch hook is optional
|
||||
settings = self.data["project_settings"][self.host_name]
|
||||
if not settings["hooks"]["FusionLaunchMenuHook"]["enabled"]:
|
||||
return
|
||||
|
||||
variant = self.application.name
|
||||
if variant.isnumeric():
|
||||
version = int(variant)
|
||||
if version < 18:
|
||||
print("Skipping launch of OpenPype menu on Fusion start "
|
||||
"because Fusion version below 18.0 does not support "
|
||||
"/execute argument on launch. "
|
||||
f"Version detected: {version}")
|
||||
return
|
||||
else:
|
||||
print(f"Application variant is not numeric: {variant}. "
|
||||
"Validation for Fusion version 18+ for /execute "
|
||||
"prelaunch argument skipped.")
|
||||
|
||||
path = os.path.join(FUSION_HOST_DIR,
|
||||
"deploy",
|
||||
"MenuScripts",
|
||||
"launch_menu.py").replace("\\", "/")
|
||||
script = f"fusion:RunScript('{path}')"
|
||||
self.launch_context.launch_args.extend(["/execute", script])
|
||||
|
|
@ -85,7 +85,6 @@ class InstallPySideToFusion(PreLaunchHook):
|
|||
administration rights.
|
||||
"""
|
||||
try:
|
||||
import win32api
|
||||
import win32con
|
||||
import win32process
|
||||
import win32event
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin):
|
|||
"pointcache",
|
||||
"render",
|
||||
}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"*"}
|
||||
|
||||
label = "Set frame range"
|
||||
|
|
@ -54,7 +54,7 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
"pointcache",
|
||||
"render",
|
||||
}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
|
||||
label = "Set frame range (with handles)"
|
||||
order = 12
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class FusionLoadAlembicMesh(load.LoaderPlugin):
|
|||
"""Load Alembic mesh into Fusion"""
|
||||
|
||||
product_types = {"pointcache", "model"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"abc"}
|
||||
|
||||
label = "Load alembic mesh"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class FusionLoadFBXMesh(load.LoaderPlugin):
|
|||
"""Load FBX mesh into Fusion"""
|
||||
|
||||
product_types = {"*"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {
|
||||
"3ds",
|
||||
"amc",
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
"image",
|
||||
"online",
|
||||
}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class FusionLoadUSD(load.LoaderPlugin):
|
|||
"""
|
||||
|
||||
product_types = {"*"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"usd", "usda", "usdz"}
|
||||
|
||||
label = "Load USD"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class FusionLoadWorkfile(load.LoaderPlugin):
|
|||
"""Load the content of a workfile into Fusion"""
|
||||
|
||||
product_types = {"workfile"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"comp"}
|
||||
|
||||
label = "Load Workfile"
|
||||
|
|
|
|||
|
|
@ -37,14 +37,13 @@ class CollectFusionRender(
|
|||
aspect_x = comp_frame_format_prefs["AspectX"]
|
||||
aspect_y = comp_frame_format_prefs["AspectY"]
|
||||
|
||||
instances = []
|
||||
instances_to_remove = []
|
||||
|
||||
current_file = context.data["currentFile"]
|
||||
version = context.data["version"]
|
||||
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
instances = []
|
||||
for inst in context:
|
||||
if not inst.data.get("active", True):
|
||||
continue
|
||||
|
|
@ -91,7 +90,10 @@ class CollectFusionRender(
|
|||
frameStep=1,
|
||||
fps=comp_frame_format_prefs.get("Rate"),
|
||||
app_version=comp.GetApp().Version,
|
||||
publish_attributes=inst.data.get("publish_attributes", {})
|
||||
publish_attributes=inst.data.get("publish_attributes", {}),
|
||||
|
||||
# The source instance this render instance replaces
|
||||
source_instance=inst
|
||||
)
|
||||
|
||||
render_target = inst.data["creator_attributes"]["render_target"]
|
||||
|
|
@ -114,13 +116,7 @@ class CollectFusionRender(
|
|||
# to skip ExtractReview locally
|
||||
instance.families.remove("review")
|
||||
|
||||
# add new instance to the list and remove the original
|
||||
# instance since it is not needed anymore
|
||||
instances.append(instance)
|
||||
instances_to_remove.append(inst)
|
||||
|
||||
for instance in instances_to_remove:
|
||||
context.remove(instance)
|
||||
|
||||
return instances
|
||||
|
||||
|
|
|
|||
|
|
@ -590,7 +590,7 @@ class ImageSequenceLoader(load.LoaderPlugin):
|
|||
"reference",
|
||||
"review",
|
||||
}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"jpeg", "png", "jpg"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ImportAudioLoader(load.LoaderPlugin):
|
|||
"""Import audio."""
|
||||
|
||||
product_types = {"shot", "audio"}
|
||||
representations = ["wav"]
|
||||
representations = {"wav"}
|
||||
label = "Import Audio"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ class BackgroundLoader(load.LoaderPlugin):
|
|||
Stores the imported asset in a container named after the asset.
|
||||
"""
|
||||
product_types = {"background"}
|
||||
representations = ["json"]
|
||||
representations = {"json"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ImageSequenceLoader(load.LoaderPlugin):
|
|||
"reference",
|
||||
"review",
|
||||
}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"jpeg", "png", "jpg"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class ImportPaletteLoader(load.LoaderPlugin):
|
|||
"""Import palettes."""
|
||||
|
||||
product_types = {"palette", "harmony.palette"}
|
||||
representations = ["plt"]
|
||||
representations = {"plt"}
|
||||
label = "Import Palette"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class TemplateLoader(load.LoaderPlugin):
|
|||
"""
|
||||
|
||||
product_types = {"template", "workfile"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
label = "Load Template"
|
||||
icon = "gift"
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class ImportTemplateLoader(load.LoaderPlugin):
|
|||
"""Import templates."""
|
||||
|
||||
product_types = {"harmony.template", "workfile"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
label = "Import Template"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
|
@ -61,5 +61,5 @@ class ImportWorkfileLoader(ImportTemplateLoader):
|
|||
"""Import workfiles."""
|
||||
|
||||
product_types = {"workfile"}
|
||||
representations = ["zip"]
|
||||
representations = {"zip"}
|
||||
label = "Import Workfile"
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class CreateShotClip(phiero.Creator):
|
|||
"value": ["<track_name>", "main", "bg", "fg", "bg",
|
||||
"animatic"],
|
||||
"type": "QComboBox",
|
||||
"label": "pRODUCT Name",
|
||||
"label": "Product Name",
|
||||
"target": "ui",
|
||||
"toolTip": "chose product name pattern, if <track_name> is selected, name of track layer will be used", # noqa
|
||||
"order": 0},
|
||||
|
|
@ -159,7 +159,7 @@ class CreateShotClip(phiero.Creator):
|
|||
"type": "QCheckBox",
|
||||
"label": "Include audio",
|
||||
"target": "tag",
|
||||
"toolTip": "Process productS with corresponding audio", # noqa
|
||||
"toolTip": "Process products with corresponding audio", # noqa
|
||||
"order": 3},
|
||||
"sourceResolution": {
|
||||
"value": False,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class LoadClip(phiero.SequenceLoader):
|
|||
"""
|
||||
|
||||
product_types = {"render2d", "source", "plate", "render", "review"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = set(
|
||||
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class LoadEffects(load.LoaderPlugin):
|
|||
"""Loading colorspace soft effect exported from nukestudio"""
|
||||
|
||||
product_types = {"effect"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extension = {"json"}
|
||||
|
||||
label = "Load Effects"
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import pyblish.api
|
|||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class ExtractThumnail(publish.Extractor):
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""
|
||||
Extractor for track item's tumnails
|
||||
Extractor for track item's tumbnails
|
||||
"""
|
||||
|
||||
label = "Extract Thumnail"
|
||||
label = "Extract Thumbnail"
|
||||
order = pyblish.api.ExtractorOrder
|
||||
families = ["plate", "take"]
|
||||
hosts = ["hiero"]
|
||||
|
|
@ -48,7 +48,7 @@ class ExtractThumnail(publish.Extractor):
|
|||
self.log.debug(
|
||||
"__ thumb_path: `{}`, frame: `{}`".format(thumbnail, thumb_frame))
|
||||
|
||||
self.log.info("Thumnail was generated to: {}".format(thumb_path))
|
||||
self.log.info("Thumbnail was generated to: {}".format(thumb_path))
|
||||
thumb_representation = {
|
||||
'files': thumb_file,
|
||||
'stagingDir': staging_dir,
|
||||
|
|
|
|||
|
|
@ -92,10 +92,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
folder_path, folder_name = self._get_folder_data(tag_data)
|
||||
|
||||
product_name = tag_data.get("productName")
|
||||
if product_name is None:
|
||||
product_name = tag_data["subset"]
|
||||
|
||||
families = [str(f) for f in tag_data["families"]]
|
||||
|
||||
# TODO: remove backward compatibility
|
||||
|
|
@ -293,7 +289,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
label += " {}".format(product_name)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(folder_path, subset),
|
||||
"name": "{}_{}".format(folder_path, product_name),
|
||||
"label": label,
|
||||
"productName": product_name,
|
||||
"productType": product_type,
|
||||
|
|
|
|||
|
|
@ -1001,6 +1001,82 @@ def add_self_publish_button(node):
|
|||
node.setParmTemplateGroup(template)
|
||||
|
||||
|
||||
def get_scene_viewer():
|
||||
"""
|
||||
Return an instance of a visible viewport.
|
||||
|
||||
There may be many, some could be closed, any visible are current
|
||||
|
||||
Returns:
|
||||
Optional[hou.SceneViewer]: A scene viewer, if any.
|
||||
"""
|
||||
panes = hou.ui.paneTabs()
|
||||
panes = [x for x in panes if x.type() == hou.paneTabType.SceneViewer]
|
||||
panes = sorted(panes, key=lambda x: x.isCurrentTab())
|
||||
if panes:
|
||||
return panes[-1]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def sceneview_snapshot(
|
||||
sceneview,
|
||||
filepath="$HIP/thumbnails/$HIPNAME.$F4.jpg",
|
||||
frame_start=None,
|
||||
frame_end=None):
|
||||
"""Take a snapshot of your scene view.
|
||||
|
||||
It takes snapshot of your scene view for the given frame range.
|
||||
So, it's capable of generating snapshots image sequence.
|
||||
It works in different Houdini context e.g. Objects, Solaris
|
||||
|
||||
Example:
|
||||
This is how the function can be used::
|
||||
|
||||
from ayon_core.hosts.houdini.api import lib
|
||||
sceneview = hou.ui.paneTabOfType(hou.paneTabType.SceneViewer)
|
||||
lib.sceneview_snapshot(sceneview)
|
||||
|
||||
Notes:
|
||||
.png output will render poorly, so use .jpg.
|
||||
|
||||
How it works:
|
||||
Get the current sceneviewer (may be more than one or hidden)
|
||||
and screengrab the perspective viewport to a file in the
|
||||
publish location to be picked up with the publish.
|
||||
|
||||
Credits:
|
||||
https://www.sidefx.com/forum/topic/42808/?page=1#post-354796
|
||||
|
||||
Args:
|
||||
sceneview (hou.SceneViewer): The scene view pane from which you want
|
||||
to take a snapshot.
|
||||
filepath (str): thumbnail filepath. it expects `$F4` token
|
||||
when frame_end is bigger than frame_star other wise
|
||||
each frame will override its predecessor.
|
||||
frame_start (int): the frame at which snapshot starts
|
||||
frame_end (int): the frame at which snapshot ends
|
||||
"""
|
||||
|
||||
if frame_start is None:
|
||||
frame_start = hou.frame()
|
||||
if frame_end is None:
|
||||
frame_end = frame_start
|
||||
|
||||
if not isinstance(sceneview, hou.SceneViewer):
|
||||
log.debug("Wrong Input. {} is not of type hou.SceneViewer."
|
||||
.format(sceneview))
|
||||
return
|
||||
viewport = sceneview.curViewport()
|
||||
|
||||
flip_settings = sceneview.flipbookSettings().stash()
|
||||
flip_settings.frameRange((frame_start, frame_end))
|
||||
flip_settings.output(filepath)
|
||||
flip_settings.outputToMPlay(False)
|
||||
sceneview.flipbook(viewport, flip_settings)
|
||||
log.debug("A snapshot of sceneview has been saved to: {}".format(filepath))
|
||||
|
||||
|
||||
def update_content_on_context_change():
|
||||
"""Update all Creator instances to current asset"""
|
||||
host = registered_host()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating alembic camera products."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance, CreatorError
|
||||
from ayon_core.pipeline import CreatorError
|
||||
|
||||
import hou
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ class CreateAlembicCamera(plugin.HoudiniCreator):
|
|||
instance = super(CreateAlembicCamera, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
parms = {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class CreateArnoldAss(plugin.HoudiniCreator):
|
|||
instance = super(CreateArnoldAss, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: plugin.CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class CreateArnoldRop(plugin.HoudiniCreator):
|
|||
instance = super(CreateArnoldRop, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: plugin.CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating pointcache bgeo files."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance, CreatorError
|
||||
from ayon_core.pipeline import CreatorError
|
||||
import hou
|
||||
from ayon_core.lib import EnumDef, BoolDef
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ class CreateBGEO(plugin.HoudiniCreator):
|
|||
instance = super(CreateBGEO, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating composite sequences."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance, CreatorError
|
||||
from ayon_core.pipeline import CreatorError
|
||||
|
||||
import hou
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ class CreateCompositeSequence(plugin.HoudiniCreator):
|
|||
instance = super(CreateCompositeSequence, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
filepath = "{}{}".format(
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class CreateHDA(plugin.HoudiniCreator):
|
|||
instance = super(CreateHDA, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: plugin.CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
return instance
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin to create Karma ROP."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance
|
||||
from ayon_core.lib import BoolDef, EnumDef, NumberDef
|
||||
|
||||
|
||||
|
|
@ -25,7 +24,7 @@ class CreateKarmaROP(plugin.HoudiniCreator):
|
|||
instance = super(CreateKarmaROP, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating pointcache alembics."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance
|
||||
from ayon_core.lib import BoolDef
|
||||
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ class CreateMantraIFD(plugin.HoudiniCreator):
|
|||
instance = super(CreateMantraIFD, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin to create Mantra ROP."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance
|
||||
from ayon_core.lib import EnumDef, BoolDef
|
||||
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ class CreateMantraROP(plugin.HoudiniCreator):
|
|||
instance = super(CreateMantraROP, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating USDs."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance
|
||||
|
||||
import hou
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ class CreateUSD(plugin.HoudiniCreator):
|
|||
instance = super(CreateUSD, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating USD renders."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance
|
||||
|
||||
|
||||
class CreateUSDRender(plugin.HoudiniCreator):
|
||||
|
|
@ -23,7 +22,7 @@ class CreateUSDRender(plugin.HoudiniCreator):
|
|||
instance = super(CreateUSDRender, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating VDB Caches."""
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance
|
||||
from ayon_core.lib import BoolDef
|
||||
|
||||
import hou
|
||||
|
|
@ -26,7 +25,7 @@ class CreateVDBCache(plugin.HoudiniCreator):
|
|||
instance = super(CreateVDBCache, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
file_path = "{}{}".format(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import hou
|
||||
|
||||
from ayon_core.hosts.houdini.api import plugin
|
||||
from ayon_core.pipeline import CreatedInstance, CreatorError
|
||||
from ayon_core.pipeline import CreatorError
|
||||
from ayon_core.lib import EnumDef, BoolDef
|
||||
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ class CreateVrayROP(plugin.HoudiniCreator):
|
|||
instance = super(CreateVrayROP, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class SetFrameRangeLoader(load.LoaderPlugin):
|
|||
"vdbcache",
|
||||
"usd",
|
||||
}
|
||||
representations = ["abc", "vdb", "usd"]
|
||||
representations = {"abc", "vdb", "usd"}
|
||||
|
||||
label = "Set frame range"
|
||||
order = 11
|
||||
|
|
@ -52,7 +52,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
"vdbcache",
|
||||
"usd",
|
||||
}
|
||||
representations = ["abc", "vdb", "usd"]
|
||||
representations = {"abc", "vdb", "usd"}
|
||||
|
||||
label = "Set frame range (with handles)"
|
||||
order = 12
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class AbcLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"model", "animation", "pointcache", "gpuCache"}
|
||||
label = "Load Alembic"
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class AbcArchiveLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"model", "animation", "pointcache", "gpuCache"}
|
||||
label = "Load Alembic as Archive"
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"abc"}
|
||||
order = -5
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class AssLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"ass"}
|
||||
label = "Load Arnold Procedural"
|
||||
representations = ["ass"]
|
||||
representations = {"ass"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ class BgeoLoader(load.LoaderPlugin):
|
|||
|
||||
label = "Load bgeo"
|
||||
product_types = {"model", "pointcache", "bgeo"}
|
||||
representations = [
|
||||
representations = {
|
||||
"bgeo", "bgeosc", "bgeogz",
|
||||
"bgeo.sc", "bgeo.gz", "bgeo.lzma", "bgeo.bz2"]
|
||||
"bgeo.sc", "bgeo.gz", "bgeo.lzma", "bgeo.bz2"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class CameraLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"camera"}
|
||||
label = "Load Camera (abc)"
|
||||
representations = ["abc"]
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class FbxLoader(load.LoaderPlugin):
|
|||
order = -10
|
||||
|
||||
product_types = {"*"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
extensions = {"fbx"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class FilePathLoader(load.LoaderPlugin):
|
|||
icon = "link"
|
||||
color = "white"
|
||||
product_types = {"*"}
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class HdaLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"hda"}
|
||||
label = "Load Hda"
|
||||
representations = ["hda"]
|
||||
representations = {"hda"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class ImageLoader(load.LoaderPlugin):
|
|||
"online",
|
||||
}
|
||||
label = "Load Image (COP2)"
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
order = -10
|
||||
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class RedshiftProxyLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"redshiftproxy"}
|
||||
label = "Load Redshift Proxy"
|
||||
representations = ["rs"]
|
||||
representations = {"rs"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class USDSublayerLoader(load.LoaderPlugin):
|
|||
"usdCamera",
|
||||
}
|
||||
label = "Sublayer USD"
|
||||
representations = ["usd", "usda", "usdlc", "usdnc", "abc"]
|
||||
representations = {"usd", "usda", "usdlc", "usdnc", "abc"}
|
||||
order = 1
|
||||
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class USDReferenceLoader(load.LoaderPlugin):
|
|||
"usdCamera",
|
||||
}
|
||||
label = "Reference USD"
|
||||
representations = ["usd", "usda", "usdlc", "usdnc", "abc"]
|
||||
representations = {"usd", "usda", "usdlc", "usdnc", "abc"}
|
||||
order = -8
|
||||
|
||||
icon = "code-fork"
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class SopUsdImportLoader(load.LoaderPlugin):
|
|||
|
||||
label = "Load USD to SOPs"
|
||||
product_types = {"*"}
|
||||
representations = ["usd"]
|
||||
representations = {"usd"}
|
||||
order = -6
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class VdbLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"vdbcache"}
|
||||
label = "Load VDB"
|
||||
representations = ["vdb"]
|
||||
representations = {"vdb"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class ShowInUsdview(load.LoaderPlugin):
|
|||
"""Open USD file in usdview"""
|
||||
|
||||
label = "Show in usdview"
|
||||
representations = ["*"]
|
||||
representations = {"*"}
|
||||
product_types = {"*"}
|
||||
extensions = {"usd", "usda", "usdlc", "usdnc", "abc"}
|
||||
order = 15
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
from collections import deque
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import registered_host
|
||||
|
||||
|
||||
def collect_input_containers(nodes):
|
||||
def get_container_members(container):
|
||||
node = container["node"]
|
||||
# Usually the loaded containers don't have any complex references
|
||||
# and the contained children should be all we need. So we disregard
|
||||
# checking for .references() on the nodes.
|
||||
members = set(node.allSubChildren())
|
||||
members.add(node) # include the node itself
|
||||
return members
|
||||
|
||||
|
||||
def collect_input_containers(containers, nodes):
|
||||
"""Collect containers that contain any of the node in `nodes`.
|
||||
|
||||
This will return any loaded Avalon container that contains at least one of
|
||||
|
|
@ -11,30 +23,13 @@ def collect_input_containers(nodes):
|
|||
there are member nodes of that container.
|
||||
|
||||
Returns:
|
||||
list: Input avalon containers
|
||||
list: Loaded containers that contain the `nodes`
|
||||
|
||||
"""
|
||||
|
||||
# Lookup by node ids
|
||||
lookup = frozenset(nodes)
|
||||
|
||||
containers = []
|
||||
host = registered_host()
|
||||
for container in host.ls():
|
||||
|
||||
node = container["node"]
|
||||
|
||||
# Usually the loaded containers don't have any complex references
|
||||
# and the contained children should be all we need. So we disregard
|
||||
# checking for .references() on the nodes.
|
||||
members = set(node.allSubChildren())
|
||||
members.add(node) # include the node itself
|
||||
|
||||
# If there's an intersection
|
||||
if not lookup.isdisjoint(members):
|
||||
containers.append(container)
|
||||
|
||||
return containers
|
||||
# Assume the containers have collected their cached '_members' data
|
||||
# in the collector.
|
||||
return [container for container in containers
|
||||
if any(node in container["_members"] for node in nodes)]
|
||||
|
||||
|
||||
def iter_upstream(node):
|
||||
|
|
@ -54,7 +49,7 @@ def iter_upstream(node):
|
|||
)
|
||||
|
||||
# Initialize process queue with the node's ancestors itself
|
||||
queue = list(upstream)
|
||||
queue = deque(upstream)
|
||||
collected = set(upstream)
|
||||
|
||||
# Traverse upstream references for all nodes and yield them as we
|
||||
|
|
@ -72,6 +67,10 @@ def iter_upstream(node):
|
|||
|
||||
# Include the references' ancestors that have not been collected yet.
|
||||
for reference in references:
|
||||
if reference in collected:
|
||||
# Might have been collected in previous iteration
|
||||
continue
|
||||
|
||||
ancestors = reference.inputAncestors(
|
||||
include_ref_inputs=True, follow_subnets=True
|
||||
)
|
||||
|
|
@ -108,13 +107,32 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
)
|
||||
return
|
||||
|
||||
# Collect all upstream parents
|
||||
nodes = list(iter_upstream(output))
|
||||
nodes.append(output)
|
||||
# For large scenes the querying of "host.ls()" can be relatively slow
|
||||
# e.g. up to a second. Many instances calling it easily slows this
|
||||
# down. As such, we cache it so we trigger it only once.
|
||||
# todo: Instead of hidden cache make "CollectContainers" plug-in
|
||||
cache_key = "__cache_containers"
|
||||
scene_containers = instance.context.data.get(cache_key, None)
|
||||
if scene_containers is None:
|
||||
# Query the scenes' containers if there's no cache yet
|
||||
host = registered_host()
|
||||
scene_containers = list(host.ls())
|
||||
for container in scene_containers:
|
||||
# Embed the members into the container dictionary
|
||||
container_members = set(get_container_members(container))
|
||||
container["_members"] = container_members
|
||||
instance.context.data[cache_key] = scene_containers
|
||||
|
||||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(nodes)
|
||||
inputs = []
|
||||
if scene_containers:
|
||||
# Collect all upstream parents
|
||||
nodes = list(iter_upstream(output))
|
||||
nodes.append(output)
|
||||
|
||||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(scene_containers, nodes)
|
||||
|
||||
inputs = [c["representation"] for c in containers]
|
||||
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
self.log.debug("Collected inputs: %s" % inputs)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import pyblish.api
|
||||
import tempfile
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.houdini.api import lib
|
||||
from ayon_core.hosts.houdini.api.pipeline import IS_HEADLESS
|
||||
|
||||
|
||||
class ExtractActiveViewThumbnail(publish.Extractor):
|
||||
"""Set instance thumbnail to a screengrab of current active viewport.
|
||||
|
||||
This makes it so that if an instance does not have a thumbnail set yet that
|
||||
it will get a thumbnail of the currently active view at the time of
|
||||
publishing as a fallback.
|
||||
|
||||
"""
|
||||
order = pyblish.api.ExtractorOrder + 0.49
|
||||
label = "Extract Active View Thumbnail"
|
||||
families = ["workfile"]
|
||||
hosts = ["houdini"]
|
||||
|
||||
def process(self, instance):
|
||||
if IS_HEADLESS:
|
||||
self.log.debug(
|
||||
"Skip extraction of active view thumbnail, due to being in"
|
||||
"headless mode."
|
||||
)
|
||||
return
|
||||
|
||||
thumbnail = instance.data.get("thumbnailPath")
|
||||
if thumbnail:
|
||||
# A thumbnail was already set for this instance
|
||||
return
|
||||
|
||||
view_thumbnail = self.get_view_thumbnail(instance)
|
||||
if not view_thumbnail:
|
||||
return
|
||||
self.log.debug("Setting instance thumbnail path to: {}"
|
||||
.format(view_thumbnail)
|
||||
)
|
||||
instance.data["thumbnailPath"] = view_thumbnail
|
||||
|
||||
def get_view_thumbnail(self, instance):
|
||||
|
||||
sceneview = lib.get_scene_viewer()
|
||||
if sceneview is None:
|
||||
self.log.debug("Skipping Extract Active View Thumbnail"
|
||||
" because no scene view was detected.")
|
||||
return
|
||||
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".jpg", delete=False) as tmp:
|
||||
lib.sceneview_snapshot(sceneview, tmp.name)
|
||||
thumbnail_path = tmp.name
|
||||
|
||||
instance.context.data["cleanupFullPaths"].append(thumbnail_path)
|
||||
return thumbnail_path
|
||||
|
|
@ -3,7 +3,6 @@ import pyblish.api
|
|||
from ayon_core.lib import version_up
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.pipeline.publish import get_errored_plugins_from_context
|
||||
from ayon_core.hosts.houdini.api import HoudiniHost
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
|
||||
|
||||
|
|
@ -39,7 +38,7 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin):
|
|||
)
|
||||
|
||||
# Filename must not have changed since collecting
|
||||
host = registered_host() # type: HoudiniHost
|
||||
host = registered_host()
|
||||
current_file = host.current_file()
|
||||
if context.data["currentFile"] != current_file:
|
||||
raise KnownPublishError(
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@ from typing import Any, Dict, Union
|
|||
import six
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import get_current_project_name, colorspace
|
||||
from ayon_core.pipeline import (
|
||||
get_current_project_name,
|
||||
get_current_folder_path,
|
||||
get_current_task_name,
|
||||
colorspace
|
||||
)
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline.context_tools import (
|
||||
get_current_folder_entity,
|
||||
get_current_task_entity
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from pymxs import runtime as rt
|
||||
|
|
@ -221,41 +226,30 @@ def reset_scene_resolution():
|
|||
scene resolution can be overwritten by a folder if the folder.attrib
|
||||
contains any information regarding scene resolution.
|
||||
"""
|
||||
|
||||
folder_entity = get_current_folder_entity(
|
||||
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"}
|
||||
)
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
width = int(folder_attributes["resolutionWidth"])
|
||||
height = int(folder_attributes["resolutionHeight"])
|
||||
task_attributes = get_current_task_entity(fields={"attrib"})["attrib"]
|
||||
width = int(task_attributes["resolutionWidth"])
|
||||
height = int(task_attributes["resolutionHeight"])
|
||||
|
||||
set_scene_resolution(width, height)
|
||||
|
||||
|
||||
def get_frame_range(folder_entiy=None) -> Union[Dict[str, Any], None]:
|
||||
"""Get the current folder frame range and handles.
|
||||
def get_frame_range(task_entity=None) -> Union[Dict[str, Any], None]:
|
||||
"""Get the current task frame range and handles
|
||||
|
||||
Args:
|
||||
folder_entiy (dict): Folder eneity.
|
||||
task_entity (dict): Task Entity.
|
||||
|
||||
Returns:
|
||||
dict: with frame start, frame end, handle start, handle end.
|
||||
"""
|
||||
# Set frame start/end
|
||||
if folder_entiy is None:
|
||||
folder_entiy = get_current_folder_entity()
|
||||
|
||||
folder_attributes = folder_entiy["attrib"]
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
frame_end = folder_attributes.get("frameEnd")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
return {}
|
||||
|
||||
frame_start = int(frame_start)
|
||||
frame_end = int(frame_end)
|
||||
handle_start = int(folder_attributes.get("handleStart", 0))
|
||||
handle_end = int(folder_attributes.get("handleEnd", 0))
|
||||
if task_entity is None:
|
||||
task_entity = get_current_task_entity(fields={"attrib"})
|
||||
task_attributes = task_entity["attrib"]
|
||||
frame_start = int(task_attributes["frameStart"])
|
||||
frame_end = int(task_attributes["frameEnd"])
|
||||
handle_start = int(task_attributes["handleStart"])
|
||||
handle_end = int(task_attributes["handleEnd"])
|
||||
frame_start_handle = frame_start - handle_start
|
||||
frame_end_handle = frame_end + handle_end
|
||||
|
||||
|
|
@ -281,9 +275,9 @@ def reset_frame_range(fps: bool = True):
|
|||
scene frame rate in frames-per-second.
|
||||
"""
|
||||
if fps:
|
||||
project_name = get_current_project_name()
|
||||
project_entity = ayon_api.get_project(project_name)
|
||||
fps_number = float(project_entity["attrib"].get("fps"))
|
||||
task_entity = get_current_task_entity()
|
||||
task_attributes = task_entity["attrib"]
|
||||
fps_number = float(task_attributes["fps"])
|
||||
rt.frameRate = fps_number
|
||||
frame_range = get_frame_range()
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class FbxLoader(load.LoaderPlugin):
|
|||
"""Fbx Loader."""
|
||||
|
||||
product_types = {"camera"}
|
||||
representations = ["fbx"]
|
||||
representations = {"fbx"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class MaxSceneLoader(load.LoaderPlugin):
|
|||
"model",
|
||||
}
|
||||
|
||||
representations = ["max"]
|
||||
representations = {"max"}
|
||||
order = -8
|
||||
icon = "code-fork"
|
||||
color = "green"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class ModelAbcLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"model"}
|
||||
label = "Load Model with Alembic"
|
||||
representations = ["abc"]
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class FbxModelLoader(load.LoaderPlugin):
|
|||
"""Fbx Model Loader."""
|
||||
|
||||
product_types = {"model"}
|
||||
representations = ["fbx"]
|
||||
representations = {"fbx"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class ObjLoader(load.LoaderPlugin):
|
|||
"""Obj Loader."""
|
||||
|
||||
product_types = {"model"}
|
||||
representations = ["obj"]
|
||||
representations = {"obj"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ModelUSDLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"model"}
|
||||
label = "Load Model(USD)"
|
||||
representations = ["usda"]
|
||||
representations = {"usda"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class AbcLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"camera", "animation", "pointcache"}
|
||||
label = "Load Alembic"
|
||||
representations = ["abc"]
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class OxAbcLoader(load.LoaderPlugin):
|
|||
|
||||
product_types = {"camera", "animation", "pointcache"}
|
||||
label = "Load Alembic with Ornatrix"
|
||||
representations = ["abc"]
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class PointCloudLoader(load.LoaderPlugin):
|
|||
"""Point Cloud Loader."""
|
||||
|
||||
product_types = {"pointcloud"}
|
||||
representations = ["prt"]
|
||||
representations = {"prt"}
|
||||
order = -8
|
||||
icon = "code-fork"
|
||||
color = "green"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class RedshiftProxyLoader(load.LoaderPlugin):
|
|||
|
||||
label = "Load Redshift Proxy"
|
||||
product_types = {"redshiftproxy"}
|
||||
representations = ["rs"]
|
||||
representations = {"rs"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class TyCacheLoader(load.LoaderPlugin):
|
|||
"""TyCache Loader."""
|
||||
|
||||
product_types = {"tycache"}
|
||||
representations = ["tyc"]
|
||||
representations = {"tyc"}
|
||||
order = -8
|
||||
icon = "code-fork"
|
||||
color = "green"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin,
|
|||
return
|
||||
|
||||
frame_range = get_frame_range(
|
||||
instance.data["folderEntity"])
|
||||
instance.data["taskEntity"])
|
||||
|
||||
inst_frame_start = instance.data.get("frameStartHandle")
|
||||
inst_frame_end = instance.data.get("frameEndHandle")
|
||||
|
|
|
|||
|
|
@ -38,15 +38,15 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin,
|
|||
context_label = "{} > {}".format(*context)
|
||||
instance_label = "{} > {}".format(folderPath, task)
|
||||
message = (
|
||||
"Instance '{}' publishes to different context than current "
|
||||
"context: {}. Current context: {}".format(
|
||||
"Instance '{}' publishes to different context(folder or task) "
|
||||
"than current context: {}. Current context: {}".format(
|
||||
instance.name, instance_label, context_label
|
||||
)
|
||||
)
|
||||
raise PublishValidationError(
|
||||
message=message,
|
||||
description=(
|
||||
"## Publishing to a different context data\n"
|
||||
"## Publishing to a different context data(folder or task)\n"
|
||||
"There are publish instances present which are publishing "
|
||||
"into a different folder path or task than your current context.\n\n"
|
||||
"Usually this is not what you want but there can be cases "
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ from ayon_core.pipeline.publish import (
|
|||
RepairAction,
|
||||
PublishValidationError
|
||||
)
|
||||
from ayon_core.hosts.max.api.lib import reset_scene_resolution
|
||||
from ayon_core.hosts.max.api.lib import (
|
||||
reset_scene_resolution,
|
||||
imprint
|
||||
)
|
||||
|
||||
|
||||
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
||||
|
|
@ -25,8 +28,10 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
if not self.is_active(instance.data):
|
||||
return
|
||||
width, height = self.get_folder_resolution(instance)
|
||||
current_width = rt.renderWidth
|
||||
current_height = rt.renderHeight
|
||||
current_width, current_height = (
|
||||
self.get_current_resolution(instance)
|
||||
)
|
||||
|
||||
if current_width != width and current_height != height:
|
||||
raise PublishValidationError("Resolution Setting "
|
||||
"not matching resolution "
|
||||
|
|
@ -41,12 +46,16 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
"not matching resolution set "
|
||||
"on asset or shot.")
|
||||
|
||||
def get_folder_resolution(self, instance):
|
||||
folder_entity = instance.data["folderEntity"]
|
||||
if folder_entity:
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
width = folder_attributes["resolutionWidth"]
|
||||
height = folder_attributes["resolutionHeight"]
|
||||
def get_current_resolution(self, instance):
|
||||
return rt.renderWidth, rt.renderHeight
|
||||
|
||||
@classmethod
|
||||
def get_folder_resolution(cls, instance):
|
||||
task_entity = instance.data.get("taskEntity")
|
||||
if task_entity:
|
||||
task_attributes = task_entity["attrib"]
|
||||
width = task_attributes["resolutionWidth"]
|
||||
height = task_attributes["resolutionHeight"]
|
||||
return int(width), int(height)
|
||||
|
||||
# Defaults if not found in folder entity
|
||||
|
|
@ -55,3 +64,29 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
|||
@classmethod
|
||||
def repair(cls, instance):
|
||||
reset_scene_resolution()
|
||||
|
||||
|
||||
class ValidateReviewResolutionSetting(ValidateResolutionSetting):
|
||||
families = ["review"]
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def get_current_resolution(self, instance):
|
||||
current_width = instance.data["review_width"]
|
||||
current_height = instance.data["review_height"]
|
||||
return current_width, current_height
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
context_width, context_height = (
|
||||
cls.get_folder_resolution(instance)
|
||||
)
|
||||
creator_attrs = instance.data["creator_attributes"]
|
||||
creator_attrs["review_width"] = context_width
|
||||
creator_attrs["review_height"] = context_height
|
||||
creator_attrs_data = {
|
||||
"creator_attributes": creator_attrs
|
||||
}
|
||||
# update the width and height of review
|
||||
# data in creator_attributes
|
||||
imprint(instance.data["instance_node"], creator_attrs_data)
|
||||
|
|
|
|||
|
|
@ -22,15 +22,15 @@ class MayaAddon(AYONAddon, IHostAddon):
|
|||
if norm_path not in new_python_paths:
|
||||
new_python_paths.append(norm_path)
|
||||
|
||||
# add vendor path
|
||||
new_python_paths.append(
|
||||
os.path.join(MAYA_ROOT_DIR, "vendor", "python")
|
||||
)
|
||||
env["PYTHONPATH"] = os.pathsep.join(new_python_paths)
|
||||
|
||||
# Set default environments
|
||||
envs = {
|
||||
"AYON_LOG_NO_COLORS": "1",
|
||||
# For python module 'qtpy'
|
||||
"QT_API": "PySide2",
|
||||
# For python module 'Qt'
|
||||
"QT_PREFERRED_BINDING": "PySide2"
|
||||
}
|
||||
for key, value in envs.items():
|
||||
env[key] = value
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
"""Tools to work with FBX."""
|
||||
import logging
|
||||
|
||||
from pyblish.api import Instance
|
||||
|
||||
from maya import cmds # noqa
|
||||
import maya.mel as mel # noqa
|
||||
from ayon_core.hosts.maya.api.lib import maintained_selection
|
||||
|
|
@ -146,7 +144,6 @@ class FBXExtractor:
|
|||
return options
|
||||
|
||||
def set_options_from_instance(self, instance):
|
||||
# type: (Instance) -> None
|
||||
"""Sets FBX export options from data in the instance.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from ayon_core.pipeline import (
|
|||
AYON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_core.lib import NumberDef
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
from ayon_core.pipeline.context_tools import get_current_task_entity
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
|
||||
|
|
@ -1519,24 +1519,30 @@ def extract_alembic(file,
|
|||
|
||||
|
||||
# region ID
|
||||
def get_id_required_nodes(referenced_nodes=False, nodes=None):
|
||||
"""Filter out any node which are locked (reference) or readOnly
|
||||
def get_id_required_nodes(referenced_nodes=False,
|
||||
nodes=None,
|
||||
existing_ids=True):
|
||||
"""Return nodes that should receive a `cbId` attribute.
|
||||
|
||||
This includes only mesh and curve nodes, parent transforms of the shape
|
||||
nodes, file texture nodes and object sets (including shading engines).
|
||||
|
||||
This filters out any node which is locked, referenced, read-only,
|
||||
intermediate object.
|
||||
|
||||
Args:
|
||||
referenced_nodes (bool): set True to filter out reference nodes
|
||||
referenced_nodes (bool): set True to include referenced nodes
|
||||
nodes (list, Optional): nodes to consider
|
||||
existing_ids (bool): set True to include nodes with `cbId` attribute
|
||||
|
||||
Returns:
|
||||
nodes (set): list of filtered nodes
|
||||
"""
|
||||
|
||||
lookup = None
|
||||
if nodes is None:
|
||||
# Consider all nodes
|
||||
nodes = cmds.ls()
|
||||
else:
|
||||
# Build a lookup for the only allowed nodes in output based
|
||||
# on `nodes` input of the function (+ ensure long names)
|
||||
lookup = set(cmds.ls(nodes, long=True))
|
||||
if nodes is not None and not nodes:
|
||||
# User supplied an empty `nodes` list to check so all we can
|
||||
# do is return the empty result
|
||||
return set()
|
||||
|
||||
def _node_type_exists(node_type):
|
||||
try:
|
||||
|
|
@ -1545,63 +1551,142 @@ def get_id_required_nodes(referenced_nodes=False, nodes=None):
|
|||
except RuntimeError:
|
||||
return False
|
||||
|
||||
def iterate(maya_iterator):
|
||||
while not maya_iterator.isDone():
|
||||
yield maya_iterator.thisNode()
|
||||
maya_iterator.next()
|
||||
|
||||
# `readOnly` flag is obsolete as of Maya 2016 therefore we explicitly
|
||||
# remove default nodes and reference nodes
|
||||
camera_shapes = ["frontShape", "sideShape", "topShape", "perspShape"]
|
||||
default_camera_shapes = {
|
||||
"frontShape", "sideShape", "topShape", "perspShape"
|
||||
}
|
||||
|
||||
ignore = set()
|
||||
if not referenced_nodes:
|
||||
ignore |= set(cmds.ls(long=True, referencedNodes=True))
|
||||
|
||||
# list all defaultNodes to filter out from the rest
|
||||
ignore |= set(cmds.ls(long=True, defaultNodes=True))
|
||||
ignore |= set(cmds.ls(camera_shapes, long=True))
|
||||
|
||||
# Remove Turtle from the result of `cmds.ls` if Turtle is loaded
|
||||
# TODO: This should be a less specific check for a single plug-in.
|
||||
if _node_type_exists("ilrBakeLayer"):
|
||||
ignore |= set(cmds.ls(type="ilrBakeLayer", long=True))
|
||||
|
||||
# Establish set of nodes types to include
|
||||
types = ["objectSet", "file", "mesh", "nurbsCurve", "nurbsSurface"]
|
||||
# The filtered types do not include transforms because we only want the
|
||||
# parent transforms that have a child shape that we filtered to, so we
|
||||
# include the parents here
|
||||
types = ["mesh", "nurbsCurve", "nurbsSurface", "file", "objectSet"]
|
||||
|
||||
# Check if plugin nodes are available for Maya by checking if the plugin
|
||||
# is loaded
|
||||
if cmds.pluginInfo("pgYetiMaya", query=True, loaded=True):
|
||||
types.append("pgYetiMaya")
|
||||
|
||||
# We *always* ignore intermediate shapes, so we filter them out directly
|
||||
nodes = cmds.ls(nodes, type=types, long=True, noIntermediate=True)
|
||||
iterator_type = OpenMaya.MIteratorType()
|
||||
# This tries to be closest matching API equivalents of `types` variable
|
||||
iterator_type.filterList = [
|
||||
OpenMaya.MFn.kMesh, # mesh
|
||||
OpenMaya.MFn.kNurbsSurface, # nurbsSurface
|
||||
OpenMaya.MFn.kNurbsCurve, # nurbsCurve
|
||||
OpenMaya.MFn.kFileTexture, # file
|
||||
OpenMaya.MFn.kSet, # objectSet
|
||||
OpenMaya.MFn.kPluginShape # pgYetiMaya
|
||||
]
|
||||
it = OpenMaya.MItDependencyNodes(iterator_type)
|
||||
|
||||
# The items which need to pass the id to their parent
|
||||
# Add the collected transform to the nodes
|
||||
dag = cmds.ls(nodes, type="dagNode", long=True) # query only dag nodes
|
||||
transforms = cmds.listRelatives(dag,
|
||||
parent=True,
|
||||
fullPath=True) or []
|
||||
fn_dep = OpenMaya.MFnDependencyNode()
|
||||
fn_dag = OpenMaya.MFnDagNode()
|
||||
result = set()
|
||||
|
||||
nodes = set(nodes)
|
||||
nodes |= set(transforms)
|
||||
def _should_include_parents(obj):
|
||||
"""Whether to include parents of obj in output"""
|
||||
if not obj.hasFn(OpenMaya.MFn.kShape):
|
||||
return False
|
||||
|
||||
nodes -= ignore # Remove the ignored nodes
|
||||
if not nodes:
|
||||
return nodes
|
||||
fn_dag.setObject(obj)
|
||||
if fn_dag.isIntermediateObject:
|
||||
return False
|
||||
|
||||
# Ensure only nodes from the input `nodes` are returned when a
|
||||
# filter was applied on function call because we also iterated
|
||||
# to parents and alike
|
||||
if lookup is not None:
|
||||
nodes &= lookup
|
||||
# Skip default cameras
|
||||
if (
|
||||
obj.hasFn(OpenMaya.MFn.kCamera) and
|
||||
fn_dag.name() in default_camera_shapes
|
||||
):
|
||||
return False
|
||||
|
||||
# Avoid locked nodes
|
||||
nodes_list = list(nodes)
|
||||
locked = cmds.lockNode(nodes_list, query=True, lock=True)
|
||||
for node, lock in zip(nodes_list, locked):
|
||||
if lock:
|
||||
log.warning("Skipping locked node: %s" % node)
|
||||
nodes.remove(node)
|
||||
return True
|
||||
|
||||
return nodes
|
||||
def _add_to_result_if_valid(obj):
|
||||
"""Add to `result` if the object should be included"""
|
||||
fn_dep.setObject(obj)
|
||||
if not existing_ids and fn_dep.hasAttribute("cbId"):
|
||||
return
|
||||
|
||||
if not referenced_nodes and fn_dep.isFromReferencedFile:
|
||||
return
|
||||
|
||||
if fn_dep.isDefaultNode:
|
||||
return
|
||||
|
||||
if fn_dep.isLocked:
|
||||
return
|
||||
|
||||
# Skip default cameras
|
||||
if (
|
||||
obj.hasFn(OpenMaya.MFn.kCamera) and
|
||||
fn_dep.name() in default_camera_shapes
|
||||
):
|
||||
return
|
||||
|
||||
if obj.hasFn(OpenMaya.MFn.kDagNode):
|
||||
# DAG nodes
|
||||
fn_dag.setObject(obj)
|
||||
|
||||
# Skip intermediate objects
|
||||
if fn_dag.isIntermediateObject:
|
||||
return
|
||||
|
||||
# DAG nodes can be instanced and thus may have multiple paths.
|
||||
# We need to identify each path
|
||||
paths = OpenMaya.MDagPath.getAllPathsTo(obj)
|
||||
for dag in paths:
|
||||
path = dag.fullPathName()
|
||||
result.add(path)
|
||||
else:
|
||||
# Dependency node
|
||||
path = fn_dep.name()
|
||||
result.add(path)
|
||||
|
||||
for obj in iterate(it):
|
||||
# For any non-intermediate shape node always include the parent
|
||||
# even if we exclude the shape itself (e.g. when locked, default)
|
||||
if _should_include_parents(obj):
|
||||
fn_dag.setObject(obj)
|
||||
parents = [
|
||||
fn_dag.parent(index) for index in range(fn_dag.parentCount())
|
||||
]
|
||||
for parent_obj in parents:
|
||||
_add_to_result_if_valid(parent_obj)
|
||||
|
||||
_add_to_result_if_valid(obj)
|
||||
|
||||
if not result:
|
||||
return result
|
||||
|
||||
# Exclude some additional types
|
||||
exclude_types = []
|
||||
if _node_type_exists("ilrBakeLayer"):
|
||||
# Remove Turtle from the result if Turtle is loaded
|
||||
exclude_types.append("ilrBakeLayer")
|
||||
|
||||
if exclude_types:
|
||||
exclude_nodes = set(cmds.ls(nodes, long=True, type=exclude_types))
|
||||
if exclude_nodes:
|
||||
result -= exclude_nodes
|
||||
|
||||
# Filter to explicit input nodes if provided
|
||||
if nodes is not None:
|
||||
# The amount of input nodes to filter to can be large and querying
|
||||
# many nodes can be slow in Maya. As such we want to try and reduce
|
||||
# it as much as possible, so we include the type filter to try and
|
||||
# reduce the result of `maya.cmds.ls` here.
|
||||
nodes = set(cmds.ls(nodes, long=True, type=types + ["dagNode"]))
|
||||
if nodes:
|
||||
result &= nodes
|
||||
else:
|
||||
return set()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_id(node):
|
||||
|
|
@ -1832,6 +1917,29 @@ def apply_attributes(attributes, nodes_by_id):
|
|||
set_attribute(attr, value, node)
|
||||
|
||||
|
||||
def is_valid_reference_node(reference_node):
|
||||
"""Return whether Maya considers the reference node a valid reference.
|
||||
|
||||
Maya might report an error when using `maya.cmds.referenceQuery`:
|
||||
Reference node 'reference_node' is not associated with a reference file.
|
||||
|
||||
Note that this does *not* check whether the reference node points to an
|
||||
existing file. Instead it only returns whether maya considers it valid
|
||||
and thus is not an unassociated reference node
|
||||
|
||||
Arguments:
|
||||
reference_node (str): Reference node name
|
||||
|
||||
Returns:
|
||||
bool: Whether reference node is a valid reference
|
||||
|
||||
"""
|
||||
sel = OpenMaya.MSelectionList()
|
||||
sel.add(reference_node)
|
||||
depend_node = sel.getDependNode(0)
|
||||
return OpenMaya.MFnReference(depend_node).isValidReference()
|
||||
|
||||
|
||||
def get_container_members(container):
|
||||
"""Returns the members of a container.
|
||||
This includes the nodes from any loaded references in the container.
|
||||
|
|
@ -1857,7 +1965,16 @@ def get_container_members(container):
|
|||
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
|
||||
continue
|
||||
|
||||
reference_members = cmds.referenceQuery(ref, nodes=True, dagPath=True)
|
||||
try:
|
||||
reference_members = cmds.referenceQuery(ref,
|
||||
nodes=True,
|
||||
dagPath=True)
|
||||
except RuntimeError:
|
||||
# Ignore reference nodes that are not associated with a
|
||||
# referenced file on which `referenceQuery` command fails
|
||||
if not is_valid_reference_node(ref):
|
||||
continue
|
||||
raise
|
||||
reference_members = cmds.ls(reference_members,
|
||||
long=True,
|
||||
objectsOnly=True)
|
||||
|
|
@ -2115,22 +2232,6 @@ def get_related_sets(node):
|
|||
|
||||
"""
|
||||
|
||||
# Ignore specific suffices
|
||||
ignore_suffices = ["out_SET", "controls_SET", "_INST", "_CON"]
|
||||
|
||||
# Default nodes to ignore
|
||||
defaults = {"defaultLightSet", "defaultObjectSet"}
|
||||
|
||||
# Ids to ignore
|
||||
ignored = {
|
||||
AVALON_INSTANCE_ID,
|
||||
AVALON_CONTAINER_ID,
|
||||
AYON_INSTANCE_ID,
|
||||
AYON_CONTAINER_ID,
|
||||
}
|
||||
|
||||
view_sets = get_isolate_view_sets()
|
||||
|
||||
sets = cmds.listSets(object=node, extendToShape=False)
|
||||
if not sets:
|
||||
return []
|
||||
|
|
@ -2141,6 +2242,14 @@ def get_related_sets(node):
|
|||
# returned by `cmds.listSets(allSets=True)`
|
||||
sets = cmds.ls(sets)
|
||||
|
||||
# Ids to ignore
|
||||
ignored = {
|
||||
AVALON_INSTANCE_ID,
|
||||
AVALON_CONTAINER_ID,
|
||||
AYON_INSTANCE_ID,
|
||||
AYON_CONTAINER_ID,
|
||||
}
|
||||
|
||||
# Ignore `avalon.container`
|
||||
sets = [
|
||||
s for s in sets
|
||||
|
|
@ -2149,21 +2258,31 @@ def get_related_sets(node):
|
|||
or cmds.getAttr(f"{s}.id") not in ignored
|
||||
)
|
||||
]
|
||||
if not sets:
|
||||
return sets
|
||||
|
||||
# Exclude deformer sets (`type=2` for `maya.cmds.listSets`)
|
||||
deformer_sets = cmds.listSets(object=node,
|
||||
extendToShape=False,
|
||||
type=2) or []
|
||||
deformer_sets = set(deformer_sets) # optimize lookup
|
||||
sets = [s for s in sets if s not in deformer_sets]
|
||||
exclude_sets = cmds.listSets(object=node,
|
||||
extendToShape=False,
|
||||
type=2) or []
|
||||
exclude_sets = set(exclude_sets) # optimize lookup
|
||||
|
||||
# Default nodes to ignore
|
||||
exclude_sets.update({"defaultLightSet", "defaultObjectSet"})
|
||||
|
||||
# Filter out the sets to exclude
|
||||
sets = [s for s in sets if s not in exclude_sets]
|
||||
|
||||
# Ignore when the set has a specific suffix
|
||||
sets = [s for s in sets if not any(s.endswith(x) for x in ignore_suffices)]
|
||||
ignore_suffices = ("out_SET", "controls_SET", "_INST", "_CON")
|
||||
sets = [s for s in sets if not s.endswith(ignore_suffices)]
|
||||
if not sets:
|
||||
return sets
|
||||
|
||||
# Ignore viewport filter view sets (from isolate select and
|
||||
# viewports)
|
||||
view_sets = get_isolate_view_sets()
|
||||
sets = [s for s in sets if s not in view_sets]
|
||||
sets = [s for s in sets if s not in defaults]
|
||||
|
||||
return sets
|
||||
|
||||
|
|
@ -2434,12 +2553,10 @@ def set_scene_fps(fps, update=True):
|
|||
cmds.currentUnit(time=unit, updateAnimation=update)
|
||||
|
||||
# Set time slider data back to previous state
|
||||
cmds.playbackOptions(edit=True, minTime=start_frame)
|
||||
cmds.playbackOptions(edit=True, maxTime=end_frame)
|
||||
|
||||
# Set animation data
|
||||
cmds.playbackOptions(edit=True, animationStartTime=animation_start)
|
||||
cmds.playbackOptions(edit=True, animationEndTime=animation_end)
|
||||
cmds.playbackOptions(minTime=start_frame,
|
||||
maxTime=end_frame,
|
||||
animationStartTime=animation_start,
|
||||
animationEndTime=animation_end)
|
||||
|
||||
cmds.currentTime(current_frame, edit=True, update=True)
|
||||
|
||||
|
|
@ -2629,21 +2746,21 @@ def reset_frame_range(playback=True, render=True, fps=True):
|
|||
def reset_scene_resolution():
|
||||
"""Apply the scene resolution from the project definition
|
||||
|
||||
scene resolution can be overwritten by an folder if the folder.attrib
|
||||
contains any information regarding scene resolution .
|
||||
The scene resolution will be retrieved from the current task entity's
|
||||
attributes.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
folder_attributes = get_current_folder_entity()["attrib"]
|
||||
task_attributes = get_current_task_entity(fields={"attrib"})["attrib"]
|
||||
|
||||
# Set resolution
|
||||
width = folder_attributes.get("resolutionWidth", 1920)
|
||||
height = folder_attributes.get("resolutionHeight", 1080)
|
||||
pixelAspect = folder_attributes.get("pixelAspect", 1)
|
||||
width = task_attributes.get("resolutionWidth", 1920)
|
||||
height = task_attributes.get("resolutionHeight", 1080)
|
||||
pixel_aspect = task_attributes.get("pixelAspect", 1)
|
||||
|
||||
set_scene_resolution(width, height, pixelAspect)
|
||||
set_scene_resolution(width, height, pixel_aspect)
|
||||
|
||||
|
||||
def set_context_settings(
|
||||
|
|
@ -3129,7 +3246,7 @@ def load_capture_preset(data):
|
|||
return options
|
||||
|
||||
|
||||
def get_attr_in_layer(attr, layer):
|
||||
def get_attr_in_layer(attr, layer, as_string=True):
|
||||
"""Return attribute value in specified renderlayer.
|
||||
|
||||
Same as cmds.getAttr but this gets the attribute's value in a
|
||||
|
|
@ -3147,6 +3264,7 @@ def get_attr_in_layer(attr, layer):
|
|||
Args:
|
||||
attr (str): attribute name, ex. "node.attribute"
|
||||
layer (str): layer name
|
||||
as_string (bool): whether attribute should convert to a string value
|
||||
|
||||
Returns:
|
||||
The return value from `maya.cmds.getAttr`
|
||||
|
|
@ -3156,7 +3274,8 @@ def get_attr_in_layer(attr, layer):
|
|||
try:
|
||||
if cmds.mayaHasRenderSetup():
|
||||
from . import lib_rendersetup
|
||||
return lib_rendersetup.get_attr_in_layer(attr, layer)
|
||||
return lib_rendersetup.get_attr_in_layer(
|
||||
attr, layer, as_string=as_string)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
|
@ -3164,7 +3283,7 @@ def get_attr_in_layer(attr, layer):
|
|||
current_layer = cmds.editRenderLayerGlobals(query=True,
|
||||
currentRenderLayer=True)
|
||||
if layer == current_layer:
|
||||
return cmds.getAttr(attr)
|
||||
return cmds.getAttr(attr, asString=as_string)
|
||||
|
||||
connections = cmds.listConnections(attr,
|
||||
plugs=True,
|
||||
|
|
@ -3215,7 +3334,7 @@ def get_attr_in_layer(attr, layer):
|
|||
value *= conversion
|
||||
return value
|
||||
|
||||
return cmds.getAttr(attr)
|
||||
return cmds.getAttr(attr, asString=as_string)
|
||||
|
||||
|
||||
def fix_incompatible_containers():
|
||||
|
|
@ -3244,33 +3363,46 @@ def update_content_on_context_change():
|
|||
"""
|
||||
This will update scene content to match new folder on context change
|
||||
"""
|
||||
scene_sets = cmds.listSets(allSets=True)
|
||||
folder_entity = get_current_folder_entity()
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
new_folder_path = folder_entity["path"]
|
||||
for s in scene_sets:
|
||||
try:
|
||||
if cmds.getAttr("{}.id".format(s)) in {
|
||||
AYON_INSTANCE_ID, AVALON_INSTANCE_ID
|
||||
}:
|
||||
attr = cmds.listAttr(s)
|
||||
print(s)
|
||||
if "folderPath" in attr:
|
||||
print(
|
||||
" - setting folder to: [ {} ]".format(new_folder_path)
|
||||
)
|
||||
cmds.setAttr(
|
||||
"{}.folderPath".format(s),
|
||||
new_folder_path, type="string"
|
||||
)
|
||||
if "frameStart" in attr:
|
||||
cmds.setAttr("{}.frameStart".format(s),
|
||||
folder_attributes["frameStart"])
|
||||
if "frameEnd" in attr:
|
||||
cmds.setAttr("{}.frameEnd".format(s),
|
||||
folder_attributes["frameEnd"],)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
host = registered_host()
|
||||
create_context = CreateContext(host)
|
||||
folder_entity = get_current_task_entity(fields={"attrib"})
|
||||
|
||||
instance_values = {
|
||||
"folderPath": create_context.get_current_folder_path(),
|
||||
"task": create_context.get_current_task_name(),
|
||||
}
|
||||
creator_attribute_values = {
|
||||
"frameStart": folder_entity["attrib"]["frameStart"],
|
||||
"frameEnd": folder_entity["attrib"]["frameEnd"],
|
||||
}
|
||||
|
||||
has_changes = False
|
||||
for instance in create_context.instances:
|
||||
for key, value in instance_values.items():
|
||||
if key not in instance or instance[key] == value:
|
||||
continue
|
||||
|
||||
# Update instance value
|
||||
print(f"Updating {instance.product_name} {key} to: {value}")
|
||||
instance[key] = value
|
||||
has_changes = True
|
||||
|
||||
creator_attributes = instance.creator_attributes
|
||||
for key, value in creator_attribute_values.items():
|
||||
if (
|
||||
key not in creator_attributes
|
||||
or creator_attributes[key] == value
|
||||
):
|
||||
continue
|
||||
|
||||
# Update instance creator attribute value
|
||||
print(f"Updating {instance.product_name} {key} to: {value}")
|
||||
instance[key] = value
|
||||
has_changes = True
|
||||
|
||||
if has_changes:
|
||||
create_context.save_changes()
|
||||
|
||||
|
||||
def show_message(title, msg):
|
||||
|
|
@ -4004,17 +4136,26 @@ def len_flattened(components):
|
|||
return n
|
||||
|
||||
|
||||
def get_all_children(nodes):
|
||||
def get_all_children(nodes, ignore_intermediate_objects=False):
|
||||
"""Return all children of `nodes` including each instanced child.
|
||||
Using maya.cmds.listRelatives(allDescendents=True) includes only the first
|
||||
instance. As such, this function acts as an optimal replacement with a
|
||||
focus on a fast query.
|
||||
|
||||
Args:
|
||||
nodes (iterable): List of nodes to get children for.
|
||||
ignore_intermediate_objects (bool): Ignore any children that
|
||||
are intermediate objects.
|
||||
|
||||
Returns:
|
||||
set: Children of input nodes.
|
||||
|
||||
"""
|
||||
|
||||
sel = OpenMaya.MSelectionList()
|
||||
traversed = set()
|
||||
iterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst)
|
||||
fn_dag = OpenMaya.MFnDagNode()
|
||||
for node in nodes:
|
||||
|
||||
if node in traversed:
|
||||
|
|
@ -4031,6 +4172,13 @@ def get_all_children(nodes):
|
|||
iterator.next() # noqa: B305
|
||||
while not iterator.isDone():
|
||||
|
||||
if ignore_intermediate_objects:
|
||||
fn_dag.setObject(iterator.currentItem())
|
||||
if fn_dag.isIntermediateObject:
|
||||
iterator.prune()
|
||||
iterator.next() # noqa: B305
|
||||
continue
|
||||
|
||||
path = iterator.fullPathName()
|
||||
|
||||
if path in traversed:
|
||||
|
|
@ -4041,7 +4189,7 @@ def get_all_children(nodes):
|
|||
traversed.add(path)
|
||||
iterator.next() # noqa: B305
|
||||
|
||||
return list(traversed)
|
||||
return traversed
|
||||
|
||||
|
||||
def get_capture_preset(
|
||||
|
|
@ -4122,6 +4270,9 @@ def get_reference_node(members, log=None):
|
|||
if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"):
|
||||
continue
|
||||
|
||||
if not is_valid_reference_node(ref):
|
||||
continue
|
||||
|
||||
references.add(ref)
|
||||
|
||||
assert references, "No reference node found in container"
|
||||
|
|
@ -4152,15 +4303,19 @@ def get_reference_node_parents(ref):
|
|||
list: The upstream parent reference nodes.
|
||||
|
||||
"""
|
||||
parent = cmds.referenceQuery(ref,
|
||||
referenceNode=True,
|
||||
parent=True)
|
||||
def _get_parent(reference_node):
|
||||
"""Return parent reference node, but ignore invalid reference nodes"""
|
||||
if not is_valid_reference_node(reference_node):
|
||||
return
|
||||
return cmds.referenceQuery(reference_node,
|
||||
referenceNode=True,
|
||||
parent=True)
|
||||
|
||||
parent = _get_parent(ref)
|
||||
parents = []
|
||||
while parent:
|
||||
parents.append(parent)
|
||||
parent = cmds.referenceQuery(parent,
|
||||
referenceNode=True,
|
||||
parent=True)
|
||||
parent = _get_parent(parent)
|
||||
return parents
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ class ARenderProducts:
|
|||
"""
|
||||
return self._get_attr("defaultRenderGlobals", attribute)
|
||||
|
||||
def _get_attr(self, node_attr, attribute=None):
|
||||
def _get_attr(self, node_attr, attribute=None, as_string=True):
|
||||
"""Return the value of the attribute in the renderlayer
|
||||
|
||||
For readability this allows passing in the attribute in two ways.
|
||||
|
|
@ -317,7 +317,7 @@ class ARenderProducts:
|
|||
else:
|
||||
plug = "{}.{}".format(node_attr, attribute)
|
||||
|
||||
return lib.get_attr_in_layer(plug, layer=self.layer)
|
||||
return lib.get_attr_in_layer(plug, layer=self.layer, as_string=as_string)
|
||||
|
||||
@staticmethod
|
||||
def extract_separator(file_prefix):
|
||||
|
|
@ -1133,9 +1133,24 @@ class RenderProductsRedshift(ARenderProducts):
|
|||
aovs = list(set(aovs) - set(ref_aovs))
|
||||
|
||||
products = []
|
||||
global_aov_enabled = bool(
|
||||
self._get_attr("redshiftOptions.aovGlobalEnableMode", as_string=False)
|
||||
)
|
||||
colorspace = lib.get_color_management_output_transform()
|
||||
if not global_aov_enabled:
|
||||
# only beauty output
|
||||
for camera in cameras:
|
||||
products.insert(0,
|
||||
RenderProduct(productName="",
|
||||
ext=ext,
|
||||
multipart=self.multipart,
|
||||
camera=camera,
|
||||
colorspace=colorspace))
|
||||
return products
|
||||
|
||||
light_groups_enabled = False
|
||||
has_beauty_aov = False
|
||||
colorspace = lib.get_color_management_output_transform()
|
||||
|
||||
for aov in aovs:
|
||||
enabled = self._get_attr(aov, "enabled")
|
||||
if not enabled:
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ def get_rendersetup_layer(layer):
|
|||
if conn.endswith(".legacyRenderLayer")), None)
|
||||
|
||||
|
||||
def get_attr_in_layer(node_attr, layer):
|
||||
def get_attr_in_layer(node_attr, layer, as_string=True):
|
||||
"""Return attribute value in Render Setup layer.
|
||||
|
||||
This will only work for attributes which can be
|
||||
|
|
@ -124,7 +124,7 @@ def get_attr_in_layer(node_attr, layer):
|
|||
node = history_overrides[-1] if history_overrides else override
|
||||
node_attr_ = node + ".original"
|
||||
|
||||
return get_attribute(node_attr_, asString=True)
|
||||
return get_attribute(node_attr_, asString=as_string)
|
||||
|
||||
layer = get_rendersetup_layer(layer)
|
||||
rs = renderSetup.instance()
|
||||
|
|
@ -144,7 +144,7 @@ def get_attr_in_layer(node_attr, layer):
|
|||
# we will let it error out.
|
||||
rs.switchToLayer(current_layer)
|
||||
|
||||
return get_attribute(node_attr, asString=True)
|
||||
return get_attribute(node_attr, asString=as_string)
|
||||
|
||||
overrides = get_attr_overrides(node_attr, layer)
|
||||
default_layer_value = get_default_layer_value(node_attr)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
|
|
@ -214,8 +215,18 @@ def install(project_settings):
|
|||
)
|
||||
return
|
||||
|
||||
config = project_settings["maya"]["scriptsmenu"]["definition"]
|
||||
_menu = project_settings["maya"]["scriptsmenu"]["name"]
|
||||
menu_settings = project_settings["maya"]["scriptsmenu"]
|
||||
menu_name = menu_settings["name"]
|
||||
config = menu_settings["definition"]
|
||||
|
||||
if menu_settings.get("definition_type") == "definition_json":
|
||||
data = menu_settings["definition_json"]
|
||||
try:
|
||||
config = json.loads(data)
|
||||
except json.JSONDecodeError as exc:
|
||||
print("Skipping studio menu, error decoding JSON definition.")
|
||||
log.error(exc)
|
||||
return
|
||||
|
||||
if not config:
|
||||
log.warning("Skipping studio menu, no definition found.")
|
||||
|
|
@ -223,8 +234,8 @@ def install(project_settings):
|
|||
|
||||
# run the launcher for Maya menu
|
||||
studio_menu = launchformaya.main(
|
||||
title=_menu.title(),
|
||||
objectName=_menu.title().lower().replace(" ", "_")
|
||||
title=menu_name.title(),
|
||||
objectName=menu_name.title().lower().replace(" ", "_")
|
||||
)
|
||||
|
||||
# apply configuration
|
||||
|
|
|
|||
|
|
@ -580,7 +580,8 @@ def on_save():
|
|||
_remove_workfile_lock()
|
||||
|
||||
# Generate ids of the current context on nodes in the scene
|
||||
nodes = lib.get_id_required_nodes(referenced_nodes=False)
|
||||
nodes = lib.get_id_required_nodes(referenced_nodes=False,
|
||||
existing_ids=False)
|
||||
for node, new_id in lib.generate_ids(nodes):
|
||||
lib.set_id(node, new_id, overwrite=False)
|
||||
|
||||
|
|
@ -653,10 +654,6 @@ def on_task_changed():
|
|||
"Can't set project for new context because path does not exist: {}"
|
||||
).format(workdir))
|
||||
|
||||
with lib.suspended_refresh():
|
||||
lib.set_context_settings()
|
||||
lib.update_content_on_context_change()
|
||||
|
||||
global _about_to_save
|
||||
if not lib.IS_HEADLESS and _about_to_save:
|
||||
# Let's prompt the user to update the context settings or not
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue