Merge branch 'develop' into enhancement/OP-8218_Loader-OTIO-export-action

This commit is contained in:
Jakub Ježek 2024-05-06 14:30:54 +02:00 committed by GitHub
commit 996a491692
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
370 changed files with 7440 additions and 3591 deletions

View file

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

View file

@ -176,9 +176,9 @@ def run(script):
# future versions might remove it.
first_arg = sys.argv[0]
if is_running_from_build():
comp_path = os.path.join(os.environ["AYON_ROOT"], "start.py")
else:
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)

View file

@ -8,14 +8,11 @@ from ayon_core.lib import Logger, register_event_callback
from ayon_core.pipeline import (
register_loader_plugin_path,
register_creator_plugin_path,
register_workfile_build_plugin_path,
AVALON_CONTAINER_ID,
AVALON_INSTANCE_ID,
AYON_INSTANCE_ID,
)
from ayon_core.hosts.aftereffects.api.workfile_template_builder import (
AEPlaceholderLoadPlugin,
AEPlaceholderCreatePlugin
)
from ayon_core.pipeline.load import any_outdated_containers
import ayon_core.hosts.aftereffects
@ -40,6 +37,7 @@ PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
WORKFILE_BUILD_PATH = os.path.join(PLUGINS_DIR, "workfile_build")
class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
@ -76,6 +74,7 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
register_workfile_build_plugin_path(WORKFILE_BUILD_PATH)
register_event_callback("application.launched", application_launch)
@ -118,12 +117,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
item["id"] = "publish_context"
self.stub.imprint(item["id"], item)
def get_workfile_build_placeholder_plugins(self):
return [
AEPlaceholderLoadPlugin,
AEPlaceholderCreatePlugin
]
# created instances section
def list_instances(self):
"""List all created instances from current workfile which

View file

@ -1,6 +1,7 @@
import os.path
import uuid
import shutil
from abc import abstractmethod
from ayon_core.pipeline import registered_host
from ayon_core.tools.workfile_template_build import (
@ -9,13 +10,9 @@ from ayon_core.tools.workfile_template_build import (
from ayon_core.pipeline.workfile.workfile_template_builder import (
AbstractTemplateBuilder,
PlaceholderPlugin,
LoadPlaceholderItem,
CreatePlaceholderItem,
PlaceholderLoadMixin,
PlaceholderCreateMixin
PlaceholderItem
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_core.hosts.aftereffects.api.lib import set_settings
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
PLACEHOLDER_ID = "openpype.placeholder"
@ -51,6 +48,10 @@ class AETemplateBuilder(AbstractTemplateBuilder):
class AEPlaceholderPlugin(PlaceholderPlugin):
"""Contains generic methods for all PlaceholderPlugins."""
@abstractmethod
def _create_placeholder_item(self, item_data: dict) -> PlaceholderItem:
pass
def collect_placeholders(self):
"""Collect info from file metadata about created placeholders.
@ -63,17 +64,7 @@ class AEPlaceholderPlugin(PlaceholderPlugin):
if item.get("plugin_identifier") != self.identifier:
continue
if isinstance(self, AEPlaceholderLoadPlugin):
item = LoadPlaceholderItem(item["uuid"],
item["data"],
self)
elif isinstance(self, AEPlaceholderCreatePlugin):
item = CreatePlaceholderItem(item["uuid"],
item["data"],
self)
else:
raise NotImplementedError(f"Not implemented for {type(self)}")
item = self._create_placeholder_item(item)
output.append(item)
return output
@ -135,87 +126,6 @@ class AEPlaceholderPlugin(PlaceholderPlugin):
stub.imprint(item_id, container_data)
class AEPlaceholderCreatePlugin(AEPlaceholderPlugin, PlaceholderCreateMixin):
"""Adds Create placeholder.
This adds composition and runs Create
"""
identifier = "aftereffects.create"
label = "AfterEffects create"
def create_placeholder(self, placeholder_data):
stub = get_stub()
name = "CREATEPLACEHOLDER"
item_id = stub.add_item(name, "COMP")
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Replace 'placeholder' with publishable instance.
Renames prepared composition name, creates publishable instance, sets
frame/duration settings according to DB.
"""
pre_create_data = {"use_selection": True}
item_id, item = self._get_item(placeholder)
get_stub().select_items([item_id])
self.populate_create_placeholder(placeholder, pre_create_data)
# apply settings for populated composition
item_id, metadata_item = self._get_item(placeholder)
set_settings(True, True, [item_id])
def get_placeholder_options(self, options=None):
return self.get_create_plugin_options(options)
class AEPlaceholderLoadPlugin(AEPlaceholderPlugin, PlaceholderLoadMixin):
identifier = "aftereffects.load"
label = "AfterEffects load"
def create_placeholder(self, placeholder_data):
"""Creates AE's Placeholder item in Project items list.
Sets dummy resolution/duration/fps settings, will be replaced when
populated.
"""
stub = get_stub()
name = "LOADERPLACEHOLDER"
item_id = stub.add_placeholder(name, 1920, 1060, 25, 10)
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Use Openpype Loader from `placeholder` to create new FootageItems
New FootageItems are created, files are imported.
"""
self.populate_load_placeholder(placeholder)
errors = placeholder.get_errors()
stub = get_stub()
if errors:
stub.print_msg("\n".join(errors))
else:
if not placeholder.data["keep_placeholder"]:
metadata = stub.get_metadata()
for item in metadata:
if not item.get("is_placeholder"):
continue
scene_identifier = item.get("uuid")
if (scene_identifier and
scene_identifier == placeholder.scene_identifier):
stub.delete_item(item["members"][0])
stub.remove_instance(placeholder.scene_identifier, metadata)
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def load_succeed(self, placeholder, container):
placeholder_item_id, _ = self._get_item(placeholder)
item_id = container.id
get_stub().add_item_instead_placeholder(placeholder_item_id, item_id)
def build_workfile_template(*args, **kwargs):
builder = AETemplateBuilder(registered_host())
builder.build_template(*args, **kwargs)

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

