Merge branch 'develop' into bugfix/houdini_fix_save_context_data

This commit is contained in:
Ondřej Samohel 2024-03-27 11:58:46 +01:00 committed by GitHub
commit 91a95f4eec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 928 additions and 262 deletions

View file

@ -1,58 +0,0 @@
import os
from ayon_core.lib import get_ayon_launcher_args
from ayon_core.lib.applications import (
get_non_python_host_kwargs,
PreLaunchHook,
LaunchTypes,
)
from ayon_core import AYON_CORE_ROOT
class NonPythonHostHook(PreLaunchHook):
"""Launch arguments preparation.
Non python host implementation do not launch host directly but use
python script which launch the host. For these cases it is necessary to
prepend python (or ayon) executable and script path before application's.
"""
app_groups = {"harmony", "photoshop", "aftereffects"}
order = 20
launch_types = {LaunchTypes.local}
def execute(self):
# Pop executable
executable_path = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
script_path = os.path.join(
AYON_CORE_ROOT,
"scripts",
"non_python_host_launch.py"
)
new_launch_args = get_ayon_launcher_args(
"run", script_path, executable_path
)
# Add workfile path if exists
workfile_path = self.data["last_workfile_path"]
if (
self.data.get("start_last_workfile")
and workfile_path
and os.path.exists(workfile_path)):
new_launch_args.append(workfile_path)
# Append as whole list as these areguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.launch_context.launch_args.extend(remainders)
self.launch_context.kwargs = \
get_non_python_host_kwargs(self.launch_context.kwargs)

View file

@ -1,6 +1,12 @@
from .addon import AfterEffectsAddon
from .addon import (
AFTEREFFECTS_ADDON_ROOT,
AfterEffectsAddon,
get_launch_script_path,
)
__all__ = (
"AFTEREFFECTS_ADDON_ROOT",
"AfterEffectsAddon",
"get_launch_script_path",
)

View file

@ -1,5 +1,9 @@
import os
from ayon_core.addon import AYONAddon, IHostAddon
AFTEREFFECTS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class AfterEffectsAddon(AYONAddon, IHostAddon):
name = "aftereffects"
@ -17,3 +21,16 @@ class AfterEffectsAddon(AYONAddon, IHostAddon):
def get_workfile_extensions(self):
return [".aep"]
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(AFTEREFFECTS_ADDON_ROOT, "hooks")
]
def get_launch_script_path():
return os.path.join(
AFTEREFFECTS_ADDON_ROOT, "api", "launch_script.py"
)

View file

