Merge branch 'develop' of https://github.com/ynput/ayon-core into enhancement/usd_workflow

# Conflicts:
#	client/ayon_core/hosts/houdini/plugins/create/create_usdrender.py
This commit is contained in:
Roy Nieterau 2024-04-15 15:40:40 +02:00
commit f38b72e11a
364 changed files with 5888 additions and 3744 deletions

View file

@ -15,7 +15,9 @@ 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
from ayon_core.settings import get_studio_settings
@ -45,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(2, 0, 0),
}
# Inherit from `object` for Python 2 hosts
class _ModuleClass(object):
@ -191,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.
@ -248,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):
@ -256,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)
@ -340,9 +397,8 @@ def _load_addons_in_core(
):
# Add current directory at first place
# - has small differences in import logic
current_dir = os.path.abspath(os.path.dirname(__file__))
hosts_dir = os.path.join(os.path.dirname(current_dir), "hosts")
modules_dir = os.path.join(os.path.dirname(current_dir), "modules")
hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts")
modules_dir = os.path.join(AYON_CORE_ROOT, "modules")
ignored_host_names = set(IGNORED_HOSTS_IN_AYON)
ignored_module_dir_filenames = (

View file

@ -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
@ -81,7 +82,7 @@ main_cli.set_alias("addon", "module")
@main_cli.command()
@click.argument("output_json_path")
@click.option("--project", help="Project name", default=None)
@click.option("--asset", help="Asset name", default=None)
@click.option("--asset", help="Folder path", default=None)
@click.option("--task", help="Task name", default=None)
@click.option("--app", help="Application name", default=None)
@click.option(
@ -96,6 +97,10 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup):
environments will be extracted.
Context options are "project", "asset", "task", "app"
Deprecated:
This function is deprecated and will be removed in future. Please use
'addon applications extractenvironments ...' instead.
"""
Commands.extractenvironments(
output_json_path, project, asset, task, app, envgroup
@ -127,7 +132,7 @@ def publish_report_viewer():
@main_cli.command()
@click.argument("output_path")
@click.option("--project", help="Define project context")
@click.option("--asset", help="Define asset in project (project must be set)")
@click.option("--folder", help="Define folder in project (project must be set)")
@click.option(
"--strict",
is_flag=True,
@ -136,18 +141,18 @@ def publish_report_viewer():
def contextselection(
output_path,
project,
asset,
folder,
strict
):
"""Show Qt dialog to select context.
Context is project name, asset name and task name. The result is stored
Context is project name, folder path and task name. The result is stored
into json file which path is passed in first argument.
"""
Commands.contextselection(
output_path,
project,
asset,
folder,
strict
)
@ -163,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()

View file

@ -2,7 +2,7 @@
"""Implementation of AYON commands."""
import os
import sys
import json
import warnings
class Commands:
@ -57,10 +57,7 @@ class Commands:
"""
from ayon_core.lib import Logger
from ayon_core.lib.applications import (
get_app_environments_for_context,
LaunchTypes,
)
from ayon_core.addon import AddonsManager
from ayon_core.pipeline import (
install_ayon_plugins,
@ -68,7 +65,6 @@ class Commands:
)
# Register target and host
import pyblish.api
import pyblish.util
if not isinstance(path, str):
@ -99,15 +95,13 @@ class Commands:
for plugin_path in publish_paths:
pyblish.api.register_plugin_path(plugin_path)
app_full_name = os.getenv("AYON_APP_NAME")
if app_full_name:
applications_addon = manager.get_enabled_addon("applications")
if applications_addon is not None:
context = get_global_context()
env = get_app_environments_for_context(
env = applications_addon.get_farm_publish_environment_variables(
context["project_name"],
context["folder_path"],
context["task_name"],
app_full_name,
launch_type=LaunchTypes.farm_publish,
)
os.environ.update(env)
@ -149,36 +143,36 @@ class Commands:
log.info("Publish finished.")
@staticmethod
def extractenvironments(output_json_path, project, asset, task, app,
env_group):
def extractenvironments(
output_json_path, project, asset, task, app, env_group
):
"""Produces json file with environment based on project and app.
Called by Deadline plugin to propagate environment into render jobs.
"""
from ayon_core.lib.applications import (
get_app_environments_for_context,
LaunchTypes,
from ayon_core.addon import AddonsManager
warnings.warn(
(
"Command 'extractenvironments' is deprecated and will be"
" removed in future. Please use "
"'addon applications extractenvironments ...' instead."
),
DeprecationWarning
)
if all((project, asset, task, app)):
env = get_app_environments_for_context(
project,
asset,
task,
app,
env_group=env_group,
launch_type=LaunchTypes.farm_render
addons_manager = AddonsManager()
applications_addon = addons_manager.get_enabled_addon("applications")
if applications_addon is None:
raise RuntimeError(
"Applications addon is not available or enabled."
)
else:
env = os.environ.copy()
output_dir = os.path.dirname(output_json_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(output_json_path, "w") as file_stream:
json.dump(env, file_stream, indent=4)
# Please ignore the fact this is using private method
applications_addon._cli_extract_environments(
output_json_path, project, asset, task, app, env_group
)
@staticmethod
def contextselection(output_path, project_name, folder_path, strict):

View file

@ -1,6 +1,6 @@
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class AddLastWorkfileToLaunchArgs(PreLaunchHook):

View file

@ -1,7 +1,7 @@
import os
import shutil
from ayon_core.settings import get_project_settings
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.pipeline.workfile import (
get_custom_workfile_template,
get_custom_workfile_template_by_string_context

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.pipeline.workfile import create_workdir_extra_folders

View file

@ -1,7 +1,7 @@
from ayon_api import get_project, get_folder_by_path, get_task_by_name
from ayon_core.lib.applications import (
PreLaunchHook,
from ayon_applications import PreLaunchHook
from ayon_applications.utils import (
EnvironmentPrepData,
prepare_app_environments,
prepare_context_environments

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class LaunchWithTerminal(PreLaunchHook):

View file

@ -1,5 +1,5 @@
import subprocess
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class LaunchNewConsoleApps(PreLaunchHook):

View file

@ -1,4 +1,4 @@
from ayon_core.lib.applications import PreLaunchHook
from ayon_applications import PreLaunchHook
from ayon_core.pipeline.colorspace import get_imageio_config
from ayon_core.pipeline.template_data import get_template_data_with_names

View file

@ -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

View file

@ -6,10 +6,7 @@ from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.aftereffects import get_launch_script_path

View file

@ -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()

View file

@ -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()

View file

@ -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):

View file

@ -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:

View file

@ -1,6 +1,6 @@
from pathlib import Path
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class AddPythonScriptToLaunchArgs(PreLaunchHook):

View file

@ -2,7 +2,7 @@ import os
import re
import subprocess
from platform import system
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class InstallPySideToBlender(PreLaunchHook):
@ -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

View file

@ -1,5 +1,5 @@
import subprocess
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class BlenderConsoleWindows(PreLaunchHook):

View file

@ -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"

View file

@ -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"

View file

@ -25,7 +25,7 @@ class BlendActionLoader(plugin.AssetLoader):
"""
product_types = {"action"}
representations = ["blend"]
representations = {"blend"}
label = "Link Action"
icon = "code-fork"

View file

@ -17,7 +17,7 @@ class BlendAnimationLoader(plugin.AssetLoader):
"""
product_types = {"animation"}
representations = ["blend"]
representations = {"blend"}
label = "Link Animation"
icon = "code-fork"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -24,7 +24,7 @@ class AbcCameraLoader(plugin.AssetLoader):
"""
product_types = {"camera"}
representations = ["abc"]
representations = {"abc"}
label = "Load Camera (ABC)"
icon = "code-fork"

View file

@ -24,7 +24,7 @@ class FbxCameraLoader(plugin.AssetLoader):
"""
product_types = {"camera"}
representations = ["fbx"]
representations = {"fbx"}
label = "Load Camera (FBX)"
icon = "code-fork"

View file

@ -24,7 +24,7 @@ class FbxModelLoader(plugin.AssetLoader):
"""
product_types = {"model", "rig"}
representations = ["fbx"]
representations = {"fbx"}
label = "Load FBX"
icon = "code-fork"

View file

@ -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)

View file

@ -24,7 +24,7 @@ class BlendLookLoader(plugin.AssetLoader):
"""
product_types = {"look"}
representations = ["json"]
representations = {"json"}
label = "Load Look"
icon = "code-fork"

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
import ayon_core.hosts.blender.api.action
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale."""

View file

@ -3,7 +3,7 @@ import shutil
import winreg
import subprocess
from ayon_core.lib import get_ayon_launcher_args
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.celaction import CELACTION_ROOT_DIR

View file

@ -3,11 +3,11 @@ import sys
from pprint import pformat
class CollectCelactionCliKwargs(pyblish.api.Collector):
class CollectCelactionCliKwargs(pyblish.api.ContextPlugin):
""" Collects all keyword arguments passed from the terminal """
label = "Collect Celaction Cli Kwargs"
order = pyblish.api.Collector.order - 0.1
order = pyblish.api.CollectorOrder - 0.1
def process(self, context):
args = list(sys.argv[1:])

View file

@ -9,7 +9,7 @@ from ayon_core.lib import (
get_ayon_username,
run_subprocess,
)
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts import flame as opflame

View file

@ -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)
)

View file

@ -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)
)

View file

@ -3,9 +3,11 @@ import sys
import re
import contextlib
from ayon_core.lib import Logger
from ayon_core.pipeline.context_tools import get_current_project_folder
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
self = sys.modules[__name__]
self._project = None
@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True,
comp.SetAttrs(attrs)
def set_current_context_framerange():
def set_current_context_framerange(folder_entity=None):
"""Set Comp's frame range based on current folder."""
folder_entity = get_current_project_folder()
if folder_entity is None:
folder_entity = get_current_folder_entity(
fields={"attrib.frameStart",
"attrib.frameEnd",
"attrib.handleStart",
"attrib.handleEnd"})
folder_attributes = folder_entity["attrib"]
start = folder_attributes["frameStart"]
end = folder_attributes["frameEnd"]
@ -65,9 +73,24 @@ def set_current_context_framerange():
handle_end=handle_end)
def set_current_context_resolution():
def set_current_context_fps(folder_entity=None):
"""Set Comp's frame rate (FPS) to based on current asset"""
if folder_entity is None:
folder_entity = get_current_folder_entity(fields={"attrib.fps"})
fps = float(folder_entity["attrib"].get("fps", 24.0))
comp = get_current_comp()
comp.SetPrefs({
"Comp.FrameFormat.Rate": fps,
})
def set_current_context_resolution(folder_entity=None):
"""Set Comp's resolution width x height default based on current folder"""
folder_entity = get_current_project_folder()
if folder_entity is None:
folder_entity = get_current_folder_entity(
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"})
folder_attributes = folder_entity["attrib"]
width = folder_attributes["resolutionWidth"]
height = folder_attributes["resolutionHeight"]
@ -101,7 +124,7 @@ def validate_comp_prefs(comp=None, force_repair=False):
"attrib.resolutionHeight",
"attrib.pixelAspect",
}
folder_entity = get_current_project_folder(fields=fields)
folder_entity = get_current_folder_entity(fields=fields)
folder_path = folder_entity["path"]
folder_attributes = folder_entity["attrib"]
@ -158,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")
@ -285,3 +307,96 @@ def comp_lock_and_undo_chunk(
finally:
comp.Unlock()
comp.EndUndo(keep_undo)
def update_content_on_context_change():
"""Update all Creator instances to current asset"""
host = registered_host()
context = host.get_current_context()
folder_path = context["folder_path"]
task = context["task_name"]
create_context = CreateContext(host, reset=True)
for instance in create_context.instances:
instance_folder_path = instance.get("folderPath")
if instance_folder_path and instance_folder_path != folder_path:
instance["folderPath"] = folder_path
instance_task = instance.get("task")
if instance_task and instance_task != task:
instance["task"] = task
create_context.save_changes()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from qtpy import QtCore
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset Comp FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset Comp start and end frame ranges",
default=True
),
BoolDef(
"resolution",
label="Comp Resolution",
tooltip="Reset Comp resolution",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowFlags(
dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
folder_entity = get_current_folder_entity()
if options["frame_range"]:
set_current_context_framerange(folder_entity)
if options["fps"]:
set_current_context_fps(folder_entity)
if options["resolution"]:
set_current_context_resolution(folder_entity)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()

View file

@ -5,6 +5,7 @@ import os
import sys
import logging
import contextlib
from pathlib import Path
import pyblish.api
from qtpy import QtCore
@ -28,7 +29,8 @@ from ayon_core.tools.utils import host_tools
from .lib import (
get_current_comp,
validate_comp_prefs
validate_comp_prefs,
prompt_reset_context
)
log = Logger.get_logger(__name__)
@ -40,6 +42,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save
_about_to_save = False
class FusionLogHandler(logging.Handler):
# Keep a reference to fusion's Print function (Remote Object)
@ -103,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
# Register events
register_event_callback("open", on_after_open)
register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback("save", on_save)
register_event_callback("new", on_new)
register_event_callback("taskChanged", on_task_changed)
# region workfile io api
def has_unsaved_changes(self):
@ -168,6 +175,19 @@ def on_save(event):
comp = event["sender"]
validate_comp_prefs(comp)
# We are now starting the actual save directly
global _about_to_save
_about_to_save = False
def on_task_changed():
global _about_to_save
print(f"Task changed: {_about_to_save}")
# TODO: Only do this if not headless
if _about_to_save:
# Let's prompt the user to update the context settings or not
prompt_reset_context()
def on_after_open(event):
comp = event["sender"]
@ -201,6 +221,28 @@ def on_after_open(event):
dialog.setStyleSheet(load_stylesheet())
def before_workfile_save(event):
# Due to Fusion's external python process design we can't really
# detect whether the current Fusion environment matches the one the artists
# expects it to be. For example, our pipeline python process might
# have been shut down, and restarted - which will restart it to the
# environment Fusion started with; not necessarily where the artist
# is currently working.
# The `_about_to_save` var is used to detect context changes when
# saving into another asset. If we keep it False it will be ignored
# as context change. As such, before we change tasks we will only
# consider it the current filepath is within the currently known
# AVALON_WORKDIR. This way we avoid false positives of thinking it's
# saving to another context and instead sometimes just have false negatives
# where we fail to show the "Update on task change" prompt.
comp = get_current_comp()
filepath = comp.GetAttrs()["COMPS_FileName"]
workdir = os.environ.get("AYON_WORKDIR")
if Path(workdir) in Path(filepath).parents:
global _about_to_save
_about_to_save = True
def ls():
"""List containers from active Fusion scene
@ -337,7 +379,6 @@ class FusionEventHandler(QtCore.QObject):
>>> handler = FusionEventHandler(parent=window)
>>> handler.start()
"""
ACTION_IDS = [
"Comp_Save",

View file

@ -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,

View file

@ -0,0 +1,36 @@
import os
from ayon_core.lib 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])

View file

@ -7,7 +7,7 @@ from ayon_core.hosts.fusion import (
FUSION_VERSIONS_DICT,
get_fusion_version,
)
from ayon_core.lib.applications import (
from ayon_applications import (
PreLaunchHook,
LaunchTypes,
ApplicationLaunchFailed,

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib.applications import (
from ayon_applications import (
PreLaunchHook,
LaunchTypes,
ApplicationLaunchFailed,

View file

@ -3,7 +3,7 @@ import subprocess
import platform
import uuid
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class InstallPySideToFusion(PreLaunchHook):
@ -85,7 +85,6 @@ class InstallPySideToFusion(PreLaunchHook):
administration rights.
"""
try:
import win32api
import win32con
import win32process
import win32event

View file

@ -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

View file

@ -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"

View file

@ -13,7 +13,7 @@ class FusionLoadFBXMesh(load.LoaderPlugin):
"""Load FBX mesh into Fusion"""
product_types = {"*"}
representations = ["*"]
representations = {"*"}
extensions = {
"3ds",
"amc",

View file

@ -137,7 +137,7 @@ class FusionLoadSequence(load.LoaderPlugin):
"image",
"online",
}
representations = ["*"]
representations = {"*"}
extensions = set(
ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS)
)

View file

@ -17,7 +17,7 @@ class FusionLoadUSD(load.LoaderPlugin):
"""
product_types = {"*"}
representations = ["*"]
representations = {"*"}
extensions = {"usd", "usda", "usdz"}
label = "Load USD"

View file

@ -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"

View file

@ -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

View file

@ -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):

View file

@ -13,7 +13,7 @@ from ayon_core.pipeline import (
AVALON_CONTAINER_ID,
)
from ayon_core.pipeline.load import get_outdated_containers
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.hosts.harmony import HARMONY_ADDON_ROOT
import ayon_core.hosts.harmony.api as harmony
@ -50,7 +50,7 @@ def get_current_context_settings():
"""
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
folder_attributes = folder_entity["attrib"]
fps = folder_attributes.get("fps")

View file

@ -6,10 +6,7 @@ from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.harmony import get_launch_script_path

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -24,7 +24,7 @@ class TemplateLoader(load.LoaderPlugin):
"""
product_types = {"template", "workfile"}
representations = ["*"]
representations = {"*"}
label = "Load Template"
icon = "gift"

View file

@ -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"

View file

@ -248,8 +248,12 @@ def get_track_items(
# collect all available active sequence track items
if not return_list:
sequence = get_current_sequence(name=sequence_name)
# get all available tracks from sequence
tracks = list(sequence.audioTracks()) + list(sequence.videoTracks())
tracks = []
if sequence is not None:
# get all available tracks from sequence
tracks.extend(sequence.audioTracks())
tracks.extend(sequence.videoTracks())
# loop all tracks
for track in tracks:
if check_locked and track.isLocked():

View file

@ -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,

View file

@ -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)
)

View file

@ -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"

View file

@ -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,

View file

@ -90,20 +90,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
if "entity_type" in parent:
parent["folder_type"] = parent.pop("entity_type")
asset, asset_name = self._get_folder_data(tag_data)
product_name = tag_data.get("productName")
if product_name is None:
product_name = tag_data["subset"]
folder_path, folder_name = self._get_folder_data(tag_data)
families = [str(f) for f in tag_data["families"]]
# form label
label = "{} -".format(asset)
if asset_name != clip_name:
label += " ({})".format(clip_name)
label += " {}".format(product_name)
# TODO: remove backward compatibility
product_name = tag_data.get("productName")
if product_name is None:
@ -113,7 +103,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# backward compatibility: product_name should not be missing
if not product_name:
self.log.error(
"Product name is not defined for: {}".format(asset))
"Product name is not defined for: {}".format(folder_path))
# TODO: remove backward compatibility
product_type = tag_data.get("productType")
@ -124,15 +114,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# backward compatibility: product_type should not be missing
if not product_type:
self.log.error(
"Product type is not defined for: {}".format(asset))
"Product type is not defined for: {}".format(folder_path))
# form label
label = "{} -".format(folder_path)
if folder_name != clip_name:
label += " ({})".format(clip_name)
label += " {}".format(product_name)
data.update({
"name": "{}_{}".format(asset, product_name),
"name": "{}_{}".format(folder_path, product_name),
"label": label,
"folderPath": asset,
"asset_name": asset_name,
"productName": product_name,
"productType": product_type,
"folderPath": folder_path,
"asset_name": folder_name,
"item": track_item,
"families": families,
"publish": tag_data["publish"],
@ -222,19 +218,19 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
if not hierarchy_data:
return
asset = data["folderPath"]
asset_name = data["asset_name"]
folder_path = data["folderPath"]
folder_name = data["asset_name"]
product_type = "shot"
# form label
label = "{} -".format(asset)
if asset_name != clip_name:
label = "{} -".format(folder_path)
if folder_name != clip_name:
label += " ({}) ".format(clip_name)
label += " {}".format(product_name)
data.update({
"name": "{}_{}".format(asset, product_name),
"name": "{}_{}".format(folder_path, product_name),
"label": label,
"productName": product_name,
"productType": product_type,
@ -281,19 +277,19 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
if not self.test_any_audio(item):
return
asset = data["folderPath"]
folder_path = data["folderPath"]
asset_name = data["asset_name"]
product_type = "audio"
# form label
label = "{} -".format(asset)
label = "{} -".format(folder_path)
if asset_name != clip_name:
label += " ({}) ".format(clip_name)
label += " {}".format(product_name)
data.update({
"name": "{}_{}".format(asset, product_name),
"name": "{}_{}".format(folder_path, product_name),
"label": label,
"productName": product_name,
"productType": product_type,

View file

@ -17,8 +17,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder - 0.491
def process(self, context):
asset = context.data["folderPath"]
asset_name = asset.split("/")[-1]
folder_path = context.data["folderPath"]
folder_name = folder_path.split("/")[-1]
active_timeline = hiero.ui.activeSequence()
project = active_timeline.project()
@ -62,12 +62,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
product_type = "workfile"
instance_data = {
"label": "{} - {}Main".format(
asset, product_type),
"name": "{}_{}".format(asset_name, product_type),
"folderPath": context.data["folderPath"],
folder_path, product_type),
"name": "{}_{}".format(folder_name, product_type),
"folderPath": folder_path,
# TODO use 'get_product_name'
"productName": "{}{}Main".format(
asset_name, product_type.capitalize()
folder_name, product_type.capitalize()
),
"item": project,
"productType": product_type,

View file

@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs):
pane = stateutils.activePane(kwargs)
if isinstance(pane, hou.NetworkEditor):
pwd = pane.pwd()
project_name = context.get_current_project_name(),
project_name = context.get_current_project_name()
folder_path = context.get_current_folder_path()
task_name = context.get_current_task_name()
folder_entity = ayon_api.get_folder_by_path(

View file

@ -22,7 +22,7 @@ from ayon_core.pipeline import (
)
from ayon_core.pipeline.create import CreateContext
from ayon_core.pipeline.template_data import get_template_data
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup
from ayon_core.tools.utils.host_tools import get_tool_by_name
@ -39,7 +39,7 @@ def get_folder_fps(folder_entity=None):
"""Return current folder fps."""
if folder_entity is None:
folder_entity = get_current_project_folder(fields=["attrib.fps"])
folder_entity = get_current_folder_entity(fields=["attrib.fps"])
return folder_entity["attrib"]["fps"]
@ -243,7 +243,10 @@ def render_rop(ropnode):
try:
ropnode.render(verbose=verbose,
# Allow Deadline to capture completion percentage
output_progress=verbose)
output_progress=verbose,
# Render only this node
# (do not render any of its dependencies)
ignore_inputs=True)
except hou.Error as exc:
# The hou.Error is not inherited from a Python Exception class,
# so we explicitly capture the houdini error, otherwise pyblish
@ -760,7 +763,7 @@ def set_camera_resolution(camera, folder_entity=None):
"""Apply resolution to camera from folder entity of the publish"""
if not folder_entity:
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
resolution = get_resolution_from_folder(folder_entity)
@ -970,7 +973,7 @@ def self_publish():
Firstly, it gets the node and its dependencies.
Then, it deactivates all other ROPs
And finaly, it triggers the publishing action.
And finally, it triggers the publishing action.
"""
result, comment = hou.ui.readInput(
@ -1020,6 +1023,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()
@ -1098,4 +1177,4 @@ def prompt_reset_context():
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()
dialog.deleteLater()

View file

@ -39,7 +39,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save
ABOUT_TO_SAVE = False
_about_to_save = False
class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
@ -292,8 +292,8 @@ def ls():
def before_workfile_save(event):
global ABOUT_TO_SAVE
ABOUT_TO_SAVE = True
global _about_to_save
_about_to_save = True
def before_save():
@ -307,18 +307,14 @@ def on_save():
# update houdini vars
lib.update_houdini_vars_context_dialog()
nodes = lib.get_id_required_nodes()
for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False)
# We are now starting the actual save directly
global ABOUT_TO_SAVE
ABOUT_TO_SAVE = False
global _about_to_save
_about_to_save = False
def on_task_changed():
global ABOUT_TO_SAVE
if not IS_HEADLESS and ABOUT_TO_SAVE:
global _about_to_save
if not IS_HEADLESS and _about_to_save:
# Let's prompt the user to update the context settings or not
lib.prompt_reset_context()

View file

@ -1,4 +1,4 @@
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class SetPath(PreLaunchHook):

View file

@ -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 = {

View file

@ -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"))

View file

@ -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"))

View file

@ -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"))

View file

@ -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(

View file

@ -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

View file

@ -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"))

View file

@ -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"))

View file

@ -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"))

View file

@ -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
@ -23,7 +22,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"))

View file

@ -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
from ayon_core.lib import BoolDef, EnumDef
import hou
@ -57,7 +56,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"))

View file

@ -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(

View file

@ -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"))

View file

@ -3,7 +3,7 @@ from ayon_core.hosts.houdini.api.lib import (
get_camera_from_container,
set_camera_resolution
)
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
class SetCameraResolution(InventoryAction):
@ -19,7 +19,7 @@ class SetCameraResolution(InventoryAction):
)
def process(self, containers):
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
for container in containers:
node = container["node"]
camera = get_camera_from_container(node)

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -89,7 +89,7 @@ class CameraLoader(load.LoaderPlugin):
product_types = {"camera"}
label = "Load Camera (abc)"
representations = ["abc"]
representations = {"abc"}
order = -10
icon = "code-fork"

View file

@ -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):

View file

@ -2,7 +2,7 @@ import os
import re
from ayon_core.pipeline import load
from openpype.hosts.houdini.api import pipeline
from ayon_core.hosts.houdini.api import pipeline
import hou
@ -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):
@ -103,8 +103,8 @@ class FilePathLoader(load.LoaderPlugin):
parm)
node.setParmTemplateGroup(parm_template_group)
def switch(self, container, representation):
self.update(container, representation)
def switch(self, container, context):
self.update(container, context)
def remove(self, container):

View file

@ -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"

View file

@ -54,7 +54,7 @@ class ImageLoader(load.LoaderPlugin):
"online",
}
label = "Load Image (COP2)"
representations = ["*"]
representations = {"*"}
order = -10
icon = "code-fork"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

Some files were not shown because too many files have changed in this diff Show more