mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/maya_validate_model_content
This commit is contained in:
commit
bbaa5c9758
312 changed files with 5257 additions and 4008 deletions
|
|
@ -14,3 +14,15 @@ AYON_SERVER_ENABLED = True
|
|||
# Indicate if AYON entities should be used instead of OpenPype entities
|
||||
USE_AYON_ENTITIES = True
|
||||
# -------------------------
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
# Deprecated
|
||||
"AYON_CORE_ROOT",
|
||||
"PACKAGE_DIR",
|
||||
"PLUGINS_DIR",
|
||||
"AYON_SERVER_ENABLED",
|
||||
"USE_AYON_ENTITIES",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import six
|
|||
import appdirs
|
||||
import ayon_api
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.lib import Logger, is_dev_mode_enabled
|
||||
from ayon_core.settings import get_studio_settings
|
||||
|
||||
|
|
@ -335,14 +336,70 @@ 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
|
||||
current_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
hosts_dir = os.path.join(os.path.dirname(current_dir), "hosts")
|
||||
modules_dir = os.path.join(os.path.dirname(current_dir), "modules")
|
||||
hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts")
|
||||
modules_dir = os.path.join(AYON_CORE_ROOT, "modules")
|
||||
|
||||
ignored_host_names = set(IGNORED_HOSTS_IN_AYON)
|
||||
ignored_module_dir_filenames = (
|
||||
|
|
@ -1075,7 +1132,7 @@ class AddonsManager:
|
|||
"""Print out report of time spent on addons initialization parts.
|
||||
|
||||
Reporting is not automated must be implemented for each initialization
|
||||
part separatelly. Reports must be stored to `_report` attribute.
|
||||
part separately. Reports must be stored to `_report` attribute.
|
||||
Print is skipped if `_report` is empty.
|
||||
|
||||
Attribute `_report` is dictionary where key is "label" describing
|
||||
|
|
@ -1267,7 +1324,7 @@ class TrayAddonsManager(AddonsManager):
|
|||
def add_doubleclick_callback(self, addon, callback):
|
||||
"""Register doubleclick callbacks on tray icon.
|
||||
|
||||
Currently there is no way how to determine which is launched. Name of
|
||||
Currently, there is no way how to determine which is launched. Name of
|
||||
callback can be defined with `doubleclick_callback` attribute.
|
||||
|
||||
Missing feature how to define default callback.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
from .constants import (
|
||||
APPLICATIONS_ADDON_ROOT,
|
||||
DEFAULT_ENV_SUBGROUP,
|
||||
PLATFORM_NAMES,
|
||||
)
|
||||
from .exceptions import (
|
||||
ApplicationNotFound,
|
||||
ApplicationExecutableNotFound,
|
||||
ApplicationLaunchFailed,
|
||||
MissingRequiredKey,
|
||||
)
|
||||
from .defs import (
|
||||
LaunchTypes,
|
||||
ApplicationExecutable,
|
||||
UndefinedApplicationExecutable,
|
||||
ApplicationGroup,
|
||||
Application,
|
||||
EnvironmentToolGroup,
|
||||
EnvironmentTool,
|
||||
)
|
||||
from .hooks import (
|
||||
LaunchHook,
|
||||
PreLaunchHook,
|
||||
PostLaunchHook,
|
||||
)
|
||||
from .manager import (
|
||||
ApplicationManager,
|
||||
ApplicationLaunchContext,
|
||||
)
|
||||
from .addon import ApplicationsAddon
|
||||
|
||||
|
||||
__all__ = (
|
||||
"DEFAULT_ENV_SUBGROUP",
|
||||
"PLATFORM_NAMES",
|
||||
|
||||
"ApplicationNotFound",
|
||||
"ApplicationExecutableNotFound",
|
||||
"ApplicationLaunchFailed",
|
||||
"MissingRequiredKey",
|
||||
|
||||
"LaunchTypes",
|
||||
"ApplicationExecutable",
|
||||
"UndefinedApplicationExecutable",
|
||||
"ApplicationGroup",
|
||||
"Application",
|
||||
"EnvironmentToolGroup",
|
||||
"EnvironmentTool",
|
||||
|
||||
"LaunchHook",
|
||||
"PreLaunchHook",
|
||||
"PostLaunchHook",
|
||||
|
||||
"ApplicationManager",
|
||||
"ApplicationLaunchContext",
|
||||
|
||||
"ApplicationsAddon",
|
||||
)
|
||||
173
client/ayon_core/addons/applications/ayon_applications/addon.py
Normal file
173
client/ayon_core/addons/applications/ayon_applications/addon.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from ayon_core.addon import AYONAddon, IPluginPaths, click_wrap
|
||||
|
||||
from .constants import APPLICATIONS_ADDON_ROOT
|
||||
from .defs import LaunchTypes
|
||||
from .manager import ApplicationManager
|
||||
|
||||
|
||||
class ApplicationsAddon(AYONAddon, IPluginPaths):
|
||||
name = "applications"
|
||||
|
||||
def initialize(self, settings):
|
||||
# TODO remove when addon is removed from ayon-core
|
||||
self.enabled = self.name in settings
|
||||
|
||||
def get_app_environments_for_context(
|
||||
self,
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
full_app_name,
|
||||
env_group=None,
|
||||
launch_type=None,
|
||||
env=None,
|
||||
):
|
||||
"""Calculate environment variables for launch context.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_path (str): Folder path.
|
||||
task_name (str): Task name.
|
||||
full_app_name (str): Full application name.
|
||||
env_group (Optional[str]): Environment group.
|
||||
launch_type (Optional[str]): Launch type.
|
||||
env (Optional[dict[str, str]]): Environment variables to update.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Environment variables for context.
|
||||
|
||||
"""
|
||||
from ayon_applications.utils import get_app_environments_for_context
|
||||
|
||||
if not full_app_name:
|
||||
return {}
|
||||
|
||||
return get_app_environments_for_context(
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
full_app_name,
|
||||
env_group=env_group,
|
||||
launch_type=launch_type,
|
||||
env=env,
|
||||
addons_manager=self.manager
|
||||
)
|
||||
|
||||
def get_farm_publish_environment_variables(
|
||||
self,
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
full_app_name=None,
|
||||
env_group=None,
|
||||
):
|
||||
"""Calculate environment variables for farm publish.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
folder_path (str): Folder path.
|
||||
task_name (str): Task name.
|
||||
env_group (Optional[str]): Environment group.
|
||||
full_app_name (Optional[str]): Full application name. Value from
|
||||
environment variable 'AYON_APP_NAME' is used if 'None' is
|
||||
passed.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Environment variables for farm publish.
|
||||
|
||||
"""
|
||||
if full_app_name is None:
|
||||
full_app_name = os.getenv("AYON_APP_NAME")
|
||||
|
||||
return self.get_app_environments_for_context(
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
full_app_name,
|
||||
env_group=env_group,
|
||||
launch_type=LaunchTypes.farm_publish
|
||||
)
|
||||
|
||||
def get_applications_manager(self, settings=None):
|
||||
"""Get applications manager.
|
||||
|
||||
Args:
|
||||
settings (Optional[dict]): Studio/project settings.
|
||||
|
||||
Returns:
|
||||
ApplicationManager: Applications manager.
|
||||
|
||||
"""
|
||||
return ApplicationManager(settings)
|
||||
|
||||
def get_plugin_paths(self):
|
||||
return {
|
||||
"publish": [
|
||||
os.path.join(APPLICATIONS_ADDON_ROOT, "plugins", "publish")
|
||||
]
|
||||
}
|
||||
|
||||
# --- CLI ---
|
||||
def cli(self, addon_click_group):
|
||||
main_group = click_wrap.group(
|
||||
self._cli_main, name=self.name, help="Applications addon"
|
||||
)
|
||||
(
|
||||
main_group.command(
|
||||
self._cli_extract_environments,
|
||||
name="extractenvironments",
|
||||
help=(
|
||||
"Extract environment variables for context into json file"
|
||||
)
|
||||
)
|
||||
.argument("output_json_path")
|
||||
.option("--project", help="Project name", default=None)
|
||||
.option("--folder", help="Folder path", default=None)
|
||||
.option("--task", help="Task name", default=None)
|
||||
.option("--app", help="Application name", default=None)
|
||||
.option(
|
||||
"--envgroup",
|
||||
help="Environment group (e.g. \"farm\")",
|
||||
default=None
|
||||
)
|
||||
)
|
||||
# Convert main command to click object and add it to parent group
|
||||
addon_click_group.add_command(
|
||||
main_group.to_click_obj()
|
||||
)
|
||||
|
||||
def _cli_main(self):
|
||||
pass
|
||||
|
||||
def _cli_extract_environments(
|
||||
self, output_json_path, project, folder, task, app, envgroup
|
||||
):
|
||||
"""Produces json file with environment based on project and app.
|
||||
|
||||
Called by farm integration to propagate environment into farm jobs.
|
||||
|
||||
Args:
|
||||
output_json_path (str): Output json file path.
|
||||
project (str): Project name.
|
||||
folder (str): Folder path.
|
||||
task (str): Task name.
|
||||
app (str): Full application name e.g. 'maya/2024'.
|
||||
envgroup (str): Environment group.
|
||||
|
||||
"""
|
||||
if all((project, folder, task, app)):
|
||||
env = self.get_farm_publish_environment_variables(
|
||||
project, folder, task, app, env_group=envgroup,
|
||||
)
|
||||
else:
|
||||
env = os.environ.copy()
|
||||
|
||||
output_dir = os.path.dirname(output_json_path)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
with open(output_json_path, "w") as file_stream:
|
||||
json.dump(env, file_stream, indent=4)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import os
|
||||
|
||||
APPLICATIONS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
PLATFORM_NAMES = {"windows", "linux", "darwin"}
|
||||
DEFAULT_ENV_SUBGROUP = "standard"
|
||||
404
client/ayon_core/addons/applications/ayon_applications/defs.py
Normal file
404
client/ayon_core/addons/applications/ayon_applications/defs.py
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
import os
|
||||
import platform
|
||||
import json
|
||||
import copy
|
||||
|
||||
from ayon_core.lib import find_executable
|
||||
|
||||
|
||||
class LaunchTypes:
|
||||
"""Launch types are filters for pre/post-launch hooks.
|
||||
|
||||
Please use these variables in case they'll change values.
|
||||
"""
|
||||
|
||||
# Local launch - application is launched on local machine
|
||||
local = "local"
|
||||
# Farm render job - application is on farm
|
||||
farm_render = "farm-render"
|
||||
# Farm publish job - integration post-render job
|
||||
farm_publish = "farm-publish"
|
||||
# Remote launch - application is launched on remote machine from which
|
||||
# can be started publishing
|
||||
remote = "remote"
|
||||
# Automated launch - application is launched with automated publishing
|
||||
automated = "automated"
|
||||
|
||||
|
||||
class ApplicationExecutable:
|
||||
"""Representation of executable loaded from settings."""
|
||||
|
||||
def __init__(self, executable):
|
||||
# Try to format executable with environments
|
||||
try:
|
||||
executable = executable.format(**os.environ)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# On MacOS check if exists path to executable when ends with `.app`
|
||||
# - it is common that path will lead to "/Applications/Blender" but
|
||||
# real path is "/Applications/Blender.app"
|
||||
if platform.system().lower() == "darwin":
|
||||
executable = self.macos_executable_prep(executable)
|
||||
|
||||
self.executable_path = executable
|
||||
|
||||
def __str__(self):
|
||||
return self.executable_path
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> {}".format(self.__class__.__name__, self.executable_path)
|
||||
|
||||
@staticmethod
|
||||
def macos_executable_prep(executable):
|
||||
"""Try to find full path to executable file.
|
||||
|
||||
Real executable is stored in '*.app/Contents/MacOS/<executable>'.
|
||||
|
||||
Having path to '*.app' gives ability to read it's plist info and
|
||||
use "CFBundleExecutable" key from plist to know what is "executable."
|
||||
|
||||
Plist is stored in '*.app/Contents/Info.plist'.
|
||||
|
||||
This is because some '*.app' directories don't have same permissions
|
||||
as real executable.
|
||||
"""
|
||||
# Try to find if there is `.app` file
|
||||
if not os.path.exists(executable):
|
||||
_executable = executable + ".app"
|
||||
if os.path.exists(_executable):
|
||||
executable = _executable
|
||||
|
||||
# Try to find real executable if executable has `Contents` subfolder
|
||||
contents_dir = os.path.join(executable, "Contents")
|
||||
if os.path.exists(contents_dir):
|
||||
executable_filename = None
|
||||
# Load plist file and check for bundle executable
|
||||
plist_filepath = os.path.join(contents_dir, "Info.plist")
|
||||
if os.path.exists(plist_filepath):
|
||||
import plistlib
|
||||
|
||||
if hasattr(plistlib, "load"):
|
||||
with open(plist_filepath, "rb") as stream:
|
||||
parsed_plist = plistlib.load(stream)
|
||||
else:
|
||||
parsed_plist = plistlib.readPlist(plist_filepath)
|
||||
executable_filename = parsed_plist.get("CFBundleExecutable")
|
||||
|
||||
if executable_filename:
|
||||
executable = os.path.join(
|
||||
contents_dir, "MacOS", executable_filename
|
||||
)
|
||||
|
||||
return executable
|
||||
|
||||
def as_args(self):
|
||||
return [self.executable_path]
|
||||
|
||||
def _realpath(self):
|
||||
"""Check if path is valid executable path."""
|
||||
# Check for executable in PATH
|
||||
result = find_executable(self.executable_path)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
# This is not 100% validation but it is better than remove ability to
|
||||
# launch .bat, .sh or extentionless files
|
||||
if os.path.exists(self.executable_path):
|
||||
return self.executable_path
|
||||
return None
|
||||
|
||||
def exists(self):
|
||||
if not self.executable_path:
|
||||
return False
|
||||
return bool(self._realpath())
|
||||
|
||||
|
||||
class UndefinedApplicationExecutable(ApplicationExecutable):
|
||||
"""Some applications do not require executable path from settings.
|
||||
|
||||
In that case this class is used to "fake" existing executable.
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}>".format(self.__class__.__name__)
|
||||
|
||||
def as_args(self):
|
||||
return []
|
||||
|
||||
def exists(self):
|
||||
return True
|
||||
|
||||
|
||||
class ApplicationGroup:
|
||||
"""Hold information about application group.
|
||||
|
||||
Application group wraps different versions(variants) of application.
|
||||
e.g. "maya" is group and "maya_2020" is variant.
|
||||
|
||||
Group hold `host_name` which is implementation name used in AYON. Also
|
||||
holds `enabled` if whole app group is enabled or `icon` for application
|
||||
icon path in resources.
|
||||
|
||||
Group has also `environment` which hold same environments for all variants.
|
||||
|
||||
Args:
|
||||
name (str): Groups' name.
|
||||
data (dict): Group defying data loaded from settings.
|
||||
manager (ApplicationManager): Manager that created the group.
|
||||
"""
|
||||
|
||||
def __init__(self, name, data, manager):
|
||||
self.name = name
|
||||
self.manager = manager
|
||||
self._data = data
|
||||
|
||||
self.enabled = data["enabled"]
|
||||
self.label = data["label"] or None
|
||||
self.icon = data["icon"] or None
|
||||
env = {}
|
||||
try:
|
||||
env = json.loads(data["environment"])
|
||||
except Exception:
|
||||
pass
|
||||
self._environment = env
|
||||
|
||||
host_name = data["host_name"] or None
|
||||
self.is_host = host_name is not None
|
||||
self.host_name = host_name
|
||||
|
||||
settings_variants = data["variants"]
|
||||
variants = {}
|
||||
for variant_data in settings_variants:
|
||||
app_variant = Application(variant_data, self)
|
||||
variants[app_variant.name] = app_variant
|
||||
|
||||
self.variants = variants
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> - {}".format(self.__class__.__name__, self.name)
|
||||
|
||||
def __iter__(self):
|
||||
for variant in self.variants.values():
|
||||
yield variant
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
return copy.deepcopy(self._environment)
|
||||
|
||||
|
||||
class Application:
|
||||
"""Hold information about application.
|
||||
|
||||
Object by itself does nothing special.
|
||||
|
||||
Args:
|
||||
data (dict): Data for the version containing information about
|
||||
executables, variant label or if is enabled.
|
||||
Only required key is `executables`.
|
||||
group (ApplicationGroup): App group object that created the application
|
||||
and under which application belongs.
|
||||
|
||||
"""
|
||||
def __init__(self, data, group):
|
||||
self._data = data
|
||||
name = data["name"]
|
||||
label = data["label"] or name
|
||||
enabled = False
|
||||
if group.enabled:
|
||||
enabled = data.get("enabled", True)
|
||||
|
||||
if group.label:
|
||||
full_label = " ".join((group.label, label))
|
||||
else:
|
||||
full_label = label
|
||||
env = {}
|
||||
try:
|
||||
env = json.loads(data["environment"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
arguments = data["arguments"]
|
||||
if isinstance(arguments, dict):
|
||||
arguments = arguments.get(platform.system().lower())
|
||||
|
||||
if not arguments:
|
||||
arguments = []
|
||||
|
||||
_executables = data["executables"].get(platform.system().lower(), [])
|
||||
executables = [
|
||||
ApplicationExecutable(executable)
|
||||
for executable in _executables
|
||||
]
|
||||
|
||||
self.group = group
|
||||
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.enabled = enabled
|
||||
self.use_python_2 = data.get("use_python_2", False)
|
||||
|
||||
self.full_name = "/".join((group.name, name))
|
||||
self.full_label = full_label
|
||||
self.arguments = arguments
|
||||
self.executables = executables
|
||||
self._environment = env
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> - {}".format(self.__class__.__name__, self.full_name)
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
return copy.deepcopy(self._environment)
|
||||
|
||||
@property
|
||||
def manager(self):
|
||||
return self.group.manager
|
||||
|
||||
@property
|
||||
def host_name(self):
|
||||
return self.group.host_name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
return self.group.icon
|
||||
|
||||
@property
|
||||
def is_host(self):
|
||||
return self.group.is_host
|
||||
|
||||
def find_executable(self):
|
||||
"""Try to find existing executable for application.
|
||||
|
||||
Returns (str): Path to executable from `executables` or None if any
|
||||
exists.
|
||||
"""
|
||||
for executable in self.executables:
|
||||
if executable.exists():
|
||||
return executable
|
||||
return None
|
||||
|
||||
def launch(self, *args, **kwargs):
|
||||
"""Launch the application.
|
||||
|
||||
For this purpose is used manager's launch method to keep logic at one
|
||||
place.
|
||||
|
||||
Arguments must match with manager's launch method. That's why *args
|
||||
**kwargs are used.
|
||||
|
||||
Returns:
|
||||
subprocess.Popen: Return executed process as Popen object.
|
||||
"""
|
||||
return self.manager.launch(self.full_name, *args, **kwargs)
|
||||
|
||||
|
||||
class EnvironmentToolGroup:
|
||||
"""Hold information about environment tool group.
|
||||
|
||||
Environment tool group may hold different variants of same tool and set
|
||||
environments that are same for all of them.
|
||||
|
||||
e.g. "mtoa" may have different versions but all environments except one
|
||||
are same.
|
||||
|
||||
Args:
|
||||
data (dict): Group information with variants.
|
||||
manager (ApplicationManager): Manager that creates the group.
|
||||
"""
|
||||
|
||||
def __init__(self, data, manager):
|
||||
name = data["name"]
|
||||
label = data["label"]
|
||||
|
||||
self.name = name
|
||||
self.label = label
|
||||
self._data = data
|
||||
self.manager = manager
|
||||
|
||||
environment = {}
|
||||
try:
|
||||
environment = json.loads(data["environment"])
|
||||
except Exception:
|
||||
pass
|
||||
self._environment = environment
|
||||
|
||||
variants = data.get("variants") or []
|
||||
variants_by_name = {}
|
||||
for variant_data in variants:
|
||||
tool = EnvironmentTool(variant_data, self)
|
||||
variants_by_name[tool.name] = tool
|
||||
self.variants = variants_by_name
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> - {}".format(self.__class__.__name__, self.name)
|
||||
|
||||
def __iter__(self):
|
||||
for variant in self.variants.values():
|
||||
yield variant
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
return copy.deepcopy(self._environment)
|
||||
|
||||
|
||||
class EnvironmentTool:
|
||||
"""Hold information about application tool.
|
||||
|
||||
Structure of tool information.
|
||||
|
||||
Args:
|
||||
variant_data (dict): Variant data with environments and
|
||||
host and app variant filters.
|
||||
group (EnvironmentToolGroup): Name of group which wraps tool.
|
||||
"""
|
||||
|
||||
def __init__(self, variant_data, group):
|
||||
# Backwards compatibility 3.9.1 - 3.9.2
|
||||
# - 'variant_data' contained only environments but contain also host
|
||||
# and application variant filters
|
||||
name = variant_data["name"]
|
||||
label = variant_data["label"]
|
||||
host_names = variant_data["host_names"]
|
||||
app_variants = variant_data["app_variants"]
|
||||
|
||||
environment = {}
|
||||
try:
|
||||
environment = json.loads(variant_data["environment"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.host_names = host_names
|
||||
self.app_variants = app_variants
|
||||
self.name = name
|
||||
self.variant_label = label
|
||||
self.label = " ".join((group.label, label))
|
||||
self.group = group
|
||||
|
||||
self._environment = environment
|
||||
self.full_name = "/".join((group.name, name))
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}> - {}".format(self.__class__.__name__, self.full_name)
|
||||
|
||||
@property
|
||||
def environment(self):
|
||||
return copy.deepcopy(self._environment)
|
||||
|
||||
def is_valid_for_app(self, app):
|
||||
"""Is tool valid for application.
|
||||
|
||||
Args:
|
||||
app (Application): Application for which are prepared environments.
|
||||
"""
|
||||
if self.app_variants and app.full_name not in self.app_variants:
|
||||
return False
|
||||
|
||||
if self.host_names and app.host_name not in self.host_names:
|
||||
return False
|
||||
return True
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
class ApplicationNotFound(Exception):
|
||||
"""Application was not found in ApplicationManager by name."""
|
||||
|
||||
def __init__(self, app_name):
|
||||
self.app_name = app_name
|
||||
super(ApplicationNotFound, self).__init__(
|
||||
"Application \"{}\" was not found.".format(app_name)
|
||||
)
|
||||
|
||||
|
||||
class ApplicationExecutableNotFound(Exception):
|
||||
"""Defined executable paths are not available on the machine."""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
details = None
|
||||
if not application.executables:
|
||||
msg = (
|
||||
"Executable paths for application \"{}\"({}) are not set."
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
"Defined executable paths for application \"{}\"({})"
|
||||
" are not available on this machine."
|
||||
)
|
||||
details = "Defined paths:"
|
||||
for executable in application.executables:
|
||||
details += "\n- " + executable.executable_path
|
||||
|
||||
self.msg = msg.format(application.full_label, application.full_name)
|
||||
self.details = details
|
||||
|
||||
exc_mgs = str(self.msg)
|
||||
if details:
|
||||
# Is good idea to pass new line symbol to exception message?
|
||||
exc_mgs += "\n" + details
|
||||
self.exc_msg = exc_mgs
|
||||
super(ApplicationExecutableNotFound, self).__init__(exc_mgs)
|
||||
|
||||
|
||||
class ApplicationLaunchFailed(Exception):
|
||||
"""Application launch failed due to known reason.
|
||||
|
||||
Message should be self explanatory as traceback won't be shown.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingRequiredKey(KeyError):
|
||||
pass
|
||||
150
client/ayon_core/addons/applications/ayon_applications/hooks.py
Normal file
150
client/ayon_core/addons/applications/ayon_applications/hooks.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import platform
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
|
||||
from ayon_core.lib import Logger
|
||||
|
||||
from .defs import LaunchTypes
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class LaunchHook:
|
||||
"""Abstract base class of launch hook."""
|
||||
# Order of prelaunch hook, will be executed as last if set to None.
|
||||
order = None
|
||||
# List of host implementations, skipped if empty.
|
||||
hosts = set()
|
||||
# Set of application groups
|
||||
app_groups = set()
|
||||
# Set of specific application names
|
||||
app_names = set()
|
||||
# Set of platform availability
|
||||
platforms = set()
|
||||
# Set of launch types for which is available
|
||||
# - if empty then is available for all launch types
|
||||
# - by default has 'local' which is most common reason for launc hooks
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def __init__(self, launch_context):
|
||||
"""Constructor of launch hook.
|
||||
|
||||
Always should be called
|
||||
"""
|
||||
self.log = Logger.get_logger(self.__class__.__name__)
|
||||
|
||||
self.launch_context = launch_context
|
||||
|
||||
is_valid = self.class_validation(launch_context)
|
||||
if is_valid:
|
||||
is_valid = self.validate()
|
||||
|
||||
self.is_valid = is_valid
|
||||
|
||||
@classmethod
|
||||
def class_validation(cls, launch_context):
|
||||
"""Validation of class attributes by launch context.
|
||||
|
||||
Args:
|
||||
launch_context (ApplicationLaunchContext): Context of launching
|
||||
application.
|
||||
|
||||
Returns:
|
||||
bool: Is launch hook valid for the context by class attributes.
|
||||
"""
|
||||
if cls.platforms:
|
||||
low_platforms = tuple(
|
||||
_platform.lower()
|
||||
for _platform in cls.platforms
|
||||
)
|
||||
if platform.system().lower() not in low_platforms:
|
||||
return False
|
||||
|
||||
if cls.hosts:
|
||||
if launch_context.host_name not in cls.hosts:
|
||||
return False
|
||||
|
||||
if cls.app_groups:
|
||||
if launch_context.app_group.name not in cls.app_groups:
|
||||
return False
|
||||
|
||||
if cls.app_names:
|
||||
if launch_context.app_name not in cls.app_names:
|
||||
return False
|
||||
|
||||
if cls.launch_types:
|
||||
if launch_context.launch_type not in cls.launch_types:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.launch_context.data
|
||||
|
||||
@property
|
||||
def application(self):
|
||||
return getattr(self.launch_context, "application", None)
|
||||
|
||||
@property
|
||||
def manager(self):
|
||||
return getattr(self.application, "manager", None)
|
||||
|
||||
@property
|
||||
def host_name(self):
|
||||
return getattr(self.application, "host_name", None)
|
||||
|
||||
@property
|
||||
def app_group(self):
|
||||
return getattr(self.application, "group", None)
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
return getattr(self.application, "full_name", None)
|
||||
|
||||
@property
|
||||
def addons_manager(self):
|
||||
return getattr(self.launch_context, "addons_manager", None)
|
||||
|
||||
@property
|
||||
def modules_manager(self):
|
||||
"""
|
||||
Deprecated:
|
||||
Use 'addons_wrapper' instead.
|
||||
"""
|
||||
return self.addons_manager
|
||||
|
||||
def validate(self):
|
||||
"""Optional validation of launch hook on initialization.
|
||||
|
||||
Returns:
|
||||
bool: Hook is valid (True) or invalid (False).
|
||||
"""
|
||||
# QUESTION Not sure if this method has any usable potential.
|
||||
# - maybe result can be based on settings
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, *args, **kwargs):
|
||||
"""Abstract execute method where logic of hook is."""
|
||||
pass
|
||||
|
||||
|
||||
class PreLaunchHook(LaunchHook):
|
||||
"""Abstract class of prelaunch hook.
|
||||
|
||||
This launch hook will be processed before application is launched.
|
||||
|
||||
If any exception will happen during processing the application won't be
|
||||
launched.
|
||||
"""
|
||||
|
||||
|
||||
class PostLaunchHook(LaunchHook):
|
||||
"""Abstract class of postlaunch hook.
|
||||
|
||||
This launch hook will be processed after application is launched.
|
||||
|
||||
Nothing will happen if any exception will happen during processing. And
|
||||
processing of other postlaunch hooks won't stop either.
|
||||
"""
|
||||
|
|
@ -0,0 +1,676 @@
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import json
|
||||
import tempfile
|
||||
import platform
|
||||
import inspect
|
||||
import subprocess
|
||||
|
||||
import six
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.settings import get_studio_settings
|
||||
from ayon_core.lib import (
|
||||
Logger,
|
||||
modules_from_path,
|
||||
classes_from_module,
|
||||
get_linux_launcher_args,
|
||||
)
|
||||
from ayon_core.addon import AddonsManager
|
||||
|
||||
from .constants import DEFAULT_ENV_SUBGROUP
|
||||
from .exceptions import (
|
||||
ApplicationNotFound,
|
||||
ApplicationExecutableNotFound,
|
||||
)
|
||||
from .hooks import PostLaunchHook, PreLaunchHook
|
||||
from .defs import EnvironmentToolGroup, ApplicationGroup, LaunchTypes
|
||||
|
||||
|
||||
class ApplicationManager:
|
||||
"""Load applications and tools and store them by their full name.
|
||||
|
||||
Args:
|
||||
studio_settings (dict): Preloaded studio settings. When passed manager
|
||||
will always use these values. Gives ability to create manager
|
||||
using different settings.
|
||||
"""
|
||||
|
||||
def __init__(self, studio_settings=None):
|
||||
self.log = Logger.get_logger(self.__class__.__name__)
|
||||
|
||||
self.app_groups = {}
|
||||
self.applications = {}
|
||||
self.tool_groups = {}
|
||||
self.tools = {}
|
||||
|
||||
self._studio_settings = studio_settings
|
||||
|
||||
self.refresh()
|
||||
|
||||
def set_studio_settings(self, studio_settings):
|
||||
"""Ability to change init system settings.
|
||||
|
||||
This will trigger refresh of manager.
|
||||
"""
|
||||
self._studio_settings = studio_settings
|
||||
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh applications from settings."""
|
||||
self.app_groups.clear()
|
||||
self.applications.clear()
|
||||
self.tool_groups.clear()
|
||||
self.tools.clear()
|
||||
|
||||
if self._studio_settings is not None:
|
||||
settings = copy.deepcopy(self._studio_settings)
|
||||
else:
|
||||
settings = get_studio_settings(
|
||||
clear_metadata=False, exclude_locals=False
|
||||
)
|
||||
|
||||
applications_addon_settings = settings["applications"]
|
||||
|
||||
# Prepare known applications
|
||||
app_defs = applications_addon_settings["applications"]
|
||||
additional_apps = app_defs.pop("additional_apps")
|
||||
for additional_app in additional_apps:
|
||||
app_name = additional_app.pop("name")
|
||||
if app_name in app_defs:
|
||||
self.log.warning((
|
||||
"Additional application '{}' is already"
|
||||
" in built-in applications."
|
||||
).format(app_name))
|
||||
app_defs[app_name] = additional_app
|
||||
|
||||
for group_name, variant_defs in app_defs.items():
|
||||
group = ApplicationGroup(group_name, variant_defs, self)
|
||||
self.app_groups[group_name] = group
|
||||
for app in group:
|
||||
self.applications[app.full_name] = app
|
||||
|
||||
tools_definitions = applications_addon_settings["tool_groups"]
|
||||
for tool_group_data in tools_definitions:
|
||||
group = EnvironmentToolGroup(tool_group_data, self)
|
||||
self.tool_groups[group.name] = group
|
||||
for tool in group:
|
||||
self.tools[tool.full_name] = tool
|
||||
|
||||
def find_latest_available_variant_for_group(self, group_name):
|
||||
group = self.app_groups.get(group_name)
|
||||
if group is None or not group.enabled:
|
||||
return None
|
||||
|
||||
output = None
|
||||
for _, variant in reversed(sorted(group.variants.items())):
|
||||
executable = variant.find_executable()
|
||||
if executable:
|
||||
output = variant
|
||||
break
|
||||
return output
|
||||
|
||||
def create_launch_context(self, app_name, **data):
|
||||
"""Prepare launch context for application.
|
||||
|
||||
Args:
|
||||
app_name (str): Name of application that should be launched.
|
||||
**data (Any): Any additional data. Data may be used during
|
||||
|
||||
Returns:
|
||||
ApplicationLaunchContext: Launch context for application.
|
||||
|
||||
Raises:
|
||||
ApplicationNotFound: Application was not found by entered name.
|
||||
"""
|
||||
|
||||
app = self.applications.get(app_name)
|
||||
if not app:
|
||||
raise ApplicationNotFound(app_name)
|
||||
|
||||
executable = app.find_executable()
|
||||
|
||||
return ApplicationLaunchContext(
|
||||
app, executable, **data
|
||||
)
|
||||
|
||||
def launch_with_context(self, launch_context):
|
||||
"""Launch application using existing launch context.
|
||||
|
||||
Args:
|
||||
launch_context (ApplicationLaunchContext): Prepared launch
|
||||
context.
|
||||
"""
|
||||
|
||||
if not launch_context.executable:
|
||||
raise ApplicationExecutableNotFound(launch_context.application)
|
||||
return launch_context.launch()
|
||||
|
||||
def launch(self, app_name, **data):
|
||||
"""Launch procedure.
|
||||
|
||||
For host application it's expected to contain "project_name",
|
||||
"folder_path" and "task_name".
|
||||
|
||||
Args:
|
||||
app_name (str): Name of application that should be launched.
|
||||
**data (dict): Any additional data. Data may be used during
|
||||
preparation to store objects usable in multiple places.
|
||||
|
||||
Raises:
|
||||
ApplicationNotFound: Application was not found by entered
|
||||
argument `app_name`.
|
||||
ApplicationExecutableNotFound: Executables in application definition
|
||||
were not found on this machine.
|
||||
ApplicationLaunchFailed: Something important for application launch
|
||||
failed. Exception should contain explanation message,
|
||||
traceback should not be needed.
|
||||
"""
|
||||
|
||||
context = self.create_launch_context(app_name, **data)
|
||||
return self.launch_with_context(context)
|
||||
|
||||
|
||||
class ApplicationLaunchContext:
|
||||
"""Context of launching application.
|
||||
|
||||
Main purpose of context is to prepare launch arguments and keyword
|
||||
arguments for new process. Most important part of keyword arguments
|
||||
preparations are environment variables.
|
||||
|
||||
During the whole process is possible to use `data` attribute to store
|
||||
object usable in multiple places.
|
||||
|
||||
Launch arguments are strings in list. It is possible to "chain" argument
|
||||
when order of them matters. That is possible to do with adding list where
|
||||
order is right and should not change.
|
||||
NOTE: This is recommendation, not requirement.
|
||||
e.g.: `["nuke.exe", "--NukeX"]` -> In this case any part of process may
|
||||
insert argument between `nuke.exe` and `--NukeX`. To keep them together
|
||||
it is better to wrap them in another list: `[["nuke.exe", "--NukeX"]]`.
|
||||
|
||||
Notes:
|
||||
It is possible to use launch context only to prepare environment
|
||||
variables. In that case `executable` may be None and can be used
|
||||
'run_prelaunch_hooks' method to run prelaunch hooks which prepare
|
||||
them.
|
||||
|
||||
Args:
|
||||
application (Application): Application definition.
|
||||
executable (ApplicationExecutable): Object with path to executable.
|
||||
env_group (Optional[str]): Environment variable group. If not set
|
||||
'DEFAULT_ENV_SUBGROUP' is used.
|
||||
launch_type (Optional[str]): Launch type. If not set 'local' is used.
|
||||
**data (dict): Any additional data. Data may be used during
|
||||
preparation to store objects usable in multiple places.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
application,
|
||||
executable,
|
||||
env_group=None,
|
||||
launch_type=None,
|
||||
**data
|
||||
):
|
||||
# Application object
|
||||
self.application = application
|
||||
|
||||
self.addons_manager = AddonsManager()
|
||||
|
||||
# Logger
|
||||
logger_name = "{}-{}".format(self.__class__.__name__,
|
||||
self.application.full_name)
|
||||
self.log = Logger.get_logger(logger_name)
|
||||
|
||||
self.executable = executable
|
||||
|
||||
if launch_type is None:
|
||||
launch_type = LaunchTypes.local
|
||||
self.launch_type = launch_type
|
||||
|
||||
if env_group is None:
|
||||
env_group = DEFAULT_ENV_SUBGROUP
|
||||
|
||||
self.env_group = env_group
|
||||
|
||||
self.data = dict(data)
|
||||
|
||||
launch_args = []
|
||||
if executable is not None:
|
||||
launch_args = executable.as_args()
|
||||
# subprocess.Popen launch arguments (first argument in constructor)
|
||||
self.launch_args = launch_args
|
||||
self.launch_args.extend(application.arguments)
|
||||
if self.data.get("app_args"):
|
||||
self.launch_args.extend(self.data.pop("app_args"))
|
||||
|
||||
# Handle launch environemtns
|
||||
src_env = self.data.pop("env", None)
|
||||
if src_env is not None and not isinstance(src_env, dict):
|
||||
self.log.warning((
|
||||
"Passed `env` kwarg has invalid type: {}. Expected: `dict`."
|
||||
" Using `os.environ` instead."
|
||||
).format(str(type(src_env))))
|
||||
src_env = None
|
||||
|
||||
if src_env is None:
|
||||
src_env = os.environ
|
||||
|
||||
ignored_env = {"QT_API", }
|
||||
env = {
|
||||
key: str(value)
|
||||
for key, value in src_env.items()
|
||||
if key not in ignored_env
|
||||
}
|
||||
# subprocess.Popen keyword arguments
|
||||
self.kwargs = {"env": env}
|
||||
|
||||
if platform.system().lower() == "windows":
|
||||
# Detach new process from currently running process on Windows
|
||||
flags = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
)
|
||||
self.kwargs["creationflags"] = flags
|
||||
|
||||
if not sys.stdout:
|
||||
self.kwargs["stdout"] = subprocess.DEVNULL
|
||||
self.kwargs["stderr"] = subprocess.DEVNULL
|
||||
|
||||
self.prelaunch_hooks = None
|
||||
self.postlaunch_hooks = None
|
||||
|
||||
self.process = None
|
||||
self._prelaunch_hooks_executed = False
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
if (
|
||||
"env" not in self.kwargs
|
||||
or self.kwargs["env"] is None
|
||||
):
|
||||
self.kwargs["env"] = {}
|
||||
return self.kwargs["env"]
|
||||
|
||||
@env.setter
|
||||
def env(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(
|
||||
"'env' attribute expect 'dict' object. Got: {}".format(
|
||||
str(type(value))
|
||||
)
|
||||
)
|
||||
self.kwargs["env"] = value
|
||||
|
||||
@property
|
||||
def modules_manager(self):
|
||||
"""
|
||||
Deprecated:
|
||||
Use 'addons_manager' instead.
|
||||
|
||||
"""
|
||||
return self.addons_manager
|
||||
|
||||
def _collect_addons_launch_hook_paths(self):
|
||||
"""Helper to collect application launch hooks from addons.
|
||||
|
||||
Module have to have implemented 'get_launch_hook_paths' method which
|
||||
can expect application as argument or nothing.
|
||||
|
||||
Returns:
|
||||
List[str]: Paths to launch hook directories.
|
||||
"""
|
||||
|
||||
expected_types = (list, tuple, set)
|
||||
|
||||
output = []
|
||||
for module in self.addons_manager.get_enabled_addons():
|
||||
# Skip module if does not have implemented 'get_launch_hook_paths'
|
||||
func = getattr(module, "get_launch_hook_paths", None)
|
||||
if func is None:
|
||||
continue
|
||||
|
||||
func = module.get_launch_hook_paths
|
||||
if hasattr(inspect, "signature"):
|
||||
sig = inspect.signature(func)
|
||||
expect_args = len(sig.parameters) > 0
|
||||
else:
|
||||
expect_args = len(inspect.getargspec(func)[0]) > 0
|
||||
|
||||
# Pass application argument if method expect it.
|
||||
try:
|
||||
if expect_args:
|
||||
hook_paths = func(self.application)
|
||||
else:
|
||||
hook_paths = func()
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to call 'get_launch_hook_paths'",
|
||||
exc_info=True
|
||||
)
|
||||
continue
|
||||
|
||||
if not hook_paths:
|
||||
continue
|
||||
|
||||
# Convert string to list
|
||||
if isinstance(hook_paths, six.string_types):
|
||||
hook_paths = [hook_paths]
|
||||
|
||||
# Skip invalid types
|
||||
if not isinstance(hook_paths, expected_types):
|
||||
self.log.warning((
|
||||
"Result of `get_launch_hook_paths`"
|
||||
" has invalid type {}. Expected {}"
|
||||
).format(type(hook_paths), expected_types))
|
||||
continue
|
||||
|
||||
output.extend(hook_paths)
|
||||
return output
|
||||
|
||||
def paths_to_launch_hooks(self):
|
||||
"""Directory paths where to look for launch hooks."""
|
||||
# This method has potential to be part of application manager (maybe).
|
||||
paths = []
|
||||
|
||||
# TODO load additional studio paths from settings
|
||||
global_hooks_dir = os.path.join(AYON_CORE_ROOT, "hooks")
|
||||
|
||||
hooks_dirs = [
|
||||
global_hooks_dir
|
||||
]
|
||||
if self.host_name:
|
||||
# If host requires launch hooks and is module then launch hooks
|
||||
# should be collected using 'collect_launch_hook_paths'
|
||||
# - module have to implement 'get_launch_hook_paths'
|
||||
host_module = self.addons_manager.get_host_addon(self.host_name)
|
||||
if not host_module:
|
||||
hooks_dirs.append(os.path.join(
|
||||
AYON_CORE_ROOT, "hosts", self.host_name, "hooks"
|
||||
))
|
||||
|
||||
for path in hooks_dirs:
|
||||
if (
|
||||
os.path.exists(path)
|
||||
and os.path.isdir(path)
|
||||
and path not in paths
|
||||
):
|
||||
paths.append(path)
|
||||
|
||||
# Load modules paths
|
||||
paths.extend(self._collect_addons_launch_hook_paths())
|
||||
|
||||
return paths
|
||||
|
||||
def discover_launch_hooks(self, force=False):
|
||||
"""Load and prepare launch hooks."""
|
||||
if (
|
||||
self.prelaunch_hooks is not None
|
||||
or self.postlaunch_hooks is not None
|
||||
):
|
||||
if not force:
|
||||
self.log.info("Launch hooks were already discovered.")
|
||||
return
|
||||
|
||||
self.prelaunch_hooks.clear()
|
||||
self.postlaunch_hooks.clear()
|
||||
|
||||
self.log.debug("Discovery of launch hooks started.")
|
||||
|
||||
paths = self.paths_to_launch_hooks()
|
||||
self.log.debug("Paths searched for launch hooks:\n{}".format(
|
||||
"\n".join("- {}".format(path) for path in paths)
|
||||
))
|
||||
|
||||
all_classes = {
|
||||
"pre": [],
|
||||
"post": []
|
||||
}
|
||||
for path in paths:
|
||||
if not os.path.exists(path):
|
||||
self.log.info(
|
||||
"Path to launch hooks does not exist: \"{}\"".format(path)
|
||||
)
|
||||
continue
|
||||
|
||||
modules, _crashed = modules_from_path(path)
|
||||
for _filepath, module in modules:
|
||||
all_classes["pre"].extend(
|
||||
classes_from_module(PreLaunchHook, module)
|
||||
)
|
||||
all_classes["post"].extend(
|
||||
classes_from_module(PostLaunchHook, module)
|
||||
)
|
||||
|
||||
for launch_type, classes in all_classes.items():
|
||||
hooks_with_order = []
|
||||
hooks_without_order = []
|
||||
for klass in classes:
|
||||
try:
|
||||
hook = klass(self)
|
||||
if not hook.is_valid:
|
||||
self.log.debug(
|
||||
"Skipped hook invalid for current launch context: "
|
||||
"{}".format(klass.__name__)
|
||||
)
|
||||
continue
|
||||
|
||||
if inspect.isabstract(hook):
|
||||
self.log.debug("Skipped abstract hook: {}".format(
|
||||
klass.__name__
|
||||
))
|
||||
continue
|
||||
|
||||
# Separate hooks by pre/post class
|
||||
if hook.order is None:
|
||||
hooks_without_order.append(hook)
|
||||
else:
|
||||
hooks_with_order.append(hook)
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Initialization of hook failed: "
|
||||
"{}".format(klass.__name__),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Sort hooks with order by order
|
||||
ordered_hooks = list(sorted(
|
||||
hooks_with_order, key=lambda obj: obj.order
|
||||
))
|
||||
# Extend ordered hooks with hooks without defined order
|
||||
ordered_hooks.extend(hooks_without_order)
|
||||
|
||||
if launch_type == "pre":
|
||||
self.prelaunch_hooks = ordered_hooks
|
||||
else:
|
||||
self.postlaunch_hooks = ordered_hooks
|
||||
|
||||
self.log.debug("Found {} prelaunch and {} postlaunch hooks.".format(
|
||||
len(self.prelaunch_hooks), len(self.postlaunch_hooks)
|
||||
))
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
return self.application.name
|
||||
|
||||
@property
|
||||
def host_name(self):
|
||||
return self.application.host_name
|
||||
|
||||
@property
|
||||
def app_group(self):
|
||||
return self.application.group
|
||||
|
||||
@property
|
||||
def manager(self):
|
||||
return self.application.manager
|
||||
|
||||
def _run_process(self):
|
||||
# Windows and MacOS have easier process start
|
||||
low_platform = platform.system().lower()
|
||||
if low_platform in ("windows", "darwin"):
|
||||
return subprocess.Popen(self.launch_args, **self.kwargs)
|
||||
|
||||
# Linux uses mid process
|
||||
# - it is possible that the mid process executable is not
|
||||
# available for this version of AYON in that case use standard
|
||||
# launch
|
||||
launch_args = get_linux_launcher_args()
|
||||
if launch_args is None:
|
||||
return subprocess.Popen(self.launch_args, **self.kwargs)
|
||||
|
||||
# Prepare data that will be passed to midprocess
|
||||
# - store arguments to a json and pass path to json as last argument
|
||||
# - pass environments to set
|
||||
app_env = self.kwargs.pop("env", {})
|
||||
json_data = {
|
||||
"args": self.launch_args,
|
||||
"env": app_env
|
||||
}
|
||||
if app_env:
|
||||
# Filter environments of subprocess
|
||||
self.kwargs["env"] = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
if key in app_env
|
||||
}
|
||||
|
||||
# Create temp file
|
||||
json_temp = tempfile.NamedTemporaryFile(
|
||||
mode="w", prefix="op_app_args", suffix=".json", delete=False
|
||||
)
|
||||
json_temp.close()
|
||||
json_temp_filpath = json_temp.name
|
||||
with open(json_temp_filpath, "w") as stream:
|
||||
json.dump(json_data, stream)
|
||||
|
||||
launch_args.append(json_temp_filpath)
|
||||
|
||||
# Create mid-process which will launch application
|
||||
process = subprocess.Popen(launch_args, **self.kwargs)
|
||||
# Wait until the process finishes
|
||||
# - This is important! The process would stay in "open" state.
|
||||
process.wait()
|
||||
# Remove the temp file
|
||||
os.remove(json_temp_filpath)
|
||||
# Return process which is already terminated
|
||||
return process
|
||||
|
||||
def run_prelaunch_hooks(self):
|
||||
"""Run prelaunch hooks.
|
||||
|
||||
This method will be executed only once, any future calls will skip
|
||||
the processing.
|
||||
"""
|
||||
|
||||
if self._prelaunch_hooks_executed:
|
||||
self.log.warning("Prelaunch hooks were already executed.")
|
||||
return
|
||||
# Discover launch hooks
|
||||
self.discover_launch_hooks()
|
||||
|
||||
# Execute prelaunch hooks
|
||||
for prelaunch_hook in self.prelaunch_hooks:
|
||||
self.log.debug("Executing prelaunch hook: {}".format(
|
||||
str(prelaunch_hook.__class__.__name__)
|
||||
))
|
||||
prelaunch_hook.execute()
|
||||
self._prelaunch_hooks_executed = True
|
||||
|
||||
def launch(self):
|
||||
"""Collect data for new process and then create it.
|
||||
|
||||
This method must not be executed more than once.
|
||||
|
||||
Returns:
|
||||
subprocess.Popen: Created process as Popen object.
|
||||
"""
|
||||
if self.process is not None:
|
||||
self.log.warning("Application was already launched.")
|
||||
return
|
||||
|
||||
if not self._prelaunch_hooks_executed:
|
||||
self.run_prelaunch_hooks()
|
||||
|
||||
self.log.debug("All prelaunch hook executed. Starting new process.")
|
||||
|
||||
# Prepare subprocess args
|
||||
args_len_str = ""
|
||||
if isinstance(self.launch_args, str):
|
||||
args = self.launch_args
|
||||
else:
|
||||
args = self.clear_launch_args(self.launch_args)
|
||||
args_len_str = " ({})".format(len(args))
|
||||
self.log.info(
|
||||
"Launching \"{}\" with args{}: {}".format(
|
||||
self.application.full_name, args_len_str, args
|
||||
)
|
||||
)
|
||||
self.launch_args = args
|
||||
|
||||
# Run process
|
||||
self.process = self._run_process()
|
||||
|
||||
# Process post launch hooks
|
||||
for postlaunch_hook in self.postlaunch_hooks:
|
||||
self.log.debug("Executing postlaunch hook: {}".format(
|
||||
str(postlaunch_hook.__class__.__name__)
|
||||
))
|
||||
|
||||
# TODO how to handle errors?
|
||||
# - store to variable to let them accessible?
|
||||
try:
|
||||
postlaunch_hook.execute()
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"After launch procedures were not successful.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
self.log.debug("Launch of {} finished.".format(
|
||||
self.application.full_name
|
||||
))
|
||||
|
||||
return self.process
|
||||
|
||||
@staticmethod
|
||||
def clear_launch_args(args):
|
||||
"""Collect launch arguments to final order.
|
||||
|
||||
Launch argument should be list that may contain another lists this
|
||||
function will upack inner lists and keep ordering.
|
||||
|
||||
```
|
||||
# source
|
||||
[ [ arg1, [ arg2, arg3 ] ], arg4, [arg5, arg6]]
|
||||
# result
|
||||
[ arg1, arg2, arg3, arg4, arg5, arg6]
|
||||
|
||||
Args:
|
||||
args (list): Source arguments in list may contain inner lists.
|
||||
|
||||
Return:
|
||||
list: Unpacked arguments.
|
||||
"""
|
||||
if isinstance(args, str):
|
||||
return args
|
||||
all_cleared = False
|
||||
while not all_cleared:
|
||||
all_cleared = True
|
||||
new_args = []
|
||||
for arg in args:
|
||||
if isinstance(arg, (list, tuple, set)):
|
||||
all_cleared = False
|
||||
for _arg in arg:
|
||||
new_args.append(_arg)
|
||||
else:
|
||||
new_args.append(arg)
|
||||
args = new_args
|
||||
|
||||
return args
|
||||
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"""
|
||||
Run after global plugin 'CollectHostName' in ayon_core.
|
||||
|
||||
Requires:
|
||||
None
|
||||
|
||||
Provides:
|
||||
context -> hostName (str)
|
||||
context -> appName (str)
|
||||
context -> appLabel (str)
|
||||
"""
|
||||
import os
|
||||
import pyblish.api
|
||||
|
||||
from ayon_applications import ApplicationManager
|
||||
|
||||
|
||||
class CollectAppName(pyblish.api.ContextPlugin):
|
||||
"""Collect avalon host name to context."""
|
||||
|
||||
label = "Collect App Name"
|
||||
order = pyblish.api.CollectorOrder - 0.499999
|
||||
|
||||
def process(self, context):
|
||||
host_name = context.data.get("hostName")
|
||||
app_name = context.data.get("appName")
|
||||
app_label = context.data.get("appLabel")
|
||||
# Don't override value if is already set
|
||||
if host_name and app_name and app_label:
|
||||
return
|
||||
|
||||
# Use AYON_APP_NAME to get full app name
|
||||
if not app_name:
|
||||
app_name = os.environ.get("AYON_APP_NAME")
|
||||
|
||||
# Fill missing values based on app full name
|
||||
if (not host_name or not app_label) and app_name:
|
||||
app_manager = ApplicationManager()
|
||||
app = app_manager.applications.get(app_name)
|
||||
if app:
|
||||
if not host_name:
|
||||
host_name = app.host_name
|
||||
if not app_label:
|
||||
app_label = app.full_label
|
||||
|
||||
context.data["hostName"] = host_name
|
||||
context.data["appName"] = app_name
|
||||
context.data["appLabel"] = app_label
|
||||
609
client/ayon_core/addons/applications/ayon_applications/utils.py
Normal file
609
client/ayon_core/addons/applications/ayon_applications/utils.py
Normal file
|
|
@ -0,0 +1,609 @@
|
|||
import os
|
||||
import copy
|
||||
import json
|
||||
import platform
|
||||
import collections
|
||||
|
||||
import six
|
||||
import acre
|
||||
|
||||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.lib import Logger, get_ayon_username
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline import HOST_WORKFILE_EXTENSIONS
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
from ayon_core.pipeline.workfile import (
|
||||
get_workfile_template_key,
|
||||
get_workdir_with_workdir_data,
|
||||
get_last_workfile,
|
||||
should_use_last_workfile_on_launch,
|
||||
should_open_workfiles_tool_on_launch,
|
||||
)
|
||||
|
||||
from .constants import PLATFORM_NAMES, DEFAULT_ENV_SUBGROUP
|
||||
from .exceptions import MissingRequiredKey, ApplicationLaunchFailed
|
||||
from .manager import ApplicationManager
|
||||
|
||||
|
||||
def parse_environments(env_data, env_group=None, platform_name=None):
|
||||
"""Parse environment values from settings byt group and platform.
|
||||
|
||||
Data may contain up to 2 hierarchical levels of dictionaries. At the end
|
||||
of the last level must be string or list. List is joined using platform
|
||||
specific joiner (';' for windows and ':' for linux and mac).
|
||||
|
||||
Hierarchical levels can contain keys for subgroups and platform name.
|
||||
Platform specific values must be always last level of dictionary. Platform
|
||||
names are "windows" (MS Windows), "linux" (any linux distribution) and
|
||||
"darwin" (any MacOS distribution).
|
||||
|
||||
Subgroups are helpers added mainly for standard and on farm usage. Farm
|
||||
may require different environments for e.g. licence related values or
|
||||
plugins. Default subgroup is "standard".
|
||||
|
||||
Examples:
|
||||
```
|
||||
{
|
||||
# Unchanged value
|
||||
"ENV_KEY1": "value",
|
||||
# Empty values are kept (unset environment variable)
|
||||
"ENV_KEY2": "",
|
||||
|
||||
# Join list values with ':' or ';'
|
||||
"ENV_KEY3": ["value1", "value2"],
|
||||
|
||||
# Environment groups
|
||||
"ENV_KEY4": {
|
||||
"standard": "DEMO_SERVER_URL",
|
||||
"farm": "LICENCE_SERVER_URL"
|
||||
},
|
||||
|
||||
# Platform specific (and only for windows and mac)
|
||||
"ENV_KEY5": {
|
||||
"windows": "windows value",
|
||||
"darwin": ["value 1", "value 2"]
|
||||
},
|
||||
|
||||
# Environment groups and platform combination
|
||||
"ENV_KEY6": {
|
||||
"farm": "FARM_VALUE",
|
||||
"standard": {
|
||||
"windows": ["value1", "value2"],
|
||||
"linux": "value1",
|
||||
"darwin": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
output = {}
|
||||
if not env_data:
|
||||
return output
|
||||
|
||||
if not env_group:
|
||||
env_group = DEFAULT_ENV_SUBGROUP
|
||||
|
||||
if not platform_name:
|
||||
platform_name = platform.system().lower()
|
||||
|
||||
for key, value in env_data.items():
|
||||
if isinstance(value, dict):
|
||||
# Look if any key is platform key
|
||||
# - expect that represents environment group if does not contain
|
||||
# platform keys
|
||||
if not PLATFORM_NAMES.intersection(set(value.keys())):
|
||||
# Skip the key if group is not available
|
||||
if env_group not in value:
|
||||
continue
|
||||
value = value[env_group]
|
||||
|
||||
# Check again if value is dictionary
|
||||
# - this time there should be only platform keys
|
||||
if isinstance(value, dict):
|
||||
value = value.get(platform_name)
|
||||
|
||||
# Check if value is list and join it's values
|
||||
# QUESTION Should empty values be skipped?
|
||||
if isinstance(value, (list, tuple)):
|
||||
value = os.pathsep.join(value)
|
||||
|
||||
# Set key to output if value is string
|
||||
if isinstance(value, six.string_types):
|
||||
output[key] = value
|
||||
return output
|
||||
|
||||
|
||||
class EnvironmentPrepData(dict):
|
||||
"""Helper dictionary for storin temp data during environment prep.
|
||||
|
||||
Args:
|
||||
data (dict): Data must contain required keys.
|
||||
"""
|
||||
required_keys = (
|
||||
"project_entity", "folder_entity", "task_entity", "app", "anatomy"
|
||||
)
|
||||
|
||||
def __init__(self, data):
|
||||
for key in self.required_keys:
|
||||
if key not in data:
|
||||
raise MissingRequiredKey(key)
|
||||
|
||||
if not data.get("log"):
|
||||
data["log"] = Logger.get_logger("EnvironmentPrepData")
|
||||
|
||||
if data.get("env") is None:
|
||||
data["env"] = os.environ.copy()
|
||||
|
||||
project_name = data["project_entity"]["name"]
|
||||
if "project_settings" not in data:
|
||||
data["project_settings"] = get_project_settings(project_name)
|
||||
|
||||
super(EnvironmentPrepData, self).__init__(data)
|
||||
|
||||
|
||||
def get_app_environments_for_context(
|
||||
project_name,
|
||||
folder_path,
|
||||
task_name,
|
||||
app_name,
|
||||
env_group=None,
|
||||
launch_type=None,
|
||||
env=None,
|
||||
addons_manager=None
|
||||
):
|
||||
"""Prepare environment variables by context.
|
||||
Args:
|
||||
project_name (str): Name of project.
|
||||
folder_path (str): Folder path.
|
||||
task_name (str): Name of task.
|
||||
app_name (str): Name of application that is launched and can be found
|
||||
by ApplicationManager.
|
||||
env_group (Optional[str]): Name of environment group. If not passed
|
||||
default group is used.
|
||||
launch_type (Optional[str]): Type for which prelaunch hooks are
|
||||
executed.
|
||||
env (Optional[dict[str, str]]): Initial environment variables.
|
||||
`os.environ` is used when not passed.
|
||||
addons_manager (Optional[AddonsManager]): Initialized modules
|
||||
manager.
|
||||
|
||||
Returns:
|
||||
dict: Environments for passed context and application.
|
||||
"""
|
||||
|
||||
# Prepare app object which can be obtained only from ApplicationManager
|
||||
app_manager = ApplicationManager()
|
||||
context = app_manager.create_launch_context(
|
||||
app_name,
|
||||
project_name=project_name,
|
||||
folder_path=folder_path,
|
||||
task_name=task_name,
|
||||
env_group=env_group,
|
||||
launch_type=launch_type,
|
||||
env=env,
|
||||
addons_manager=addons_manager,
|
||||
modules_manager=addons_manager,
|
||||
)
|
||||
context.run_prelaunch_hooks()
|
||||
return context.env
|
||||
|
||||
|
||||
def _merge_env(env, current_env):
|
||||
"""Modified function(merge) from acre module."""
|
||||
result = current_env.copy()
|
||||
for key, value in env.items():
|
||||
# Keep missing keys by not filling `missing` kwarg
|
||||
value = acre.lib.partial_format(value, data=current_env)
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _add_python_version_paths(app, env, logger, addons_manager):
|
||||
"""Add vendor packages specific for a Python version."""
|
||||
|
||||
for addon in addons_manager.get_enabled_addons():
|
||||
addon.modify_application_launch_arguments(app, env)
|
||||
|
||||
# Skip adding if host name is not set
|
||||
if not app.host_name:
|
||||
return
|
||||
|
||||
# Add Python 2/3 modules
|
||||
python_vendor_dir = os.path.join(
|
||||
AYON_CORE_ROOT,
|
||||
"vendor",
|
||||
"python"
|
||||
)
|
||||
if app.use_python_2:
|
||||
pythonpath = os.path.join(python_vendor_dir, "python_2")
|
||||
else:
|
||||
pythonpath = os.path.join(python_vendor_dir, "python_3")
|
||||
|
||||
if not os.path.exists(pythonpath):
|
||||
return
|
||||
|
||||
logger.debug("Adding Python version specific paths to PYTHONPATH")
|
||||
python_paths = [pythonpath]
|
||||
|
||||
# Load PYTHONPATH from current launch context
|
||||
python_path = env.get("PYTHONPATH")
|
||||
if python_path:
|
||||
python_paths.append(python_path)
|
||||
|
||||
# Set new PYTHONPATH to launch context environments
|
||||
env["PYTHONPATH"] = os.pathsep.join(python_paths)
|
||||
|
||||
|
||||
def prepare_app_environments(
|
||||
data, env_group=None, implementation_envs=True, addons_manager=None
|
||||
):
|
||||
"""Modify launch environments based on launched app and context.
|
||||
|
||||
Args:
|
||||
data (EnvironmentPrepData): Dictionary where result and intermediate
|
||||
result will be stored.
|
||||
|
||||
"""
|
||||
app = data["app"]
|
||||
log = data["log"]
|
||||
source_env = data["env"].copy()
|
||||
|
||||
if addons_manager is None:
|
||||
addons_manager = AddonsManager()
|
||||
|
||||
_add_python_version_paths(app, source_env, log, addons_manager)
|
||||
|
||||
# Use environments from local settings
|
||||
filtered_local_envs = {}
|
||||
# NOTE Overrides for environment variables are not implemented in AYON.
|
||||
# project_settings = data["project_settings"]
|
||||
# whitelist_envs = project_settings["general"].get("local_env_white_list")
|
||||
# if whitelist_envs:
|
||||
# local_settings = get_local_settings()
|
||||
# local_envs = local_settings.get("environments") or {}
|
||||
# filtered_local_envs = {
|
||||
# key: value
|
||||
# for key, value in local_envs.items()
|
||||
# if key in whitelist_envs
|
||||
# }
|
||||
|
||||
# Apply local environment variables for already existing values
|
||||
for key, value in filtered_local_envs.items():
|
||||
if key in source_env:
|
||||
source_env[key] = value
|
||||
|
||||
# `app_and_tool_labels` has debug purpose
|
||||
app_and_tool_labels = [app.full_name]
|
||||
# Environments for application
|
||||
environments = [
|
||||
app.group.environment,
|
||||
app.environment
|
||||
]
|
||||
|
||||
folder_entity = data.get("folder_entity")
|
||||
# Add tools environments
|
||||
groups_by_name = {}
|
||||
tool_by_group_name = collections.defaultdict(dict)
|
||||
if folder_entity:
|
||||
# Make sure each tool group can be added only once
|
||||
for key in folder_entity["attrib"].get("tools") or []:
|
||||
tool = app.manager.tools.get(key)
|
||||
if not tool or not tool.is_valid_for_app(app):
|
||||
continue
|
||||
groups_by_name[tool.group.name] = tool.group
|
||||
tool_by_group_name[tool.group.name][tool.name] = tool
|
||||
|
||||
for group_name in sorted(groups_by_name.keys()):
|
||||
group = groups_by_name[group_name]
|
||||
environments.append(group.environment)
|
||||
for tool_name in sorted(tool_by_group_name[group_name].keys()):
|
||||
tool = tool_by_group_name[group_name][tool_name]
|
||||
environments.append(tool.environment)
|
||||
app_and_tool_labels.append(tool.full_name)
|
||||
|
||||
log.debug(
|
||||
"Will add environments for apps and tools: {}".format(
|
||||
", ".join(app_and_tool_labels)
|
||||
)
|
||||
)
|
||||
|
||||
env_values = {}
|
||||
for _env_values in environments:
|
||||
if not _env_values:
|
||||
continue
|
||||
|
||||
# Choose right platform
|
||||
tool_env = parse_environments(_env_values, env_group)
|
||||
|
||||
# Apply local environment variables
|
||||
# - must happen between all values because they may be used during
|
||||
# merge
|
||||
for key, value in filtered_local_envs.items():
|
||||
if key in tool_env:
|
||||
tool_env[key] = value
|
||||
|
||||
# Merge dictionaries
|
||||
env_values = _merge_env(tool_env, env_values)
|
||||
|
||||
merged_env = _merge_env(env_values, source_env)
|
||||
|
||||
loaded_env = acre.compute(merged_env, cleanup=False)
|
||||
|
||||
final_env = None
|
||||
# Add host specific environments
|
||||
if app.host_name and implementation_envs:
|
||||
host_addon = addons_manager.get_host_addon(app.host_name)
|
||||
add_implementation_envs = None
|
||||
if host_addon:
|
||||
add_implementation_envs = getattr(
|
||||
host_addon, "add_implementation_envs", None
|
||||
)
|
||||
if add_implementation_envs:
|
||||
# Function may only modify passed dict without returning value
|
||||
final_env = add_implementation_envs(loaded_env, app)
|
||||
|
||||
if final_env is None:
|
||||
final_env = loaded_env
|
||||
|
||||
keys_to_remove = set(source_env.keys()) - set(final_env.keys())
|
||||
|
||||
# Update env
|
||||
data["env"].update(final_env)
|
||||
for key in keys_to_remove:
|
||||
data["env"].pop(key, None)
|
||||
|
||||
|
||||
def apply_project_environments_value(
|
||||
project_name, env, project_settings=None, env_group=None
|
||||
):
|
||||
"""Apply project specific environments on passed environments.
|
||||
|
||||
The environments are applied on passed `env` argument value so it is not
|
||||
required to apply changes back.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project for which environments should be
|
||||
received.
|
||||
env (dict): Environment values on which project specific environments
|
||||
will be applied.
|
||||
project_settings (dict): Project settings for passed project name.
|
||||
Optional if project settings are already prepared.
|
||||
|
||||
Returns:
|
||||
dict: Passed env values with applied project environments.
|
||||
|
||||
Raises:
|
||||
KeyError: If project settings do not contain keys for project specific
|
||||
environments.
|
||||
|
||||
"""
|
||||
if project_settings is None:
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
env_value = project_settings["core"]["project_environments"]
|
||||
if env_value:
|
||||
env_value = json.loads(env_value)
|
||||
parsed_value = parse_environments(env_value, env_group)
|
||||
env.update(acre.compute(
|
||||
_merge_env(parsed_value, env),
|
||||
cleanup=False
|
||||
))
|
||||
return env
|
||||
|
||||
|
||||
def prepare_context_environments(data, env_group=None, addons_manager=None):
|
||||
"""Modify launch environments with context data for launched host.
|
||||
|
||||
Args:
|
||||
data (EnvironmentPrepData): Dictionary where result and intermediate
|
||||
result will be stored.
|
||||
|
||||
"""
|
||||
# Context environments
|
||||
log = data["log"]
|
||||
|
||||
project_entity = data["project_entity"]
|
||||
folder_entity = data["folder_entity"]
|
||||
task_entity = data["task_entity"]
|
||||
if not project_entity:
|
||||
log.info(
|
||||
"Skipping context environments preparation."
|
||||
" Launch context does not contain required data."
|
||||
)
|
||||
return
|
||||
|
||||
# Load project specific environments
|
||||
project_name = project_entity["name"]
|
||||
project_settings = get_project_settings(project_name)
|
||||
data["project_settings"] = project_settings
|
||||
|
||||
app = data["app"]
|
||||
context_env = {
|
||||
"AYON_PROJECT_NAME": project_entity["name"],
|
||||
"AYON_APP_NAME": app.full_name
|
||||
}
|
||||
if folder_entity:
|
||||
folder_path = folder_entity["path"]
|
||||
context_env["AYON_FOLDER_PATH"] = folder_path
|
||||
|
||||
if task_entity:
|
||||
context_env["AYON_TASK_NAME"] = task_entity["name"]
|
||||
|
||||
log.debug(
|
||||
"Context environments set:\n{}".format(
|
||||
json.dumps(context_env, indent=4)
|
||||
)
|
||||
)
|
||||
data["env"].update(context_env)
|
||||
|
||||
# Apply project specific environments on current env value
|
||||
# - apply them once the context environments are set
|
||||
apply_project_environments_value(
|
||||
project_name, data["env"], project_settings, env_group
|
||||
)
|
||||
|
||||
if not app.is_host:
|
||||
return
|
||||
|
||||
data["env"]["AYON_HOST_NAME"] = app.host_name
|
||||
|
||||
if not folder_entity or not task_entity:
|
||||
# QUESTION replace with log.info and skip workfile discovery?
|
||||
# - technically it should be possible to launch host without context
|
||||
raise ApplicationLaunchFailed(
|
||||
"Host launch require folder and task context."
|
||||
)
|
||||
|
||||
workdir_data = get_template_data(
|
||||
project_entity,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
app.host_name,
|
||||
project_settings
|
||||
)
|
||||
data["workdir_data"] = workdir_data
|
||||
|
||||
anatomy = data["anatomy"]
|
||||
|
||||
task_type = workdir_data["task"]["type"]
|
||||
# Temp solution how to pass task type to `_prepare_last_workfile`
|
||||
data["task_type"] = task_type
|
||||
|
||||
try:
|
||||
workdir = get_workdir_with_workdir_data(
|
||||
workdir_data,
|
||||
anatomy.project_name,
|
||||
anatomy,
|
||||
project_settings=project_settings
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Error in anatomy.format: {}".format(str(exc))
|
||||
)
|
||||
|
||||
if not os.path.exists(workdir):
|
||||
log.debug(
|
||||
"Creating workdir folder: \"{}\"".format(workdir)
|
||||
)
|
||||
try:
|
||||
os.makedirs(workdir)
|
||||
except Exception as exc:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Couldn't create workdir because: {}".format(str(exc))
|
||||
)
|
||||
|
||||
data["env"]["AYON_WORKDIR"] = workdir
|
||||
|
||||
_prepare_last_workfile(data, workdir, addons_manager)
|
||||
|
||||
|
||||
def _prepare_last_workfile(data, workdir, addons_manager):
|
||||
"""last workfile workflow preparation.
|
||||
|
||||
Function check if should care about last workfile workflow and tries
|
||||
to find the last workfile. Both information are stored to `data` and
|
||||
environments.
|
||||
|
||||
Last workfile is filled always (with version 1) even if any workfile
|
||||
exists yet.
|
||||
|
||||
Args:
|
||||
data (EnvironmentPrepData): Dictionary where result and intermediate
|
||||
result will be stored.
|
||||
workdir (str): Path to folder where workfiles should be stored.
|
||||
|
||||
"""
|
||||
if not addons_manager:
|
||||
addons_manager = AddonsManager()
|
||||
|
||||
log = data["log"]
|
||||
|
||||
_workdir_data = data.get("workdir_data")
|
||||
if not _workdir_data:
|
||||
log.info(
|
||||
"Skipping last workfile preparation."
|
||||
" Key `workdir_data` not filled."
|
||||
)
|
||||
return
|
||||
|
||||
app = data["app"]
|
||||
workdir_data = copy.deepcopy(_workdir_data)
|
||||
project_name = data["project_name"]
|
||||
task_name = data["task_name"]
|
||||
task_type = data["task_type"]
|
||||
|
||||
start_last_workfile = data.get("start_last_workfile")
|
||||
if start_last_workfile is None:
|
||||
start_last_workfile = should_use_last_workfile_on_launch(
|
||||
project_name, app.host_name, task_name, task_type
|
||||
)
|
||||
else:
|
||||
log.info("Opening of last workfile was disabled by user")
|
||||
|
||||
data["start_last_workfile"] = start_last_workfile
|
||||
|
||||
workfile_startup = should_open_workfiles_tool_on_launch(
|
||||
project_name, app.host_name, task_name, task_type
|
||||
)
|
||||
data["workfile_startup"] = workfile_startup
|
||||
|
||||
# Store boolean as "0"(False) or "1"(True)
|
||||
data["env"]["AVALON_OPEN_LAST_WORKFILE"] = (
|
||||
str(int(bool(start_last_workfile)))
|
||||
)
|
||||
data["env"]["AYON_WORKFILE_TOOL_ON_START"] = (
|
||||
str(int(bool(workfile_startup)))
|
||||
)
|
||||
|
||||
_sub_msg = "" if start_last_workfile else " not"
|
||||
log.debug(
|
||||
"Last workfile should{} be opened on start.".format(_sub_msg)
|
||||
)
|
||||
|
||||
# Last workfile path
|
||||
last_workfile_path = data.get("last_workfile_path") or ""
|
||||
if not last_workfile_path:
|
||||
host_addon = addons_manager.get_host_addon(app.host_name)
|
||||
if host_addon:
|
||||
extensions = host_addon.get_workfile_extensions()
|
||||
else:
|
||||
extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name)
|
||||
|
||||
if extensions:
|
||||
anatomy = data["anatomy"]
|
||||
project_settings = data["project_settings"]
|
||||
task_type = workdir_data["task"]["type"]
|
||||
template_key = get_workfile_template_key(
|
||||
project_name,
|
||||
task_type,
|
||||
app.host_name,
|
||||
project_settings=project_settings
|
||||
)
|
||||
# Find last workfile
|
||||
file_template = anatomy.get_template_item(
|
||||
"work", template_key, "file"
|
||||
).template
|
||||
|
||||
workdir_data.update({
|
||||
"version": 1,
|
||||
"user": get_ayon_username(),
|
||||
"ext": extensions[0]
|
||||
})
|
||||
|
||||
last_workfile_path = get_last_workfile(
|
||||
workdir, file_template, workdir_data, extensions, True
|
||||
)
|
||||
|
||||
if os.path.exists(last_workfile_path):
|
||||
log.debug((
|
||||
"Workfiles for launch context does not exists"
|
||||
" yet but path will be set."
|
||||
))
|
||||
log.debug(
|
||||
"Setting last workfile path: {}".format(last_workfile_path)
|
||||
)
|
||||
|
||||
data["env"]["AYON_LAST_WORKFILE"] = last_workfile_path
|
||||
data["last_workfile_path"] = last_workfile_path
|
||||
|
|
@ -4,6 +4,7 @@ import os
|
|||
import sys
|
||||
import code
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import acre
|
||||
|
|
@ -11,7 +12,7 @@ import acre
|
|||
from ayon_core import AYON_CORE_ROOT
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.settings import get_general_environments
|
||||
from ayon_core.lib import initialize_ayon_connection
|
||||
from ayon_core.lib import initialize_ayon_connection, is_running_from_build
|
||||
|
||||
from .cli_commands import Commands
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ main_cli.set_alias("addon", "module")
|
|||
@main_cli.command()
|
||||
@click.argument("output_json_path")
|
||||
@click.option("--project", help="Project name", default=None)
|
||||
@click.option("--asset", help="Asset name", default=None)
|
||||
@click.option("--asset", help="Folder path", default=None)
|
||||
@click.option("--task", help="Task name", default=None)
|
||||
@click.option("--app", help="Application name", default=None)
|
||||
@click.option(
|
||||
|
|
@ -96,6 +97,10 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup):
|
|||
environments will be extracted.
|
||||
|
||||
Context options are "project", "asset", "task", "app"
|
||||
|
||||
Deprecated:
|
||||
This function is deprecated and will be removed in future. Please use
|
||||
'addon applications extractenvironments ...' instead.
|
||||
"""
|
||||
Commands.extractenvironments(
|
||||
output_json_path, project, asset, task, app, envgroup
|
||||
|
|
@ -127,7 +132,7 @@ def publish_report_viewer():
|
|||
@main_cli.command()
|
||||
@click.argument("output_path")
|
||||
@click.option("--project", help="Define project context")
|
||||
@click.option("--asset", help="Define asset in project (project must be set)")
|
||||
@click.option("--folder", help="Define folder in project (project must be set)")
|
||||
@click.option(
|
||||
"--strict",
|
||||
is_flag=True,
|
||||
|
|
@ -136,18 +141,18 @@ def publish_report_viewer():
|
|||
def contextselection(
|
||||
output_path,
|
||||
project,
|
||||
asset,
|
||||
folder,
|
||||
strict
|
||||
):
|
||||
"""Show Qt dialog to select context.
|
||||
|
||||
Context is project name, asset name and task name. The result is stored
|
||||
Context is project name, folder path and task name. The result is stored
|
||||
into json file which path is passed in first argument.
|
||||
"""
|
||||
Commands.contextselection(
|
||||
output_path,
|
||||
project,
|
||||
asset,
|
||||
folder,
|
||||
strict
|
||||
)
|
||||
|
||||
|
|
@ -163,16 +168,27 @@ def run(script):
|
|||
|
||||
if not script:
|
||||
print("Error: missing path to script file.")
|
||||
return
|
||||
|
||||
# Remove first argument if it is the same as AYON executable
|
||||
# - Forward compatibility with future AYON versions.
|
||||
# - Current AYON launcher keeps the arguments with first argument but
|
||||
# future versions might remove it.
|
||||
first_arg = sys.argv[0]
|
||||
if is_running_from_build():
|
||||
comp_path = os.path.join(os.environ["AYON_ROOT"], "start.py")
|
||||
else:
|
||||
comp_path = os.getenv("AYON_EXECUTABLE")
|
||||
# Compare paths and remove first argument if it is the same as AYON
|
||||
if Path(first_arg).resolve() == Path(comp_path).resolve():
|
||||
sys.argv.pop(0)
|
||||
|
||||
args = sys.argv
|
||||
args.remove("run")
|
||||
args.remove(script)
|
||||
sys.argv = args
|
||||
# Remove 'run' command from sys.argv
|
||||
sys.argv.remove("run")
|
||||
|
||||
args_string = " ".join(args[1:])
|
||||
print(f"... running: {script} {args_string}")
|
||||
runpy.run_path(script, run_name="__main__", )
|
||||
args_string = " ".join(sys.argv[1:])
|
||||
print(f"... running: {script} {args_string}")
|
||||
runpy.run_path(script, run_name="__main__")
|
||||
|
||||
|
||||
@main_cli.command()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
"""Implementation of AYON commands."""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import warnings
|
||||
|
||||
|
||||
|
|
@ -58,10 +57,7 @@ class Commands:
|
|||
|
||||
"""
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.lib.applications import (
|
||||
get_app_environments_for_context,
|
||||
LaunchTypes,
|
||||
)
|
||||
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline import (
|
||||
install_ayon_plugins,
|
||||
|
|
@ -69,7 +65,6 @@ class Commands:
|
|||
)
|
||||
|
||||
# Register target and host
|
||||
import pyblish.api
|
||||
import pyblish.util
|
||||
|
||||
if not isinstance(path, str):
|
||||
|
|
@ -100,15 +95,13 @@ class Commands:
|
|||
for plugin_path in publish_paths:
|
||||
pyblish.api.register_plugin_path(plugin_path)
|
||||
|
||||
app_full_name = os.getenv("AYON_APP_NAME")
|
||||
if app_full_name:
|
||||
applications_addon = manager.get_enabled_addon("applications")
|
||||
if applications_addon is not None:
|
||||
context = get_global_context()
|
||||
env = get_app_environments_for_context(
|
||||
env = applications_addon.get_farm_publish_environment_variables(
|
||||
context["project_name"],
|
||||
context["folder_path"],
|
||||
context["task_name"],
|
||||
app_full_name,
|
||||
launch_type=LaunchTypes.farm_publish,
|
||||
)
|
||||
os.environ.update(env)
|
||||
|
||||
|
|
@ -150,36 +143,36 @@ class Commands:
|
|||
log.info("Publish finished.")
|
||||
|
||||
@staticmethod
|
||||
def extractenvironments(output_json_path, project, asset, task, app,
|
||||
env_group):
|
||||
def extractenvironments(
|
||||
output_json_path, project, asset, task, app, env_group
|
||||
):
|
||||
"""Produces json file with environment based on project and app.
|
||||
|
||||
Called by Deadline plugin to propagate environment into render jobs.
|
||||
"""
|
||||
|
||||
from ayon_core.lib.applications import (
|
||||
get_app_environments_for_context,
|
||||
LaunchTypes,
|
||||
from ayon_core.addon import AddonsManager
|
||||
|
||||
warnings.warn(
|
||||
(
|
||||
"Command 'extractenvironments' is deprecated and will be"
|
||||
" removed in future. Please use "
|
||||
"'addon applications extractenvironments ...' instead."
|
||||
),
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
if all((project, asset, task, app)):
|
||||
env = get_app_environments_for_context(
|
||||
project,
|
||||
asset,
|
||||
task,
|
||||
app,
|
||||
env_group=env_group,
|
||||
launch_type=LaunchTypes.farm_render
|
||||
addons_manager = AddonsManager()
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
if applications_addon is None:
|
||||
raise RuntimeError(
|
||||
"Applications addon is not available or enabled."
|
||||
)
|
||||
else:
|
||||
env = os.environ.copy()
|
||||
|
||||
output_dir = os.path.dirname(output_json_path)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
with open(output_json_path, "w") as file_stream:
|
||||
json.dump(env, file_stream, indent=4)
|
||||
# Please ignore the fact this is using private method
|
||||
applications_addon._cli_extract_environments(
|
||||
output_json_path, project, asset, task, app, env_group
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def contextselection(output_path, project_name, folder_path, strict):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import shutil
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.pipeline.workfile import (
|
||||
get_custom_workfile_template,
|
||||
get_custom_workfile_template_by_string_context
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.pipeline.workfile import create_workdir_extra_folders
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from ayon_api import get_project, get_folder_by_path, get_task_by_name
|
||||
|
||||
from ayon_core.lib.applications import (
|
||||
PreLaunchHook,
|
||||
from ayon_applications import PreLaunchHook
|
||||
from ayon_applications.utils import (
|
||||
EnvironmentPrepData,
|
||||
prepare_app_environments,
|
||||
prepare_context_environments
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class LaunchWithTerminal(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import subprocess
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class LaunchNewConsoleApps(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ayon_core.lib.applications import PreLaunchHook
|
||||
from ayon_applications import PreLaunchHook
|
||||
|
||||
from ayon_core.pipeline.colorspace import get_imageio_config
|
||||
from ayon_core.pipeline.template_data import get_template_data_with_names
|
||||
|
|
|
|||
|
|
@ -36,23 +36,23 @@ class HostDirmap(object):
|
|||
host_name,
|
||||
project_name,
|
||||
project_settings=None,
|
||||
sync_module=None
|
||||
sitesync_addon=None
|
||||
):
|
||||
self.host_name = host_name
|
||||
self.project_name = project_name
|
||||
self._project_settings = project_settings
|
||||
self._sync_module = sync_module
|
||||
self._sitesync_addon = sitesync_addon
|
||||
# to limit reinit of Modules
|
||||
self._sync_module_discovered = sync_module is not None
|
||||
self._sitesync_addon_discovered = sitesync_addon is not None
|
||||
self._log = None
|
||||
|
||||
@property
|
||||
def sync_module(self):
|
||||
if not self._sync_module_discovered:
|
||||
self._sync_module_discovered = True
|
||||
def sitesync_addon(self):
|
||||
if not self._sitesync_addon_discovered:
|
||||
self._sitesync_addon_discovered = True
|
||||
manager = AddonsManager()
|
||||
self._sync_module = manager.get("sync_server")
|
||||
return self._sync_module
|
||||
self._sitesync_addon = manager.get("sitesync")
|
||||
return self._sitesync_addon
|
||||
|
||||
@property
|
||||
def project_settings(self):
|
||||
|
|
@ -158,25 +158,25 @@ class HostDirmap(object):
|
|||
"""
|
||||
project_name = self.project_name
|
||||
|
||||
sync_module = self.sync_module
|
||||
sitesync_addon = self.sitesync_addon
|
||||
mapping = {}
|
||||
if (
|
||||
sync_module is None
|
||||
or not sync_module.enabled
|
||||
or project_name not in sync_module.get_enabled_projects()
|
||||
sitesync_addon is None
|
||||
or not sitesync_addon.enabled
|
||||
or project_name not in sitesync_addon.get_enabled_projects()
|
||||
):
|
||||
return mapping
|
||||
|
||||
active_site = sync_module.get_local_normalized_site(
|
||||
sync_module.get_active_site(project_name))
|
||||
remote_site = sync_module.get_local_normalized_site(
|
||||
sync_module.get_remote_site(project_name))
|
||||
active_site = sitesync_addon.get_local_normalized_site(
|
||||
sitesync_addon.get_active_site(project_name))
|
||||
remote_site = sitesync_addon.get_local_normalized_site(
|
||||
sitesync_addon.get_remote_site(project_name))
|
||||
self.log.debug(
|
||||
"active {} - remote {}".format(active_site, remote_site)
|
||||
)
|
||||
|
||||
if active_site == "local" and active_site != remote_site:
|
||||
sync_settings = sync_module.get_sync_project_setting(
|
||||
sync_settings = sitesync_addon.get_sync_project_setting(
|
||||
project_name,
|
||||
exclude_locals=False,
|
||||
cached=False)
|
||||
|
|
@ -194,7 +194,7 @@ class HostDirmap(object):
|
|||
self.log.debug("remote overrides {}".format(remote_overrides))
|
||||
|
||||
current_platform = platform.system().lower()
|
||||
remote_provider = sync_module.get_provider_for_site(
|
||||
remote_provider = sitesync_addon.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
# dirmap has sense only with regular disk provider, in the workfile
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ __all__ = [
|
|||
"get_stub",
|
||||
|
||||
# pipeline
|
||||
"AfterEffectsHost",
|
||||
"ls",
|
||||
"containerise",
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ from ayon_core.lib import (
|
|||
get_ayon_launcher_args,
|
||||
is_using_ayon_console,
|
||||
)
|
||||
from ayon_core.lib.applications import (
|
||||
PreLaunchHook,
|
||||
LaunchTypes,
|
||||
)
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts.aftereffects import get_launch_script_path
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import attr
|
||||
|
||||
import attr
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline.publish import RenderInstance
|
||||
|
||||
from ayon_core.hosts.aftereffects.api import get_stub
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ def _process_app_events() -> Optional[float]:
|
|||
|
||||
|
||||
class LaunchQtApp(bpy.types.Operator):
|
||||
"""A Base class for opertors to launch a Qt app."""
|
||||
"""A Base class for operators to launch a Qt app."""
|
||||
|
||||
_app: QtWidgets.QApplication
|
||||
_window = Union[QtWidgets.QDialog, ModuleType]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from pathlib import Path
|
||||
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class AddPythonScriptToLaunchArgs(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
import re
|
||||
import subprocess
|
||||
from platform import system
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InstallPySideToBlender(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import subprocess
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class BlenderConsoleWindows(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ class BlendLoader(plugin.AssetLoader):
|
|||
obj.animation_data_create()
|
||||
obj.animation_data.action = actions[obj.name]
|
||||
|
||||
# Restore the old data, but reset memebers, as they don't exist anymore
|
||||
# Restore the old data, but reset members, as they don't exist anymore
|
||||
# This avoids a crash, because the memory addresses of those members
|
||||
# are not valid anymore
|
||||
old_data["members"] = []
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import bpy
|
|||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.blender.api import plugin
|
||||
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import bpy
|
|||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.blender.api import plugin
|
||||
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
|
|||
tree = bpy.context.scene.node_tree
|
||||
output_type = "CompositorNodeOutputFile"
|
||||
output_node = None
|
||||
# Remove all output nodes that inlcude "AYON" in the name.
|
||||
# Remove all output nodes that include "AYON" in the name.
|
||||
# There should be only one.
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == output_type and "AYON" in node.name:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
|
|||
import ayon_core.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
|
||||
class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Ensure that meshes don't have a negative scale."""
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import shutil
|
|||
import winreg
|
||||
import subprocess
|
||||
from ayon_core.lib import get_ayon_launcher_args
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts.celaction import CELACTION_ROOT_DIR
|
||||
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
def workfile_path(self):
|
||||
workfile_path = self.data["last_workfile_path"]
|
||||
|
||||
# copy workfile from template if doesnt exist any on path
|
||||
# copy workfile from template if doesn't exist any on path
|
||||
if not os.path.exists(workfile_path):
|
||||
# TODO add ability to set different template workfile path via
|
||||
# settings
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import sys
|
|||
from pprint import pformat
|
||||
|
||||
|
||||
class CollectCelactionCliKwargs(pyblish.api.Collector):
|
||||
class CollectCelactionCliKwargs(pyblish.api.ContextPlugin):
|
||||
""" Collects all keyword arguments passed from the terminal """
|
||||
|
||||
label = "Collect Celaction Cli Kwargs"
|
||||
order = pyblish.api.Collector.order - 0.1
|
||||
order = pyblish.api.CollectorOrder - 0.1
|
||||
|
||||
def process(self, context):
|
||||
args = list(sys.argv[1:])
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
render_path = r_template_item["path"].format_strict(anatomy_data)
|
||||
self.log.debug("__ render_path: `{}`".format(render_path))
|
||||
|
||||
# create dir if it doesnt exists
|
||||
# create dir if it doesn't exists
|
||||
try:
|
||||
if not os.path.isdir(render_dir):
|
||||
os.makedirs(render_dir, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from .lib import (
|
|||
reset_segment_selection,
|
||||
get_segment_attributes,
|
||||
get_clips_in_reels,
|
||||
get_reformated_filename,
|
||||
get_reformatted_filename,
|
||||
get_frame_from_filename,
|
||||
get_padding_from_filename,
|
||||
maintained_object_duplication,
|
||||
|
|
@ -101,7 +101,7 @@ __all__ = [
|
|||
"reset_segment_selection",
|
||||
"get_segment_attributes",
|
||||
"get_clips_in_reels",
|
||||
"get_reformated_filename",
|
||||
"get_reformatted_filename",
|
||||
"get_frame_from_filename",
|
||||
"get_padding_from_filename",
|
||||
"maintained_object_duplication",
|
||||
|
|
|
|||
|
|
@ -607,7 +607,7 @@ def get_clips_in_reels(project):
|
|||
return output_clips
|
||||
|
||||
|
||||
def get_reformated_filename(filename, padded=True):
|
||||
def get_reformatted_filename(filename, padded=True):
|
||||
"""
|
||||
Return fixed python expression path
|
||||
|
||||
|
|
@ -615,10 +615,10 @@ def get_reformated_filename(filename, padded=True):
|
|||
filename (str): file name
|
||||
|
||||
Returns:
|
||||
type: string with reformated path
|
||||
type: string with reformatted path
|
||||
|
||||
Example:
|
||||
get_reformated_filename("plate.1001.exr") > plate.%04d.exr
|
||||
get_reformatted_filename("plate.1001.exr") > plate.%04d.exr
|
||||
|
||||
"""
|
||||
found = FRAME_PATTERN.search(filename)
|
||||
|
|
@ -980,7 +980,7 @@ class MediaInfoFile(object):
|
|||
|
||||
@property
|
||||
def file_pattern(self):
|
||||
"""Clips file patter
|
||||
"""Clips file pattern.
|
||||
|
||||
Returns:
|
||||
str: file pattern. ex. file.[1-2].exr
|
||||
|
|
|
|||
|
|
@ -644,13 +644,13 @@ class PublishableClip:
|
|||
"families": [self.base_product_type, self.product_type]
|
||||
}
|
||||
|
||||
def _convert_to_entity(self, type, template):
|
||||
def _convert_to_entity(self, src_type, template):
|
||||
""" Converting input key to key with type. """
|
||||
# convert to entity type
|
||||
entity_type = self.types.get(type, None)
|
||||
folder_type = self.types.get(src_type, None)
|
||||
|
||||
assert entity_type, "Missing entity type for `{}`".format(
|
||||
type
|
||||
assert folder_type, "Missing folder type for `{}`".format(
|
||||
src_type
|
||||
)
|
||||
|
||||
# first collect formatting data to use for formatting template
|
||||
|
|
@ -661,7 +661,7 @@ class PublishableClip:
|
|||
formatting_data[_k] = value
|
||||
|
||||
return {
|
||||
"entity_type": entity_type,
|
||||
"folder_type": folder_type,
|
||||
"entity_name": template.format(
|
||||
**formatting_data
|
||||
)
|
||||
|
|
@ -1018,7 +1018,7 @@ class OpenClipSolver(flib.MediaInfoFile):
|
|||
self.feed_version_name))
|
||||
else:
|
||||
self.log.debug("adding new track element ..")
|
||||
# create new track as it doesnt exists yet
|
||||
# create new track as it doesn't exist yet
|
||||
# set current version to feeds on tmp
|
||||
tmp_xml_feeds = tmp_xml_track.find('feeds')
|
||||
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from ayon_core.lib import (
|
|||
get_ayon_username,
|
||||
run_subprocess,
|
||||
)
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts import flame as opflame
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ def create_otio_reference(clip_data, fps=None):
|
|||
|
||||
if not otio_ex_ref_item:
|
||||
dirname, file_name = os.path.split(path)
|
||||
file_name = utils.get_reformated_filename(file_name, padded=False)
|
||||
file_name = utils.get_reformatted_filename(file_name, padded=False)
|
||||
reformated_path = os.path.join(dirname, file_name)
|
||||
# in case old OTIO or video file create `ExternalReference`
|
||||
otio_ex_ref_item = otio.schema.ExternalReference(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def frames_to_seconds(frames, framerate):
|
|||
return otio.opentime.to_seconds(rt)
|
||||
|
||||
|
||||
def get_reformated_filename(filename, padded=True):
|
||||
def get_reformatted_filename(filename, padded=True):
|
||||
"""
|
||||
Return fixed python expression path
|
||||
|
||||
|
|
@ -29,10 +29,10 @@ def get_reformated_filename(filename, padded=True):
|
|||
filename (str): file name
|
||||
|
||||
Returns:
|
||||
type: string with reformated path
|
||||
type: string with reformatted path
|
||||
|
||||
Example:
|
||||
get_reformated_filename("plate.1001.exr") > plate.%04d.exr
|
||||
get_reformatted_filename("plate.1001.exr") > plate.%04d.exr
|
||||
|
||||
"""
|
||||
found = FRAME_PATTERN.search(filename)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class CreateShotClip(opfapi.Creator):
|
|||
presets = deepcopy(self.presets)
|
||||
gui_inputs = self.get_gui_inputs()
|
||||
|
||||
# get key pares from presets and match it on ui inputs
|
||||
# get key pairs from presets and match it on ui inputs
|
||||
for k, v in gui_inputs.items():
|
||||
if v["type"] in ("dict", "section"):
|
||||
# nested dictionary (only one level allowed
|
||||
|
|
@ -236,7 +236,7 @@ class CreateShotClip(opfapi.Creator):
|
|||
"type": "QCheckBox",
|
||||
"label": "Source resolution",
|
||||
"target": "tag",
|
||||
"toolTip": "Is resloution taken from timeline or source?", # noqa
|
||||
"toolTip": "Is resolution taken from timeline or source?", # noqa
|
||||
"order": 4},
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
self.otio_timeline = context.data["otioTimeline"]
|
||||
self.fps = context.data["fps"]
|
||||
|
||||
# process all sellected
|
||||
# process all selected
|
||||
for segment in selected_segments:
|
||||
# get openpype tag data
|
||||
marker_data = opfapi.get_segment_data_marker(segment)
|
||||
|
|
@ -100,6 +100,12 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
marker_data["handleEnd"] = min(
|
||||
marker_data["handleEnd"], tail)
|
||||
|
||||
# Backward compatibility fix of 'entity_type' > 'folder_type'
|
||||
if "parents" in marker_data:
|
||||
for parent in marker_data["parents"]:
|
||||
if "entity_type" in parent:
|
||||
parent["folder_type"] = parent.pop("entity_type")
|
||||
|
||||
workfile_start = self._set_workfile_start(marker_data)
|
||||
|
||||
with_audio = bool(marker_data.pop("audio"))
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ class FtrackEntityOperator:
|
|||
|
||||
entity = session.query(query).first()
|
||||
|
||||
# if entity doesnt exist then create one
|
||||
# if entity doesn't exist then create one
|
||||
if not entity:
|
||||
entity = self.create_ftrack_entity(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import contextlib
|
|||
|
||||
from ayon_core.lib import Logger
|
||||
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline import registered_host
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self._project = None
|
||||
|
|
@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True,
|
|||
comp.SetAttrs(attrs)
|
||||
|
||||
|
||||
def set_current_context_framerange():
|
||||
def set_current_context_framerange(folder_entity=None):
|
||||
"""Set Comp's frame range based on current folder."""
|
||||
folder_entity = get_current_project_folder()
|
||||
if folder_entity is None:
|
||||
folder_entity = get_current_folder_entity(
|
||||
fields={"attrib.frameStart",
|
||||
"attrib.frameEnd",
|
||||
"attrib.handleStart",
|
||||
"attrib.handleEnd"})
|
||||
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
start = folder_attributes["frameStart"]
|
||||
end = folder_attributes["frameEnd"]
|
||||
|
|
@ -65,9 +73,24 @@ def set_current_context_framerange():
|
|||
handle_end=handle_end)
|
||||
|
||||
|
||||
def set_current_context_resolution():
|
||||
def set_current_context_fps(folder_entity=None):
|
||||
"""Set Comp's frame rate (FPS) to based on current asset"""
|
||||
if folder_entity is None:
|
||||
folder_entity = get_current_folder_entity(fields={"attrib.fps"})
|
||||
|
||||
fps = float(folder_entity["attrib"].get("fps", 24.0))
|
||||
comp = get_current_comp()
|
||||
comp.SetPrefs({
|
||||
"Comp.FrameFormat.Rate": fps,
|
||||
})
|
||||
|
||||
|
||||
def set_current_context_resolution(folder_entity=None):
|
||||
"""Set Comp's resolution width x height default based on current folder"""
|
||||
folder_entity = get_current_project_folder()
|
||||
if folder_entity is None:
|
||||
folder_entity = get_current_folder_entity(
|
||||
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"})
|
||||
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
width = folder_attributes["resolutionWidth"]
|
||||
height = folder_attributes["resolutionHeight"]
|
||||
|
|
@ -101,7 +124,7 @@ def validate_comp_prefs(comp=None, force_repair=False):
|
|||
"attrib.resolutionHeight",
|
||||
"attrib.pixelAspect",
|
||||
}
|
||||
folder_entity = get_current_project_folder(fields=fields)
|
||||
folder_entity = get_current_folder_entity(fields=fields)
|
||||
folder_path = folder_entity["path"]
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
|
||||
|
|
@ -285,3 +308,98 @@ def comp_lock_and_undo_chunk(
|
|||
finally:
|
||||
comp.Unlock()
|
||||
comp.EndUndo(keep_undo)
|
||||
|
||||
|
||||
def update_content_on_context_change():
|
||||
"""Update all Creator instances to current asset"""
|
||||
host = registered_host()
|
||||
context = host.get_current_context()
|
||||
|
||||
folder_path = context["folder_path"]
|
||||
task = context["task_name"]
|
||||
|
||||
create_context = CreateContext(host, reset=True)
|
||||
|
||||
for instance in create_context.instances:
|
||||
instance_folder_path = instance.get("folderPath")
|
||||
if instance_folder_path and instance_folder_path != folder_path:
|
||||
instance["folderPath"] = folder_path
|
||||
instance_task = instance.get("task")
|
||||
if instance_task and instance_task != task:
|
||||
instance["task"] = task
|
||||
|
||||
create_context.save_changes()
|
||||
|
||||
|
||||
def prompt_reset_context():
|
||||
"""Prompt the user what context settings to reset.
|
||||
This prompt is used on saving to a different task to allow the scene to
|
||||
get matched to the new context.
|
||||
"""
|
||||
# TODO: Cleanup this prototyped mess of imports and odd dialog
|
||||
from ayon_core.tools.attribute_defs.dialog import (
|
||||
AttributeDefinitionsDialog
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.lib import BoolDef, UILabelDef
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
||||
definitions = [
|
||||
UILabelDef(
|
||||
label=(
|
||||
"You are saving your workfile into a different folder or task."
|
||||
"\n\n"
|
||||
"Would you like to update some settings to the new context?\n"
|
||||
)
|
||||
),
|
||||
BoolDef(
|
||||
"fps",
|
||||
label="FPS",
|
||||
tooltip="Reset Comp FPS",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"frame_range",
|
||||
label="Frame Range",
|
||||
tooltip="Reset Comp start and end frame ranges",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"resolution",
|
||||
label="Comp Resolution",
|
||||
tooltip="Reset Comp resolution",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"instances",
|
||||
label="Publish instances",
|
||||
tooltip="Update all publish instance's folder and task to match "
|
||||
"the new folder and task",
|
||||
default=True
|
||||
),
|
||||
]
|
||||
|
||||
dialog = AttributeDefinitionsDialog(definitions)
|
||||
dialog.setWindowFlags(
|
||||
dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
dialog.setWindowTitle("Saving to different context.")
|
||||
dialog.setStyleSheet(load_stylesheet())
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
options = dialog.get_values()
|
||||
folder_entity = get_current_folder_entity()
|
||||
if options["frame_range"]:
|
||||
set_current_context_framerange(folder_entity)
|
||||
|
||||
if options["fps"]:
|
||||
set_current_context_fps(folder_entity)
|
||||
|
||||
if options["resolution"]:
|
||||
set_current_context_resolution(folder_entity)
|
||||
|
||||
if options["instances"]:
|
||||
update_content_on_context_change()
|
||||
|
||||
dialog.deleteLater()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import os
|
|||
import sys
|
||||
import logging
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
|
||||
import pyblish.api
|
||||
from qtpy import QtCore
|
||||
|
|
@ -28,8 +29,8 @@ from ayon_core.tools.utils import host_tools
|
|||
|
||||
from .lib import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk,
|
||||
validate_comp_prefs
|
||||
validate_comp_prefs,
|
||||
prompt_reset_context
|
||||
)
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
|
@ -41,6 +42,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
|||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
# Track whether the workfile tool is about to save
|
||||
_about_to_save = False
|
||||
|
||||
|
||||
class FusionLogHandler(logging.Handler):
|
||||
# Keep a reference to fusion's Print function (Remote Object)
|
||||
|
|
@ -104,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
|
||||
# Register events
|
||||
register_event_callback("open", on_after_open)
|
||||
register_event_callback("workfile.save.before", before_workfile_save)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("new", on_new)
|
||||
register_event_callback("taskChanged", on_task_changed)
|
||||
|
||||
# region workfile io api
|
||||
def has_unsaved_changes(self):
|
||||
|
|
@ -169,6 +175,19 @@ def on_save(event):
|
|||
comp = event["sender"]
|
||||
validate_comp_prefs(comp)
|
||||
|
||||
# We are now starting the actual save directly
|
||||
global _about_to_save
|
||||
_about_to_save = False
|
||||
|
||||
|
||||
def on_task_changed():
|
||||
global _about_to_save
|
||||
print(f"Task changed: {_about_to_save}")
|
||||
# TODO: Only do this if not headless
|
||||
if _about_to_save:
|
||||
# Let's prompt the user to update the context settings or not
|
||||
prompt_reset_context()
|
||||
|
||||
|
||||
def on_after_open(event):
|
||||
comp = event["sender"]
|
||||
|
|
@ -202,6 +221,28 @@ def on_after_open(event):
|
|||
dialog.setStyleSheet(load_stylesheet())
|
||||
|
||||
|
||||
def before_workfile_save(event):
|
||||
# Due to Fusion's external python process design we can't really
|
||||
# detect whether the current Fusion environment matches the one the artists
|
||||
# expects it to be. For example, our pipeline python process might
|
||||
# have been shut down, and restarted - which will restart it to the
|
||||
# environment Fusion started with; not necessarily where the artist
|
||||
# is currently working.
|
||||
# The `_about_to_save` var is used to detect context changes when
|
||||
# saving into another asset. If we keep it False it will be ignored
|
||||
# as context change. As such, before we change tasks we will only
|
||||
# consider it the current filepath is within the currently known
|
||||
# AVALON_WORKDIR. This way we avoid false positives of thinking it's
|
||||
# saving to another context and instead sometimes just have false negatives
|
||||
# where we fail to show the "Update on task change" prompt.
|
||||
comp = get_current_comp()
|
||||
filepath = comp.GetAttrs()["COMPS_FileName"]
|
||||
workdir = os.environ.get("AYON_WORKDIR")
|
||||
if Path(workdir) in Path(filepath).parents:
|
||||
global _about_to_save
|
||||
_about_to_save = True
|
||||
|
||||
|
||||
def ls():
|
||||
"""List containers from active Fusion scene
|
||||
|
||||
|
|
@ -338,7 +379,6 @@ class FusionEventHandler(QtCore.QObject):
|
|||
>>> handler = FusionEventHandler(parent=window)
|
||||
>>> handler.start()
|
||||
|
||||
|
||||
"""
|
||||
ACTION_IDS = [
|
||||
"Comp_Save",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from ayon_core.hosts.fusion import (
|
|||
FUSION_VERSIONS_DICT,
|
||||
get_fusion_version,
|
||||
)
|
||||
from ayon_core.lib.applications import (
|
||||
from ayon_applications import (
|
||||
PreLaunchHook,
|
||||
LaunchTypes,
|
||||
ApplicationLaunchFailed,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
from ayon_core.lib.applications import (
|
||||
from ayon_applications import (
|
||||
PreLaunchHook,
|
||||
LaunchTypes,
|
||||
ApplicationLaunchFailed,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import subprocess
|
|||
import platform
|
||||
import uuid
|
||||
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InstallPySideToFusion(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
from ayon_core.lib import EnumDef
|
||||
from ayon_core.lib import (
|
||||
UILabelDef,
|
||||
NumberDef,
|
||||
EnumDef
|
||||
)
|
||||
|
||||
from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver
|
||||
from ayon_core.hosts.fusion.api.lib import get_current_comp
|
||||
|
||||
|
||||
class CreateSaver(GenericCreateSaver):
|
||||
|
|
@ -45,6 +50,7 @@ class CreateSaver(GenericCreateSaver):
|
|||
self._get_reviewable_bool(),
|
||||
self._get_frame_range_enum(),
|
||||
self._get_image_format_enum(),
|
||||
*self._get_custom_frame_range_attribute_defs()
|
||||
]
|
||||
return attr_defs
|
||||
|
||||
|
|
@ -53,6 +59,7 @@ class CreateSaver(GenericCreateSaver):
|
|||
"current_folder": "Current Folder context",
|
||||
"render_range": "From render in/out",
|
||||
"comp_range": "From composition timeline",
|
||||
"custom_range": "Custom frame range",
|
||||
}
|
||||
|
||||
return EnumDef(
|
||||
|
|
@ -61,3 +68,82 @@ class CreateSaver(GenericCreateSaver):
|
|||
label="Frame range source",
|
||||
default=self.default_frame_range_option
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_custom_frame_range_attribute_defs() -> list:
|
||||
|
||||
# Define custom frame range defaults based on current comp
|
||||
# timeline settings (if a comp is currently open)
|
||||
comp = get_current_comp()
|
||||
if comp is not None:
|
||||
attrs = comp.GetAttrs()
|
||||
frame_defaults = {
|
||||
"frameStart": int(attrs["COMPN_GlobalStart"]),
|
||||
"frameEnd": int(attrs["COMPN_GlobalEnd"]),
|
||||
"handleStart": int(
|
||||
attrs["COMPN_RenderStart"] - attrs["COMPN_GlobalStart"]
|
||||
),
|
||||
"handleEnd": int(
|
||||
attrs["COMPN_GlobalEnd"] - attrs["COMPN_RenderEnd"]
|
||||
),
|
||||
}
|
||||
else:
|
||||
frame_defaults = {
|
||||
"frameStart": 1001,
|
||||
"frameEnd": 1100,
|
||||
"handleStart": 0,
|
||||
"handleEnd": 0
|
||||
}
|
||||
|
||||
return [
|
||||
UILabelDef(
|
||||
label="<br><b>Custom Frame Range</b><br>"
|
||||
"<i>only used with 'Custom frame range' source</i>"
|
||||
),
|
||||
NumberDef(
|
||||
"custom_frameStart",
|
||||
label="Frame Start",
|
||||
default=frame_defaults["frameStart"],
|
||||
minimum=0,
|
||||
decimals=0,
|
||||
tooltip=(
|
||||
"Set the start frame for the export.\n"
|
||||
"Only used if frame range source is 'Custom frame range'."
|
||||
)
|
||||
),
|
||||
NumberDef(
|
||||
"custom_frameEnd",
|
||||
label="Frame End",
|
||||
default=frame_defaults["frameEnd"],
|
||||
minimum=0,
|
||||
decimals=0,
|
||||
tooltip=(
|
||||
"Set the end frame for the export.\n"
|
||||
"Only used if frame range source is 'Custom frame range'."
|
||||
)
|
||||
),
|
||||
NumberDef(
|
||||
"custom_handleStart",
|
||||
label="Handle Start",
|
||||
default=frame_defaults["handleStart"],
|
||||
minimum=0,
|
||||
decimals=0,
|
||||
tooltip=(
|
||||
"Set the start handles for the export, this will be "
|
||||
"added before the start frame.\n"
|
||||
"Only used if frame range source is 'Custom frame range'."
|
||||
)
|
||||
),
|
||||
NumberDef(
|
||||
"custom_handleEnd",
|
||||
label="Handle End",
|
||||
default=frame_defaults["handleEnd"],
|
||||
minimum=0,
|
||||
decimals=0,
|
||||
tooltip=(
|
||||
"Set the end handles for the export, this will be added "
|
||||
"after the end frame.\n"
|
||||
"Only used if frame range source is 'Custom frame range'."
|
||||
)
|
||||
)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -57,6 +57,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
|
|||
start_with_handle = comp_start
|
||||
end_with_handle = comp_end
|
||||
|
||||
if frame_range_source == "custom_range":
|
||||
start = int(instance.data["custom_frameStart"])
|
||||
end = int(instance.data["custom_frameEnd"])
|
||||
handle_start = int(instance.data["custom_handleStart"])
|
||||
handle_end = int(instance.data["custom_handleEnd"])
|
||||
start_with_handle = start - handle_start
|
||||
end_with_handle = end + handle_end
|
||||
|
||||
frame = instance.data["creator_attributes"].get("frame")
|
||||
# explicitly publishing only single frame
|
||||
if frame is not None:
|
||||
|
|
|
|||
|
|
@ -568,7 +568,7 @@ def save_scene():
|
|||
"""Save the Harmony scene safely.
|
||||
|
||||
The built-in (to Avalon) background zip and moving of the Harmony scene
|
||||
folder, interfers with server/client communication by sending two requests
|
||||
folder, interferes with server/client communication by sending two requests
|
||||
at the same time. This only happens when sending "scene.saveAll()". This
|
||||
method prevents this double request and safely saves the scene.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from ayon_core.pipeline import (
|
|||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_core.pipeline.load import get_outdated_containers
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
||||
from ayon_core.hosts.harmony import HARMONY_ADDON_ROOT
|
||||
import ayon_core.hosts.harmony.api as harmony
|
||||
|
|
@ -50,7 +50,7 @@ def get_current_context_settings():
|
|||
|
||||
"""
|
||||
|
||||
folder_entity = get_current_project_folder()
|
||||
folder_entity = get_current_folder_entity()
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
|
||||
fps = folder_attributes.get("fps")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ from ayon_core.lib import (
|
|||
get_ayon_launcher_args,
|
||||
is_using_ayon_console,
|
||||
)
|
||||
from ayon_core.lib.applications import (
|
||||
PreLaunchHook,
|
||||
LaunchTypes,
|
||||
)
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts.harmony import get_launch_script_path
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ class CreateFarmRender(plugin.Creator):
|
|||
path = "render/{0}/{0}.".format(node.split("/")[-1])
|
||||
harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.Creators.CreateRender.create",
|
||||
"function": "PypeHarmony.Creators.CreateRender.create",
|
||||
"args": [node, path]
|
||||
})
|
||||
harmony.send(
|
||||
{
|
||||
"function": f"PypeHarmony.color",
|
||||
"function": "PypeHarmony.color",
|
||||
"args": [[0.9, 0.75, 0.3, 1.0]]
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectAudio(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Collect relative path for audio file to instance.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class CollectScene(pyblish.api.ContextPlugin):
|
|||
"""Plugin entry point."""
|
||||
result = harmony.send(
|
||||
{
|
||||
f"function": "PypeHarmony.getSceneSettings",
|
||||
"function": "PypeHarmony.getSceneSettings",
|
||||
"args": []}
|
||||
)["result"]
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ class CollectScene(pyblish.api.ContextPlugin):
|
|||
|
||||
result = harmony.send(
|
||||
{
|
||||
f"function": "PypeHarmony.getVersion",
|
||||
"function": "PypeHarmony.getVersion",
|
||||
"args": []}
|
||||
)["result"]
|
||||
context.data["harmonyVersion"] = "{}.{}".format(result[0], result[1])
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import os
|
||||
|
||||
import hiero.core.events
|
||||
|
||||
from ayon_core.lib import Logger, register_event_callback
|
||||
|
||||
from .lib import (
|
||||
sync_avalon_data_to_workfile,
|
||||
launch_workfiles_app,
|
||||
selection_changed_timeline,
|
||||
before_project_save,
|
||||
)
|
||||
from .tags import add_tags_to_workfile
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ def get_current_track(sequence, name, audio=False):
|
|||
Creates new if none is found.
|
||||
|
||||
Args:
|
||||
sequence (hiero.core.Sequence): hiero sequene object
|
||||
sequence (hiero.core.Sequence): hiero sequence object
|
||||
name (str): name of track we want to return
|
||||
audio (bool)[optional]: switch to AudioTrack
|
||||
|
||||
|
|
@ -248,8 +248,12 @@ def get_track_items(
|
|||
# collect all available active sequence track items
|
||||
if not return_list:
|
||||
sequence = get_current_sequence(name=sequence_name)
|
||||
# get all available tracks from sequence
|
||||
tracks = list(sequence.audioTracks()) + list(sequence.videoTracks())
|
||||
tracks = []
|
||||
if sequence is not None:
|
||||
# get all available tracks from sequence
|
||||
tracks.extend(sequence.audioTracks())
|
||||
tracks.extend(sequence.videoTracks())
|
||||
|
||||
# loop all tracks
|
||||
for track in tracks:
|
||||
if check_locked and track.isLocked():
|
||||
|
|
@ -846,8 +850,8 @@ def create_nuke_workfile_clips(nuke_workfiles, seq=None):
|
|||
[{
|
||||
'path': 'P:/Jakub_testy_pipeline/test_v01.nk',
|
||||
'name': 'test',
|
||||
'handleStart': 15, # added asymetrically to handles
|
||||
'handleEnd': 10, # added asymetrically to handles
|
||||
'handleStart': 15, # added asymmetrically to handles
|
||||
'handleEnd': 10, # added asymmetrically to handles
|
||||
"clipIn": 16,
|
||||
"frameStart": 991,
|
||||
"frameEnd": 1023,
|
||||
|
|
@ -1192,7 +1196,7 @@ def get_sequence_pattern_and_padding(file):
|
|||
|
||||
Return:
|
||||
string: any matching sequence pattern
|
||||
int: padding of sequnce numbering
|
||||
int: padding of sequence numbering
|
||||
"""
|
||||
foundall = re.findall(
|
||||
r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file)
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track):
|
|||
if isinstance(track, hiero.core.AudioTrack):
|
||||
kind = 'Audio'
|
||||
|
||||
# Gather TrackItems involved in trasition
|
||||
# Gather TrackItems involved in transition
|
||||
item_in, item_out = get_neighboring_trackitems(
|
||||
otio_item,
|
||||
otio_track,
|
||||
|
|
@ -101,7 +101,7 @@ def apply_transition(otio_track, otio_item, track):
|
|||
if transition_type == 'dissolve':
|
||||
transition_func = getattr(
|
||||
hiero.core.Transition,
|
||||
'create{kind}DissolveTransition'.format(kind=kind)
|
||||
"create{kind}DissolveTransition".format(kind=kind)
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -109,7 +109,7 @@ def apply_transition(otio_track, otio_item, track):
|
|||
item_in,
|
||||
item_out,
|
||||
otio_item.in_offset.value,
|
||||
otio_item.out_offset.value
|
||||
otio_item.out_offset.value,
|
||||
)
|
||||
|
||||
# Catch error raised if transition is bigger than TrackItem source
|
||||
|
|
@ -134,7 +134,7 @@ def apply_transition(otio_track, otio_item, track):
|
|||
|
||||
transition = transition_func(
|
||||
item_out,
|
||||
otio_item.out_offset.value
|
||||
otio_item.out_offset.value,
|
||||
)
|
||||
|
||||
elif transition_type == 'fade_out':
|
||||
|
|
@ -183,9 +183,7 @@ def prep_url(url_in):
|
|||
def create_offline_mediasource(otio_clip, path=None):
|
||||
global _otio_old
|
||||
|
||||
hiero_rate = hiero.core.TimeBase(
|
||||
otio_clip.source_range.start_time.rate
|
||||
)
|
||||
hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate)
|
||||
|
||||
try:
|
||||
legal_media_refs = (
|
||||
|
|
@ -212,7 +210,7 @@ def create_offline_mediasource(otio_clip, path=None):
|
|||
source_range.start_time.value,
|
||||
source_range.duration.value,
|
||||
hiero_rate,
|
||||
source_range.start_time.value
|
||||
source_range.start_time.value,
|
||||
)
|
||||
|
||||
return media
|
||||
|
|
@ -385,7 +383,8 @@ def create_trackitem(playhead, track, otio_clip, clip):
|
|||
# Only reverse effect can be applied here
|
||||
if abs(time_scalar) == 1.:
|
||||
trackitem.setPlaybackSpeed(
|
||||
trackitem.playbackSpeed() * time_scalar)
|
||||
trackitem.playbackSpeed() * time_scalar
|
||||
)
|
||||
|
||||
elif isinstance(effect, otio.schema.FreezeFrame):
|
||||
# For freeze frame, playback speed must be set after range
|
||||
|
|
@ -397,28 +396,21 @@ def create_trackitem(playhead, track, otio_clip, clip):
|
|||
source_in = source_range.end_time_inclusive().value
|
||||
|
||||
timeline_in = playhead + source_out
|
||||
timeline_out = (
|
||||
timeline_in +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
timeline_out = (timeline_in + source_range.duration.value) - 1
|
||||
else:
|
||||
# Normal playback speed
|
||||
source_in = source_range.start_time.value
|
||||
source_out = source_range.end_time_inclusive().value
|
||||
|
||||
timeline_in = playhead
|
||||
timeline_out = (
|
||||
timeline_in +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
timeline_out = (timeline_in + source_range.duration.value) - 1
|
||||
|
||||
# Set source and timeline in/out points
|
||||
trackitem.setTimes(
|
||||
timeline_in,
|
||||
timeline_out,
|
||||
source_in,
|
||||
source_out
|
||||
|
||||
source_out,
|
||||
)
|
||||
|
||||
# Apply playback speed for freeze frames
|
||||
|
|
@ -435,7 +427,8 @@ def create_trackitem(playhead, track, otio_clip, clip):
|
|||
|
||||
|
||||
def build_sequence(
|
||||
otio_timeline, project=None, sequence=None, track_kind=None):
|
||||
otio_timeline, project=None, sequence=None, track_kind=None
|
||||
):
|
||||
if project is None:
|
||||
if sequence:
|
||||
project = sequence.project()
|
||||
|
|
@ -509,10 +502,7 @@ def build_sequence(
|
|||
|
||||
# Create TrackItem
|
||||
trackitem = create_trackitem(
|
||||
playhead,
|
||||
track,
|
||||
otio_clip,
|
||||
clip
|
||||
playhead, track, otio_clip, clip
|
||||
)
|
||||
|
||||
# Add markers
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ def get_reformated_path(path, padded=True):
|
|||
path (str): path url or simple file name
|
||||
|
||||
Returns:
|
||||
type: string with reformated path
|
||||
type: string with reformatted path
|
||||
|
||||
Example:
|
||||
get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr
|
||||
|
|
|
|||
|
|
@ -449,7 +449,6 @@ class ClipLoader:
|
|||
repr = self.context["representation"]
|
||||
repr_cntx = repr["context"]
|
||||
folder_path = self.context["folder"]["path"]
|
||||
folder_name = self.context["folder"]["name"]
|
||||
product_name = self.context["product"]["name"]
|
||||
representation = repr["name"]
|
||||
self.data["clip_name"] = self.clip_name_template.format(**repr_cntx)
|
||||
|
|
@ -906,16 +905,16 @@ class PublishClip:
|
|||
"hierarchyData": hierarchy_formatting_data,
|
||||
"productName": self.product_name,
|
||||
"productType": self.product_type,
|
||||
"families": [self.product_type, self.data["family"]]
|
||||
"families": [self.product_type, self.data["productType"]]
|
||||
}
|
||||
|
||||
def _convert_to_entity(self, type, template):
|
||||
def _convert_to_entity(self, src_type, template):
|
||||
""" Converting input key to key with type. """
|
||||
# convert to entity type
|
||||
entity_type = self.types.get(type, None)
|
||||
folder_type = self.types.get(src_type, None)
|
||||
|
||||
assert entity_type, "Missing entity type for `{}`".format(
|
||||
type
|
||||
assert folder_type, "Missing folder type for `{}`".format(
|
||||
src_type
|
||||
)
|
||||
|
||||
# first collect formatting data to use for formatting template
|
||||
|
|
@ -926,7 +925,7 @@ class PublishClip:
|
|||
formatting_data[_k] = value
|
||||
|
||||
return {
|
||||
"entity_type": entity_type,
|
||||
"folder_type": folder_type,
|
||||
"entity_name": template.format(
|
||||
**formatting_data
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
# Note: This only prints the text data that is visible in the active Spreadsheet View.
|
||||
# If you've filtered text, only the visible text will be printed to the CSV file
|
||||
# Usage: Copy to ~/.hiero/Python/StartupUI
|
||||
import os
|
||||
import csv
|
||||
|
||||
import hiero.core.events
|
||||
import hiero.ui
|
||||
import os, csv
|
||||
try:
|
||||
from PySide.QtGui import *
|
||||
from PySide.QtCore import *
|
||||
|
|
|
|||
|
|
@ -641,7 +641,7 @@ def _setStatus(self, status):
|
|||
global gStatusTags
|
||||
|
||||
# Get a valid Tag object from the Global list of statuses
|
||||
if not status in gStatusTags.keys():
|
||||
if status not in gStatusTags.keys():
|
||||
print("Status requested was not a valid Status string.")
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track):
|
|||
kind = "Audio"
|
||||
|
||||
try:
|
||||
# Gather TrackItems involved in trasition
|
||||
# Gather TrackItems involved in transition
|
||||
item_in, item_out = get_neighboring_trackitems(
|
||||
otio_item,
|
||||
otio_track,
|
||||
|
|
@ -101,14 +101,14 @@ def apply_transition(otio_track, otio_item, track):
|
|||
if transition_type == "dissolve":
|
||||
transition_func = getattr(
|
||||
hiero.core.Transition,
|
||||
'create{kind}DissolveTransition'.format(kind=kind)
|
||||
"create{kind}DissolveTransition".format(kind=kind)
|
||||
)
|
||||
|
||||
transition = transition_func(
|
||||
item_in,
|
||||
item_out,
|
||||
otio_item.in_offset.value,
|
||||
otio_item.out_offset.value
|
||||
otio_item.out_offset.value,
|
||||
)
|
||||
|
||||
elif transition_type == "fade_in":
|
||||
|
|
@ -116,20 +116,14 @@ def apply_transition(otio_track, otio_item, track):
|
|||
hiero.core.Transition,
|
||||
'create{kind}FadeInTransition'.format(kind=kind)
|
||||
)
|
||||
transition = transition_func(
|
||||
item_out,
|
||||
otio_item.out_offset.value
|
||||
)
|
||||
transition = transition_func(item_out, otio_item.out_offset.value)
|
||||
|
||||
elif transition_type == "fade_out":
|
||||
transition_func = getattr(
|
||||
hiero.core.Transition,
|
||||
'create{kind}FadeOutTransition'.format(kind=kind)
|
||||
)
|
||||
transition = transition_func(
|
||||
item_in,
|
||||
otio_item.in_offset.value
|
||||
"create{kind}FadeOutTransition".format(kind=kind)
|
||||
)
|
||||
transition = transition_func(item_in, otio_item.in_offset.value)
|
||||
|
||||
else:
|
||||
# Unknown transition
|
||||
|
|
@ -138,11 +132,10 @@ def apply_transition(otio_track, otio_item, track):
|
|||
# Apply transition to track
|
||||
track.addTransition(transition)
|
||||
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
sys.stderr.write(
|
||||
'Unable to apply transition "{t}": "{e}"\n'.format(
|
||||
t=otio_item,
|
||||
e=e
|
||||
t=otio_item, e=e
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -153,18 +146,14 @@ def prep_url(url_in):
|
|||
if url.startswith("file://localhost/"):
|
||||
return url.replace("file://localhost/", "")
|
||||
|
||||
url = '{url}'.format(
|
||||
sep=url.startswith(os.sep) and "" or os.sep,
|
||||
url=url.startswith(os.sep) and url[1:] or url
|
||||
)
|
||||
if url.startswith(os.sep):
|
||||
url = url[1:]
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def create_offline_mediasource(otio_clip, path=None):
|
||||
hiero_rate = hiero.core.TimeBase(
|
||||
otio_clip.source_range.start_time.rate
|
||||
)
|
||||
hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate)
|
||||
|
||||
if isinstance(otio_clip.media_reference, otio.schema.ExternalReference):
|
||||
source_range = otio_clip.available_range()
|
||||
|
|
@ -180,7 +169,7 @@ def create_offline_mediasource(otio_clip, path=None):
|
|||
source_range.start_time.value,
|
||||
source_range.duration.value,
|
||||
hiero_rate,
|
||||
source_range.start_time.value
|
||||
source_range.start_time.value,
|
||||
)
|
||||
|
||||
return media
|
||||
|
|
@ -203,7 +192,7 @@ marker_color_map = {
|
|||
"MAGENTA": "Magenta",
|
||||
"BLACK": "Blue",
|
||||
"WHITE": "Green",
|
||||
"MINT": "Cyan"
|
||||
"MINT": "Cyan",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -254,12 +243,6 @@ def add_markers(otio_item, hiero_item, tagsbin):
|
|||
if _tag is None:
|
||||
_tag = hiero.core.Tag(marker_color_map[marker.color])
|
||||
|
||||
start = marker.marked_range.start_time.value
|
||||
end = (
|
||||
marker.marked_range.start_time.value +
|
||||
marker.marked_range.duration.value
|
||||
)
|
||||
|
||||
tag = hiero_item.addTag(_tag)
|
||||
tag.setName(marker.name or marker_color_map[marker_color])
|
||||
|
||||
|
|
@ -275,12 +258,12 @@ def create_track(otio_track, tracknum, track_kind):
|
|||
# Create a Track
|
||||
if otio_track.kind == otio.schema.TrackKind.Video:
|
||||
track = hiero.core.VideoTrack(
|
||||
otio_track.name or 'Video{n}'.format(n=tracknum)
|
||||
otio_track.name or "Video{n}".format(n=tracknum)
|
||||
)
|
||||
|
||||
else:
|
||||
track = hiero.core.AudioTrack(
|
||||
otio_track.name or 'Audio{n}'.format(n=tracknum)
|
||||
otio_track.name or "Audio{n}".format(n=tracknum)
|
||||
)
|
||||
|
||||
return track
|
||||
|
|
@ -315,34 +298,25 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin):
|
|||
for effect in otio_clip.effects:
|
||||
if isinstance(effect, otio.schema.LinearTimeWarp):
|
||||
trackitem.setPlaybackSpeed(
|
||||
trackitem.playbackSpeed() *
|
||||
effect.time_scalar
|
||||
trackitem.playbackSpeed() * effect.time_scalar
|
||||
)
|
||||
|
||||
# If reverse playback speed swap source in and out
|
||||
if trackitem.playbackSpeed() < 0:
|
||||
source_out = source_range.start_time.value
|
||||
source_in = (
|
||||
source_range.start_time.value +
|
||||
source_range.duration.value
|
||||
source_range.start_time.value + source_range.duration.value
|
||||
) - 1
|
||||
timeline_in = playhead + source_out
|
||||
timeline_out = (
|
||||
timeline_in +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
timeline_out = (timeline_in + source_range.duration.value) - 1
|
||||
else:
|
||||
# Normal playback speed
|
||||
source_in = source_range.start_time.value
|
||||
source_out = (
|
||||
source_range.start_time.value +
|
||||
source_range.duration.value
|
||||
source_range.start_time.value + source_range.duration.value
|
||||
) - 1
|
||||
timeline_in = playhead
|
||||
timeline_out = (
|
||||
timeline_in +
|
||||
source_range.duration.value
|
||||
) - 1
|
||||
timeline_out = (timeline_in + source_range.duration.value) - 1
|
||||
|
||||
# Set source and timeline in/out points
|
||||
trackitem.setSourceIn(source_in)
|
||||
|
|
@ -357,7 +331,8 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin):
|
|||
|
||||
|
||||
def build_sequence(
|
||||
otio_timeline, project=None, sequence=None, track_kind=None):
|
||||
otio_timeline, project=None, sequence=None, track_kind=None
|
||||
):
|
||||
|
||||
if project is None:
|
||||
if sequence:
|
||||
|
|
@ -414,8 +389,7 @@ def build_sequence(
|
|||
if isinstance(otio_clip, otio.schema.Stack):
|
||||
bar = hiero.ui.mainWindow().statusBar()
|
||||
bar.showMessage(
|
||||
"Nested sequences are created separately.",
|
||||
timeout=3000
|
||||
"Nested sequences are created separately.", timeout=3000
|
||||
)
|
||||
build_sequence(otio_clip, project, otio_track.kind)
|
||||
|
||||
|
|
@ -428,11 +402,7 @@ def build_sequence(
|
|||
|
||||
# Create TrackItem
|
||||
trackitem = create_trackitem(
|
||||
playhead,
|
||||
track,
|
||||
otio_clip,
|
||||
clip,
|
||||
tagsbin
|
||||
playhead, track, otio_clip, clip, tagsbin
|
||||
)
|
||||
|
||||
# Add trackitem to track
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ def update_tag(tag, data):
|
|||
# set all data metadata to tag metadata
|
||||
for _k, _v in data_mtd.items():
|
||||
value = str(_v)
|
||||
if type(_v) == dict:
|
||||
if isinstance(_v, dict):
|
||||
value = json.dumps(_v)
|
||||
|
||||
# set the value
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class CreateShotClip(phiero.Creator):
|
|||
"type": "QCheckBox",
|
||||
"label": "Source resolution",
|
||||
"target": "tag",
|
||||
"toolTip": "Is resloution taken from timeline or source?", # noqa
|
||||
"toolTip": "Is resolution taken from timeline or source?", # noqa
|
||||
"order": 4},
|
||||
}
|
||||
},
|
||||
|
|
@ -211,7 +211,7 @@ class CreateShotClip(phiero.Creator):
|
|||
presets = deepcopy(self.presets)
|
||||
gui_inputs = deepcopy(self.gui_inputs)
|
||||
|
||||
# get key pares from presets and match it on ui inputs
|
||||
# get key pairs from presets and match it on ui inputs
|
||||
for k, v in gui_inputs.items():
|
||||
if v["type"] in ("dict", "section"):
|
||||
# nested dictionary (only one level allowed
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from itertools import product
|
||||
import re
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
tracks_effect_items = self.collect_sub_track_items(all_tracks)
|
||||
context.data["tracksEffectItems"] = tracks_effect_items
|
||||
|
||||
# process all sellected timeline track items
|
||||
# process all selected timeline track items
|
||||
for track_item in selected_timeline_items:
|
||||
data = {}
|
||||
clip_name = track_item.name()
|
||||
|
|
@ -62,7 +62,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
}:
|
||||
continue
|
||||
|
||||
# get clips subtracks and anotations
|
||||
# get clips subtracks and annotations
|
||||
annotations = self.clip_annotations(source_clip)
|
||||
subtracks = self.clip_subtrack(track_item)
|
||||
self.log.debug("Annotations: {}".format(annotations))
|
||||
|
|
@ -84,8 +84,13 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
k: v for k, v in tag_data.items()
|
||||
if k not in ("id", "applieswhole", "label")
|
||||
})
|
||||
# Backward compatibility fix of 'entity_type' > 'folder_type'
|
||||
if "parents" in data:
|
||||
for parent in data["parents"]:
|
||||
if "entity_type" in parent:
|
||||
parent["folder_type"] = parent.pop("entity_type")
|
||||
|
||||
asset, asset_name = self._get_folder_data(tag_data)
|
||||
folder_path, folder_name = self._get_folder_data(tag_data)
|
||||
|
||||
product_name = tag_data.get("productName")
|
||||
if product_name is None:
|
||||
|
|
@ -93,12 +98,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
families = [str(f) for f in tag_data["families"]]
|
||||
|
||||
# form label
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({})".format(clip_name)
|
||||
label += " {}".format(product_name)
|
||||
|
||||
# TODO: remove backward compatibility
|
||||
product_name = tag_data.get("productName")
|
||||
if product_name is None:
|
||||
|
|
@ -108,7 +107,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
# backward compatibility: product_name should not be missing
|
||||
if not product_name:
|
||||
self.log.error(
|
||||
"Product name is not defined for: {}".format(asset))
|
||||
"Product name is not defined for: {}".format(folder_path))
|
||||
|
||||
# TODO: remove backward compatibility
|
||||
product_type = tag_data.get("productType")
|
||||
|
|
@ -119,15 +118,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
# backward compatibility: product_type should not be missing
|
||||
if not product_type:
|
||||
self.log.error(
|
||||
"Product type is not defined for: {}".format(asset))
|
||||
"Product type is not defined for: {}".format(folder_path))
|
||||
|
||||
# form label
|
||||
label = "{} -".format(folder_path)
|
||||
if folder_name != clip_name:
|
||||
label += " ({})".format(clip_name)
|
||||
label += " {}".format(product_name)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, product_name),
|
||||
"name": "{}_{}".format(folder_path, product_name),
|
||||
"label": label,
|
||||
"folderPath": asset,
|
||||
"asset_name": asset_name,
|
||||
"productName": product_name,
|
||||
"productType": product_type,
|
||||
"folderPath": folder_path,
|
||||
"asset_name": folder_name,
|
||||
"item": track_item,
|
||||
"families": families,
|
||||
"publish": tag_data["publish"],
|
||||
|
|
@ -217,19 +222,19 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
if not hierarchy_data:
|
||||
return
|
||||
|
||||
asset = data["folderPath"]
|
||||
asset_name = data["asset_name"]
|
||||
folder_path = data["folderPath"]
|
||||
folder_name = data["asset_name"]
|
||||
|
||||
product_type = "shot"
|
||||
|
||||
# form label
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label = "{} -".format(folder_path)
|
||||
if folder_name != clip_name:
|
||||
label += " ({}) ".format(clip_name)
|
||||
label += " {}".format(product_name)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, product_name),
|
||||
"name": "{}_{}".format(folder_path, product_name),
|
||||
"label": label,
|
||||
"productName": product_name,
|
||||
"productType": product_type,
|
||||
|
|
@ -276,19 +281,19 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
if not self.test_any_audio(item):
|
||||
return
|
||||
|
||||
asset = data["folderPath"]
|
||||
folder_path = data["folderPath"]
|
||||
asset_name = data["asset_name"]
|
||||
|
||||
product_type = "audio"
|
||||
|
||||
# form label
|
||||
label = "{} -".format(asset)
|
||||
label = "{} -".format(folder_path)
|
||||
if asset_name != clip_name:
|
||||
label += " ({}) ".format(clip_name)
|
||||
label += " {}".format(product_name)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, product_name),
|
||||
"name": "{}_{}".format(folder_path, subset),
|
||||
"label": label,
|
||||
"productName": product_name,
|
||||
"productType": product_type,
|
||||
|
|
@ -378,12 +383,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
# collect all subtrack items
|
||||
sub_track_items = {}
|
||||
for track in tracks:
|
||||
items = track.items()
|
||||
|
||||
effet_items = track.subTrackItems()
|
||||
effect_items = track.subTrackItems()
|
||||
|
||||
# skip if no clips on track > need track with effect only
|
||||
if not effet_items:
|
||||
if not effect_items:
|
||||
continue
|
||||
|
||||
# skip all disabled tracks
|
||||
|
|
@ -391,7 +394,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
continue
|
||||
|
||||
track_index = track.trackIndex()
|
||||
_sub_track_items = phiero.flatten(effet_items)
|
||||
_sub_track_items = phiero.flatten(effect_items)
|
||||
|
||||
_sub_track_items = list(_sub_track_items)
|
||||
# continue only if any subtrack items are collected
|
||||
|
|
@ -439,10 +442,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
for item in subTrackItems:
|
||||
if "TimeWarp" in item.name():
|
||||
continue
|
||||
# avoid all anotation
|
||||
# avoid all annotation
|
||||
if isinstance(item, hiero.core.Annotation):
|
||||
continue
|
||||
# # avoid all not anaibled
|
||||
# avoid all disabled
|
||||
if not item.isEnabled():
|
||||
continue
|
||||
subtracks.append(item)
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.CollectorOrder - 0.491
|
||||
|
||||
def process(self, context):
|
||||
asset = context.data["folderPath"]
|
||||
asset_name = asset.split("/")[-1]
|
||||
folder_path = context.data["folderPath"]
|
||||
folder_name = folder_path.split("/")[-1]
|
||||
|
||||
active_timeline = hiero.ui.activeSequence()
|
||||
project = active_timeline.project()
|
||||
|
|
@ -62,12 +62,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
product_type = "workfile"
|
||||
instance_data = {
|
||||
"label": "{} - {}Main".format(
|
||||
asset, product_type),
|
||||
"name": "{}_{}".format(asset_name, product_type),
|
||||
"folderPath": context.data["folderPath"],
|
||||
folder_path, product_type),
|
||||
"name": "{}_{}".format(folder_name, product_type),
|
||||
"folderPath": folder_path,
|
||||
# TODO use 'get_product_name'
|
||||
"productName": "{}{}Main".format(
|
||||
asset_name, product_type.capitalize()
|
||||
folder_name, product_type.capitalize()
|
||||
),
|
||||
"item": project,
|
||||
"productType": product_type,
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@ class PrecollectRetime(api.InstancePlugin):
|
|||
source_out = int(track_item.sourceOut())
|
||||
speed = track_item.playbackSpeed()
|
||||
|
||||
# calculate available material before retime
|
||||
available_in = int(track_item.handleInLength() * speed)
|
||||
available_out = int(track_item.handleOutLength() * speed)
|
||||
|
||||
self.log.debug((
|
||||
"_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n "
|
||||
"source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n "
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs):
|
|||
pane = stateutils.activePane(kwargs)
|
||||
if isinstance(pane, hou.NetworkEditor):
|
||||
pwd = pane.pwd()
|
||||
project_name = context.get_current_project_name(),
|
||||
project_name = context.get_current_project_name()
|
||||
folder_path = context.get_current_folder_path()
|
||||
task_name = context.get_current_task_name()
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import sys
|
|||
import os
|
||||
import errno
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -23,7 +22,7 @@ from ayon_core.pipeline import (
|
|||
)
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
from ayon_core.pipeline.template_data import get_template_data
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup
|
||||
from ayon_core.tools.utils.host_tools import get_tool_by_name
|
||||
|
||||
|
|
@ -40,88 +39,10 @@ def get_folder_fps(folder_entity=None):
|
|||
"""Return current folder fps."""
|
||||
|
||||
if folder_entity is None:
|
||||
folder_entity = get_current_project_folder(fields=["attrib.fps"])
|
||||
folder_entity = get_current_folder_entity(fields=["attrib.fps"])
|
||||
return folder_entity["attrib"]["fps"]
|
||||
|
||||
|
||||
def set_id(node, unique_id, overwrite=False):
|
||||
exists = node.parm("id")
|
||||
if not exists:
|
||||
imprint(node, {"id": unique_id})
|
||||
|
||||
if not exists and overwrite:
|
||||
node.setParm("id", unique_id)
|
||||
|
||||
|
||||
def get_id(node):
|
||||
"""Get the `cbId` attribute of the given node.
|
||||
|
||||
Args:
|
||||
node (hou.Node): the name of the node to retrieve the attribute from
|
||||
|
||||
Returns:
|
||||
str: cbId attribute of the node.
|
||||
|
||||
"""
|
||||
|
||||
if node is not None:
|
||||
return node.parm("id")
|
||||
|
||||
|
||||
def generate_ids(nodes, folder_id=None):
|
||||
"""Returns new unique ids for the given nodes.
|
||||
|
||||
Note: This does not assign the new ids, it only generates the values.
|
||||
|
||||
To assign new ids using this method:
|
||||
>>> nodes = ["a", "b", "c"]
|
||||
>>> for node, id in generate_ids(nodes):
|
||||
>>> set_id(node, id)
|
||||
|
||||
To also override any existing values (and assign regenerated ids):
|
||||
>>> nodes = ["a", "b", "c"]
|
||||
>>> for node, id in generate_ids(nodes):
|
||||
>>> set_id(node, id, overwrite=True)
|
||||
|
||||
Args:
|
||||
nodes (list): List of nodes.
|
||||
folder_id (str): Folder id . Use current folder id if is ``None``.
|
||||
|
||||
Returns:
|
||||
list: A list of (node, id) tuples.
|
||||
|
||||
"""
|
||||
|
||||
if folder_id is None:
|
||||
project_name = get_current_project_name()
|
||||
folder_path = get_current_folder_path()
|
||||
# Get folder id of current context folder
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path, fields={"id"}
|
||||
)
|
||||
if not folder_entity:
|
||||
raise ValueError("No current folder is set.")
|
||||
|
||||
folder_id = folder_entity["id"]
|
||||
|
||||
node_ids = []
|
||||
for node in nodes:
|
||||
_, uid = str(uuid.uuid4()).rsplit("-", 1)
|
||||
unique_id = "{}:{}".format(folder_id, uid)
|
||||
node_ids.append((node, unique_id))
|
||||
|
||||
return node_ids
|
||||
|
||||
|
||||
def get_id_required_nodes():
|
||||
|
||||
valid_types = ["geometry"]
|
||||
nodes = {n for n in hou.node("/out").children() if
|
||||
n.type().name() in valid_types}
|
||||
|
||||
return list(nodes)
|
||||
|
||||
|
||||
def get_output_parameter(node):
|
||||
"""Return the render output parameter of the given node
|
||||
|
||||
|
|
@ -322,7 +243,10 @@ def render_rop(ropnode):
|
|||
try:
|
||||
ropnode.render(verbose=verbose,
|
||||
# Allow Deadline to capture completion percentage
|
||||
output_progress=verbose)
|
||||
output_progress=verbose,
|
||||
# Render only this node
|
||||
# (do not render any of its dependencies)
|
||||
ignore_inputs=True)
|
||||
except hou.Error as exc:
|
||||
# The hou.Error is not inherited from a Python Exception class,
|
||||
# so we explicitly capture the houdini error, otherwise pyblish
|
||||
|
|
@ -526,7 +450,7 @@ def maintained_selection():
|
|||
node.setSelected(on=True)
|
||||
|
||||
|
||||
def reset_framerange():
|
||||
def reset_framerange(fps=True, frame_range=True):
|
||||
"""Set frame range and FPS to current folder."""
|
||||
|
||||
project_name = get_current_project_name()
|
||||
|
|
@ -535,29 +459,32 @@ def reset_framerange():
|
|||
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
|
||||
# Get FPS
|
||||
fps = get_folder_fps(folder_entity)
|
||||
# Set FPS
|
||||
if fps:
|
||||
fps = get_folder_fps(folder_entity)
|
||||
print("Setting scene FPS to {}".format(int(fps)))
|
||||
set_scene_fps(fps)
|
||||
|
||||
# Get Start and End Frames
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
frame_end = folder_attributes.get("frameEnd")
|
||||
if frame_range:
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
log.warning("No edit information found for '{}'".format(folder_path))
|
||||
return
|
||||
# Set Start and End Frames
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
frame_end = folder_attributes.get("frameEnd")
|
||||
|
||||
handle_start = folder_attributes.get("handleStart", 0)
|
||||
handle_end = folder_attributes.get("handleEnd", 0)
|
||||
if frame_start is None or frame_end is None:
|
||||
log.warning("No edit information found for '%s'", folder_path)
|
||||
return
|
||||
|
||||
frame_start -= int(handle_start)
|
||||
frame_end += int(handle_end)
|
||||
handle_start = folder_attributes.get("handleStart", 0)
|
||||
handle_end = folder_attributes.get("handleEnd", 0)
|
||||
|
||||
# Set frame range and FPS
|
||||
print("Setting scene FPS to {}".format(int(fps)))
|
||||
set_scene_fps(fps)
|
||||
hou.playbar.setFrameRange(frame_start, frame_end)
|
||||
hou.playbar.setPlaybackRange(frame_start, frame_end)
|
||||
hou.setFrame(frame_start)
|
||||
frame_start -= int(handle_start)
|
||||
frame_end += int(handle_end)
|
||||
|
||||
# Set frame range and FPS
|
||||
hou.playbar.setFrameRange(frame_start, frame_end)
|
||||
hou.playbar.setPlaybackRange(frame_start, frame_end)
|
||||
hou.setFrame(frame_start)
|
||||
|
||||
|
||||
def get_main_window():
|
||||
|
|
@ -814,7 +741,7 @@ def set_camera_resolution(camera, folder_entity=None):
|
|||
"""Apply resolution to camera from folder entity of the publish"""
|
||||
|
||||
if not folder_entity:
|
||||
folder_entity = get_current_project_folder()
|
||||
folder_entity = get_current_folder_entity()
|
||||
|
||||
resolution = get_resolution_from_folder(folder_entity)
|
||||
|
||||
|
|
@ -1024,7 +951,7 @@ def self_publish():
|
|||
|
||||
Firstly, it gets the node and its dependencies.
|
||||
Then, it deactivates all other ROPs
|
||||
And finaly, it triggers the publishing action.
|
||||
And finally, it triggers the publishing action.
|
||||
"""
|
||||
|
||||
result, comment = hou.ui.readInput(
|
||||
|
|
@ -1072,3 +999,84 @@ def add_self_publish_button(node):
|
|||
template = node.parmTemplateGroup()
|
||||
template.insertBefore((0,), button_parm)
|
||||
node.setParmTemplateGroup(template)
|
||||
|
||||
|
||||
def update_content_on_context_change():
|
||||
"""Update all Creator instances to current asset"""
|
||||
host = registered_host()
|
||||
context = host.get_current_context()
|
||||
|
||||
folder_path = context["folder_path"]
|
||||
task = context["task_name"]
|
||||
|
||||
create_context = CreateContext(host, reset=True)
|
||||
|
||||
for instance in create_context.instances:
|
||||
instance_folder_path = instance.get("folderPath")
|
||||
if instance_folder_path and instance_folder_path != folder_path:
|
||||
instance["folderPath"] = folder_path
|
||||
instance_task = instance.get("task")
|
||||
if instance_task and instance_task != task:
|
||||
instance["task"] = task
|
||||
|
||||
create_context.save_changes()
|
||||
|
||||
|
||||
def prompt_reset_context():
|
||||
"""Prompt the user what context settings to reset.
|
||||
This prompt is used on saving to a different task to allow the scene to
|
||||
get matched to the new context.
|
||||
"""
|
||||
# TODO: Cleanup this prototyped mess of imports and odd dialog
|
||||
from ayon_core.tools.attribute_defs.dialog import (
|
||||
AttributeDefinitionsDialog
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.lib import BoolDef, UILabelDef
|
||||
|
||||
definitions = [
|
||||
UILabelDef(
|
||||
label=(
|
||||
"You are saving your workfile into a different folder or task."
|
||||
"\n\n"
|
||||
"Would you like to update some settings to the new context?\n"
|
||||
)
|
||||
),
|
||||
BoolDef(
|
||||
"fps",
|
||||
label="FPS",
|
||||
tooltip="Reset workfile FPS",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"frame_range",
|
||||
label="Frame Range",
|
||||
tooltip="Reset workfile start and end frame ranges",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"instances",
|
||||
label="Publish instances",
|
||||
tooltip="Update all publish instance's folder and task to match "
|
||||
"the new folder and task",
|
||||
default=True
|
||||
),
|
||||
]
|
||||
|
||||
dialog = AttributeDefinitionsDialog(definitions)
|
||||
dialog.setWindowTitle("Saving to different context.")
|
||||
dialog.setStyleSheet(load_stylesheet())
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
options = dialog.get_values()
|
||||
if options["fps"] or options["frame_range"]:
|
||||
reset_framerange(
|
||||
fps=options["fps"],
|
||||
frame_range=options["frame_range"]
|
||||
)
|
||||
|
||||
if options["instances"]:
|
||||
update_content_on_context_change()
|
||||
|
||||
dialog.deleteLater()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pipeline tools for OpenPype Houdini integration."""
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
import hou # noqa
|
||||
|
|
@ -39,6 +38,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
|||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
# Track whether the workfile tool is about to save
|
||||
_about_to_save = False
|
||||
|
||||
|
||||
class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
||||
name = "houdini"
|
||||
|
|
@ -61,10 +63,12 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
log.info("Installing callbacks ... ")
|
||||
# register_event_callback("init", on_init)
|
||||
self._register_callbacks()
|
||||
register_event_callback("workfile.save.before", before_workfile_save)
|
||||
register_event_callback("before.save", before_save)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("open", on_open)
|
||||
register_event_callback("new", on_new)
|
||||
register_event_callback("taskChanged", on_task_changed)
|
||||
|
||||
self._has_been_setup = True
|
||||
|
||||
|
|
@ -166,7 +170,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
if not op_ctx:
|
||||
op_ctx = self.create_context_node()
|
||||
|
||||
lib.imprint(op_ctx, data)
|
||||
lib.imprint(op_ctx, data, update=True)
|
||||
|
||||
def get_context_data(self):
|
||||
op_ctx = hou.node(CONTEXT_CONTAINER)
|
||||
|
|
@ -287,6 +291,11 @@ def ls():
|
|||
yield parse_container(container)
|
||||
|
||||
|
||||
def before_workfile_save(event):
|
||||
global _about_to_save
|
||||
_about_to_save = True
|
||||
|
||||
|
||||
def before_save():
|
||||
return lib.validate_fps()
|
||||
|
||||
|
|
@ -298,9 +307,16 @@ def on_save():
|
|||
# update houdini vars
|
||||
lib.update_houdini_vars_context_dialog()
|
||||
|
||||
nodes = lib.get_id_required_nodes()
|
||||
for node, new_id in lib.generate_ids(nodes):
|
||||
lib.set_id(node, new_id, overwrite=False)
|
||||
# We are now starting the actual save directly
|
||||
global _about_to_save
|
||||
_about_to_save = False
|
||||
|
||||
|
||||
def on_task_changed():
|
||||
global _about_to_save
|
||||
if not IS_HEADLESS and _about_to_save:
|
||||
# Let's prompt the user to update the context settings or not
|
||||
lib.prompt_reset_context()
|
||||
|
||||
|
||||
def _show_outdated_content_popup():
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class SetPath(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
product_type = "redshift_rop"
|
||||
icon = "magic"
|
||||
ext = "exr"
|
||||
multi_layered_mode = "No Multi-Layered EXR File"
|
||||
|
||||
# Default to split export and render jobs
|
||||
split_render = True
|
||||
|
|
@ -55,25 +56,36 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
|
||||
# Set the linked rop to the Redshift ROP
|
||||
ipr_rop.parm("linked_rop").set(instance_node.path())
|
||||
|
||||
ext = pre_create_data.get("image_format")
|
||||
filepath = "{renders_dir}{product_name}/{product_name}.{fmt}".format(
|
||||
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
|
||||
product_name=product_name,
|
||||
fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext)
|
||||
)
|
||||
multi_layered_mode = pre_create_data.get("multi_layered_mode")
|
||||
|
||||
ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3}
|
||||
multilayer_mode_index = {"No Multi-Layered EXR File": "1",
|
||||
"Full Multi-Layered EXR File": "2" }
|
||||
|
||||
filepath = "{renders_dir}{product_name}/{product_name}.{fmt}".format(
|
||||
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
|
||||
product_name=product_name,
|
||||
fmt="$AOV.$F4.{ext}".format(ext=ext)
|
||||
)
|
||||
|
||||
if multilayer_mode_index[multi_layered_mode] == "1":
|
||||
multipart = False
|
||||
|
||||
elif multilayer_mode_index[multi_layered_mode] == "2":
|
||||
multipart = True
|
||||
|
||||
parms = {
|
||||
# Render frame range
|
||||
"trange": 1,
|
||||
# Redshift ROP settings
|
||||
"RS_outputFileNamePrefix": filepath,
|
||||
"RS_outputMultilayerMode": "1", # no multi-layered exr
|
||||
"RS_outputBeautyAOVSuffix": "beauty",
|
||||
"RS_outputFileFormat": ext_format_index[ext],
|
||||
}
|
||||
if ext == "exr":
|
||||
parms["RS_outputMultilayerMode"] = multilayer_mode_index[multi_layered_mode]
|
||||
parms["RS_aovMultipart"] = multipart
|
||||
|
||||
if self.selected_nodes:
|
||||
# set up the render camera from the selected node
|
||||
|
|
@ -111,6 +123,11 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
image_format_enum = [
|
||||
"exr", "tif", "jpg", "png",
|
||||
]
|
||||
multi_layered_mode = [
|
||||
"No Multi-Layered EXR File",
|
||||
"Full Multi-Layered EXR File"
|
||||
]
|
||||
|
||||
|
||||
return attrs + [
|
||||
BoolDef("farm",
|
||||
|
|
@ -122,5 +139,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default=self.ext,
|
||||
label="Image Format Options")
|
||||
label="Image Format Options"),
|
||||
EnumDef("multi_layered_mode",
|
||||
multi_layered_mode,
|
||||
default=self.multi_layered_mode,
|
||||
label="Multi-Layered EXR")
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from ayon_core.hosts.houdini.api.lib import (
|
|||
get_camera_from_container,
|
||||
set_camera_resolution
|
||||
)
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
||||
|
||||
class SetCameraResolution(InventoryAction):
|
||||
|
|
@ -19,7 +19,7 @@ class SetCameraResolution(InventoryAction):
|
|||
)
|
||||
|
||||
def process(self, containers):
|
||||
folder_entity = get_current_project_folder()
|
||||
folder_entity = get_current_folder_entity()
|
||||
for container in containers:
|
||||
node = container["node"]
|
||||
camera = get_camera_from_container(node)
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
return
|
||||
|
||||
# Include handles
|
||||
start -= version_data.get("handleStart", 0)
|
||||
end += version_data.get("handleEnd", 0)
|
||||
start -= version_attributes.get("handleStart", 0)
|
||||
end += version_attributes.get("handleEnd", 0)
|
||||
|
||||
hou.playbar.setFrameRange(start, end)
|
||||
hou.playbar.setPlaybackRange(start, end)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class AbcLoader(load.LoaderPlugin):
|
|||
|
||||
normal_node.setInput(0, unpack)
|
||||
|
||||
null = container.createNode("null", node_name="OUT".format(name))
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 = ")
|
||||
|
|
|
|||
129
client/ayon_core/hosts/houdini/plugins/load/load_filepath.py
Normal file
129
client/ayon_core/hosts/houdini/plugins/load/load_filepath.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from ayon_core.pipeline import load
|
||||
from ayon_core.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, context):
|
||||
self.update(container, context)
|
||||
|
||||
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("\\", "/")
|
||||
77
client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py
Normal file
77
client/ayon_core/hosts/houdini/plugins/load/load_usd_sop.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -60,15 +60,22 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
instance.data["ifdFile"] = beauty_export_product
|
||||
instance.data["exportFiles"] = list(export_products)
|
||||
|
||||
# Default beauty AOV
|
||||
full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2")
|
||||
if full_exr_mode:
|
||||
# Ignore beauty suffix if full mode is enabled
|
||||
# As this is what the rop does.
|
||||
beauty_suffix = ""
|
||||
|
||||
# Default beauty/main layer AOV
|
||||
beauty_product = self.get_render_product_name(
|
||||
prefix=default_prefix, suffix=beauty_suffix
|
||||
)
|
||||
render_products = [beauty_product]
|
||||
files_by_aov = {
|
||||
"_": self.generate_expected_files(instance,
|
||||
beauty_product)}
|
||||
|
||||
beauty_suffix: self.generate_expected_files(instance,
|
||||
beauty_product)
|
||||
}
|
||||
|
||||
aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode()
|
||||
if aovs_rop:
|
||||
rop = aovs_rop
|
||||
|
|
@ -89,11 +96,14 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
if not aov_prefix:
|
||||
aov_prefix = default_prefix
|
||||
|
||||
aov_product = self.get_render_product_name(aov_prefix, aov_suffix)
|
||||
render_products.append(aov_product)
|
||||
if rop.parm(f"RS_aovID_{i}").evalAsString() == "CRYPTOMATTE" or \
|
||||
not full_exr_mode:
|
||||
|
||||
aov_product = self.get_render_product_name(aov_prefix, aov_suffix)
|
||||
render_products.append(aov_product)
|
||||
|
||||
files_by_aov[aov_suffix] = self.generate_expected_files(instance,
|
||||
aov_product) # noqa
|
||||
files_by_aov[aov_suffix] = self.generate_expected_files(instance,
|
||||
aov_product) # noqa
|
||||
|
||||
for product in render_products:
|
||||
self.log.debug("Found render product: %s" % product)
|
||||
|
|
@ -121,7 +131,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
|
||||
# When AOV is explicitly defined in prefix we just swap it out
|
||||
# directly with the AOV suffix to embed it.
|
||||
# Note: ${AOV} seems to be evaluated in the parameter as %AOV%
|
||||
# Note: '$AOV' seems to be evaluated in the parameter as '%AOV%'
|
||||
has_aov_in_prefix = "%AOV%" in prefix
|
||||
if has_aov_in_prefix:
|
||||
# It seems that when some special separator characters are present
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
|
|||
# the isinstance check above should be stricter than this category
|
||||
if output_node.type().category().name() != "Cop2":
|
||||
raise PublishValidationError(
|
||||
("Output node %s is not of category Cop2. "
|
||||
"This is a bug...").format(output_node.path()),
|
||||
(
|
||||
"Output node {} is not of category Cop2."
|
||||
" This is a bug..."
|
||||
).format(output_node.path()),
|
||||
title=cls.label)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import ayon_api
|
|||
from ayon_core.pipeline import get_current_project_name, colorspace
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline.context_tools import (
|
||||
get_current_project_folder,
|
||||
get_current_folder_entity,
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from pymxs import runtime as rt
|
||||
|
|
@ -222,7 +222,7 @@ def reset_scene_resolution():
|
|||
contains any information regarding scene resolution.
|
||||
"""
|
||||
|
||||
folder_entity = get_current_project_folder(
|
||||
folder_entity = get_current_folder_entity(
|
||||
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"}
|
||||
)
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
|
|
@ -243,7 +243,7 @@ def get_frame_range(folder_entiy=None) -> Union[Dict[str, Any], None]:
|
|||
"""
|
||||
# Set frame start/end
|
||||
if folder_entiy is None:
|
||||
folder_entiy = get_current_project_folder()
|
||||
folder_entiy = get_current_folder_entity()
|
||||
|
||||
folder_attributes = folder_entiy["attrib"]
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from pymxs import runtime as rt
|
|||
from ayon_core.lib import Logger
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
||||
from ayon_core.hosts.max.api.lib import (
|
||||
set_render_frame_range,
|
||||
|
|
@ -57,7 +57,7 @@ class RenderSettings(object):
|
|||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
# hard-coded, should be customized in the setting
|
||||
folder_attributes = get_current_project_folder()["attrib"]
|
||||
folder_attributes = get_current_folder_entity()["attrib"]
|
||||
|
||||
# get project resolution
|
||||
width = folder_attributes.get("resolutionWidth")
|
||||
|
|
|
|||
|
|
@ -240,10 +240,10 @@ def get_previous_loaded_object(container: str):
|
|||
node_list(list): list of nodes which are previously loaded
|
||||
"""
|
||||
node_list = []
|
||||
sel_list = rt.getProperty(container.modifiers[0].openPypeData, "sel_list")
|
||||
for obj in rt.Objects:
|
||||
if str(obj) in sel_list:
|
||||
node_list.append(obj)
|
||||
node_transform_monitor_list = rt.getProperty(
|
||||
container.modifiers[0].openPypeData, "all_handles")
|
||||
for node_transform_monitor in node_transform_monitor_list:
|
||||
node_list.append(node_transform_monitor.node)
|
||||
return node_list
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"""Pre-launch to force 3ds max startup script."""
|
||||
import os
|
||||
from ayon_core.hosts.max import MAX_HOST_DIR
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class ForceStartupScript(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch hook to inject python environment."""
|
||||
import os
|
||||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InjectPythonPath(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class SetPath(PreLaunchHook):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from ayon_core.hosts.max.api.lib import (
|
|||
maintained_selection,
|
||||
object_transform_set
|
||||
)
|
||||
from ayon_core.hosts.max.api.lib import maintained_selection
|
||||
from ayon_core.hosts.max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateCameraContent(pyblish.api.InstancePlugin):
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ class ValidateRenderPasses(OptionalPyblishPluginMixin,
|
|||
invalid = []
|
||||
if instance.name not in file_name:
|
||||
cls.log.error("The renderpass filename should contain the instance name.")
|
||||
invalid.append((f"Invalid instance name",
|
||||
invalid.append(("Invalid instance name",
|
||||
file_name))
|
||||
if renderpass is not None:
|
||||
if not file_name.rstrip(".").endswith(renderpass):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ from __future__ import absolute_import
|
|||
import pyblish.api
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline.publish import get_errored_instances_from_context
|
||||
from ayon_core.pipeline.publish import (
|
||||
get_errored_instances_from_context,
|
||||
get_errored_plugins_from_context
|
||||
)
|
||||
|
||||
|
||||
class GenerateUUIDsOnInvalidAction(pyblish.api.Action):
|
||||
|
|
@ -112,20 +115,25 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
except ImportError:
|
||||
raise ImportError("Current host is not Maya")
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.extend(invalid_nodes)
|
||||
else:
|
||||
self.log.warning("Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes.")
|
||||
if issubclass(plugin, pyblish.api.ContextPlugin):
|
||||
errored_plugins = get_errored_plugins_from_context(context)
|
||||
if plugin in errored_plugins:
|
||||
invalid = plugin.get_invalid(context)
|
||||
else:
|
||||
errored_instances = get_errored_instances_from_context(
|
||||
context, plugin=plugin
|
||||
)
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.extend(invalid_nodes)
|
||||
else:
|
||||
self.log.warning("Plug-in returned to be invalid, "
|
||||
"but has no selectable nodes.")
|
||||
|
||||
# Ensure unique (process each node only once)
|
||||
invalid = list(set(invalid))
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ def override_toolbox_ui():
|
|||
annotation="Look Manager",
|
||||
label="Look Manager",
|
||||
image=os.path.join(icons, "lookmanager.png"),
|
||||
command=show_look_assigner,
|
||||
command=lambda: show_look_assigner(
|
||||
parent=parent_widget
|
||||
),
|
||||
width=icon_size,
|
||||
height=icon_size,
|
||||
parent=parent
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ from ayon_core.pipeline import (
|
|||
AYON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_core.lib import NumberDef
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
from ayon_core.pipeline.create import CreateContext
|
||||
from ayon_core.lib.profiles_filtering import filter_profiles
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ def get_main_window():
|
|||
def suspended_refresh(suspend=True):
|
||||
"""Suspend viewport refreshes
|
||||
|
||||
cmds.ogs(pause=True) is a toggle so we cant pass False.
|
||||
cmds.ogs(pause=True) is a toggle so we can't pass False.
|
||||
"""
|
||||
if IS_HEADLESS:
|
||||
yield
|
||||
|
|
@ -583,7 +583,7 @@ def pairwise(iterable):
|
|||
|
||||
|
||||
def collect_animation_defs(fps=False):
|
||||
"""Get the basic animation attribute defintions for the publisher.
|
||||
"""Get the basic animation attribute definitions for the publisher.
|
||||
|
||||
Returns:
|
||||
OrderedDict
|
||||
|
|
@ -1876,18 +1876,9 @@ def list_looks(project_name, folder_id):
|
|||
list[dict[str, Any]]: List of look products.
|
||||
|
||||
"""
|
||||
# # get all products with look leading in
|
||||
# the name associated with the asset
|
||||
# TODO this should probably look for product type 'look' instead of
|
||||
# checking product name that can not start with product type
|
||||
product_entities = ayon_api.get_products(
|
||||
project_name, folder_ids=[folder_id]
|
||||
)
|
||||
return [
|
||||
product_entity
|
||||
for product_entity in product_entities
|
||||
if product_entity["name"].startswith("look")
|
||||
]
|
||||
return list(ayon_api.get_products(
|
||||
project_name, folder_ids=[folder_id], product_types={"look"}
|
||||
))
|
||||
|
||||
|
||||
def assign_look_by_version(nodes, version_id):
|
||||
|
|
@ -1906,12 +1897,15 @@ def assign_look_by_version(nodes, version_id):
|
|||
project_name = get_current_project_name()
|
||||
|
||||
# Get representations of shader file and relationships
|
||||
look_representation = ayon_api.get_representation_by_name(
|
||||
project_name, "ma", version_id
|
||||
)
|
||||
json_representation = ayon_api.get_representation_by_name(
|
||||
project_name, "json", version_id
|
||||
)
|
||||
representations = list(ayon_api.get_representations(
|
||||
project_name=project_name,
|
||||
representation_names={"ma", "json"},
|
||||
version_ids=[version_id]
|
||||
))
|
||||
look_representation = next(
|
||||
repre for repre in representations if repre["name"] == "ma")
|
||||
json_representation = next(
|
||||
repre for repre in representations if repre["name"] == "json")
|
||||
|
||||
# See if representation is already loaded, if so reuse it.
|
||||
host = registered_host()
|
||||
|
|
@ -1948,7 +1942,7 @@ def assign_look_by_version(nodes, version_id):
|
|||
apply_shaders(relationships, shader_nodes, nodes)
|
||||
|
||||
|
||||
def assign_look(nodes, product_name="lookDefault"):
|
||||
def assign_look(nodes, product_name="lookMain"):
|
||||
"""Assigns a look to a node.
|
||||
|
||||
Optimizes the nodes by grouping by folder id and finding
|
||||
|
|
@ -1981,14 +1975,10 @@ def assign_look(nodes, product_name="lookDefault"):
|
|||
product_entity["id"]
|
||||
for product_entity in product_entities_by_folder_id.values()
|
||||
}
|
||||
last_version_entities = ayon_api.get_last_versions(
|
||||
last_version_entities_by_product_id = ayon_api.get_last_versions(
|
||||
project_name,
|
||||
product_ids
|
||||
)
|
||||
last_version_entities_by_product_id = {
|
||||
last_version_entity["productId"]: last_version_entity
|
||||
for last_version_entity in last_version_entities
|
||||
}
|
||||
|
||||
for folder_id, asset_nodes in grouped.items():
|
||||
product_entity = product_entities_by_folder_id.get(folder_id)
|
||||
|
|
@ -2125,22 +2115,6 @@ def get_related_sets(node):
|
|||
|
||||
"""
|
||||
|
||||
# Ignore specific suffices
|
||||
ignore_suffices = ["out_SET", "controls_SET", "_INST", "_CON"]
|
||||
|
||||
# Default nodes to ignore
|
||||
defaults = {"defaultLightSet", "defaultObjectSet"}
|
||||
|
||||
# Ids to ignore
|
||||
ignored = {
|
||||
AVALON_INSTANCE_ID,
|
||||
AVALON_CONTAINER_ID,
|
||||
AYON_INSTANCE_ID,
|
||||
AYON_CONTAINER_ID,
|
||||
}
|
||||
|
||||
view_sets = get_isolate_view_sets()
|
||||
|
||||
sets = cmds.listSets(object=node, extendToShape=False)
|
||||
if not sets:
|
||||
return []
|
||||
|
|
@ -2151,25 +2125,47 @@ def get_related_sets(node):
|
|||
# returned by `cmds.listSets(allSets=True)`
|
||||
sets = cmds.ls(sets)
|
||||
|
||||
# Ids to ignore
|
||||
ignored = {
|
||||
AVALON_INSTANCE_ID,
|
||||
AVALON_CONTAINER_ID,
|
||||
AYON_INSTANCE_ID,
|
||||
AYON_CONTAINER_ID,
|
||||
}
|
||||
|
||||
# Ignore `avalon.container`
|
||||
sets = [s for s in sets if
|
||||
not cmds.attributeQuery("id", node=s, exists=True) or
|
||||
not cmds.getAttr("%s.id" % s) in ignored]
|
||||
sets = [
|
||||
s for s in sets
|
||||
if (
|
||||
not cmds.attributeQuery("id", node=s, exists=True)
|
||||
or cmds.getAttr(f"{s}.id") not in ignored
|
||||
)
|
||||
]
|
||||
if not sets:
|
||||
return sets
|
||||
|
||||
# Exclude deformer sets (`type=2` for `maya.cmds.listSets`)
|
||||
deformer_sets = cmds.listSets(object=node,
|
||||
extendToShape=False,
|
||||
type=2) or []
|
||||
deformer_sets = set(deformer_sets) # optimize lookup
|
||||
sets = [s for s in sets if s not in deformer_sets]
|
||||
exclude_sets = cmds.listSets(object=node,
|
||||
extendToShape=False,
|
||||
type=2) or []
|
||||
exclude_sets = set(exclude_sets) # optimize lookup
|
||||
|
||||
# Default nodes to ignore
|
||||
exclude_sets.update({"defaultLightSet", "defaultObjectSet"})
|
||||
|
||||
# Filter out the sets to exclude
|
||||
sets = [s for s in sets if s not in exclude_sets]
|
||||
|
||||
# Ignore when the set has a specific suffix
|
||||
sets = [s for s in sets if not any(s.endswith(x) for x in ignore_suffices)]
|
||||
ignore_suffices = ("out_SET", "controls_SET", "_INST", "_CON")
|
||||
sets = [s for s in sets if not s.endswith(ignore_suffices)]
|
||||
if not sets:
|
||||
return sets
|
||||
|
||||
# Ignore viewport filter view sets (from isolate select and
|
||||
# viewports)
|
||||
view_sets = get_isolate_view_sets()
|
||||
sets = [s for s in sets if s not in view_sets]
|
||||
sets = [s for s in sets if s not in defaults]
|
||||
|
||||
return sets
|
||||
|
||||
|
|
@ -2440,12 +2436,10 @@ def set_scene_fps(fps, update=True):
|
|||
cmds.currentUnit(time=unit, updateAnimation=update)
|
||||
|
||||
# Set time slider data back to previous state
|
||||
cmds.playbackOptions(edit=True, minTime=start_frame)
|
||||
cmds.playbackOptions(edit=True, maxTime=end_frame)
|
||||
|
||||
# Set animation data
|
||||
cmds.playbackOptions(edit=True, animationStartTime=animation_start)
|
||||
cmds.playbackOptions(edit=True, animationEndTime=animation_end)
|
||||
cmds.playbackOptions(minTime=start_frame,
|
||||
maxTime=end_frame,
|
||||
animationStartTime=animation_start,
|
||||
animationEndTime=animation_end)
|
||||
|
||||
cmds.currentTime(current_frame, edit=True, update=True)
|
||||
|
||||
|
|
@ -2521,7 +2515,7 @@ def get_fps_for_current_context():
|
|||
|
||||
|
||||
def get_frame_range(include_animation_range=False):
|
||||
"""Get the current folder frame range and handles.
|
||||
"""Get the current task frame range and handles.
|
||||
|
||||
Args:
|
||||
include_animation_range (bool, optional): Whether to include
|
||||
|
|
@ -2529,25 +2523,34 @@ def get_frame_range(include_animation_range=False):
|
|||
range of the timeline. It is excluded by default.
|
||||
|
||||
Returns:
|
||||
dict: Folder's expected frame range values.
|
||||
dict: Task's expected frame range values.
|
||||
|
||||
"""
|
||||
|
||||
# Set frame start/end
|
||||
project_name = get_current_project_name()
|
||||
folder_path = get_current_folder_path()
|
||||
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
task_name = get_current_task_name()
|
||||
|
||||
frame_start = folder_attributes.get("frameStart")
|
||||
frame_end = folder_attributes.get("frameEnd")
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name,
|
||||
folder_path,
|
||||
fields={"id"})
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
|
||||
task_attributes = task_entity["attrib"]
|
||||
|
||||
frame_start = task_attributes.get("frameStart")
|
||||
frame_end = task_attributes.get("frameEnd")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
cmds.warning("No edit information found for '{}'".format(folder_path))
|
||||
return
|
||||
|
||||
handle_start = folder_attributes.get("handleStart") or 0
|
||||
handle_end = folder_attributes.get("handleEnd") or 0
|
||||
handle_start = task_attributes.get("handleStart") or 0
|
||||
handle_end = task_attributes.get("handleEnd") or 0
|
||||
|
||||
frame_range = {
|
||||
"frameStart": frame_start,
|
||||
|
|
@ -2561,14 +2564,10 @@ def get_frame_range(include_animation_range=False):
|
|||
# Some usages of this function use the full dictionary to define
|
||||
# instance attributes for which we want to exclude the animation
|
||||
# keys. That is why these are excluded by default.
|
||||
task_name = get_current_task_name()
|
||||
|
||||
settings = get_project_settings(project_name)
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
task_type = None
|
||||
if task_entity:
|
||||
task_type = task_entity["taskType"]
|
||||
|
||||
task_type = task_entity["taskType"]
|
||||
|
||||
include_handles_settings = settings["maya"]["include_handles"]
|
||||
|
||||
|
|
@ -2637,7 +2636,7 @@ def reset_scene_resolution():
|
|||
None
|
||||
"""
|
||||
|
||||
folder_attributes = get_current_project_folder()["attrib"]
|
||||
folder_attributes = get_current_folder_entity()["attrib"]
|
||||
|
||||
# Set resolution
|
||||
width = folder_attributes.get("resolutionWidth", 1920)
|
||||
|
|
@ -2647,31 +2646,114 @@ def reset_scene_resolution():
|
|||
set_scene_resolution(width, height, pixelAspect)
|
||||
|
||||
|
||||
def set_context_settings():
|
||||
def set_context_settings(
|
||||
fps=True,
|
||||
resolution=True,
|
||||
frame_range=True,
|
||||
colorspace=True
|
||||
):
|
||||
"""Apply the project settings from the project definition
|
||||
|
||||
Settings can be overwritten by an folder if the folder.attrib contains
|
||||
Settings can be overwritten by an asset if the asset.data contains
|
||||
any information regarding those settings.
|
||||
|
||||
Examples of settings:
|
||||
fps
|
||||
resolution
|
||||
renderer
|
||||
Args:
|
||||
fps (bool): Whether to set the scene FPS.
|
||||
resolution (bool): Whether to set the render resolution.
|
||||
frame_range (bool): Whether to reset the time slide frame ranges.
|
||||
colorspace (bool): Whether to reset the colorspace.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
# Set project fps
|
||||
set_scene_fps(get_fps_for_current_context())
|
||||
if fps:
|
||||
# Set project fps
|
||||
set_scene_fps(get_fps_for_current_context())
|
||||
|
||||
reset_scene_resolution()
|
||||
if resolution:
|
||||
reset_scene_resolution()
|
||||
|
||||
# Set frame range.
|
||||
reset_frame_range()
|
||||
if frame_range:
|
||||
reset_frame_range(fps=False)
|
||||
|
||||
# Set colorspace
|
||||
set_colorspace()
|
||||
if colorspace:
|
||||
set_colorspace()
|
||||
|
||||
|
||||
def prompt_reset_context():
|
||||
"""Prompt the user what context settings to reset.
|
||||
This prompt is used on saving to a different task to allow the scene to
|
||||
get matched to the new context.
|
||||
"""
|
||||
# TODO: Cleanup this prototyped mess of imports and odd dialog
|
||||
from ayon_core.tools.attribute_defs.dialog import (
|
||||
AttributeDefinitionsDialog
|
||||
)
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.lib import BoolDef, UILabelDef
|
||||
|
||||
definitions = [
|
||||
UILabelDef(
|
||||
label=(
|
||||
"You are saving your workfile into a different folder or task."
|
||||
"\n\n"
|
||||
"Would you like to update some settings to the new context?\n"
|
||||
)
|
||||
),
|
||||
BoolDef(
|
||||
"fps",
|
||||
label="FPS",
|
||||
tooltip="Reset workfile FPS",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"frame_range",
|
||||
label="Frame Range",
|
||||
tooltip="Reset workfile start and end frame ranges",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"resolution",
|
||||
label="Resolution",
|
||||
tooltip="Reset workfile resolution",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"colorspace",
|
||||
label="Colorspace",
|
||||
tooltip="Reset workfile resolution",
|
||||
default=True
|
||||
),
|
||||
BoolDef(
|
||||
"instances",
|
||||
label="Publish instances",
|
||||
tooltip="Update all publish instance's folder and task to match "
|
||||
"the new folder and task",
|
||||
default=True
|
||||
),
|
||||
]
|
||||
|
||||
dialog = AttributeDefinitionsDialog(definitions)
|
||||
dialog.setWindowTitle("Saving to different context.")
|
||||
dialog.setStyleSheet(load_stylesheet())
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
options = dialog.get_values()
|
||||
with suspended_refresh():
|
||||
set_context_settings(
|
||||
fps=options["fps"],
|
||||
resolution=options["resolution"],
|
||||
frame_range=options["frame_range"],
|
||||
colorspace=options["colorspace"]
|
||||
)
|
||||
if options["instances"]:
|
||||
update_content_on_context_change()
|
||||
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
# Valid FPS
|
||||
|
|
@ -3163,7 +3245,7 @@ def update_content_on_context_change():
|
|||
This will update scene content to match new folder on context change
|
||||
"""
|
||||
scene_sets = cmds.listSets(allSets=True)
|
||||
folder_entity = get_current_project_folder()
|
||||
folder_entity = get_current_folder_entity()
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
new_folder_path = folder_entity["path"]
|
||||
for s in scene_sets:
|
||||
|
|
@ -3834,7 +3916,7 @@ def get_color_management_output_transform():
|
|||
|
||||
def image_info(file_path):
|
||||
# type: (str) -> dict
|
||||
"""Based on tha texture path, get its bit depth and format information.
|
||||
"""Based on the texture path, get its bit depth and format information.
|
||||
Take reference from makeTx.py in Arnold:
|
||||
ImageInfo(filename): Get Image Information for colorspace
|
||||
AiTextureGetFormat(filename): Get Texture Format
|
||||
|
|
@ -3922,17 +4004,26 @@ def len_flattened(components):
|
|||
return n
|
||||
|
||||
|
||||
def get_all_children(nodes):
|
||||
def get_all_children(nodes, ignore_intermediate_objects=False):
|
||||
"""Return all children of `nodes` including each instanced child.
|
||||
Using maya.cmds.listRelatives(allDescendents=True) includes only the first
|
||||
instance. As such, this function acts as an optimal replacement with a
|
||||
focus on a fast query.
|
||||
|
||||
Args:
|
||||
nodes (iterable): List of nodes to get children for.
|
||||
ignore_intermediate_objects (bool): Ignore any children that
|
||||
are intermediate objects.
|
||||
|
||||
Returns:
|
||||
set: Children of input nodes.
|
||||
|
||||
"""
|
||||
|
||||
sel = OpenMaya.MSelectionList()
|
||||
traversed = set()
|
||||
iterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst)
|
||||
fn_dag = OpenMaya.MFnDagNode()
|
||||
for node in nodes:
|
||||
|
||||
if node in traversed:
|
||||
|
|
@ -3949,6 +4040,13 @@ def get_all_children(nodes):
|
|||
iterator.next() # noqa: B305
|
||||
while not iterator.isDone():
|
||||
|
||||
if ignore_intermediate_objects:
|
||||
fn_dag.setObject(iterator.currentItem())
|
||||
if fn_dag.isIntermediateObject:
|
||||
iterator.prune()
|
||||
iterator.next() # noqa: B305
|
||||
continue
|
||||
|
||||
path = iterator.fullPathName()
|
||||
|
||||
if path in traversed:
|
||||
|
|
@ -3959,7 +4057,7 @@ def get_all_children(nodes):
|
|||
traversed.add(path)
|
||||
iterator.next() # noqa: B305
|
||||
|
||||
return list(traversed)
|
||||
return traversed
|
||||
|
||||
|
||||
def get_capture_preset(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from ayon_core.lib import Logger
|
|||
from ayon_core.settings import get_project_settings
|
||||
|
||||
from ayon_core.pipeline import CreatorError, get_current_project_name
|
||||
from ayon_core.pipeline.context_tools import get_current_project_folder
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
from ayon_core.hosts.maya.api.lib import reset_frame_range
|
||||
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ class RenderSettings(object):
|
|||
renderer = cmds.getAttr(
|
||||
'defaultRenderGlobals.currentRenderer').lower()
|
||||
|
||||
folder_entity = get_current_project_folder()
|
||||
folder_entity = get_current_folder_entity()
|
||||
folder_attributes = folder_entity["attrib"]
|
||||
# project_settings/maya/create/CreateRender/aov_separator
|
||||
try:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue