mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
f99aa73fe5
11 changed files with 219 additions and 60 deletions
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,7 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.12-nightly.2
|
||||
- 3.15.12-nightly.1
|
||||
- 3.15.11
|
||||
- 3.15.11-nightly.5
|
||||
|
|
@ -134,7 +135,6 @@ body:
|
|||
- 3.14.4-nightly.4
|
||||
- 3.14.4-nightly.3
|
||||
- 3.14.4-nightly.2
|
||||
- 3.14.4-nightly.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
|
|||
cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern)
|
||||
cls.log.debug("Checking with path attribute: %s" % path_attr)
|
||||
|
||||
if not hasattr(output_node, "geometry"):
|
||||
# In the case someone has explicitly set an Object
|
||||
# node instead of a SOP node in Geometry context
|
||||
# then for now we ignore - this allows us to also
|
||||
# export object transforms.
|
||||
cls.log.warning("No geometry output node found, skipping check..")
|
||||
return
|
||||
|
||||
# Check if the primitive attribute exists
|
||||
frame = instance.data.get("frameStart", 0)
|
||||
geo = output_node.geometryAtFrame(frame)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
|
|||
|
||||
cls.log.debug("Checking for attribute: %s" % path_attr)
|
||||
|
||||
if not hasattr(output_node, "geometry"):
|
||||
# In the case someone has explicitly set an Object
|
||||
# node instead of a SOP node in Geometry context
|
||||
# then for now we ignore - this allows us to also
|
||||
# export object transforms.
|
||||
cls.log.warning("No geometry output node found, skipping check..")
|
||||
return
|
||||
|
||||
# Check if the primitive attribute exists
|
||||
frame = instance.data.get("frameStart", 0)
|
||||
geo = output_node.geometryAtFrame(frame)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ def update_mode_context(mode):
|
|||
|
||||
def get_geometry_at_frame(sop_node, frame, force=True):
|
||||
"""Return geometry at frame but force a cooked value."""
|
||||
if not hasattr(sop_node, "geometry"):
|
||||
return
|
||||
with update_mode_context(hou.updateMode.AutoUpdate):
|
||||
sop_node.cook(force=force, frame_range=(frame, frame))
|
||||
return sop_node.geometryAtFrame(frame)
|
||||
|
|
|
|||
|
|
@ -3238,36 +3238,21 @@ def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None):
|
|||
|
||||
|
||||
def set_colorspace():
|
||||
"""Set Colorspace from project configuration
|
||||
"""
|
||||
"""Set Colorspace from project configuration"""
|
||||
|
||||
# set color spaces for rendering space and view transforms
|
||||
def _colormanage(**kwargs):
|
||||
"""Wrapper around `cmds.colorManagementPrefs`.
|
||||
|
||||
This logs errors instead of raising an error so color management
|
||||
settings get applied as much as possible.
|
||||
|
||||
"""
|
||||
assert len(kwargs) == 1, "Must receive one keyword argument"
|
||||
try:
|
||||
cmds.colorManagementPrefs(edit=True, **kwargs)
|
||||
log.debug("Setting Color Management Preference: {}".format(kwargs))
|
||||
except RuntimeError as exc:
|
||||
log.error(exc)
|
||||
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
project_name = get_current_project_name()
|
||||
imageio = get_project_settings(project_name)["maya"]["imageio"]
|
||||
|
||||
# ocio compatibility variables
|
||||
ocio_v2_maya_version = 2022
|
||||
maya_version = int(cmds.about(version=True))
|
||||
ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version
|
||||
is_ocio_set = bool(os.environ.get("OCIO"))
|
||||
|
||||
root_dict = {}
|
||||
use_workfile_settings = imageio.get("workfile", {}).get("enabled")
|
||||
|
||||
if use_workfile_settings:
|
||||
root_dict = imageio["workfile"]
|
||||
else:
|
||||
# TODO: deprecated code from 3.15.5 - remove
|
||||
# Maya 2022+ introduces new OCIO v2 color management settings that
|
||||
# can override the old color management preferences. OpenPype has
|
||||
|
|
@ -3290,40 +3275,63 @@ def set_colorspace():
|
|||
if not isinstance(root_dict, dict):
|
||||
msg = "set_colorspace(): argument should be dictionary"
|
||||
log.error(msg)
|
||||
return
|
||||
|
||||
else:
|
||||
root_dict = imageio["workfile"]
|
||||
# backward compatibility
|
||||
# TODO: deprecated code from 3.15.5 - remove with deprecated code above
|
||||
view_name = root_dict.get("viewTransform")
|
||||
if view_name is None:
|
||||
view_name = root_dict.get("viewName")
|
||||
|
||||
log.debug(">> root_dict: {}".format(pformat(root_dict)))
|
||||
if not root_dict:
|
||||
return
|
||||
|
||||
if root_dict:
|
||||
# enable color management
|
||||
cmds.colorManagementPrefs(e=True, cmEnabled=True)
|
||||
cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True)
|
||||
# set color spaces for rendering space and view transforms
|
||||
def _colormanage(**kwargs):
|
||||
"""Wrapper around `cmds.colorManagementPrefs`.
|
||||
|
||||
# backward compatibility
|
||||
# TODO: deprecated code from 3.15.5 - refactor to use new settings
|
||||
view_name = root_dict.get("viewTransform")
|
||||
if view_name is None:
|
||||
view_name = root_dict.get("viewName")
|
||||
This logs errors instead of raising an error so color management
|
||||
settings get applied as much as possible.
|
||||
|
||||
if use_ocio_v2:
|
||||
# Use Maya 2022+ default OCIO v2 config
|
||||
"""
|
||||
assert len(kwargs) == 1, "Must receive one keyword argument"
|
||||
try:
|
||||
cmds.colorManagementPrefs(edit=True, **kwargs)
|
||||
log.debug("Setting Color Management Preference: {}".format(kwargs))
|
||||
except RuntimeError as exc:
|
||||
log.error(exc)
|
||||
|
||||
# enable color management
|
||||
cmds.colorManagementPrefs(edit=True, cmEnabled=True)
|
||||
cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True)
|
||||
|
||||
if use_ocio_v2:
|
||||
log.info("Using Maya OCIO v2")
|
||||
if not is_ocio_set:
|
||||
# Set the Maya 2022+ default OCIO v2 config file path
|
||||
log.info("Setting default Maya OCIO v2 config")
|
||||
cmds.colorManagementPrefs(edit=True, configFilePath="")
|
||||
# Note: Setting "" as value also sets this default however
|
||||
# introduces a bug where launching a file on startup will prompt
|
||||
# to save the empty scene before it, so we set using the path.
|
||||
# This value has been the same for 2022, 2023 and 2024
|
||||
path = "<MAYA_RESOURCES>/OCIO-configs/Maya2022-default/config.ocio"
|
||||
cmds.colorManagementPrefs(edit=True, configFilePath=path)
|
||||
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewName=view_name)
|
||||
_colormanage(displayName=root_dict["displayName"])
|
||||
else:
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewName=view_name)
|
||||
_colormanage(displayName=root_dict["displayName"])
|
||||
else:
|
||||
log.info("Using Maya OCIO v1 (legacy)")
|
||||
if not is_ocio_set:
|
||||
# Set the Maya default config file path
|
||||
log.info("Setting default Maya OCIO v1 legacy config")
|
||||
cmds.colorManagementPrefs(edit=True, configFilePath="legacy")
|
||||
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewTransformName=view_name)
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewTransformName=view_name)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import errno
|
||||
import logging
|
||||
import contextlib
|
||||
import shutil
|
||||
|
||||
from maya import utils, cmds, OpenMaya
|
||||
import maya.api.OpenMaya as om
|
||||
|
|
@ -113,6 +114,9 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
|
|||
register_event_callback("taskChanged", on_task_changed)
|
||||
register_event_callback("workfile.open.before", before_workfile_open)
|
||||
register_event_callback("workfile.save.before", before_workfile_save)
|
||||
register_event_callback(
|
||||
"workfile.save.before", workfile_save_before_xgen
|
||||
)
|
||||
register_event_callback("workfile.save.after", after_workfile_save)
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
|
|
@ -681,6 +685,91 @@ def before_workfile_save(event):
|
|||
create_workspace_mel(workdir_path, project_name)
|
||||
|
||||
|
||||
def workfile_save_before_xgen(event):
|
||||
"""Manage Xgen external files when switching context.
|
||||
|
||||
Xgen has various external files that needs to be unique and relative to the
|
||||
workfile, so we need to copy and potentially overwrite these files when
|
||||
switching context.
|
||||
|
||||
Args:
|
||||
event (Event) - openpype/lib/events.py
|
||||
"""
|
||||
if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True):
|
||||
return
|
||||
|
||||
import xgenm
|
||||
|
||||
current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/")
|
||||
expected_work_dir = event.data["workdir_path"].replace("\\", "/")
|
||||
if current_work_dir == expected_work_dir:
|
||||
return
|
||||
|
||||
palettes = cmds.ls(type="xgmPalette", long=True)
|
||||
if not palettes:
|
||||
return
|
||||
|
||||
transfers = []
|
||||
overwrites = []
|
||||
attribute_changes = {}
|
||||
attrs = ["xgFileName", "xgBaseFile"]
|
||||
for palette in palettes:
|
||||
sanitized_palette = palette.replace("|", "")
|
||||
project_path = xgenm.getAttr("xgProjectPath", sanitized_palette)
|
||||
_, maya_extension = os.path.splitext(event.data["filename"])
|
||||
|
||||
for attr in attrs:
|
||||
node_attr = "{}.{}".format(palette, attr)
|
||||
attr_value = cmds.getAttr(node_attr)
|
||||
|
||||
if not attr_value:
|
||||
continue
|
||||
|
||||
source = os.path.join(project_path, attr_value)
|
||||
|
||||
attr_value = event.data["filename"].replace(
|
||||
maya_extension,
|
||||
"__{}{}".format(
|
||||
sanitized_palette.replace(":", "__"),
|
||||
os.path.splitext(attr_value)[1]
|
||||
)
|
||||
)
|
||||
target = os.path.join(expected_work_dir, attr_value)
|
||||
|
||||
transfers.append((source, target))
|
||||
attribute_changes[node_attr] = attr_value
|
||||
|
||||
relative_path = xgenm.getAttr(
|
||||
"xgDataPath", sanitized_palette
|
||||
).split(os.pathsep)[0]
|
||||
absolute_path = relative_path.replace("${PROJECT}", project_path)
|
||||
for root, _, files in os.walk(absolute_path):
|
||||
for f in files:
|
||||
source = os.path.join(root, f).replace("\\", "/")
|
||||
target = source.replace(project_path, expected_work_dir + "/")
|
||||
transfers.append((source, target))
|
||||
if os.path.exists(target):
|
||||
overwrites.append(target)
|
||||
|
||||
# Ask user about overwriting files.
|
||||
if overwrites:
|
||||
log.warning(
|
||||
"WARNING! Potential loss of data.\n\n"
|
||||
"Found duplicate Xgen files in new context.\n{}".format(
|
||||
"\n".join(overwrites)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
for source, destination in transfers:
|
||||
if not os.path.exists(os.path.dirname(destination)):
|
||||
os.makedirs(os.path.dirname(destination))
|
||||
shutil.copy(source, destination)
|
||||
|
||||
for attribute, value in attribute_changes.items():
|
||||
cmds.setAttr(attribute, value, type="string")
|
||||
|
||||
|
||||
def after_workfile_save(event):
|
||||
workfile_name = event["filename"]
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -272,7 +272,12 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
|
|||
return
|
||||
|
||||
roots = cmds.sets(container, q=True)
|
||||
ref_node = get_reference_node(roots)
|
||||
ref_node = None
|
||||
try:
|
||||
ref_node = get_reference_node(roots)
|
||||
except AssertionError as e:
|
||||
self.log.info(e.args[0])
|
||||
|
||||
nodes_to_parent = []
|
||||
for root in roots:
|
||||
if ref_node:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from .utils import (
|
|||
LoadError,
|
||||
IncompatibleLoaderError,
|
||||
InvalidRepresentationContext,
|
||||
LoaderSwitchNotImplementedError,
|
||||
LoaderNotFoundError,
|
||||
|
||||
get_repres_contexts,
|
||||
get_contexts_for_repre_docs,
|
||||
|
|
@ -55,6 +57,8 @@ __all__ = (
|
|||
"LoadError",
|
||||
"IncompatibleLoaderError",
|
||||
"InvalidRepresentationContext",
|
||||
"LoaderSwitchNotImplementedError",
|
||||
"LoaderNotFoundError",
|
||||
|
||||
"get_repres_contexts",
|
||||
"get_contexts_for_repre_docs",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,16 @@ class InvalidRepresentationContext(ValueError):
|
|||
pass
|
||||
|
||||
|
||||
class LoaderSwitchNotImplementedError(NotImplementedError):
|
||||
"""Error when `switch` is used with Loader that has no implementation."""
|
||||
pass
|
||||
|
||||
|
||||
class LoaderNotFoundError(RuntimeError):
|
||||
"""Error when Loader plugin for a loader name is not found."""
|
||||
pass
|
||||
|
||||
|
||||
def get_repres_contexts(representation_ids, dbcon=None):
|
||||
"""Return parenthood context for representation.
|
||||
|
||||
|
|
@ -432,7 +442,10 @@ def remove_container(container):
|
|||
|
||||
Loader = _get_container_loader(container)
|
||||
if not Loader:
|
||||
raise RuntimeError("Can't remove container. See log for details.")
|
||||
raise LoaderNotFoundError(
|
||||
"Can't remove container because loader '{}' was not found."
|
||||
.format(container.get("loader"))
|
||||
)
|
||||
|
||||
loader = Loader(get_representation_context(container["representation"]))
|
||||
return loader.remove(container)
|
||||
|
|
@ -480,7 +493,10 @@ def update_container(container, version=-1):
|
|||
# Run update on the Loader for this container
|
||||
Loader = _get_container_loader(container)
|
||||
if not Loader:
|
||||
raise RuntimeError("Can't update container. See log for details.")
|
||||
raise LoaderNotFoundError(
|
||||
"Can't update container because loader '{}' was not found."
|
||||
.format(container.get("loader"))
|
||||
)
|
||||
|
||||
loader = Loader(get_representation_context(container["representation"]))
|
||||
return loader.update(container, new_representation)
|
||||
|
|
@ -502,15 +518,18 @@ def switch_container(container, representation, loader_plugin=None):
|
|||
loader_plugin = _get_container_loader(container)
|
||||
|
||||
if not loader_plugin:
|
||||
raise RuntimeError("Can't switch container. See log for details.")
|
||||
raise LoaderNotFoundError(
|
||||
"Can't switch container because loader '{}' was not found."
|
||||
.format(container.get("loader"))
|
||||
)
|
||||
|
||||
if not hasattr(loader_plugin, "switch"):
|
||||
# Backwards compatibility (classes without switch support
|
||||
# might be better to just have "switch" raise NotImplementedError
|
||||
# on the base class of Loader\
|
||||
raise RuntimeError("Loader '{}' does not support 'switch'".format(
|
||||
loader_plugin.label
|
||||
))
|
||||
raise LoaderSwitchNotImplementedError(
|
||||
"Loader {} does not support 'switch'".format(loader_plugin.label)
|
||||
)
|
||||
|
||||
# Get the new representation to switch to
|
||||
project_name = legacy_io.active_project()
|
||||
|
|
@ -520,7 +539,11 @@ def switch_container(container, representation, loader_plugin=None):
|
|||
|
||||
new_context = get_representation_context(new_representation)
|
||||
if not is_compatible_loader(loader_plugin, new_context):
|
||||
raise AssertionError("Must be compatible Loader")
|
||||
raise IncompatibleLoaderError(
|
||||
"Loader {} is incompatible with {}".format(
|
||||
loader_plugin.__name__, new_context["subset"]["name"]
|
||||
)
|
||||
)
|
||||
|
||||
loader = loader_plugin(new_context)
|
||||
|
||||
|
|
|
|||
|
|
@ -421,9 +421,9 @@
|
|||
},
|
||||
"workfile": {
|
||||
"enabled": false,
|
||||
"renderSpace": "ACEScg",
|
||||
"displayName": "sRGB",
|
||||
"viewName": "ACES 1.0 SDR-video"
|
||||
"renderSpace": "ACES - ACEScg",
|
||||
"displayName": "ACES",
|
||||
"viewName": "sRGB"
|
||||
},
|
||||
"colorManagementPreference_v2": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ from openpype.pipeline.load import (
|
|||
switch_container,
|
||||
get_repres_contexts,
|
||||
loaders_from_repre_context,
|
||||
LoaderSwitchNotImplementedError,
|
||||
IncompatibleLoaderError,
|
||||
LoaderNotFoundError
|
||||
)
|
||||
|
||||
from .widgets import (
|
||||
|
|
@ -1298,19 +1301,28 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
else:
|
||||
repre_doc = repres_by_name[container_repre_name]
|
||||
|
||||
error = None
|
||||
try:
|
||||
switch_container(container, repre_doc, loader)
|
||||
except (
|
||||
LoaderSwitchNotImplementedError,
|
||||
IncompatibleLoaderError,
|
||||
LoaderNotFoundError,
|
||||
) as exc:
|
||||
error = str(exc)
|
||||
except Exception:
|
||||
msg = (
|
||||
error = (
|
||||
"Switch asset failed. "
|
||||
"Search console log for more details."
|
||||
)
|
||||
if error is not None:
|
||||
log.warning((
|
||||
"Couldn't switch asset."
|
||||
"See traceback for more information."
|
||||
)
|
||||
log.warning(msg, exc_info=True)
|
||||
), exc_info=True)
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Switch asset failed")
|
||||
dialog.setText(
|
||||
"Switch asset failed. Search console log for more details"
|
||||
)
|
||||
dialog.setText(error)
|
||||
dialog.exec_()
|
||||
|
||||
self.switched.emit()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue