From 8c4b303c586669f36f7ffaae8b8e4cdc8dee89d0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 18 Apr 2023 12:45:28 +0100 Subject: [PATCH 01/83] Use get_reference_node and parent in hierarchy. --- openpype/hosts/maya/api/lib.py | 65 +++++++++++++++++++ .../maya/api/workfile_template_builder.py | 17 ++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 61ea3d59df..1af1cb569e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3913,3 +3913,68 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): capture_preset = plugin_settings["capture_preset"] return capture_preset or {} + + +def get_reference_node(members, log=None): + """Get the reference node from the container members + Args: + members: list of node names + + Returns: + str: Reference node name. + + """ + + # Collect the references without .placeHolderList[] attributes as + # unique entries (objects only) and skipping the sharedReferenceNode. + references = set() + for ref in cmds.ls(members, exactType="reference", objectsOnly=True): + + # Ignore any `:sharedReferenceNode` + if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): + continue + + # Ignore _UNKNOWN_REF_NODE_ (PLN-160) + if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): + continue + + references.add(ref) + + assert references, "No reference node found in container" + + # Get highest reference node (least parents) + highest = min(references, + key=lambda x: len(get_reference_node_parents(x))) + + # Warn the user when we're taking the highest reference node + if len(references) > 1: + if not log: + log = logging.getLogger(__name__) + + log.warning("More than one reference node found in " + "container, using highest reference node: " + "%s (in: %s)", highest, list(references)) + + return highest + + +def get_reference_node_parents(ref): + """Return all parent reference nodes of reference node + + Args: + ref (str): reference node. + + Returns: + list: The upstream parent reference nodes. + + """ + parent = cmds.referenceQuery(ref, + referenceNode=True, + parent=True) + parents = [] + while parent: + parents.append(parent) + parent = cmds.referenceQuery(parent, + referenceNode=True, + parent=True) + return parents diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d65e4c74d2..f8169adb52 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -14,7 +14,7 @@ from openpype.tools.workfile_template_build import ( WorkfileBuildPlaceholderDialog, ) -from .lib import read, imprint +from .lib import read, imprint, get_reference_node PLACEHOLDER_SET = "PLACEHOLDERS_SET" @@ -258,8 +258,16 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): return roots = cmds.sets(container, q=True) + ref_node = get_reference_node(roots) nodes_to_parent = [] for root in roots: + if ref_node: + ref_root = cmds.referenceQuery(root, n=True)[0] + ref_root = ( + cmds.listRelatives(ref_root, parent=True) or [ref_root] + ) + nodes_to_parent.extend(ref_root) + continue if root.endswith("_RN"): refRoot = cmds.referenceQuery(root, n=True)[0] refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] @@ -277,10 +285,17 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): matrix=True, worldSpace=True ) + scene_parent = cmds.listRelatives( + placeholder.scene_identifier, parent=True + ) for node in set(nodes_to_parent): cmds.reorder(node, front=True) cmds.reorder(node, relative=placeholder.data["index"]) cmds.xform(node, matrix=placeholder_form, ws=True) + if scene_parent: + cmds.parent(node, scene_parent) + else: + cmds.parent(node, world=True) holding_sets = cmds.listSets(object=placeholder.scene_identifier) if not holding_sets: From b3237d4cd549da30c5df3e13a44176d46ec9a30d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 18 Apr 2023 15:10:36 +0100 Subject: [PATCH 02/83] Update openpype/hosts/maya/api/workfile_template_builder.py --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index f8169adb52..bbcc2f802a 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -262,7 +262,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): nodes_to_parent = [] for root in roots: if ref_node: - ref_root = cmds.referenceQuery(root, n=True)[0] + ref_root = cmds.referenceQuery(root, nodes=True)[0] ref_root = ( cmds.listRelatives(ref_root, parent=True) or [ref_root] ) From 78a1d4f9664354201c49cf1b85ee61e8abf718a5 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 20 Apr 2023 08:44:57 +0100 Subject: [PATCH 03/83] Update openpype/hosts/maya/api/workfile_template_builder.py --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index bbcc2f802a..9dc91ed772 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -286,7 +286,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): worldSpace=True ) scene_parent = cmds.listRelatives( - placeholder.scene_identifier, parent=True + placeholder.scene_identifier, parent=True, fullPath=True ) for node in set(nodes_to_parent): cmds.reorder(node, front=True) From b253e4366dbc52bad090b453d1697bc4dcf531d0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 20 Apr 2023 09:07:47 +0100 Subject: [PATCH 04/83] Remove methods from plugin.py --- openpype/hosts/maya/api/plugin.py | 67 ++----------------------------- 1 file changed, 3 insertions(+), 64 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 714278ba6c..f065c081f4 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -18,70 +18,9 @@ from openpype.settings import get_project_settings from .pipeline import containerise from . import lib - -def get_reference_node(members, log=None): - """Get the reference node from the container members - Args: - members: list of node names - - Returns: - str: Reference node name. - - """ - - # Collect the references without .placeHolderList[] attributes as - # unique entries (objects only) and skipping the sharedReferenceNode. - references = set() - for ref in cmds.ls(members, exactType="reference", objectsOnly=True): - - # Ignore any `:sharedReferenceNode` - if ref.rsplit(":", 1)[-1].startswith("sharedReferenceNode"): - continue - - # Ignore _UNKNOWN_REF_NODE_ (PLN-160) - if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): - continue - - references.add(ref) - - assert references, "No reference node found in container" - - # Get highest reference node (least parents) - highest = min(references, - key=lambda x: len(get_reference_node_parents(x))) - - # Warn the user when we're taking the highest reference node - if len(references) > 1: - if not log: - log = Logger.get_logger(__name__) - - log.warning("More than one reference node found in " - "container, using highest reference node: " - "%s (in: %s)", highest, list(references)) - - return highest - - -def get_reference_node_parents(ref): - """Return all parent reference nodes of reference node - - Args: - ref (str): reference node. - - Returns: - list: The upstream parent reference nodes. - - """ - parent = cmds.referenceQuery(ref, - referenceNode=True, - parent=True) - parents = [] - while parent: - parents.append(parent) - parent = cmds.referenceQuery(parent, - referenceNode=True, - parent=True) - return parents +# Backwards compatibility: these functions has been moved to lib. +get_reference_node = lib.get_reference_node +get_reference_node_parents = lib.get_reference_node_parents def get_custom_namespace(custom_namespace): From f6c32300c20689e6fe33c368f2108c4fc7334fe8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 21 Apr 2023 10:32:26 +0100 Subject: [PATCH 05/83] Log warning when using plugin methods. --- openpype/hosts/maya/api/plugin.py | 32 ++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index f065c081f4..87015a0ae9 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -18,9 +18,31 @@ from openpype.settings import get_project_settings from .pipeline import containerise from . import lib + +log = Logger.get_logger() + + # Backwards compatibility: these functions has been moved to lib. -get_reference_node = lib.get_reference_node -get_reference_node_parents = lib.get_reference_node_parents +def get_reference_node(*args, **kwargs): + """ + Deprecated: + This funcation was moved ... and will be removed in 3.16.x. + """ + msg = "Function 'get_reference_node' has been moved." + log.warning(msg) + cmds.warning(msg) + return lib.get_reference_node(*args, **kwargs) + + +def get_reference_node_parents(*args, **kwargs): + """ + Deprecated: + This funcation was moved ... and will be removed in 3.16.x. + """ + msg = "Function 'get_reference_node_parents' has been moved." + log.warning(msg) + cmds.warning(msg) + return lib.get_reference_node_parents(*args, **kwargs) def get_custom_namespace(custom_namespace): @@ -182,7 +204,7 @@ class ReferenceLoader(Loader): if not nodes: return - ref_node = get_reference_node(nodes, self.log) + ref_node = lib.get_reference_node(nodes, self.log) container = containerise( name=name, namespace=namespace, @@ -211,7 +233,7 @@ class ReferenceLoader(Loader): # Get reference node from container members members = get_container_members(node) - reference_node = get_reference_node(members, self.log) + reference_node = lib.get_reference_node(members, self.log) namespace = cmds.referenceQuery(reference_node, namespace=True) file_type = { @@ -359,7 +381,7 @@ class ReferenceLoader(Loader): # Assume asset has been referenced members = cmds.sets(node, query=True) - reference_node = get_reference_node(members, self.log) + reference_node = lib.get_reference_node(members, self.log) assert reference_node, ("Imported container not supported; " "container must be referenced.") From 14458ed1f13eb0b6a7fec547d170e94bda4f4aa4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Apr 2023 10:19:30 +0200 Subject: [PATCH 06/83] Allow user to set the colorspace explicitly instead of relying on Project Settings' file rules --- .../publish/collect_explicit_colorspace.py | 36 +++++++++++++ .../plugins/publish/validate_colorspace.py | 53 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py new file mode 100644 index 0000000000..fda789e9d5 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -0,0 +1,36 @@ +import pyblish.api + +from openpype.pipeline import publish +from openpype.lib import TextDef + + +class CollectColorspace(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin, + publish.ColormanagedPyblishPluginMixin): + """Collect explicit user defined representation colorspaces""" + + label = "Choose representation colorspace" + order = pyblish.api.CollectorOrder + 0.49 + hosts = ["traypublisher"] + + def process(self, instance): + values = self.get_attr_values_from_data(instance.data) + colorspace = values.get("colorspace", None) + if not colorspace: + return + + context = instance.context + for repre in instance.data.get("representations", {}): + self.set_representation_colorspace( + representation=repre, + context=context, + colorspace=colorspace + ) + + @classmethod + def get_attribute_defs(cls): + return [ + TextDef("colorspace", + label="Override Colorspace", + placeholder="") + ] diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py new file mode 100644 index 0000000000..69cc7e5f1f --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py @@ -0,0 +1,53 @@ +import pyblish.api + +from openpype.pipeline import ( + publish, + PublishValidationError +) + +from openpype.pipeline.colorspace import ( + get_ocio_config_colorspaces +) + + +class ValidateColorspace(pyblish.api.InstancePlugin, + publish.OpenPypePyblishPluginMixin, + publish.ColormanagedPyblishPluginMixin): + """Validate representation colorspaces""" + + label = "Validate representation colorspace" + order = pyblish.api.ValidatorOrder + hosts = ["traypublisher"] + + def process(self, instance): + + config_colorspaces = {} # cache of colorspaces per config path + for repre in instance.data.get("representations", {}): + + colorspace_data = repre.get("colorspaceData", {}) + if not colorspace_data: + # Nothing to validate + continue + + config_path = colorspace_data["config"]["path"] + if config_path not in config_colorspaces: + colorspaces = get_ocio_config_colorspaces(config_path) + config_colorspaces[config_path] = set(colorspaces) + + colorspace = colorspace_data["colorspace"] + self.log.debug( + f"Validating representation '{repre['name']}' " + f"colorspace '{colorspace}'" + ) + if colorspace not in config_colorspaces[config_path]: + message = ( + f"Representation '{repre['name']}' colorspace " + f"'{colorspace}' does not exist in OCIO config: " + f"{config_path}" + ) + + raise PublishValidationError( + title="Representation colorspace", + message=message, + description=message + ) \ No newline at end of file From 7fe0728a0ab9076d283ba563184158addc9f98e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Apr 2023 10:27:21 +0200 Subject: [PATCH 07/83] Add new line --- .../hosts/traypublisher/plugins/publish/validate_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py index 69cc7e5f1f..901defa1f3 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py @@ -50,4 +50,4 @@ class ValidateColorspace(pyblish.api.InstancePlugin, title="Representation colorspace", message=message, description=message - ) \ No newline at end of file + ) From 9752637b9e620002e05de3ca3d0c2b19c0d9b311 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Apr 2023 10:27:38 +0200 Subject: [PATCH 08/83] Cosmetics --- .../traypublisher/plugins/publish/validate_colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py index 901defa1f3..75b41cf606 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_colorspace.py @@ -11,8 +11,8 @@ from openpype.pipeline.colorspace import ( class ValidateColorspace(pyblish.api.InstancePlugin, - publish.OpenPypePyblishPluginMixin, - publish.ColormanagedPyblishPluginMixin): + publish.OpenPypePyblishPluginMixin, + publish.ColormanagedPyblishPluginMixin): """Validate representation colorspaces""" label = "Validate representation colorspace" From f4117a7a5cd84df6ce53e4afd2a91fbb601fc705 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 26 Apr 2023 10:35:40 +0100 Subject: [PATCH 09/83] Clean up of placeholder. --- .../hosts/maya/api/workfile_template_builder.py | 4 ++++ .../pipeline/workfile/workfile_template_builder.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 9dc91ed772..eb90dfdc37 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -242,6 +242,10 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): cmds.hide(node) cmds.setAttr(node + ".hiddenInOutliner", True) + def delete_placeholder(self, placeholder): + """Remove placeholder if building was successful""" + cmds.delete(placeholder._scene_identifier) + def load_succeed(self, placeholder, container): self._parent_in_hierarchy(placeholder, container) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a3d7340367..3d28d862a3 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1555,6 +1555,15 @@ class PlaceholderLoadMixin(object): self.load_succeed(placeholder, container) self.cleanup_placeholder(placeholder, failed) + if failed: + self.log.debug( + "Placeholder cleanup skipped due to failed placeholder " + "population." + ) + return + if not placeholder.data["keep_placeholder"]: + self.delete_placeholder(placeholder) + def load_failed(self, placeholder, representation): if hasattr(placeholder, "load_failed"): placeholder.load_failed(representation) @@ -1577,6 +1586,10 @@ class PlaceholderLoadMixin(object): pass + def delete_placeholder(self, placeholder, failed): + """Called when all item population is done.""" + self.log.debug("Clean up of placeholder is not implemented.") + class PlaceholderCreateMixin(object): """Mixin prepared for creating placeholder plugins. From fa73bbb568b78f2abae63ac8979dac99a163a9bc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 26 Apr 2023 11:26:35 +0100 Subject: [PATCH 10/83] Account for "create_first_version" setting. --- openpype/pipeline/workfile/workfile_template_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 3d28d862a3..ef8930daf7 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -477,7 +477,9 @@ class AbstractTemplateBuilder(object): create_first_version = template_preset["create_first_version"] # check if first version is created - created_version_workfile = self.create_first_workfile_version() + created_version_workfile = False + if create_first_version: + created_version_workfile = self.create_first_workfile_version() # if first version is created, import template # and populate placeholders From 9747398ce8cb4803139bdae489dbc3eb01de1634 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 28 Apr 2023 15:25:37 +0100 Subject: [PATCH 11/83] Rename "cleanup_placeholder" to "post_representation_load" --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- openpype/hosts/nuke/api/workfile_template_builder.py | 4 ++-- openpype/pipeline/workfile/workfile_template_builder.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index eb90dfdc37..11b8c8bd4b 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -233,7 +233,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) - def cleanup_placeholder(self, placeholder, failed): + def post_representation_load(self, placeholder, failed): """Hide placeholder, add them to placeholder set """ node = placeholder._scene_identifier diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 72d4ffb476..235a8d1c59 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -189,7 +189,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) - def cleanup_placeholder(self, placeholder, failed): + def post_representation_load(self, placeholder, failed): # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) @@ -603,7 +603,7 @@ class NukePlaceholderCreatePlugin( def get_placeholder_options(self, options=None): return self.get_create_plugin_options(options) - def cleanup_placeholder(self, placeholder, failed): + def post_representation_load(self, placeholder, failed): # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index ef8930daf7..e8738bd259 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1555,7 +1555,7 @@ class PlaceholderLoadMixin(object): else: failed = False self.load_succeed(placeholder, container) - self.cleanup_placeholder(placeholder, failed) + self.post_representation_load(placeholder, failed) if failed: self.log.debug( @@ -1574,7 +1574,7 @@ class PlaceholderLoadMixin(object): if hasattr(placeholder, "load_succeed"): placeholder.load_succeed(container) - def cleanup_placeholder(self, placeholder, failed): + def post_representation_load(self, placeholder, failed): """Cleanup placeholder after load of single representation. Can be called multiple times during placeholder item populating and is @@ -1751,7 +1751,7 @@ class PlaceholderCreateMixin(object): failed = False self.create_succeed(placeholder, creator_instance) - self.cleanup_placeholder(placeholder, failed) + self.post_representation_load(placeholder, failed) def create_failed(self, placeholder, creator_data): if hasattr(placeholder, "create_failed"): @@ -1761,7 +1761,7 @@ class PlaceholderCreateMixin(object): if hasattr(placeholder, "create_succeed"): placeholder.create_succeed(creator_instance) - def cleanup_placeholder(self, placeholder, failed): + def post_representation_load(self, placeholder, failed): """Cleanup placeholder after load of single representation. Can be called multiple times during placeholder item populating and is From 8cf509fbd105b3e54362db39a35975ad6ace3683 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 May 2023 10:17:51 +0100 Subject: [PATCH 12/83] Fix for multiple placeholder matches. --- openpype/hosts/maya/api/workfile_template_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 11b8c8bd4b..1380d65ba9 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -268,11 +268,13 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): if ref_node: ref_root = cmds.referenceQuery(root, nodes=True)[0] ref_root = ( - cmds.listRelatives(ref_root, parent=True) or [ref_root] + cmds.listRelatives(ref_root, parent=True, path=True) or + [ref_root] ) nodes_to_parent.extend(ref_root) continue if root.endswith("_RN"): + # Backwards compatibility for hardcoded reference names. refRoot = cmds.referenceQuery(root, n=True)[0] refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] nodes_to_parent.extend(refRoot) From d2842b0cd7586f1a842a9f957575d49b7e1a2def Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 11 May 2023 17:55:25 +0100 Subject: [PATCH 13/83] post_representation_load > post_placeholder_process --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- openpype/hosts/nuke/api/workfile_template_builder.py | 4 ++-- openpype/pipeline/workfile/workfile_template_builder.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 1380d65ba9..d392ceebec 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -233,7 +233,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) - def post_representation_load(self, placeholder, failed): + def post_placeholder_process(self, placeholder, failed): """Hide placeholder, add them to placeholder set """ node = placeholder._scene_identifier diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 235a8d1c59..8cb1e7839a 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -189,7 +189,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): def get_placeholder_options(self, options=None): return self.get_load_plugin_options(options) - def post_representation_load(self, placeholder, failed): + def post_placeholder_process(self, placeholder, failed): # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) @@ -603,7 +603,7 @@ class NukePlaceholderCreatePlugin( def get_placeholder_options(self, options=None): return self.get_create_plugin_options(options) - def post_representation_load(self, placeholder, failed): + def post_placeholder_process(self, placeholder, failed): # deselect all selected nodes placeholder_node = nuke.toNode(placeholder.scene_identifier) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index e8738bd259..f4dca685d1 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1555,7 +1555,7 @@ class PlaceholderLoadMixin(object): else: failed = False self.load_succeed(placeholder, container) - self.post_representation_load(placeholder, failed) + self.post_placeholder_process(placeholder, failed) if failed: self.log.debug( @@ -1574,7 +1574,7 @@ class PlaceholderLoadMixin(object): if hasattr(placeholder, "load_succeed"): placeholder.load_succeed(container) - def post_representation_load(self, placeholder, failed): + def post_placeholder_process(self, placeholder, failed): """Cleanup placeholder after load of single representation. Can be called multiple times during placeholder item populating and is @@ -1751,7 +1751,7 @@ class PlaceholderCreateMixin(object): failed = False self.create_succeed(placeholder, creator_instance) - self.post_representation_load(placeholder, failed) + self.post_placeholder_process(placeholder, failed) def create_failed(self, placeholder, creator_data): if hasattr(placeholder, "create_failed"): @@ -1761,7 +1761,7 @@ class PlaceholderCreateMixin(object): if hasattr(placeholder, "create_succeed"): placeholder.create_succeed(creator_instance) - def post_representation_load(self, placeholder, failed): + def post_placeholder_process(self, placeholder, failed): """Cleanup placeholder after load of single representation. Can be called multiple times during placeholder item populating and is From 3e02ea62632627e1ced226e0ffbceba7f19aafc6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 7 Jun 2023 15:30:59 +0800 Subject: [PATCH 14/83] autosave nk script will be loaded if the script is found in the work directory --- openpype/hosts/nuke/api/workio.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 5692f8e63c..50e22391b2 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -25,7 +25,13 @@ def open_file(filepath): # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - nuke.scriptReadFile(filepath) + autosave = f"{filepath}.autosave" + autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa + if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): + filepath = autosave + nuke.scriptReadFile(filepath) + else: + nuke.scriptReadFile(filepath) nuke.Root()["name"].setValue(filepath) nuke.Root()["project_directory"].setValue(os.path.dirname(filepath)) nuke.Root().setModified(False) From 8c5b9321e4c29b7d11245eea0315447279ac83ab Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 7 Jun 2023 18:24:05 +0800 Subject: [PATCH 15/83] Jakub's comment --- openpype/hosts/nuke/api/workio.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 50e22391b2..99e95dbd77 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -25,13 +25,12 @@ def open_file(filepath): # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - autosave = f"{filepath}.autosave" + autosave = "{}.autosave".format(filepath) autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): filepath = autosave - nuke.scriptReadFile(filepath) - else: - nuke.scriptReadFile(filepath) + + nuke.scriptReadFile(filepath) nuke.Root()["name"].setValue(filepath) nuke.Root()["project_directory"].setValue(os.path.dirname(filepath)) nuke.Root().setModified(False) From 47b2a38cd4d97ae1d0eaf5dfcc9399952a8bafff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 8 Jun 2023 17:06:37 +0800 Subject: [PATCH 16/83] add allow_autosave argument as kwargs in open_file function in nuke --- openpype/hosts/nuke/api/workio.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 99e95dbd77..d674e3ba4d 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -19,16 +19,17 @@ def save_file(filepath): nuke.Root().setModified(False) -def open_file(filepath): +def open_file(filepath, allow_autosave=True): filepath = filepath.replace("\\", "/") # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - autosave = "{}.autosave".format(filepath) - autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa - if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): - filepath = autosave + if allow_autosave: + autosave = "{}.autosave".format(filepath) + autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa + if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): + filepath = autosave nuke.scriptReadFile(filepath) nuke.Root()["name"].setValue(filepath) From 82d18355c8342ea923db368129fbdae0e7a90136 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 8 Jun 2023 21:15:30 +0800 Subject: [PATCH 17/83] Jakub's comment --- openpype/hosts/nuke/api/workio.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index d674e3ba4d..88d555cbb8 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -1,6 +1,7 @@ """Host API required Work Files tool""" import os import nuke +from qtpy import QtWidgets def file_extensions(): @@ -19,13 +20,14 @@ def save_file(filepath): nuke.Root().setModified(False) -def open_file(filepath, allow_autosave=True): +def open_file(filepath): filepath = filepath.replace("\\", "/") # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - if allow_autosave: + headless = QtWidgets.QApplication.instance() is None + if not headless: autosave = "{}.autosave".format(filepath) autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): From 67f012e441a18e669c93c7b15ce17a6d9dbcc05d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 12 Jun 2023 16:03:26 +0200 Subject: [PATCH 18/83] Fix typo + cosmetics --- openpype/hosts/maya/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 691c7da6cf..967d39674c 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -26,7 +26,7 @@ log = Logger.get_logger() def get_reference_node(*args, **kwargs): """ Deprecated: - This funcation was moved ... and will be removed in 3.16.x. + This function was moved and will be removed in 3.16.x. """ msg = "Function 'get_reference_node' has been moved." log.warning(msg) @@ -37,7 +37,7 @@ def get_reference_node(*args, **kwargs): def get_reference_node_parents(*args, **kwargs): """ Deprecated: - This funcation was moved ... and will be removed in 3.16.x. + This function was moved and will be removed in 3.16.x. """ msg = "Function 'get_reference_node_parents' has been moved." log.warning(msg) From b150e0b6a166c495eb21503ddddb82900df94478 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 13 Jun 2023 21:31:45 +0800 Subject: [PATCH 19/83] jiri's comment --- openpype/hosts/nuke/api/workio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 88d555cbb8..032a9fdda8 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -28,7 +28,7 @@ def open_file(filepath): nuke.scriptClear() headless = QtWidgets.QApplication.instance() is None if not headless: - autosave = "{}.autosave".format(filepath) + autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate() autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): filepath = autosave From 56a90a47f544e5c71e5edefd83d4d224773f08a7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 19:02:18 +0800 Subject: [PATCH 20/83] abstract the headless command as function in lib.py --- openpype/hosts/nuke/api/lib.py | 9 +++++++++ openpype/hosts/nuke/api/workio.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4a57bc3165..c469612954 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3116,3 +3116,12 @@ def get_viewer_config_from_string(input_string): ).format(input_string)) return (display, viewer) + + +def is_headless(): + """ + Returns: + bool: headless + """ + headless = QtWidgets.QApplication.instance() is None + return headless diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 032a9fdda8..8b5bae3b2c 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -2,6 +2,7 @@ import os import nuke from qtpy import QtWidgets +from openpype.hosts.nuke.api.lib import is_headless def file_extensions(): @@ -26,7 +27,7 @@ def open_file(filepath): # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - headless = QtWidgets.QApplication.instance() is None + headless = is_headless() if not headless: autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate() autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa From d9af1d91670bfdd2747a81feeb251bf0566b7762 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 19:03:29 +0800 Subject: [PATCH 21/83] remove unused library --- openpype/hosts/nuke/api/workio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 8b5bae3b2c..12f069a386 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -1,7 +1,6 @@ """Host API required Work Files tool""" import os import nuke -from qtpy import QtWidgets from openpype.hosts.nuke.api.lib import is_headless From 68c42d486bba7c5767c14d71ba84d7ca3e1ce011 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 22 Jun 2023 12:10:45 +0100 Subject: [PATCH 22/83] Setting render range to include handles. --- openpype/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b02d3c9b39..83bb73c23f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2297,8 +2297,8 @@ def reset_frame_range(playback=True, render=True, fps=True): cmds.currentTime(frame_start) if render: - cmds.setAttr("defaultRenderGlobals.startFrame", frame_start) - cmds.setAttr("defaultRenderGlobals.endFrame", frame_end) + cmds.setAttr("defaultRenderGlobals.startFrame", animation_start) + cmds.setAttr("defaultRenderGlobals.endFrame", animation_end) def reset_scene_resolution(): From 31f76563b8cdc3c49344d702e3c4ea2bfd64aa83 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 21:51:58 +0800 Subject: [PATCH 23/83] jakub's comment --- openpype/hosts/nuke/api/lib.py | 3 +-- openpype/hosts/nuke/api/workio.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1251e1a718..4a254f7c2f 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3162,5 +3162,4 @@ def is_headless(): Returns: bool: headless """ - headless = QtWidgets.QApplication.instance() is None - return headless + return QtWidgets.QApplication.instance() is None diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 12f069a386..4ec0766599 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -26,8 +26,7 @@ def open_file(filepath): # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - headless = is_headless() - if not headless: + if not is_headless(): autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate() autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): From 5c399cbc1781428f099e43ee648fedbc81b2773e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 23:13:24 +0800 Subject: [PATCH 24/83] revert the headless function --- openpype/hosts/nuke/api/workio.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 4ec0766599..032a9fdda8 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -1,7 +1,7 @@ """Host API required Work Files tool""" import os import nuke -from openpype.hosts.nuke.api.lib import is_headless +from qtpy import QtWidgets def file_extensions(): @@ -26,7 +26,8 @@ def open_file(filepath): # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - if not is_headless(): + headless = QtWidgets.QApplication.instance() is None + if not headless: autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate() autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): From 22ee8e1f9337b71b6939b6f490ba9cf13cf92701 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 00:10:34 +0800 Subject: [PATCH 25/83] add headless abstraction into command.py --- openpype/hosts/nuke/api/command.py | 10 ++++++++++ openpype/hosts/nuke/api/lib.py | 8 -------- openpype/hosts/nuke/api/workio.py | 5 ++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/api/command.py b/openpype/hosts/nuke/api/command.py index 2f772469d8..2e2c5b2b93 100644 --- a/openpype/hosts/nuke/api/command.py +++ b/openpype/hosts/nuke/api/command.py @@ -2,6 +2,8 @@ import logging import contextlib import nuke +from qtpy import QtWidgets + log = logging.getLogger(__name__) @@ -19,3 +21,11 @@ def viewer_update_and_undo_stop(): yield finally: nuke.Undo.enable() + + +def is_headless(): + """ + Returns: + bool: headless + """ + return QtWidgets.QApplication.instance() is None diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4a254f7c2f..e3b34222d4 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3155,11 +3155,3 @@ def get_viewer_config_from_string(input_string): ).format(input_string)) return (display, viewer) - - -def is_headless(): - """ - Returns: - bool: headless - """ - return QtWidgets.QApplication.instance() is None diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 032a9fdda8..b7c9d01097 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -1,7 +1,7 @@ """Host API required Work Files tool""" import os import nuke -from qtpy import QtWidgets +from .command import is_headless def file_extensions(): @@ -26,8 +26,7 @@ def open_file(filepath): # To remain in the same window, we have to clear the script and read # in the contents of the workfile. nuke.scriptClear() - headless = QtWidgets.QApplication.instance() is None - if not headless: + if not is_headless(): autosave = nuke.toNode("preferences")["AutoSaveName"].evaluate() autosave_prmpt = "Autosave detected.\nWould you like to load the autosave file?" # noqa if os.path.isfile(autosave) and nuke.ask(autosave_prmpt): From c414c8c6e933e82e6e7703b8e509b711e36f9d2e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 16:45:41 +0800 Subject: [PATCH 26/83] add scripts to utils --- openpype/hosts/nuke/api/command.py | 9 --------- openpype/hosts/nuke/api/utils.py | 11 ++++++++++- openpype/hosts/nuke/api/workio.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/api/command.py b/openpype/hosts/nuke/api/command.py index 2e2c5b2b93..486f637a55 100644 --- a/openpype/hosts/nuke/api/command.py +++ b/openpype/hosts/nuke/api/command.py @@ -2,7 +2,6 @@ import logging import contextlib import nuke -from qtpy import QtWidgets log = logging.getLogger(__name__) @@ -21,11 +20,3 @@ def viewer_update_and_undo_stop(): yield finally: nuke.Undo.enable() - - -def is_headless(): - """ - Returns: - bool: headless - """ - return QtWidgets.QApplication.instance() is None diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index 2b3c35c23a..7b02585892 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -2,7 +2,7 @@ import os import nuke from openpype import resources -from .lib import maintained_selection +from qtpy import QtWidgets def set_context_favorites(favorites=None): @@ -55,6 +55,7 @@ def bake_gizmos_recursively(in_group=None): Arguments: is_group (nuke.Node)[optonal]: group node or all nodes """ + from .lib import maintained_selection if in_group is None: in_group = nuke.Root() # preserve selection after all is done @@ -129,3 +130,11 @@ def get_colorspace_list(colorspace_knob): reduced_clrs.append(clrs) return reduced_clrs + + +def is_headless(): + """ + Returns: + bool: headless + """ + return QtWidgets.QApplication.instance() is None diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index b7c9d01097..8d29e0441f 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -1,7 +1,7 @@ """Host API required Work Files tool""" import os import nuke -from .command import is_headless +from .utils import is_headless def file_extensions(): From cbf21cf95d0876508f2ea77992b66f405b9fbf19 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 18:00:15 +0800 Subject: [PATCH 27/83] update --- openpype/hosts/nuke/api/command.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/api/command.py b/openpype/hosts/nuke/api/command.py index 486f637a55..2f772469d8 100644 --- a/openpype/hosts/nuke/api/command.py +++ b/openpype/hosts/nuke/api/command.py @@ -2,7 +2,6 @@ import logging import contextlib import nuke - log = logging.getLogger(__name__) From dcf3b2749326d173a9a6b635f51f213b83baa6ab Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 23 Jun 2023 14:49:46 +0100 Subject: [PATCH 28/83] Update openpype/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 83d602fa79..489297efe4 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1574,7 +1574,7 @@ class PlaceholderLoadMixin(object): "population." ) return - if not placeholder.data["keep_placeholder"]: + if not placeholder.data.get("keep_placeholder", True): self.delete_placeholder(placeholder) def load_failed(self, placeholder, representation): From 4a8e5c48179464e59a533b87777c81738d0f255c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 23 Jun 2023 14:56:44 +0100 Subject: [PATCH 29/83] Illicit feedback --- openpype/hosts/maya/api/workfile_template_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 4471fc2b3b..38ac8f9f8a 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -253,7 +253,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): def post_placeholder_process(self, placeholder, failed): """Hide placeholder, add them to placeholder set """ - node = placeholder._scene_identifier + node = placeholder.scene_identifier cmds.sets(node, addElement=PLACEHOLDER_SET) cmds.hide(node) @@ -261,7 +261,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): def delete_placeholder(self, placeholder): """Remove placeholder if building was successful""" - cmds.delete(placeholder._scene_identifier) + cmds.delete(placeholder.scene_identifier) def load_succeed(self, placeholder, container): self._parent_in_hierarchy(placeholder, container) From 33f83f587d05bbd259bdb6979c1f9a2ae88d8063 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jun 2023 16:26:14 +0200 Subject: [PATCH 30/83] progress of workfile save file in resolve --- openpype/hosts/resolve/api/workio.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 5966fa6a43..58f9e44e60 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -30,13 +30,22 @@ def save_file(filepath): project = get_current_project() name = project.GetName() - if "Untitled Project" not in name: - log.info("Saving project: `{}` as '{}'".format(name, file)) - pm.ExportProject(name, filepath) - else: + log.info("name: `{}`, file: '{}'".format(name, file)) + log.info("fname: `{}`, filepath: '{}'".format(fname, filepath)) + + if "Untitled Project" in name: log.info("Creating new project...") - pm.CreateProject(fname) - pm.ExportProject(name, filepath) + response = pm.CreateProject(fname) + log.info("New project created: {}".format(response)) + else: + log.info("Saving project: `{}` as '{}'".format(name, file)) + response = project.SetName(fname) + log.info("Project renamed: {}".format(response)) + + if response: + # only export if project was saved or renamed + exported = pm.ExportProject(fname, filepath) + log.info("Project exported: {}".format(exported)) def open_file(filepath): From 6e69d3a87f3c8ec6168c59bbf7e7d7fdadd58b61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:48:09 +0200 Subject: [PATCH 31/83] Publisher: Fix disappearing actions (#5184) * store action ids by plugin ids * fix underscore in variables * fix access to action --- openpype/tools/publisher/control.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 502e871edd..d4e0ae0453 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -443,29 +443,29 @@ class PublishPluginsProxy: def __init__(self, plugins): plugins_by_id = {} - actions_by_id = {} + actions_by_plugin_id = {} action_ids_by_plugin_id = {} for plugin in plugins: plugin_id = plugin.id plugins_by_id[plugin_id] = plugin action_ids = [] + actions_by_id = {} action_ids_by_plugin_id[plugin_id] = action_ids + actions_by_plugin_id[plugin_id] = actions_by_id actions = getattr(plugin, "actions", None) or [] for action in actions: action_id = action.id - if action_id in actions_by_id: - continue action_ids.append(action_id) actions_by_id[action_id] = action self._plugins_by_id = plugins_by_id - self._actions_by_id = actions_by_id + self._actions_by_plugin_id = actions_by_plugin_id self._action_ids_by_plugin_id = action_ids_by_plugin_id - def get_action(self, action_id): - return self._actions_by_id[action_id] + def get_action(self, plugin_id, action_id): + return self._actions_by_plugin_id[plugin_id][action_id] def get_plugin(self, plugin_id): return self._plugins_by_id[plugin_id] @@ -497,7 +497,9 @@ class PublishPluginsProxy: """ return [ - self._create_action_item(self._actions_by_id[action_id], plugin_id) + self._create_action_item( + self.get_action(plugin_id, action_id), plugin_id + ) for action_id in self._action_ids_by_plugin_id[plugin_id] ] @@ -2308,7 +2310,7 @@ class PublisherController(BasePublisherController): def run_action(self, plugin_id, action_id): # TODO handle result in UI plugin = self._publish_plugins_proxy.get_plugin(plugin_id) - action = self._publish_plugins_proxy.get_action(action_id) + action = self._publish_plugins_proxy.get_action(plugin_id, action_id) result = pyblish.plugin.process( plugin, self._publish_context, None, action.id From 4bb7ac0cb8effd913492d6b1e6bade392bd25d3f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 24 Jun 2023 03:31:20 +0000 Subject: [PATCH 32/83] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 3a218f3a06..a59b902534 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.11-nightly.4" +__version__ = "3.15.11-nightly.5" From 6f7f633e868b33d01c7aff84f61653d09f74195c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Jun 2023 03:31:59 +0000 Subject: [PATCH 33/83] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 203ac1df23..2f185a3f87 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.11-nightly.5 - 3.15.11-nightly.4 - 3.15.11-nightly.3 - 3.15.11-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.4-nightly.1 - 3.14.3 - 3.14.3-nightly.7 - - 3.14.3-nightly.6 validations: required: true - type: dropdown From 24f4fd8c23df9128f0aadde97d9718a3b035578e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 24 Jun 2023 13:30:30 +0800 Subject: [PATCH 34/83] fix the reset frame range not setting up the right timeline in max --- openpype/hosts/max/api/lib.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 1d53802ecf..b6f09fc227 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -250,10 +250,7 @@ def reset_frame_range(fps: bool = True): frame_range["handleStart"] ) frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - frange_cmd = ( - f"animationRange = interval {frame_start_handle} {frame_end_handle}" - ) - rt.Execute(frange_cmd) + set_timeline(frame_start_handle, frame_end_handle) set_render_frame_range(frame_start_handle, frame_end_handle) @@ -285,3 +282,11 @@ def get_max_version(): """ max_info = rt.MaxVersion() return max_info[7] + + +@contextlib.contextmanager +def set_timeline(frameStart, frameEnd): + """Set frame range for timeline editor in Max + """ + rt.animationRange = rt.interval(frameStart, frameEnd) + return rt.animationRange From a1ac05986d4c70e47ef95fef4dc61663b1c9ea01 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 24 Jun 2023 13:38:46 +0800 Subject: [PATCH 35/83] remove contextmanager --- openpype/hosts/max/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b6f09fc227..dbaa6e4a24 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -284,7 +284,6 @@ def get_max_version(): return max_info[7] -@contextlib.contextmanager def set_timeline(frameStart, frameEnd): """Set frame range for timeline editor in Max """ From baf88f1358828a0386e0bd1e592d85cab70ccb18 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 11:46:27 +0200 Subject: [PATCH 36/83] fixing exporting new project --- openpype/hosts/resolve/api/lib.py | 8 +++++++- openpype/hosts/resolve/api/workio.py | 11 +++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index a44c527f13..396d2234bb 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -89,11 +89,17 @@ def get_current_project(): """Get current project object. """ if not self.current_project: - self.current_project = get_project_manager().GetCurrentProject() + set_current_project() return self.current_project +def set_current_project(): + """Set current project object. + """ + self.current_project = get_project_manager().GetCurrentProject() + + def get_current_timeline(new=False): """Get current timeline object. diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 58f9e44e60..5de6b251af 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -5,6 +5,7 @@ from openpype.lib import Logger from .lib import ( get_project_manager, get_current_project, + set_current_project, set_project_manager_to_folder_name ) @@ -33,12 +34,14 @@ def save_file(filepath): log.info("name: `{}`, file: '{}'".format(name, file)) log.info("fname: `{}`, filepath: '{}'".format(fname, filepath)) - if "Untitled Project" in name: - log.info("Creating new project...") + response = False + if name == "Untitled Project": response = pm.CreateProject(fname) + # re-cash new current project after renaming + set_current_project() log.info("New project created: {}".format(response)) - else: - log.info("Saving project: `{}` as '{}'".format(name, file)) + pm.SaveProject() + elif name != fname: response = project.SetName(fname) log.info("Project renamed: {}".format(response)) From 14e7a914cecce47c30e268f115e3db82eb09cb51 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 15:37:22 +0200 Subject: [PATCH 37/83] ensure startup.py will by always added to environment --- .../hosts/resolve/hooks/pre_resolve_launch_last_workfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 0e27ddb8c3..94c123e6f9 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -16,6 +16,8 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): app_groups = ["resolve"] def execute(self): + self.set_startup_script() + if not self.data.get("start_last_workfile"): self.log.info("It is set to not start last workfile on start.") return @@ -35,6 +37,7 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" self.launch_context.env[key] = last_workfile + def set_startup_script(self): # Set the openpype prelaunch startup script path for easy access # in the LUA .scriptlib code op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__) From 0864d3a70577aa2c386331295317eaea17491e88 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 15:38:19 +0200 Subject: [PATCH 38/83] dev testing of startup py --- .../resolve/utility_scripts/tests/testing_startup_script.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py b/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py new file mode 100644 index 0000000000..b64714ab16 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py @@ -0,0 +1,5 @@ +#! python3 +from openpype.hosts.resolve.startup import main + +if __name__ == "__main__": + main() From 856ff06b4dd0d2056ef50351ad54b84692998717 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 15:38:50 +0200 Subject: [PATCH 39/83] removing ambiguous project nesting --- openpype/hosts/resolve/api/workio.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 5de6b251af..61425e19f1 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -69,10 +69,8 @@ def open_file(filepath): file = os.path.basename(filepath) fname, _ = os.path.splitext(file) - dname, _ = fname.split("_v") + try: - if not set_project_manager_to_folder_name(dname): - raise # load project from input path project = pm.LoadProject(fname) log.info(f"Project {project.GetName()} opened...") From 43f611cbdfb5532e407836104697f05c3e0f5070 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 15:50:50 +0200 Subject: [PATCH 40/83] fix starting openpype menu automatically even no first version of workfile is available --- openpype/hosts/resolve/startup.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py index 79a64e0fbf..f64792d2ff 100644 --- a/openpype/hosts/resolve/startup.py +++ b/openpype/hosts/resolve/startup.py @@ -10,9 +10,11 @@ This code runs in a separate process to the main Resolve process. """ import os - +from openpype.lib import Logger import openpype.hosts.resolve.api +log = Logger.get_logger(__name__) + def ensure_installed_host(): """Install resolve host with openpype and return the registered host. @@ -44,17 +46,22 @@ def open_file(path): def main(): # Open last workfile workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH") - if workfile_path: + + if workfile_path and os.path.exists(workfile_path): + log.info(f"Opening last workfile: {workfile_path}") open_file(workfile_path) else: - print("No last workfile set to open. Skipping..") + log.info("No last workfile set to open. Skipping..") # Launch OpenPype menu from openpype.settings import get_project_settings from openpype.pipeline.context_tools import get_current_project_name project_name = get_current_project_name() + log.info(f"Current project name in context: {project_name}") + settings = get_project_settings(project_name) if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True): + self.log.info("Launching OpenPype menu..") launch_menu() From 0145a299ebc0f840b4c281ee7daf0c0b65becf3d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 15:57:59 +0200 Subject: [PATCH 41/83] removing self --- openpype/hosts/resolve/startup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py index f64792d2ff..e807a48f5a 100644 --- a/openpype/hosts/resolve/startup.py +++ b/openpype/hosts/resolve/startup.py @@ -61,7 +61,7 @@ def main(): settings = get_project_settings(project_name) if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True): - self.log.info("Launching OpenPype menu..") + log.info("Launching OpenPype menu..") launch_menu() From fcde80dcc3898e72705854ab751c0fb7e6a9c9f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 15:58:42 +0200 Subject: [PATCH 42/83] removed debug prints --- openpype/hosts/resolve/api/workio.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 61425e19f1..77cd4d488a 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -31,9 +31,6 @@ def save_file(filepath): project = get_current_project() name = project.GetName() - log.info("name: `{}`, file: '{}'".format(name, file)) - log.info("fname: `{}`, filepath: '{}'".format(fname, filepath)) - response = False if name == "Untitled Project": response = pm.CreateProject(fname) From 5e0da9a474aa6b8e8021f4e614d3909e42e0ccc8 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 26 Jun 2023 14:28:12 +0000 Subject: [PATCH 43/83] [Automated] Release --- CHANGELOG.md | 419 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 421 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 882620f26c..095e0d96e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,425 @@ # Changelog +## [3.15.11](https://github.com/ynput/OpenPype/tree/3.15.11) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.10...3.15.11) + +### **🆕 New features** + + +
+Ftrack: Task status during publishing #5123 + +Added option to change task status during publishing for 3 possible cases: "sending to farm", "local integration" and "on farm integration". + + +___ + +
+ + +
+Nuke: Allow for more complex temp rendering paths #5132 + +When changing the temporary rendering template (i.e., add `{asset}` to the path) to something a bit more complex the formatting was erroring due to missing keys. + + +___ + +
+ + +
+Blender: Add support for custom path for app templates #5137 + +This PR adds support for a custom App Templates path in Blender by setting the `BLENDER_USER_SCRIPTS` environment variable to the path specified in `OPENPYPE_APP_TEMPLATES_PATH`. This allows users to use their own custom app templates in Blender. + + +___ + +
+ + +
+TrayPublisher & StandalonePublisher: Specify version #5142 + +Simple creators in TrayPublisher can affect which version will be integrated. Standalone publisher respects the version change from UI. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Workfile Builder UI: Workfile builder window is not modal #5131 + +Workfile Templates Builder: +- Create dialog is not a modal dialog +- Create dialog remains open after create, so you can directly create a new placeholder with similar settings +- In Maya allow to create root level placeholders (no selection during create) - **this felt more like a bugfix than anything else.** + + +___ + +
+ + +
+3dsmax: Use custom modifiers to hold instance members #4931 + +Moving logic to handle members of publishing instance from children/parent relationship on Container to tracking via custom attribute on modifier. This eliminates limitations where you couldn't have one node multiple times under one Container and because it stores those relationships as weak references, they are easily transferable even when original nodes are renamed. + + +___ + +
+ + +
+Add height, width and fps setup to project manager #5075 + +Add Width, Height, FPS, Pixel Aspect and Frame Start/End to the Project creation dialogue in the Project Manager.I understand that the Project manager will be replaced in the upcoming Ayon, but for the time being I believe setting new project with these options available would be more fun. + + +___ + +
+ + +
+Nuke: connect custom write node script to the OP setting #5113 + +Allows user to customize the values of knobs attribute in the OP setting and use it in custom write node + + +___ + +
+ + +
+Keep `publisher.create_widget` variant when creating subsets #5119 + +Whenever a person is creating a subset to publish, the "creator" widget resets (where you choose the variant, product, etc.) so if the person is publishing several images of the a variant which is not the default one, they have to keep selecting the correct one after every "create". + +This commit resets the original variant upon successful creation of a subset for publishing. + +Demo: +[Screencast from 2023-06-08 10-46-40.webm](https://github.com/ynput/OpenPype/assets/1800151/ca1c91d4-b8f3-43d2-a7b7-35987f5b6a3f) + +## Testing notes: +1. Launch AYON/OP +2. Launch the publisher (select a project, shot, etc.) +3. Crete a publish type (any works) +4. Choose a variant for the publish that is not the default +5. "Create >>" + +The Variant fields should still have the variant you choose. + + + +___ + +
+ + +
+Color Management- added color management support for simple expected files on Deadline #5122 + +Running of `ExtractOIIOTranscode` during Deadline publish was previously implemented only on DCCs with AOVs (Maya, Max).This PR extends this for other DCCs with flat structure of expected files. + + +___ + +
+ + +
+hide macos dock icon on build #5133 + +Set `LSUIElement` to `1` in the `Info.plist` to hide OP icon from the macos dock by default. + + +___ + +
+ + +
+Pack project: Raise exception with reasonable message #5145 + +Pack project crashes with relevant message when destination directory is not set. + + +___ + +
+ + +
+Allow "inventory" actions to be supplied by a Module/Addon. #5146 + +Adds "inventory" as a possible key to the plugin paths to be returned from a module. + + +___ + +
+ + +
+3dsmax: make code compatible with 3dsmax 2022 #5164 + +Python 3.7 in 3dsmax 2022 is not supporting walrus operator. This is removing it from the code for the sake of compatibility + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Support same attribute names on different node types. #5054 + +When validating render settings attributes, support same attribute names on different node types. + + +___ + +
+ + +
+Maya: bug fix the standin being not loaded when they are first loaded #5143 + +fix the bug of raising error when the first two standins are loaded through the loaderThe bug mentioned in the related issue: https://github.com/ynput/OpenPype/issues/5129For some reason, `defaultArnoldRenderOptions.operator` is not listed in the connection node attribute even if `cmds.loadPlugin("mtoa", quiet=True)` executed before loading the object as standins for the first time.But if you manually turn on mtoa through plugin preference and load the standins for the first time, it won't raise the related `defaultArnoldRenderOptions.operator` error. + + +___ + +
+ + +
+Maya: bug fix arnoldExportAss unable to export selected set members #5150 + +See #5108 fix the bug arnoldExportAss being not able to export and error out during extraction. + + +___ + +
+ + +
+Maya: Xgen multiple descriptions on single shape - OP-6039 #5160 + +When having multiple descriptions on the same geometry, the extraction would produce redundant duplicate geometries. + + +___ + +
+ + +
+Maya: Xgen export of Abc's during Render Publishing - OP-6206 #5167 + +Shading assignments was missing duplicating the setup for Xgen publishing and the exporting of patches was getting the end frame incorrectly. + + +___ + +
+ + +
+Maya: Include handles - OP-6236 #5175 + +Render range was missing the handles. + + +___ + +
+ + +
+OCIO: Support working with single frame renders #5053 + +When there is only 1 file, the datamember `files` on the representation should be a string. + + +___ + +
+ + +
+Burnins: Refactored burnins script #5094 + +Refactored list value for burnins and fixed command length limit by using temp file for filters string. + + +___ + +
+ + +
+Nuke: open_file function can open autosave script #5107 + +Fix the bug of the workfile dialog being unable to open autosave nuke script + + +___ + +
+ + +
+ImageIO: Minor fixes #5147 + +Resolve few minor fixes related to latest image io changes from PR. + + +___ + +
+ + +
+Publisher: Fix save shortcut #5148 + +Save shortcut should work for both PySide2 and PySide6. + + +___ + +
+ + +
+Pack Project: Fix files packing #5154 + +Packing of project with files does work again. + + +___ + +
+ + +
+Maya: Xgen version mismatch after publish - OP-6204 #5161 + +Xgen was not updating correctly when for example adding or removing descriptions. This resolve the issue by overwritting the workspace xgen file. + + +___ + +
+ + +
+Publisher: Edge case fixes #5165 + +Fix few edge case issues that may cause issues in Publisher UI. + + +___ + +
+ + +
+Colorspace: host config path backward compatibility #5166 + +Old project settings overrides are now fully backward compatible. The issue with host config paths overrides were solved and now once a project used to be set to ocio_config **enabled** with found filepaths - this is now considered as activated host ocio_config paths overrides.Nuke is having an popup dialogue which is letting know to a user that settings for config path were changed. + + +___ + +
+ + +
+Maya: import workfile missing - OP-6233 #5174 + +Missing `workfile` family to import. + + +___ + +
+ + +
+Ftrack: Fix ignore sync filter #5176 + +Ftrack ignore filter does not crash because of dictionary modifications during it's iteration. + + +___ + +
+ + +
+Webpublisher - headless publish shouldn't be blocking operation #5177 + +`subprocess.call` was blocking, which resulted in UI non responsiveness as it was waiting for publish to finish. + + +___ + +
+ + +
+Publisher: Fix disappearing actions #5184 + +Pyblish plugin actions are visible as expected. + + +___ + +
+ +### **Merged pull requests** + + +
+Enhancement:animation family loaded as standing (abc) uses "use file sequence" #5110 + +The changes are the following. We started by updating the the is_sequence(files) function allowing it to return True for a list of files which has only one file, since our animation in this provides just one alembic file. For the correct FPS number, we got the fps from the published ass/abc from the version data. + + +___ + +
+ + +
+add label to matching family #5128 + +I added the possibility to filter the `family smart select` with the label in addition to the family. + + +___ + +
+ + + + ## [3.15.10](https://github.com/ynput/OpenPype/tree/3.15.10) diff --git a/openpype/version.py b/openpype/version.py index a59b902534..0dd2a8ae14 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.11-nightly.5" +__version__ = "3.15.11" diff --git a/pyproject.toml b/pyproject.toml index 56c130982c..06a74d9126 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.10" # OpenPype +version = "3.15.11" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From eb7aceb7e538dfff0dad121c0128369943a13871 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 26 Jun 2023 14:29:19 +0000 Subject: [PATCH 44/83] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2f185a3f87..57a49406f0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.11 - 3.15.11-nightly.5 - 3.15.11-nightly.4 - 3.15.11-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.4-nightly.2 - 3.14.4-nightly.1 - 3.14.3 - - 3.14.3-nightly.7 validations: required: true - type: dropdown From 33f2adff54c87e4216fac77314ffdaa9afd20c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 26 Jun 2023 16:58:49 +0200 Subject: [PATCH 45/83] Update openpype/hosts/resolve/api/workio.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/workio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 77cd4d488a..c04320d063 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -34,7 +34,7 @@ def save_file(filepath): response = False if name == "Untitled Project": response = pm.CreateProject(fname) - # re-cash new current project after renaming + # recache new current project after creating new project set_current_project() log.info("New project created: {}".format(response)) pm.SaveProject() From e83cf51da903a8b606e332fa13796cabc5384208 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 17:04:16 +0200 Subject: [PATCH 46/83] removing current project cashing --- openpype/hosts/resolve/api/lib.py | 12 +----------- openpype/hosts/resolve/api/workio.py | 6 +----- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 396d2234bb..eaee3bb9ba 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -15,7 +15,6 @@ log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None self.media_storage = None -self.current_project = None # OpenPype sequential rename variables self.rename_index = 0 @@ -88,16 +87,7 @@ def get_media_storage(): def get_current_project(): """Get current project object. """ - if not self.current_project: - set_current_project() - - return self.current_project - - -def set_current_project(): - """Set current project object. - """ - self.current_project = get_project_manager().GetCurrentProject() + return get_project_manager().GetCurrentProject() def get_current_timeline(new=False): diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 77cd4d488a..59a1a07e9b 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -4,9 +4,7 @@ import os from openpype.lib import Logger from .lib import ( get_project_manager, - get_current_project, - set_current_project, - set_project_manager_to_folder_name + get_current_project ) @@ -34,8 +32,6 @@ def save_file(filepath): response = False if name == "Untitled Project": response = pm.CreateProject(fname) - # re-cash new current project after renaming - set_current_project() log.info("New project created: {}".format(response)) pm.SaveProject() elif name != fname: From bcd8aea40d7e80b1842c48afae07cb88e331b9b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 17:08:07 +0200 Subject: [PATCH 47/83] recommit after conflict merge --- openpype/hosts/resolve/api/workio.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 44b15cde9b..59a1a07e9b 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -32,8 +32,6 @@ def save_file(filepath): response = False if name == "Untitled Project": response = pm.CreateProject(fname) - # recache new current project after creating new project - set_current_project() log.info("New project created: {}".format(response)) pm.SaveProject() elif name != fname: From 117706bca4bbed08117522d65da61c80036cf234 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 17:08:17 +0200 Subject: [PATCH 48/83] typo --- openpype/hosts/resolve/api/workio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 59a1a07e9b..55b69f7734 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -10,11 +10,11 @@ from .lib import ( log = Logger.get_logger(__name__) -exported_projet_ext = ".drp" +exported_project_ext = ".drp" def file_extensions(): - return [exported_projet_ext] + return [exported_project_ext] def has_unsaved_changes(): @@ -85,7 +85,7 @@ def current_file(): current_dir = os.getenv("AVALON_WORKDIR") project = pm.GetCurrentProject() name = project.GetName() - fname = name + exported_projet_ext + fname = name + exported_project_ext current_file = os.path.join(current_dir, fname) if not current_file: return None From e81a16116943c644d8e4a5d0a9b69eb97da5fe52 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 17:09:33 +0200 Subject: [PATCH 49/83] code readability --- openpype/hosts/resolve/api/workio.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 55b69f7734..bc32f7802e 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -87,9 +87,7 @@ def current_file(): name = project.GetName() fname = name + exported_project_ext current_file = os.path.join(current_dir, fname) - if not current_file: - return None - return os.path.normpath(current_file) + return os.path.normpath(current_file) if current_file else None def work_root(session): From 2b1fe7abb65178b09d29fd503667a25112293256 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jun 2023 18:09:33 +0200 Subject: [PATCH 50/83] colorspace as enumerator form actual config --- .../publish/collect_explicit_colorspace.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py index fda789e9d5..a36c3edbaa 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_explicit_colorspace.py @@ -1,7 +1,8 @@ import pyblish.api - +from openpype.pipeline import registered_host from openpype.pipeline import publish -from openpype.lib import TextDef +from openpype.lib import EnumDef +from openpype.pipeline import colorspace class CollectColorspace(pyblish.api.InstancePlugin, @@ -13,9 +14,14 @@ class CollectColorspace(pyblish.api.InstancePlugin, order = pyblish.api.CollectorOrder + 0.49 hosts = ["traypublisher"] + colorspace_items = [ + (None, "Don't override") + ] + def process(self, instance): values = self.get_attr_values_from_data(instance.data) colorspace = values.get("colorspace", None) + self.log.debug("colorspace: {}".format(colorspace)) if not colorspace: return @@ -27,10 +33,38 @@ class CollectColorspace(pyblish.api.InstancePlugin, colorspace=colorspace ) + @classmethod + def apply_settings(cls, project_settings): + host = registered_host() + host_name = host.name + project_name = host.get_current_project_name() + config_data = colorspace.get_imageio_config( + project_name, host_name, + project_settings=project_settings + ) + + if config_data: + filepath = config_data["path"] + config_items = colorspace.get_ocio_config_colorspaces(filepath) + cls.colorspace_items.extend(( + (name, name) + for name, family in config_items.items() + )) + else: + cls.colorspace_items.extend([ + ("sRGB", "sRGB"), + ("rec709", "rec709"), + ("ACES", "ACES"), + ("ACEScg", "ACEScg") + ]) + @classmethod def get_attribute_defs(cls): return [ - TextDef("colorspace", - label="Override Colorspace", - placeholder="") + EnumDef( + "colorspace", + cls.colorspace_items, + default="Don't override", + label="Override Colorspace" + ) ] From 83242487fd9ad2d1ead7a13baca6da1951c2350a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Jun 2023 10:58:55 +0200 Subject: [PATCH 51/83] AfterEffects: support for workfile template builder (#5163) * OP-5661 - added new menu item to AE extension * OP-5661 - added support for addPlaceholder into extension * OP-5661 - fix broken abstract loader In some cases discover couldn't recognize that base plugin is abstract. Handled properly * OP-5661 - WIP of implementing workfile template builder * OP-5661 - added route to add placeholder * OP-5661 - added route to build workfile * OP-5661 - refactored arguments * OP-5661 - implemented build workfile operation moved from wrong position * OP-5661 - removed wrong base plugin * OP-5661 - implemented update placeholder logic * OP-5661 - fix update placeholder metadata * OP-5661 - added Creator placeholder plugin * OP-5661 - pre_create_data could be passed into Creator This allow to modify `pre_create_data` before automatically creating an instance. (Useful for overridding defaults.) * OP-5661 - implemented populate of Create placeholder * OP-5661 - implemented import of template * OP-5661 - fix to populate correct create placeholder Implemented function to select item(s) by their id. (CreateRender expects selected composition.) * OP-5661 - fix workfile builder schema to multiplatform path Path should be separated by platform. * OP-5661 - implemented adding loaded items instead of placeholders * OP-5661 - add Create Placeholder plugin to dropdown * OP-5661 - add templated workfile to Settings * OP-5661 - fix to choose template file from multiplatform * OP-5661 - added documentation --- openpype/hosts/aftereffects/api/extension.zxp | Bin 101866 -> 102930 bytes .../api/extension/CSXS/manifest.xml | 2 +- .../aftereffects/api/extension/index.html | 39 +++ .../aftereffects/api/extension/js/main.js | 46 ++- .../api/extension/jsx/hostscript.jsx | 113 +++++++- .../hosts/aftereffects/api/launch_logic.py | 30 ++ openpype/hosts/aftereffects/api/pipeline.py | 10 + openpype/hosts/aftereffects/api/plugin.py | 4 + .../api/workfile_template_builder.py | 271 ++++++++++++++++++ openpype/hosts/aftereffects/api/ws_stub.py | 78 ++++- .../plugins/load/load_background.py | 11 +- .../aftereffects/plugins/load/load_file.py | 9 +- .../workfile/workfile_template_builder.py | 12 +- .../project_settings/aftereffects.json | 3 + .../schema_project_aftereffects.json | 4 + .../schema_templated_workfile_build.json | 2 +- website/docs/admin_hosts_aftereffects.md | 15 + website/docs/artist_hosts_aftereffects.md | 41 ++- .../aftereffects_create_placeholder.png | Bin 0 -> 20572 bytes .../assets/aftereffects_load_placeholder.png | Bin 0 -> 52653 bytes .../aftereffects_populated_template.png | Bin 0 -> 33441 bytes .../assets/aftereffects_prepared_template.png | Bin 0 -> 31476 bytes 22 files changed, 664 insertions(+), 26 deletions(-) create mode 100644 openpype/hosts/aftereffects/api/workfile_template_builder.py create mode 100644 website/docs/assets/aftereffects_create_placeholder.png create mode 100644 website/docs/assets/aftereffects_load_placeholder.png create mode 100644 website/docs/assets/aftereffects_populated_template.png create mode 100644 website/docs/assets/aftereffects_prepared_template.png diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index 50fda416f806515e8a6e90e745153e341f73c7b2..358e9740d3c2479b88dff08af47eba3d74be63ce 100644 GIT binary patch delta 15296 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 - diff --git a/openpype/hosts/aftereffects/api/extension/index.html b/openpype/hosts/aftereffects/api/extension/index.html index 291965559f..480b814a57 100644 --- a/openpype/hosts/aftereffects/api/extension/index.html +++ b/openpype/hosts/aftereffects/api/extension/index.html @@ -104,6 +104,39 @@ }); + + + + + +