@ -0,0 +1,49 @@
from ayon_core.pipeline.workfile.workfile_template_builder import (
CreatePlaceholderItem,
PlaceholderCreateMixin
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_core.hosts.aftereffects.api.lib import set_settings
import ayon_core.hosts.aftereffects.api.workfile_template_builder as wtb
class AEPlaceholderCreatePlugin(wtb.AEPlaceholderPlugin,
PlaceholderCreateMixin):
"""Adds Create placeholder.
This adds composition and runs Create
"""
identifier = "aftereffects.create"
label = "AfterEffects create"
def _create_placeholder_item(self, item_data) -> CreatePlaceholderItem:
return CreatePlaceholderItem(
scene_identifier=item_data["uuid"],
data=item_data["data"],
plugin=self
)
def create_placeholder(self, placeholder_data):
stub = get_stub()
name = "CREATEPLACEHOLDER"
item_id = stub.add_item(name, "COMP")
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Replace 'placeholder' with publishable instance.
Renames prepared composition name, creates publishable instance, sets
frame/duration settings according to DB.
"""
pre_create_data = {"use_selection": True}
item_id, item = self._get_item(placeholder)
get_stub().select_items([item_id])
self.populate_create_placeholder(placeholder, pre_create_data)
# apply settings for populated composition
item_id, metadata_item = self._get_item(placeholder)
set_settings(True, True, [item_id])
def get_placeholder_options(self, options=None):
return self.get_create_plugin_options(options)

View file

@ -0,0 +1,60 @@
from ayon_core.pipeline.workfile.workfile_template_builder import (
LoadPlaceholderItem,
PlaceholderLoadMixin
)
from ayon_core.hosts.aftereffects.api import get_stub
import ayon_core.hosts.aftereffects.api.workfile_template_builder as wtb
class AEPlaceholderLoadPlugin(wtb.AEPlaceholderPlugin, PlaceholderLoadMixin):
identifier = "aftereffects.load"
label = "AfterEffects load"
def _create_placeholder_item(self, item_data) -> LoadPlaceholderItem:
return LoadPlaceholderItem(
scene_identifier=item_data["uuid"],
data=item_data["data"],
plugin=self
)
def create_placeholder(self, placeholder_data):
"""Creates AE's Placeholder item in Project items list.
Sets dummy resolution/duration/fps settings, will be replaced when
populated.
"""
stub = get_stub()
name = "LOADERPLACEHOLDER"
item_id = stub.add_placeholder(name, 1920, 1060, 25, 10)
self._imprint_item(item_id, name, placeholder_data, stub)
def populate_placeholder(self, placeholder):
"""Use Openpype Loader from `placeholder` to create new FootageItems
New FootageItems are created, files are imported.
"""
self.populate_load_placeholder(placeholder)
errors = placeholder.get_errors()
stub = get_stub()
if errors:
stub.print_msg("\n".join(errors))
else:
if not placeholder.data["keep_placeholder"]:
metadata = stub.get_metadata()
for item in metadata:
if not item.get("is_placeholder"):
continue
scene_identifier = item.get("uuid")
if (scene_identifier and
scene_identifier == placeholder.scene_identifier):
stub.delete_item(item["members"][0])
stub.remove_instance(placeholder.scene_identifier, metadata)
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def load_succeed(self, placeholder, container):
placeholder_item_id, _ = self._get_item(placeholder)
item_id = container.id
get_stub().add_item_instead_placeholder(placeholder_item_id, item_id)

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

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

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

@ -2,6 +2,7 @@ import os
import bpy
from ayon_core.lib import BoolDef
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
@ -17,6 +18,8 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
if not self.is_active(instance.data):
return
attr_values = self.get_attr_values_from_data(instance.data)
# Define extract output file path
stagingdir = self.staging_dir(instance)
folder_name = instance.data["folderEntity"]["name"]
@ -46,7 +49,8 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
bpy.ops.wm.alembic_export(
filepath=filepath,
selected=True,
flatten=False
flatten=False,
subdiv_schema=attr_values.get("subdiv_schema", False)
)
plugin.deselect_all()
@ -65,6 +69,21 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
self.log.debug("Extracted instance '%s' to: %s",
instance.name, representation)
@classmethod
def get_attribute_defs(cls):
return [
BoolDef(
"subdiv_schema",
label="Alembic Mesh Subdiv Schema",
tooltip="Export Meshes using Alembic's subdivision schema.\n"
"Enabling this includes creases with the export but "
"excludes the mesh's normals.\n"
"Enabling this usually result in smaller file size "
"due to lack of normals.",
default=False
)
]
class ExtractModelABC(ExtractABC):
"""Extract model as ABC."""

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,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
@ -169,7 +169,7 @@ def validate_comp_prefs(comp=None, force_repair=False):
def _on_repair():
attributes = dict()
for key, comp_key, _label in validations:
value = folder_value[key]
value = folder_attributes[key]
comp_key_full = "Comp.FrameFormat.{}".format(comp_key)
attributes[comp_key_full] = value
comp.SetPrefs(attributes)
@ -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(

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib import PreLaunchHook
from ayon_applications import PreLaunchHook
from ayon_core.hosts.fusion import FUSION_HOST_DIR

View file

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

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

@ -8,6 +8,7 @@ from .lib import (
sync_avalon_data_to_workfile,
launch_workfiles_app,
before_project_save,
apply_colorspace_project
)
from .tags import add_tags_to_workfile
from .menu import update_menu_task_label
@ -44,6 +45,8 @@ def afterNewProjectCreated(event):
# reset workfiles startup not to open any more in session
os.environ["WORKFILES_STARTUP"] = "0"
apply_colorspace_project()
def beforeProjectLoad(event):
log.info("before project load event...")
@ -122,6 +125,7 @@ def register_hiero_events():
except RuntimeError:
pass
def register_events():
"""
Adding all callbacks.

View file

@ -11,7 +11,6 @@ import warnings
import json
import ast
import secrets
import shutil
import hiero
from qtpy import QtWidgets, QtCore
@ -36,9 +35,6 @@ from .constants import (
DEFAULT_SEQUENCE_NAME,
DEFAULT_BIN_NAME
)
from ayon_core.pipeline.colorspace import (
get_imageio_config
)
class _CTX:
@ -105,9 +101,9 @@ def flatten(list_):
def get_current_project(remove_untitled=False):
projects = flatten(hiero.core.projects())
projects = hiero.core.projects()
if not remove_untitled:
return next(iter(projects))
return projects[0]
# if remove_untitled
for proj in projects:
@ -1050,18 +1046,68 @@ def _set_hrox_project_knobs(doc, **knobs):
def apply_colorspace_project():
project_name = get_current_project_name()
# get path the the active projects
project = get_current_project(remove_untitled=True)
current_file = project.path()
# close the active project
project.close()
"""Apply colorspaces from settings.
Due to not being able to set the project settings through the Python API,
we need to do use some dubious code to find the widgets and set them. It is
possible to set the project settings without traversing through the widgets
but it involves reading the hrox files from disk with XML, so no in-memory
support. See https://community.foundry.com/discuss/topic/137771/change-a-project-s-default-color-transform-with-python # noqa
for more details.
"""
# get presets for hiero
project_name = get_current_project_name()
imageio = get_project_settings(project_name)["hiero"]["imageio"]
presets = imageio.get("workfile")
# Open Project Settings UI.
for act in hiero.ui.registeredActions():
if act.objectName() == "foundry.project.settings":
act.trigger()
# Find widgets from their sibling label.
labels = {
"Working Space:": "workingSpace",
"Viewer:": "viewerLut",
"Thumbnails:": "thumbnailLut",
"Monitor Out:": "monitorOutLut",
"8 Bit Files:": "eightBitLut",
"16 Bit Files:": "sixteenBitLut",
"Log Files:": "logLut",
"Floating Point Files:": "floatLut"
}
widgets = {x: None for x in labels.values()}
def _recursive_children(widget, labels, widgets):
children = widget.children()
for count, child in enumerate(children):
if isinstance(child, QtWidgets.QLabel):
if child.text() in labels.keys():
widgets[labels[child.text()]] = children[count + 1]
_recursive_children(child, labels, widgets)
app = QtWidgets.QApplication.instance()
title = "Project Settings"
for widget in app.topLevelWidgets():
if isinstance(widget, QtWidgets.QMainWindow):
if widget.windowTitle() != title:
continue
_recursive_children(widget, labels, widgets)
widget.close()
msg = "Setting value \"{}\" is not a valid option for \"{}\""
for key, widget in widgets.items():
options = [widget.itemText(i) for i in range(widget.count())]
setting_value = presets[key]
assert setting_value in options, msg.format(setting_value, key)
widget.setCurrentText(presets[key])
# This code block is for setting up project colorspaces for files on disk.
# Due to not having Python API access to set the project settings, the
# Foundry recommended way is to modify the hrox files on disk with XML. See
# this forum thread for more details;
# https://community.foundry.com/discuss/topic/137771/change-a-project-s-default-color-transform-with-python # noqa
'''
# backward compatibility layer
# TODO: remove this after some time
config_data = get_imageio_config(
@ -1074,6 +1120,13 @@ def apply_colorspace_project():
"ocioConfigName": "custom"
})
# get path the the active projects
project = get_current_project()
current_file = project.path()
msg = "The project needs to be saved to disk to apply colorspace settings."
assert current_file, msg
# save the workfile as subversion "comment:_colorspaceChange"
split_current_file = os.path.splitext(current_file)
copy_current_file = current_file
@ -1116,6 +1169,7 @@ def apply_colorspace_project():
# open the file as current project
hiero.core.openProject(copy_current_file)
'''
def apply_colorspace_clips():
@ -1125,10 +1179,8 @@ def apply_colorspace_clips():
# get presets for hiero
imageio = get_project_settings(project_name)["hiero"]["imageio"]
from pprint import pprint
presets = imageio.get("regexInputs", {}).get("inputs", {})
pprint(presets)
for clip in clips:
clip_media_source_path = clip.mediaSource().firstpath()
clip_name = clip.name()

View file

@ -144,7 +144,7 @@ def add_tags_to_workfile():
# Get project task types.
project_name = get_current_project_name()
project_entity = ayon_api.get_project(project_name)
task_types = project_entity["taskType"]
task_types = project_entity["taskTypes"]
nks_pres_tags["[Tasks]"] = {}
log.debug("__ tasks: {}".format(task_types))
for task_type in task_types:

View file

@ -51,13 +51,12 @@ def open_file(filepath):
project = hiero.core.projects()[-1]
# open project file
hiero.core.openProject(filepath.replace(os.path.sep, "/"))
# close previous project
project.close()
# Close previous project if its different to the current project.
filepath = filepath.replace(os.path.sep, "/")
if project.path().replace(os.path.sep, "/") != filepath:
# open project file
hiero.core.openProject(filepath)
project.close()
return True

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

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

View file

@ -811,6 +811,43 @@ def get_current_context_template_data_with_folder_attrs():
return template_data
def set_review_color_space(opengl_node, review_color_space="", log=None):
"""Set ociocolorspace parameter for the given OpenGL node.
Set `ociocolorspace` parameter of the given OpenGl node
to to the given review_color_space value.
If review_color_space is empty, a default colorspace corresponding to
the display & view of the current Houdini session will be used.
Args:
opengl_node (hou.Node): ROP node to set its ociocolorspace parm.
review_color_space (str): Colorspace value for ociocolorspace parm.
log (logging.Logger): Logger to log to.
"""
if log is None:
log = self.log
# Set Color Correction parameter to OpenColorIO
colorcorrect_parm = opengl_node.parm("colorcorrect")
if colorcorrect_parm.eval() != 2:
colorcorrect_parm.set(2)
log.debug(
"'Color Correction' parm on '{}' has been set to"
" 'OpenColorIO'".format(opengl_node.path())
)
opengl_node.setParms(
{"ociocolorspace": review_color_space}
)
log.debug(
"'OCIO Colorspace' parm on '{}' has been set to "
"the view color space '{}'"
.format(opengl_node, review_color_space)
)
def get_context_var_changes():
"""get context var changes."""
@ -1001,6 +1038,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()

View file

@ -0,0 +1,64 @@
from ayon_applications import PreLaunchHook, LaunchTypes
class SetDefaultDisplayView(PreLaunchHook):
"""Set default view and default display for houdini via OpenColorIO.
Houdini's defaultDisplay and defaultView are set by
setting 'OCIO_ACTIVE_DISPLAYS' and 'OCIO_ACTIVE_VIEWS'
environment variables respectively.
More info: https://www.sidefx.com/docs/houdini/io/ocio.html#set-up
"""
app_groups = {"houdini"}
launch_types = {LaunchTypes.local}
def execute(self):
OCIO = self.launch_context.env.get("OCIO")
# This is a cheap way to skip this hook if either global color
# management or houdini color management was disabled because the
# OCIO var would be set by the global OCIOEnvHook
if not OCIO:
return
# workfile settings added in '0.2.13'
houdini_color_settings = \
self.data["project_settings"]["houdini"]["imageio"].get("workfile")
if not houdini_color_settings:
self.log.info("Hook 'SetDefaultDisplayView' requires Houdini "
"addon version >= '0.2.13'")
return
if not houdini_color_settings["enabled"]:
self.log.info(
"Houdini workfile color management is disabled."
)
return
# 'OCIO_ACTIVE_DISPLAYS', 'OCIO_ACTIVE_VIEWS' are checked
# as Admins can add them in Ayon env vars or Ayon tools.
default_display = houdini_color_settings["default_display"]
if default_display:
# get 'OCIO_ACTIVE_DISPLAYS' value if exists.
self._set_context_env("OCIO_ACTIVE_DISPLAYS", default_display)
default_view = houdini_color_settings["default_view"]
if default_view:
# get 'OCIO_ACTIVE_VIEWS' value if exists.
self._set_context_env("OCIO_ACTIVE_VIEWS", default_view)
def _set_context_env(self, env_var, default_value):
env_value = self.launch_context.env.get(env_var, "")
new_value = ":".join(
key for key in [default_value, env_value] if key
)
self.log.info(
"Setting {} environment to: {}"
.format(env_var, new_value)
)
self.launch_context.env[env_var] = new_value

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,6 +1,6 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating openGL reviews."""
from ayon_core.hosts.houdini.api import plugin
from ayon_core.hosts.houdini.api import lib, plugin
from ayon_core.lib import EnumDef, BoolDef, NumberDef
import os
@ -14,6 +14,16 @@ class CreateReview(plugin.HoudiniCreator):
label = "Review"
product_type = "review"
icon = "video-camera"
review_color_space = ""
def apply_settings(self, project_settings):
super(CreateReview, self).apply_settings(project_settings)
# workfile settings added in '0.2.13'
color_settings = project_settings["houdini"]["imageio"].get(
"workfile", {}
)
if color_settings.get("enabled"):
self.review_color_space = color_settings.get("review_color_space")
def create(self, product_name, instance_data, pre_create_data):
@ -85,10 +95,20 @@ class CreateReview(plugin.HoudiniCreator):
instance_node.setParms(parms)
# Set OCIO Colorspace to the default output colorspace
# Set OCIO Colorspace to the default colorspace
# if there's OCIO
if os.getenv("OCIO"):
self.set_colorcorrect_to_default_view_space(instance_node)
# Fall to the default value if cls.review_color_space is empty.
if not self.review_color_space:
# cls.review_color_space is an empty string
# when the imageio/workfile setting is disabled or
# when the Review colorspace setting is empty.
from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa
self.review_color_space = get_default_display_view_colorspace()
lib.set_review_color_space(instance_node,
self.review_color_space,
self.log)
to_lock = ["id", "productType"]
@ -131,23 +151,3 @@ class CreateReview(plugin.HoudiniCreator):
minimum=0.0001,
decimals=3)
]
def set_colorcorrect_to_default_view_space(self,
instance_node):
"""Set ociocolorspace to the default output space."""
from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa
# set Color Correction parameter to OpenColorIO
instance_node.setParms({"colorcorrect": 2})
# Get default view space for ociocolorspace parm.
default_view_space = get_default_display_view_colorspace()
instance_node.setParms(
{"ociocolorspace": default_view_space}
)
self.log.debug(
"'OCIO Colorspace' parm on '{}' has been set to "
"the default view color space '{}'"
.format(instance_node, default_view_space)
)

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

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

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

@ -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"
@ -45,33 +45,11 @@ class AbcLoader(load.LoaderPlugin):
alembic = container.createNode("alembic", node_name=node_name)
alembic.setParms({"fileName": file_path})
# Add unpack node
unpack_name = "unpack_{}".format(name)
unpack = container.createNode("unpack", node_name=unpack_name)
unpack.setInput(0, alembic)
unpack.setParms({"transfer_attributes": "path"})
# Position nodes nicely
container.moveToGoodPosition()
container.layoutChildren()
# Add normal to points
# Order of menu ['point', 'vertex', 'prim', 'detail']
normal_name = "normal_{}".format(name)
normal_node = container.createNode("normal", node_name=normal_name)
normal_node.setParms({"type": 0})
normal_node.setInput(0, unpack)
null = container.createNode("null", node_name="OUT")
null.setInput(0, normal_node)
# Ensure display flag is on the Alembic input node and not on the OUT
# node to optimize "debug" displaying in the viewport.
alembic.setDisplayFlag(True)
# Set new position for unpack node else it gets cluttered
nodes = [container, alembic, unpack, normal_node, null]
for nr, node in enumerate(nodes):
node.setPosition([0, (0 - nr)])
self[:] = nodes
nodes = [container, alembic]
return pipeline.containerise(
node_name,

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

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

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"

View file

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

View file

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

View file

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

View file

@ -7,7 +7,8 @@ from ayon_core.hosts.houdini.api.lib import render_rop, splitext
import hou
class ExtractComposite(publish.Extractor):
class ExtractComposite(publish.Extractor,
publish.ColormanagedPyblishPluginMixin):
order = pyblish.api.ExtractorOrder
label = "Extract Composite (Image Sequence)"
@ -45,8 +46,14 @@ class ExtractComposite(publish.Extractor):
"frameEnd": instance.data["frameEndHandle"],
}
from pprint import pformat
self.log.info(pformat(representation))
if ext.lower() == "exr":
# Inject colorspace with 'scene_linear' as that's the
# default Houdini working colorspace and all extracted
# OpenEXR images should be in that colorspace.
# https://www.sidefx.com/docs/houdini/render/linear.html#image-formats
self.set_representation_colorspace(
representation, instance.context,
colorspace="scene_linear"
)
instance.data["representations"].append(representation)

View file

@ -8,7 +8,8 @@ from ayon_core.hosts.houdini.api.lib import render_rop
import hou
class ExtractOpenGL(publish.Extractor):
class ExtractOpenGL(publish.Extractor,
publish.ColormanagedPyblishPluginMixin):
order = pyblish.api.ExtractorOrder - 0.01
label = "Extract OpenGL"
@ -46,6 +47,14 @@ class ExtractOpenGL(publish.Extractor):
"camera_name": instance.data.get("review_camera")
}
if ropnode.evalParm("colorcorrect") == 2: # OpenColorIO enabled
colorspace = ropnode.evalParm("ociocolorspace")
# inject colorspace data
self.set_representation_colorspace(
representation, instance.context,
colorspace=colorspace
)
if "representations" not in instance.data:
instance.data["representations"] = []
instance.data["representations"].append(representation)

View file

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

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import sys
import hou
import pyblish.api
import six
from ayon_core.pipeline import PublishValidationError
@ -26,28 +25,21 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
("Output node(s) `{}` are incorrect. "
"See plug-in log for details.").format(invalid),
title=self.label
"Output node '{}' is incorrect. "
"See plug-in log for details.".format(invalid),
title=self.label,
description=(
"### Invalid COP output node\n\n"
"The output node path for the instance must be set to a "
"valid COP node path.\n\nSee the log for more details."
)
)
@classmethod
def get_invalid(cls, instance):
output_node = instance.data.get("output_node")
import hou
try:
output_node = instance.data["output_node"]
except KeyError:
six.reraise(
PublishValidationError,
PublishValidationError(
"Can't determine COP output node.",
title=cls.__name__),
sys.exc_info()[2]
)
if output_node is None:
if not output_node:
node = hou.node(instance.data.get("instance_node"))
cls.log.error(
"COP Output node in '%s' does not exist. "
@ -61,8 +53,8 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
cls.log.error(
"Output node %s is not a COP node. "
"COP Path must point to a COP node, "
"instead found category type: %s"
% (output_node.path(), output_node.type().category().name())
"instead found category type: %s",
output_node.path(), output_node.type().category().name()
)
return [output_node.path()]
@ -70,9 +62,7 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
# is Cop2 to avoid potential edge case scenarios even though
# the isinstance check above should be stricter than this category
if output_node.type().category().name() != "Cop2":
raise PublishValidationError(
(
"Output node {} is not of category Cop2."
" This is a bug..."
).format(output_node.path()),
title=cls.label)
cls.log.error(
"Output node %s is not of category Cop2.", output_node.path()
)
return [output_node.path()]

View file

@ -4,15 +4,19 @@ from ayon_core.pipeline import (
PublishValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.pipeline.publish import RepairAction
from ayon_core.pipeline.publish import (
RepairAction,
get_plugin_settings,
apply_plugin_settings_automatically
)
from ayon_core.hosts.houdini.api.action import SelectROPAction
import os
import hou
class SetDefaultViewSpaceAction(RepairAction):
label = "Set default view colorspace"
class ResetViewSpaceAction(RepairAction):
label = "Reset OCIO colorspace parm"
icon = "mdi.monitor"
@ -27,9 +31,28 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin,
families = ["review"]
hosts = ["houdini"]
label = "Validate Review Colorspace"
actions = [SetDefaultViewSpaceAction, SelectROPAction]
actions = [ResetViewSpaceAction, SelectROPAction]
optional = True
review_color_space = ""
@classmethod
def apply_settings(cls, project_settings):
# Preserve automatic settings applying logic
settings = get_plugin_settings(plugin=cls,
project_settings=project_settings,
log=cls.log,
category="houdini")
apply_plugin_settings_automatically(cls, settings, logger=cls.log)
# workfile settings added in '0.2.13'
color_settings = project_settings["houdini"]["imageio"].get(
"workfile", {}
)
# Add review color settings
if color_settings.get("enabled"):
cls.review_color_space = color_settings.get("review_color_space")
def process(self, instance):
@ -52,39 +75,54 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin,
" 'OpenColorIO'".format(rop_node.path())
)
if rop_node.evalParm("ociocolorspace") not in \
hou.Color.ocio_spaces():
current_color_space = rop_node.evalParm("ociocolorspace")
if current_color_space not in hou.Color.ocio_spaces():
raise PublishValidationError(
"Invalid value: Colorspace name doesn't exist.\n"
"Check 'OCIO Colorspace' parameter on '{}' ROP"
.format(rop_node.path())
)
@classmethod
def repair(cls, instance):
"""Set Default View Space Action.
# if houdini/imageio/workfile is enabled and
# Review colorspace setting is empty then this check should
# actually check if the current_color_space setting equals
# the default colorspace value.
# However, it will make the black cmd screen show up more often
# which is very annoying.
if self.review_color_space and \
self.review_color_space != current_color_space:
It is a helper action more than a repair action,
used to set colorspace on opengl node to the default view.
"""
from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa
rop_node = hou.node(instance.data["instance_node"])
if rop_node.evalParm("colorcorrect") != 2:
rop_node.setParms({"colorcorrect": 2})
cls.log.debug(
"'Color Correction' parm on '{}' has been set to"
" 'OpenColorIO'".format(rop_node.path())
raise PublishValidationError(
"Invalid value: Colorspace name doesn't match"
"the Colorspace specified in settings."
)
# Get default view colorspace name
default_view_space = get_default_display_view_colorspace()
@classmethod
def repair(cls, instance):
"""Reset view colorspace.
rop_node.setParms({"ociocolorspace": default_view_space})
cls.log.info(
"'OCIO Colorspace' parm on '{}' has been set to "
"the default view color space '{}'"
.format(rop_node, default_view_space)
)
It is used to set colorspace on opengl node.
It uses the colorspace value specified in the Houdini addon settings.
If the value in the Houdini addon settings is empty,
it will fall to the default colorspace.
Note:
This repair action assumes that OCIO is enabled.
As if OCIO is disabled the whole validation is skipped
and this repair action won't show up.
"""
from ayon_core.hosts.houdini.api.lib import set_review_color_space
# Fall to the default value if cls.review_color_space is empty.
if not cls.review_color_space:
# cls.review_color_space is an empty string
# when the imageio/workfile setting is disabled or
# when the Review colorspace setting is empty.
from ayon_core.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa
cls.review_color_space = get_default_display_view_colorspace()
rop_node = hou.node(instance.data["instance_node"])
set_review_color_space(rop_node,
cls.review_color_space,
cls.log)

View file

@ -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()
@ -502,9 +496,9 @@ def object_transform_set(container_children):
"""
transform_set = {}
for node in container_children:
name = f"{node.name}.transform"
name = f"{node}.transform"
transform_set[name] = node.pos
name = f"{node.name}.scale"
name = f"{node}.scale"
transform_set[name] = node.scale
return transform_set
@ -525,6 +519,36 @@ def get_plugins() -> list:
return plugin_info_list
def update_modifier_node_names(event, node):
"""Update the name of the nodes after renaming
Args:
event (pymxs.MXSWrapperBase): Event Name (
Mandatory argument for rt.NodeEventCallback)
node (list): Event Number (
Mandatory argument for rt.NodeEventCallback)
"""
containers = [
obj
for obj in rt.Objects
if (
rt.ClassOf(obj) == rt.Container
and rt.getUserProp(obj, "id") == "pyblish.avalon.instance"
and rt.getUserProp(obj, "productType") not in {
"workfile", "tyflow"
}
)
]
if not containers:
return
for container in containers:
ayon_data = container.modifiers[0].openPypeData
updated_node_names = [str(node.node) for node
in ayon_data.all_handles]
rt.setProperty(ayon_data, "sel_list", updated_node_names)
@contextlib.contextmanager
def render_resolution(width, height):
"""Set render resolution option during context

View file

@ -63,6 +63,8 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
rt.callbacks.addScript(rt.Name('postWorkspaceChange'),
self._deferred_menu_creation)
rt.NodeEventCallback(
nameChanged=lib.update_modifier_node_names)
def workfile_has_unsaved_changes(self):
return rt.getSaveRequired()

View file

@ -19,7 +19,7 @@ class FbxLoader(load.LoaderPlugin):
"""Fbx Loader."""
product_types = {"camera"}
representations = ["fbx"]
representations = {"fbx"}
order = -9
icon = "code-fork"
color = "white"

View file

@ -78,7 +78,7 @@ class MaxSceneLoader(load.LoaderPlugin):
"model",
}
representations = ["max"]
representations = {"max"}
order = -8
icon = "code-fork"
color = "green"
@ -117,7 +117,7 @@ class MaxSceneLoader(load.LoaderPlugin):
)
for max_obj, obj_name in zip(max_objects, max_object_names):
max_obj.name = f"{namespace}:{obj_name}"
max_container.append(rt.getNodeByName(max_obj.name))
max_container.append(max_obj)
return containerise(
name, max_container, context,
namespace, loader=self.__class__.__name__)
@ -158,11 +158,11 @@ class MaxSceneLoader(load.LoaderPlugin):
current_max_object_names):
max_obj.name = f"{namespace}:{obj_name}"
max_objects.append(max_obj)
max_transform = f"{max_obj.name}.transform"
max_transform = f"{max_obj}.transform"
if max_transform in transform_data.keys():
max_obj.pos = transform_data[max_transform] or 0
max_obj.scale = transform_data[
f"{max_obj.name}.scale"] or 0
f"{max_obj}.scale"] or 0
update_custom_attribute_data(node, max_objects)
lib.imprint(container["instance_node"], {

View file

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

View file

@ -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"
@ -76,11 +76,11 @@ class FbxModelLoader(load.LoaderPlugin):
for fbx_object in current_fbx_objects:
fbx_object.name = f"{namespace}:{fbx_object.name}"
fbx_objects.append(fbx_object)
fbx_transform = f"{fbx_object.name}.transform"
fbx_transform = f"{fbx_object}.transform"
if fbx_transform in transform_data.keys():
fbx_object.pos = transform_data[fbx_transform] or 0
fbx_object.scale = transform_data[
f"{fbx_object.name}.scale"] or 0
f"{fbx_object}.scale"] or 0
with maintained_selection():
rt.Select(node)

View file

@ -20,7 +20,7 @@ class ObjLoader(load.LoaderPlugin):
"""Obj Loader."""
product_types = {"model"}
representations = ["obj"]
representations = {"obj"}
order = -9
icon = "code-fork"
color = "white"
@ -67,11 +67,11 @@ class ObjLoader(load.LoaderPlugin):
selections = rt.GetCurrentSelection()
for selection in selections:
selection.name = f"{namespace}:{selection.name}"
selection_transform = f"{selection.name}.transform"
selection_transform = f"{selection}.transform"
if selection_transform in transform_data.keys():
selection.pos = transform_data[selection_transform] or 0
selection.scale = transform_data[
f"{selection.name}.scale"] or 0
f"{selection}.scale"] or 0
update_custom_attribute_data(node, selections)
with maintained_selection():
rt.Select(node)

View file

@ -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"
@ -95,11 +95,11 @@ class ModelUSDLoader(load.LoaderPlugin):
for children in asset.Children:
children.name = f"{namespace}:{children.name}"
usd_objects.append(children)
children_transform = f"{children.name}.transform"
children_transform = f"{children}.transform"
if children_transform in transform_data.keys():
children.pos = transform_data[children_transform] or 0
children.scale = transform_data[
f"{children.name}.scale"] or 0
f"{children}.scale"] or 0
asset.name = f"{namespace}:{asset.name}"
usd_objects.append(asset)

View file

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

View file

@ -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"
@ -92,10 +92,10 @@ class OxAbcLoader(load.LoaderPlugin):
abc.Parent = container
abc.name = f"{namespace}:{abc.name}"
ox_abc_objects.append(abc)
ox_transform = f"{abc.name}.transform"
ox_transform = f"{abc}.transform"
if ox_transform in transform_data.keys():
abc.pos = transform_data[ox_transform] or 0
abc.scale = transform_data[f"{abc.name}.scale"] or 0
abc.scale = transform_data[f"{abc}.scale"] or 0
update_custom_attribute_data(node, ox_abc_objects)
lib.imprint(
container["instance_node"],

View file

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

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