@ -7,7 +7,6 @@ import asyncio
import functools
import traceback
from wsrpc_aiohttp import (
WebSocketRoute,
WebSocketAsync

View file

@ -1,4 +1,4 @@
"""Script wraps launch mechanism of non python host implementations.
"""Script wraps launch mechanism of AfterEffects implementations.
Arguments passed to the script are passed to launch function in host
implementation. In all cases requires host app executable and may contain
@ -8,6 +8,8 @@ workfile or others.
import os
import sys
from ayon_core.hosts.aftereffects.api.launch_logic import main as host_main
# Get current file to locate start point of sys.argv
CURRENT_FILE = os.path.abspath(__file__)
@ -79,26 +81,9 @@ def main(argv):
if after_script_idx is not None:
launch_args = sys_args[after_script_idx:]
host_name = os.environ["AYON_HOST_NAME"].lower()
if host_name == "photoshop":
# TODO refactor launch logic according to AE
from ayon_core.hosts.photoshop.api.lib import main
elif host_name == "aftereffects":
from ayon_core.hosts.aftereffects.api.launch_logic import main
elif host_name == "harmony":
from ayon_core.hosts.harmony.api.lib import main
else:
title = "Unknown host name"
message = (
"BUG: Environment variable AYON_HOST_NAME contains unknown"
" host name \"{}\""
).format(host_name)
show_error_messagebox(title, message)
return
if launch_args:
# Launch host implementation
main(*launch_args)
host_main(*launch_args)
else:
# Show message box
on_invalid_args(after_script_idx is None)

View file

@ -0,0 +1,91 @@
import os
import platform
import subprocess
from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_core.hosts.aftereffects import get_launch_script_path
def get_launch_kwargs(kwargs):
"""Explicit setting of kwargs for Popen for AfterEffects.
Expected behavior
- ayon_console opens window with logs
- ayon has stdout/stderr available for capturing
Args:
kwargs (Union[dict, None]): Current kwargs or None.
"""
if kwargs is None:
kwargs = {}
if platform.system().lower() != "windows":
return kwargs
if is_using_ayon_console():
kwargs.update({
"creationflags": subprocess.CREATE_NEW_CONSOLE
})
else:
kwargs.update({
"creationflags": subprocess.CREATE_NO_WINDOW,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL
})
return kwargs
class AEPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and script path to AE implementation before
AE executable and add last workfile path to launch arguments.
Existence of last workfile is checked. If workfile does not exists tries
to copy templated workfile from predefined path.
"""
app_groups = {"aftereffects"}
order = 20
launch_types = {LaunchTypes.local}
def execute(self):
# Pop executable
executable_path = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
script_path = get_launch_script_path()
new_launch_args = get_ayon_launcher_args(
"run", script_path, executable_path
)
# Add workfile path if exists
workfile_path = self.data["last_workfile_path"]
if (
self.data.get("start_last_workfile")
and workfile_path
and os.path.exists(workfile_path)
):
new_launch_args.append(workfile_path)
# Append as whole list as these arguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.launch_context.launch_args.extend(remainders)
self.launch_context.kwargs = get_launch_kwargs(
self.launch_context.kwargs
)

View file

@ -1,10 +1,12 @@
from .addon import (
HARMONY_HOST_DIR,
HARMONY_ADDON_ROOT,
HarmonyAddon,
get_launch_script_path,
)
__all__ = (
"HARMONY_HOST_DIR",
"HARMONY_ADDON_ROOT",
"HarmonyAddon",
"get_launch_script_path",
)

View file

@ -1,7 +1,7 @@
import os
from ayon_core.addon import AYONAddon, IHostAddon
HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
HARMONY_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class HarmonyAddon(AYONAddon, IHostAddon):
@ -11,10 +11,23 @@ class HarmonyAddon(AYONAddon, IHostAddon):
def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
openharmony_path = os.path.join(
HARMONY_HOST_DIR, "vendor", "OpenHarmony"
HARMONY_ADDON_ROOT, "vendor", "OpenHarmony"
)
# TODO check if is already set? What to do if is already set?
env["LIB_OPENHARMONY_PATH"] = openharmony_path
def get_workfile_extensions(self):
return [".zip"]
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(HARMONY_ADDON_ROOT, "hooks")
]
def get_launch_script_path():
return os.path.join(
HARMONY_ADDON_ROOT, "api", "launch_script.py"
)

View file

@ -0,0 +1,93 @@
"""Script wraps launch mechanism of Harmony implementations.
Arguments passed to the script are passed to launch function in host
implementation. In all cases requires host app executable and may contain
workfile or others.
"""
import os
import sys
from ayon_core.hosts.harmony.api.lib import main as host_main
# Get current file to locate start point of sys.argv
CURRENT_FILE = os.path.abspath(__file__)
def show_error_messagebox(title, message, detail_message=None):
"""Function will show message and process ends after closing it."""
from qtpy import QtWidgets, QtCore
from ayon_core import style
app = QtWidgets.QApplication([])
app.setStyleSheet(style.load_stylesheet())
msgbox = QtWidgets.QMessageBox()
msgbox.setWindowTitle(title)
msgbox.setText(message)
if detail_message:
msgbox.setDetailedText(detail_message)
msgbox.setWindowModality(QtCore.Qt.ApplicationModal)
msgbox.show()
sys.exit(app.exec_())
def on_invalid_args(script_not_found):
"""Show to user message box saying that something went wrong.
Tell user that arguments to launch implementation are invalid with
arguments details.
Args:
script_not_found (bool): Use different message based on this value.
"""
title = "Invalid arguments"
joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv)
if script_not_found:
submsg = "Where couldn't find script path:\n\"{}\""
else:
submsg = "Expected Host executable after script path:\n\"{}\""
message = "BUG: Got invalid arguments so can't launch Host application."
detail_message = "Process was launched with arguments:\n{}\n\n{}".format(
joined_args,
submsg.format(CURRENT_FILE)
)
show_error_messagebox(title, message, detail_message)
def main(argv):
# Modify current file path to find match in sys.argv which may be different
# on windows (different letter cases and slashes).
modified_current_file = CURRENT_FILE.replace("\\", "/").lower()
# Create a copy of sys argv
sys_args = list(argv)
after_script_idx = None
# Find script path in sys.argv to know index of argv where host
# executable should be.
for idx, item in enumerate(sys_args):
if item.replace("\\", "/").lower() == modified_current_file:
after_script_idx = idx + 1
break
# Validate that there is at least one argument after script path
launch_args = None
if after_script_idx is not None:
launch_args = sys_args[after_script_idx:]
if launch_args:
# Launch host implementation
host_main(*launch_args)
else:
# Show message box
on_invalid_args(after_script_idx is None)
if __name__ == "__main__":
main(sys.argv)

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""Utility functions used for Avalon - Harmony integration."""
import platform
import subprocess
import threading
import os
@ -14,15 +15,16 @@ import json
import signal
import time
from uuid import uuid4
from qtpy import QtWidgets, QtCore, QtGui
import collections
from .server import Server
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.lib import is_using_ayon_console
from ayon_core.tools.stdout_broker.app import StdOutBroker
from ayon_core.tools.utils import host_tools
from ayon_core import style
from ayon_core.lib.applications import get_non_python_host_kwargs
from .server import Server
# Setup logging.
log = logging.getLogger(__name__)
@ -324,7 +326,18 @@ def launch_zip_file(filepath):
return
print("Launching {}".format(scene_path))
kwargs = get_non_python_host_kwargs({}, False)
# QUESTION Could we use 'run_detached_process' from 'ayon_core.lib'?
kwargs = {}
if (
platform.system().lower() == "windows"
and not is_using_ayon_console()
):
kwargs.update({
"creationflags": subprocess.CREATE_NO_WINDOW,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL
})
process = subprocess.Popen(
[ProcessContext.application_path, scene_path],
**kwargs

View file

@ -15,13 +15,13 @@ from ayon_core.pipeline import (
from ayon_core.pipeline.load import get_outdated_containers
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.hosts.harmony import HARMONY_HOST_DIR
from ayon_core.hosts.harmony import HARMONY_ADDON_ROOT
import ayon_core.hosts.harmony.api as harmony
log = logging.getLogger("ayon_core.hosts.harmony")
PLUGINS_DIR = os.path.join(HARMONY_HOST_DIR, "plugins")
PLUGINS_DIR = os.path.join(HARMONY_ADDON_ROOT, "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")

View file

@ -0,0 +1,91 @@
import os
import platform
import subprocess
from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_core.hosts.harmony import get_launch_script_path
def get_launch_kwargs(kwargs):
"""Explicit setting of kwargs for Popen for Harmony.
Expected behavior
- ayon_console opens window with logs
- ayon has stdout/stderr available for capturing
Args:
kwargs (Union[dict, None]): Current kwargs or None.
"""
if kwargs is None:
kwargs = {}
if platform.system().lower() != "windows":
return kwargs
if is_using_ayon_console():
kwargs.update({
"creationflags": subprocess.CREATE_NEW_CONSOLE
})
else:
kwargs.update({
"creationflags": subprocess.CREATE_NO_WINDOW,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL
})
return kwargs
class HarmonyPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and script path to Harmony implementation
before Harmony executable and add last workfile path to launch arguments.
Existence of last workfile is checked. If workfile does not exists tries
to copy templated workfile from predefined path.
"""
app_groups = {"harmony"}
order = 20
launch_types = {LaunchTypes.local}
def execute(self):
# Pop executable
executable_path = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
script_path = get_launch_script_path()
new_launch_args = get_ayon_launcher_args(
"run", script_path, executable_path
)
# Add workfile path if exists
workfile_path = self.data["last_workfile_path"]
if (
self.data.get("start_last_workfile")
and workfile_path
and os.path.exists(workfile_path)
):
new_launch_args.append(workfile_path)
# Append as whole list as these arguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.launch_context.launch_args.extend(remainders)
self.launch_context.kwargs = get_launch_kwargs(
self.launch_context.kwargs
)

View file

@ -167,6 +167,9 @@ class CameraLoader(load.LoaderPlugin):
temp_camera.destroy()
def switch(self, container, context):
self.update(container, context)
def remove(self, container):
node = container["node"]
@ -195,7 +198,6 @@ class CameraLoader(load.LoaderPlugin):
def _match_maya_render_mask(self, camera):
"""Workaround to match Maya render mask in Houdini"""
# print("Setting match maya render mask ")
parm = camera.parm("aperture")
expression = parm.expression()
expression = expression.replace("return ", "aperture = ")

View file

@ -0,0 +1,129 @@
import os
import re
from ayon_core.pipeline import load
from openpype.hosts.houdini.api import pipeline
import hou
class FilePathLoader(load.LoaderPlugin):
"""Load a managed filepath to a null node.
This is useful if for a particular workflow there is no existing loader
yet. A Houdini artists can load as the generic filepath loader and then
reference the relevant Houdini parm to use the exact value. The benefit
is that this filepath will be managed and can be updated as usual.
"""
label = "Load filepath to node"
order = 9
icon = "link"
color = "white"
product_types = {"*"}
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a null node
container = obj.createNode("null", node_name=node_name)
# Destroy any children
for node in container.children():
node.destroy()
# Add filepath attribute, set value as default value
filepath = self.format_path(
path=self.filepath_from_context(context),
representation=context["representation"]
)
parm_template_group = container.parmTemplateGroup()
attr_folder = hou.FolderParmTemplate("attributes_folder", "Attributes")
parm = hou.StringParmTemplate(name="filepath",
label="Filepath",
num_components=1,
default_value=(filepath,))
attr_folder.addParmTemplate(parm)
parm_template_group.append(attr_folder)
# Hide some default labels
for folder_label in ["Transform", "Render", "Misc", "Redshift OBJ"]:
folder = parm_template_group.findFolder(folder_label)
if not folder:
continue
parm_template_group.hideFolder(folder_label, True)
container.setParmTemplateGroup(parm_template_group)
container.setDisplayFlag(False)
container.setSelectableInViewport(False)
container.useXray(False)
nodes = [container]
self[:] = nodes
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
def update(self, container, context):
# Update the file path
representation_entity = context["representation"]
file_path = self.format_path(
path=self.filepath_from_context(context),
representation=representation_entity
)
node = container["node"]
node.setParms({
"filepath": file_path,
"representation": str(representation_entity["id"])
})
# Update the parameter default value (cosmetics)
parm_template_group = node.parmTemplateGroup()
parm = parm_template_group.find("filepath")
parm.setDefaultValue((file_path,))
parm_template_group.replace(parm_template_group.find("filepath"),
parm)
node.setParmTemplateGroup(parm_template_group)
def switch(self, container, representation):
self.update(container, representation)
def remove(self, container):
node = container["node"]
node.destroy()
@staticmethod
def format_path(path: str, representation: dict) -> str:
"""Format file path for sequence with $F."""
if not os.path.exists(path):
raise RuntimeError("Path does not exist: %s" % path)
# The path is either a single file or sequence in a folder.
frame = representation["context"].get("frame")
if frame is not None:
# Substitute frame number in sequence with $F with padding
ext = representation.get("ext", representation["name"])
token = "$F{}".format(len(frame)) # e.g. $F4
pattern = r"\.(\d+)\.{ext}$".format(ext=re.escape(ext))
path = re.sub(pattern, ".{}.{}".format(token, ext), path)
return os.path.normpath(path).replace("\\", "/")

View file

@ -0,0 +1,77 @@
import os
from ayon_core.pipeline import load
from ayon_core.hosts.houdini.api import pipeline
class SopUsdImportLoader(load.LoaderPlugin):
"""Load USD to SOPs via `usdimport`"""
label = "Load USD to SOPs"
product_types = {"*"}
representations = ["usd"]
order = -6
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Format file name, Houdini only wants forward slashes
file_path = self.filepath_from_context(context)
file_path = os.path.normpath(file_path)
file_path = file_path.replace("\\", "/")
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)
# Create a usdimport node
usdimport = container.createNode("usdimport", node_name=node_name)
usdimport.setParms({"filepath1": file_path})
# Set new position for unpack node else it gets cluttered
nodes = [container, usdimport]
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
def update(self, container, context):
node = container["node"]
try:
usdimport_node = next(
n for n in node.children() if n.type().name() == "usdimport"
)
except StopIteration:
self.log.error("Could not find node of type `usdimport`")
return
# Update the file path
file_path = self.filepath_from_context(context)
file_path = file_path.replace("\\", "/")
usdimport_node.setParms({"filepath1": file_path})
# Update attribute
node.setParms({"representation": context["representation"]["id"]})
def remove(self, container):
node = container["node"]
node.destroy()
def switch(self, container, representation):
self.update(container, representation)

View file

@ -41,23 +41,23 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "picture")
render_products = []
default_prefix = evalParmNoFrame(rop, "picture")
render_products = []
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -41,57 +41,57 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = []
default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = []
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("soho_outputmode").eval())
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
export_prefix = evalParmNoFrame(
rop, "soho_diskfile", pad_character="0"
)
beauty_export_product = self.get_render_product_name(
prefix=export_prefix,
suffix=None)
export_products.append(beauty_export_product)
self.log.debug(
"Found export product: {}".format(beauty_export_product)
)
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("soho_outputmode").eval())
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
export_prefix = evalParmNoFrame(
rop, "soho_diskfile", pad_character="0"
)
render_products.append(beauty_product)
beauty_export_product = self.get_render_product_name(
prefix=export_prefix,
suffix=None)
export_products.append(beauty_export_product)
self.log.debug(
"Found export product: {}".format(beauty_export_product)
)
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
for i in range(1, aov_numbers + 1):
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
for i in range(1, aov_numbers + 1):
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -32,10 +32,7 @@ class RedshiftProxyLoader(load.LoaderPlugin):
def load(self, context, name=None, namespace=None, options=None):
"""Plugin entry point."""
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "redshiftproxy"
product_type = context["product"]["productType"]
folder_name = context["folder"]["name"]
namespace = namespace or unique_namespace(

View file

@ -117,11 +117,7 @@ class ReferenceLoader(plugin.ReferenceLoader):
def process_reference(self, context, name, namespace, options):
import maya.cmds as cmds
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "model"
product_type = context["product"]["productType"]
project_name = context["project"]["name"]
# True by default to keep legacy behaviours
attach_to_root = options.get("attach_to_root", True)

View file

@ -25,10 +25,7 @@ class LoadVDBtoArnold(load.LoaderPlugin):
from ayon_core.hosts.maya.api.pipeline import containerise
from ayon_core.hosts.maya.api.lib import unique_namespace
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "vdbcache"
product_type = context["product"]["productType"]
# Check if the plugin for arnold is available on the pc
try:
@ -64,7 +61,7 @@ class LoadVDBtoArnold(load.LoaderPlugin):
path = self.filepath_from_context(context)
self._set_path(grid_node,
path=path,
representation=context["representation"])
repre_entity=context["representation"])
# Lock the shape node so the user can't delete the transform/shape
# as if it was referenced
@ -94,7 +91,7 @@ class LoadVDBtoArnold(load.LoaderPlugin):
assert len(grid_nodes) == 1, "This is a bug"
# Update the VRayVolumeGrid
self._set_path(grid_nodes[0], path=path, representation=repre_entity)
self._set_path(grid_nodes[0], path=path, repre_entity=repre_entity)
# Update container representation
cmds.setAttr(container["objectName"] + ".representation",

View file

@ -31,10 +31,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin):
from ayon_core.hosts.maya.api.pipeline import containerise
from ayon_core.hosts.maya.api.lib import unique_namespace
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "vdbcache"
product_type = context["product"]["productType"]
# Check if the plugin for redshift is available on the pc
try:

View file

@ -94,10 +94,7 @@ class LoadVDBtoVRay(load.LoaderPlugin):
"Path does not exist: %s" % path
)
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "vdbcache"
product_type = context["product"]["productType"]
# Ensure V-ray is loaded with the vrayvolumegrid
if not cmds.pluginInfo("vrayformaya", query=True, loaded=True):

View file

@ -47,10 +47,7 @@ class VRayProxyLoader(load.LoaderPlugin):
"""
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "vrayproxy"
product_type = context["product"]["productType"]
# get all representations for this version
filename = self._get_abc(

View file

@ -26,10 +26,7 @@ class VRaySceneLoader(load.LoaderPlugin):
color = "orange"
def load(self, context, name, namespace, data):
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "vrayscene_layer"
product_type = context["product"]["productType"]
folder_name = context["folder"]["name"]
namespace = namespace or unique_namespace(

View file

@ -56,10 +56,7 @@ class YetiCacheLoader(load.LoaderPlugin):
"""
try:
product_type = context["representation"]["context"]["family"]
except ValueError:
product_type = "yeticache"
product_type = context["product"]["productType"]
# Build namespace
folder_name = context["folder"]["name"]

View file

@ -5,13 +5,13 @@ from maya import cmds
from ayon_core.pipeline import publish
class ExtractYetiCache(publish.Extractor):
class ExtractUnrealYetiCache(publish.Extractor):
"""Producing Yeti cache files using scene time range.
This will extract Yeti cache file sequence and fur settings.
"""
label = "Extract Yeti Cache"
label = "Extract Yeti Cache (Unreal)"
hosts = ["maya"]
families = ["yeticacheUE"]

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Shape IDs mismatch original shape</title>
<description>## Shapes mismatch IDs with original shape
Meshes are detected in the **rig** where the (deformed) mesh has a different
`cbId` than the same mesh in its deformation history.
Theses should normally be the same.
### How to repair?
By using the repair action the IDs from the shape in history will be
copied to the deformed shape. For rig instances, in many cases the
correct fix is to use the repair action **unless** you explicitly tried
to update the `cbId` values on the meshes - in that case you actually want
to do to the reverse and copy the IDs from the deformed mesh to the history
mesh instead.
</description>
<detail>
### How does this happen?
When a deformer is applied in the scene on a referenced mesh that had no
deformers then Maya will create a new shape node for the mesh that
does not have the original id. Then on scene save new ids get created for the
meshes lacking a `cbId` and thus the mesh then has a different `cbId` than
the mesh in the deformation history.
</detail>
</error>
</root>

View file

@ -49,11 +49,17 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance)
if invalid:
invalid_namespaces = {get_namespace(node) for node in invalid}
raise PublishValidationError(
"Namespaces found:\n\n{0}".format(
_as_report_list(sorted(invalid))
message="Namespaces found:\n\n{0}".format(
_as_report_list(sorted(invalid_namespaces))
),
title="Namespaces in model"
title="Namespaces in model",
description=(
"## Namespaces found in model\n"
"It is not allowed to publish a model that contains "
"namespaces."
)
)
@classmethod

View file

@ -7,7 +7,7 @@ from ayon_core.hosts.maya.api import lib
from ayon_core.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError,
PublishXmlValidationError,
OptionalPyblishPluginMixin,
get_plugin_settings,
apply_plugin_settings_automatically
@ -58,8 +58,20 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin,
# if a deformer has been created on the shape
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
"Nodes found with mismatching IDs: {0}".format(invalid)
# Use the short names
invalid = cmds.ls(invalid)
invalid.sort()
# Construct a human-readable list
invalid = "\n".join("- {}".format(node) for node in invalid)
raise PublishXmlValidationError(
plugin=ValidateRigOutSetNodeIds,
message=(
"Rig nodes have different IDs than their input "
"history: \n{0}".format(invalid)
)
)
@classmethod

View file

@ -29,7 +29,7 @@ class ValidateStepSize(pyblish.api.InstancePlugin,
@classmethod
def get_invalid(cls, instance):
objset = instance.data['name']
objset = instance.data['instance_node']
step = instance.data.get("step", 1.0)
if step < cls.MIN or step > cls.MAX:
@ -47,4 +47,4 @@ class ValidateStepSize(pyblish.api.InstancePlugin,
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
"Invalid instances found: {0}".format(invalid))
"Instance found with invalid step size: {0}".format(invalid))

View file

@ -1,10 +1,12 @@
from .addon import (
PHOTOSHOP_ADDON_ROOT,
PhotoshopAddon,
PHOTOSHOP_HOST_DIR,
get_launch_script_path,
)
__all__ = (
"PHOTOSHOP_ADDON_ROOT",
"PhotoshopAddon",
"PHOTOSHOP_HOST_DIR",
"get_launch_script_path",
)

View file

@ -1,7 +1,7 @@
import os
from ayon_core.addon import AYONAddon, IHostAddon
PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
PHOTOSHOP_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class PhotoshopAddon(AYONAddon, IHostAddon):
@ -20,3 +20,17 @@ class PhotoshopAddon(AYONAddon, IHostAddon):
def get_workfile_extensions(self):
return [".psd", ".psb"]
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(PHOTOSHOP_ADDON_ROOT, "hooks")
]
def get_launch_script_path():
return os.path.join(
PHOTOSHOP_ADDON_ROOT, "api", "launch_script.py"
)

View file

@ -0,0 +1,93 @@
"""Script wraps launch mechanism of Photoshop implementations.
Arguments passed to the script are passed to launch function in host
implementation. In all cases requires host app executable and may contain
workfile or others.
"""
import os
import sys
from ayon_core.hosts.photoshop.api.lib import main as host_main
# Get current file to locate start point of sys.argv
CURRENT_FILE = os.path.abspath(__file__)
def show_error_messagebox(title, message, detail_message=None):
"""Function will show message and process ends after closing it."""
from qtpy import QtWidgets, QtCore
from ayon_core import style
app = QtWidgets.QApplication([])
app.setStyleSheet(style.load_stylesheet())
msgbox = QtWidgets.QMessageBox()
msgbox.setWindowTitle(title)
msgbox.setText(message)
if detail_message:
msgbox.setDetailedText(detail_message)
msgbox.setWindowModality(QtCore.Qt.ApplicationModal)
msgbox.show()
sys.exit(app.exec_())
def on_invalid_args(script_not_found):
"""Show to user message box saying that something went wrong.
Tell user that arguments to launch implementation are invalid with
arguments details.
Args:
script_not_found (bool): Use different message based on this value.
"""
title = "Invalid arguments"
joined_args = ", ".join("\"{}\"".format(arg) for arg in sys.argv)
if script_not_found:
submsg = "Where couldn't find script path:\n\"{}\""
else:
submsg = "Expected Host executable after script path:\n\"{}\""
message = "BUG: Got invalid arguments so can't launch Host application."
detail_message = "Process was launched with arguments:\n{}\n\n{}".format(
joined_args,
submsg.format(CURRENT_FILE)
)
show_error_messagebox(title, message, detail_message)
def main(argv):
# Modify current file path to find match in sys.argv which may be different
# on windows (different letter cases and slashes).
modified_current_file = CURRENT_FILE.replace("\\", "/").lower()
# Create a copy of sys argv
sys_args = list(argv)
after_script_idx = None
# Find script path in sys.argv to know index of argv where host
# executable should be.
for idx, item in enumerate(sys_args):
if item.replace("\\", "/").lower() == modified_current_file:
after_script_idx = idx + 1
break
# Validate that there is at least one argument after script path
launch_args = None
if after_script_idx is not None:
launch_args = sys_args[after_script_idx:]
if launch_args:
# Launch host implementation
host_main(*launch_args)
else:
# Show message box
on_invalid_args(after_script_idx is None)
if __name__ == "__main__":
main(sys.argv)

View file

@ -21,14 +21,14 @@ from ayon_core.host import (
)
from ayon_core.pipeline.load import any_outdated_containers
from ayon_core.hosts.photoshop import PHOTOSHOP_HOST_DIR
from ayon_core.hosts.photoshop import PHOTOSHOP_ADDON_ROOT
from ayon_core.tools.utils import get_ayon_qt_app
from . import lib
log = Logger.get_logger(__name__)
PLUGINS_DIR = os.path.join(PHOTOSHOP_HOST_DIR, "plugins")
PLUGINS_DIR = os.path.join(PHOTOSHOP_ADDON_ROOT, "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")

View file

@ -0,0 +1,91 @@
import os
import platform
import subprocess
from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_core.hosts.photoshop import get_launch_script_path
def get_launch_kwargs(kwargs):
"""Explicit setting of kwargs for Popen for Photoshop.
Expected behavior
- ayon_console opens window with logs
- ayon has stdout/stderr available for capturing
Args:
kwargs (Union[dict, None]): Current kwargs or None.
"""
if kwargs is None:
kwargs = {}
if platform.system().lower() != "windows":
return kwargs
if not is_using_ayon_console():
kwargs.update({
"creationflags": subprocess.CREATE_NEW_CONSOLE
})
else:
kwargs.update({
"creationflags": subprocess.CREATE_NO_WINDOW,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL
})
return kwargs
class PhotoshopPrelaunchHook(PreLaunchHook):
"""Launch arguments preparation.
Hook add python executable and script path to Photoshop implementation
before Photoshop executable and add last workfile path to launch arguments.
Existence of last workfile is checked. If workfile does not exists tries
to copy templated workfile from predefined path.
"""
app_groups = {"photoshop"}
order = 20
launch_types = {LaunchTypes.local}
def execute(self):
# Pop executable
executable_path = self.launch_context.launch_args.pop(0)
# Pop rest of launch arguments - There should not be other arguments!
remainders = []
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
script_path = get_launch_script_path()
new_launch_args = get_ayon_launcher_args(
"run", script_path, executable_path
)
# Add workfile path if exists
workfile_path = self.data["last_workfile_path"]
if (
self.data.get("start_last_workfile")
and workfile_path
and os.path.exists(workfile_path)
):
new_launch_args.append(workfile_path)
# Append as whole list as these arguments should not be separated
self.launch_context.launch_args.append(new_launch_args)
if remainders:
self.launch_context.launch_args.extend(remainders)
self.launch_context.kwargs = get_launch_kwargs(
self.launch_context.kwargs
)

View file

@ -48,6 +48,7 @@ class AYONMenu(QtWidgets.QWidget):
QtCore.Qt.Window
| QtCore.Qt.CustomizeWindowHint
| QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowMinimizeButtonHint
| QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowStaysOnTopHint
)

View file

@ -72,7 +72,7 @@ class AnimationAlembicLoader(plugin.Loader):
root = unreal_pipeline.AYON_ASSET_DIR
folder_name = context["folder"]["name"]
folder_path = context["folder"]["path"]
product_type = context["representation"]["context"]["family"]
product_type = context["product"]["productType"]
suffix = "_CON"
if folder_name:
asset_name = "{}_{}".format(folder_name, name)

View file

@ -659,7 +659,7 @@ class LayoutLoader(plugin.Loader):
"loader": str(self.__class__.__name__),
"representation": context["representation"]["id"],
"parent": context["representation"]["versionId"],
"family": context["representation"]["context"]["family"],
"family": context["product"]["productType"],
"loaded_assets": loaded_assets
}
imprint(

View file

@ -393,7 +393,7 @@ class ExistingLayoutLoader(plugin.Loader):
folder_name = context["folder"]["name"]
folder_path = context["folder"]["path"]
product_type = context["representation"]["context"]["family"]
product_type = context["product"]["productType"]
asset_name = f"{folder_name}_{name}" if folder_name else name
container_name = f"{folder_name}_{name}_CON"

View file

@ -152,6 +152,7 @@ from .path_tools import (
from .ayon_info import (
is_running_from_build,
is_using_ayon_console,
is_staging_enabled,
is_dev_mode_enabled,
is_in_tests,
@ -269,6 +270,7 @@ __all__ = [
"Logger",
"is_running_from_build",
"is_using_ayon_console",
"is_staging_enabled",
"is_dev_mode_enabled",
"is_in_tests",

View file

@ -1891,42 +1891,3 @@ def _prepare_last_workfile(data, workdir, addons_manager):
data["env"]["AYON_LAST_WORKFILE"] = last_workfile_path
data["last_workfile_path"] = last_workfile_path
def get_non_python_host_kwargs(kwargs, allow_console=True):
"""Explicit setting of kwargs for Popen for AE/PS/Harmony.
Expected behavior
- ayon_console opens window with logs
- ayon has stdout/stderr available for capturing
Args:
kwargs (dict) or None
allow_console (bool): use False for inner Popen opening app itself or
it will open additional console (at least for Harmony)
"""
if kwargs is None:
kwargs = {}
if platform.system().lower() != "windows":
return kwargs
executable_path = os.environ.get("AYON_EXECUTABLE")
executable_filename = ""
if executable_path:
executable_filename = os.path.basename(executable_path)
is_gui_executable = "ayon_console" not in executable_filename
if is_gui_executable:
kwargs.update({
"creationflags": subprocess.CREATE_NO_WINDOW,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL
})
elif allow_console:
kwargs.update({
"creationflags": subprocess.CREATE_NEW_CONSOLE
})
return kwargs

View file

@ -10,6 +10,12 @@ from .local_settings import get_local_site_id
def get_ayon_launcher_version():
"""Get AYON launcher version.
Returns:
str: Version string.
"""
version_filepath = os.path.join(os.environ["AYON_ROOT"], "version.py")
if not os.path.exists(version_filepath):
return None
@ -24,8 +30,8 @@ def is_running_from_build():
Returns:
bool: True if running from build.
"""
"""
executable_path = os.environ["AYON_EXECUTABLE"]
executable_filename = os.path.basename(executable_path)
if "python" in executable_filename.lower():
@ -33,6 +39,32 @@ def is_running_from_build():
return True
def is_using_ayon_console():
"""AYON launcher console executable is used.
This function make sense only on Windows platform. For other platforms
always returns True. True is also returned if process is running from
code.
AYON launcher on windows has 2 executable files. First 'ayon_console.exe'
works as 'python.exe' executable, the second 'ayon.exe' works as
'pythonw.exe' executable. The difference is way how stdout/stderr is
handled (especially when calling subprocess).
Returns:
bool: True if console executable is used.
"""
if (
platform.system().lower() != "windows"
or is_running_from_build()
):
return True
executable_path = os.environ["AYON_EXECUTABLE"]
executable_filename = os.path.basename(executable_path)
return "ayon_console" in executable_filename
def is_staging_enabled():
return os.getenv("AYON_USE_STAGING") == "1"

View file

@ -37,20 +37,6 @@ class LauncherAction(AYONAddon, ITrayAction):
if path and os.path.exists(path):
register_launcher_action_path(path)
paths_str = os.environ.get("AVALON_ACTIONS") or ""
if paths_str:
self.log.warning(
"WARNING: 'AVALON_ACTIONS' is deprecated. Support of this"
" environment variable will be removed in future versions."
" Please consider using 'OpenPypeModule' to define custom"
" action paths. Planned version to drop the support"
" is 3.17.2 or 3.18.0 ."
)
for path in paths_str.split(os.pathsep):
if path and os.path.exists(path):
register_launcher_action_path(path)
def on_action_trigger(self):
"""Implementation for ITrayAction interface.

View file

@ -23,7 +23,7 @@ log = Logger.get_logger(__name__)
class CachedData:
remapping = None
remapping = {}
has_compatible_ocio_package = None
config_version_data = {}
ocio_config_colorspaces = {}

View file

@ -315,14 +315,13 @@ class ExtractMayaSceneRawModel(BaseSettingsModel):
class ExtractCameraAlembicModel(BaseSettingsModel):
"""
List of attributes that will be added to the baked alembic camera. Needs to be written in python list syntax.
"""
enabled: bool = SettingsField(title="ExtractCameraAlembic")
optional: bool = SettingsField(title="Optional")
active: bool = SettingsField(title="Active")
bake_attributes: str = SettingsField(
"[]", title="Base Attributes", widget="textarea"
"[]", title="Bake Attributes", widget="textarea",
description="List of attributes that will be included in the alembic "
"camera export. Needs to be written as a JSON list.",
)
@validator("bake_attributes")