From 12766377b43e9391509e52af68a95d1121411a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 14 Feb 2022 18:34:36 +0100 Subject: [PATCH 001/180] fix case with single mesh and prefixes --- .../create/create_unreal_staticmesh.py | 1 + .../hosts/maya/plugins/load/load_reference.py | 3 ++- .../publish/collect_unreal_staticmesh.py | 17 ++++++++++-- .../publish/extract_unreal_staticmesh.py | 27 ++++++++++++++----- .../validate_unreal_staticmesh_naming.py | 9 ++++++- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py index 9ad560ab7c..1fe7e57abc 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -33,6 +33,7 @@ class CreateUnrealStaticMesh(plugin.Creator): def process(self): with lib.undo_chunk(): + self.name = "{}_{}".format(self.family, self.name) instance = super(CreateUnrealStaticMesh, self).process() content = cmds.sets(instance, query=True) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 0565b0b95c..7cdd91a7ea 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -20,7 +20,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "camera", "rig", "camerarig", - "xgen"] + "xgen", + "unrealStaticMesh"] representations = ["ma", "abc", "fbx", "mb"] label = "Reference" diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index b1fb0542f2..8d9b88ed32 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- from maya import cmds import pyblish.api +from avalon.api import Session +from openpype.api import get_project_settings class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): @@ -16,10 +18,21 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): families = ["unrealStaticMesh"] def process(self, instance): + project_settings = get_project_settings(Session["AVALON_PROJECT"]) + sm_prefix = ( + project_settings + ["maya"] + ["create"] + ["CreateUnrealStaticMesh"] + ["static_mesh_prefix"] + ) # add fbx family to trigger fbx extractor instance.data["families"].append("fbx") - # take the name from instance (without the `S_` prefix) - instance.data["staticMeshCombinedName"] = instance.name[2:] + # take the name from instance (without the `unrealStaticMesh_` prefix) + instance.data["staticMeshCombinedName"] = "{}_{}".format( + sm_prefix, + instance.name[len(instance.data.get("family"))+3:] + ) geometry_set = [i for i in instance if i == "geometry_SET"] instance.data["membersToCombine"] = cmds.sets( diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 32dc9d1d1c..f46360e34a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -15,13 +15,24 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): def process(self, instance): to_combine = instance.data.get("membersToCombine") static_mesh_name = instance.data.get("staticMeshCombinedName") - self.log.info( - "merging {} into {}".format( - " + ".join(to_combine), static_mesh_name)) - duplicates = cmds.duplicate(to_combine, ic=True) - cmds.polyUnite( - *duplicates, - n=static_mesh_name, ch=False) + duplicates = [] + + # if we have more objects, combine them into one + # or just duplicate the single one + if len(to_combine) > 1: + self.log.info( + "merging {} into {}".format( + " + ".join(to_combine), static_mesh_name)) + duplicates = cmds.duplicate(to_combine, ic=True) + cmds.polyUnite( + *duplicates, + n=static_mesh_name, ch=False) + else: + self.log.info( + "duplicating {} to {} for export".format( + to_combine[0], static_mesh_name) + ) + cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) if not instance.data.get("cleanNodes"): instance.data["cleanNodes"] = [] @@ -31,3 +42,5 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): instance.data["setMembers"] = [static_mesh_name] instance.data["setMembers"] += instance.data["collisionMembers"] + + self.log.debug(instance.data["setMembers"]) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 901a2ec75e..b886e7da75 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -71,6 +71,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["CreateUnrealStaticMesh"] ["collision_prefixes"] ) + static_mesh_prefix = ( + project_settings + ["maya"] + ["create"] + ["CreateUnrealStaticMesh"] + ["static_mesh_prefix"] + ) combined_geometry_name = instance.data.get( "staticMeshCombinedName", None) @@ -107,7 +114,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): else: expected_collision = "{}_{}".format( cl_m.group("prefix"), - combined_geometry_name + combined_geometry_name[len(static_mesh_prefix)+1:] ) if not obj.startswith(expected_collision): From 1c0601518a3488b450b9c97d30e7c9d3011b1fb4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 15 Feb 2022 15:58:47 +0100 Subject: [PATCH 002/180] remove debug print --- .../hosts/maya/plugins/publish/extract_unreal_staticmesh.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index f46360e34a..0799d574a2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -42,5 +42,3 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): instance.data["setMembers"] = [static_mesh_name] instance.data["setMembers"] += instance.data["collisionMembers"] - - self.log.debug(instance.data["setMembers"]) From 3f7602ce8c7a463ff0472c33ac0e7a3df177cf80 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 15 Feb 2022 16:01:18 +0100 Subject: [PATCH 003/180] fix prefix --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 24e8e4a29b..4aaf6c705a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -52,7 +52,7 @@ "", "_Main" ], - "static_mesh_prefix": "S_", + "static_mesh_prefix": "S", "collision_prefixes": [ "UBX", "UCP", From 3cf48636b1a885704e8e30ddf7104595d1d23df9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 17 Feb 2022 13:18:44 +0100 Subject: [PATCH 004/180] rename family to staticMesh --- .../maya/plugins/create/create_unreal_staticmesh.py | 5 ++--- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- .../maya/plugins/publish/collect_unreal_staticmesh.py | 4 ++-- .../maya/plugins/publish/extract_unreal_staticmesh.py | 2 +- .../publish/validate_unreal_mesh_triangulated.py | 2 +- .../publish/validate_unreal_staticmesh_naming.py | 2 +- .../maya/plugins/publish/validate_unreal_up_axis.py | 2 +- openpype/plugins/publish/integrate_new.py | 3 ++- .../settings/defaults/project_anatomy/templates.json | 2 +- .../settings/defaults/project_settings/global.json | 11 +++++++++++ 10 files changed, 23 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py index 1fe7e57abc..f62d15fe62 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -10,7 +10,7 @@ class CreateUnrealStaticMesh(plugin.Creator): """Unreal Static Meshes with collisions.""" name = "staticMeshMain" label = "Unreal - Static Mesh" - family = "unrealStaticMesh" + family = "staticMesh" icon = "cube" dynamic_subset_keys = ["asset"] @@ -28,12 +28,11 @@ class CreateUnrealStaticMesh(plugin.Creator): variant, task_name, asset_id, project_name, host_name ) dynamic_data["asset"] = Session.get("AVALON_ASSET") - return dynamic_data def process(self): + self.name = "{}_{}".format(self.family, self.name) with lib.undo_chunk(): - self.name = "{}_{}".format(self.family, self.name) instance = super(CreateUnrealStaticMesh, self).process() content = cmds.sets(instance, query=True) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 7cdd91a7ea..8713182d3f 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -21,7 +21,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "rig", "camerarig", "xgen", - "unrealStaticMesh"] + "staticMesh"] representations = ["ma", "abc", "fbx", "mb"] label = "Reference" diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 8d9b88ed32..604aa58b50 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -15,7 +15,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.2 label = "Collect Unreal Static Meshes" - families = ["unrealStaticMesh"] + families = ["staticMesh"] def process(self, instance): project_settings = get_project_settings(Session["AVALON_PROJECT"]) @@ -28,7 +28,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): ) # add fbx family to trigger fbx extractor instance.data["families"].append("fbx") - # take the name from instance (without the `unrealStaticMesh_` prefix) + # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, instance.name[len(instance.data.get("family"))+3:] diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 0799d574a2..6153417de4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -10,7 +10,7 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Unreal Static Mesh" - families = ["unrealStaticMesh"] + families = ["staticMesh"] def process(self, instance): to_combine = instance.data.get("membersToCombine") diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py index b2ef174374..737664ffd3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py @@ -10,7 +10,7 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): order = openpype.api.ValidateMeshOrder hosts = ["maya"] - families = ["unrealStaticMesh"] + families = ["staticMesh"] category = "geometry" label = "Mesh is Triangulated" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index b886e7da75..89769a3421 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -52,7 +52,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): optional = True order = openpype.api.ValidateContentsOrder hosts = ["maya"] - families = ["unrealStaticMesh"] + families = ["staticMesh"] label = "Unreal StaticMesh Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] regex_mesh = r"(?P.*))" diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py index 5a8c29c22d..b3af643048 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py @@ -11,7 +11,7 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): optional = True order = openpype.api.ValidateContentsOrder hosts = ["maya"] - families = ["unrealStaticMesh"] + families = ["staticMesh"] label = "Unreal Up-Axis check" actions = [openpype.api.RepairAction] diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf214d9139..9ced6a1d7d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -100,7 +100,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "redshiftproxy", "effect", "xgen", - "hda" + "hda", + "staticMesh" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index d46d449c77..2ab3ff5c54 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -28,7 +28,7 @@ }, "delivery": {}, "unreal": { - "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", "path": "{@folder}/{@file}" }, diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index f08bee8b2d..93aba808db 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -192,6 +192,17 @@ "task_types": [], "tasks": [], "template_name": "render" + }, + { + "families": [ + "staticMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template_name": "unreal" } ], "subset_grouping_profiles": [ From 487b273a09e3cc67bc0386d047bfb4309db66439 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 18:05:07 +0100 Subject: [PATCH 005/180] refactor fbx extractor --- openpype/hosts/maya/api/fbx.py | 208 ++++++++++++++++++ .../hosts/maya/plugins/publish/clean_nodes.py | 31 --- .../publish/collect_unreal_staticmesh.py | 2 - .../hosts/maya/plugins/publish/extract_fbx.py | 196 ++--------------- .../publish/extract_unreal_staticmesh.py | 89 ++++++-- 5 files changed, 287 insertions(+), 239 deletions(-) create mode 100644 openpype/hosts/maya/api/fbx.py delete mode 100644 openpype/hosts/maya/plugins/publish/clean_nodes.py diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py new file mode 100644 index 0000000000..3a8ae19ff7 --- /dev/null +++ b/openpype/hosts/maya/api/fbx.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +"""Tools to work with FBX.""" +import os +import logging + +from pyblish.api import Instance + +from maya import cmds # noqa +import maya.mel as mel # noqa + + +class FBXExtractor: + """Extract FBX from Maya. + + This extracts reproducible FBX exports ignoring any of the settings set + on the local machine in the FBX export options window. + + All export settings are applied with the `FBXExport*` commands prior + to the `FBXExport` call itself. The options can be overridden with + their + nice names as seen in the "options" property on this class. + + For more information on FBX exports see: + - https://knowledge.autodesk.com/support/maya/learn-explore/caas + /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4 + -9CB19C28F4E0-htm.html + - http://forums.cgsociety.org/archive/index.php?t-1032853.html + - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE + /LKs9hakE28kJ + + """ + @property + def options(self): + """Overridable options for FBX Export + + Given in the following format + - {NAME: EXPECTED TYPE} + + If the overridden option's type does not match, + the option is not included and a warning is logged. + + """ + + return { + "cameras": bool, + "smoothingGroups": bool, + "hardEdges": bool, + "tangents": bool, + "smoothMesh": bool, + "instances": bool, + # "referencedContainersContent": bool, # deprecated in Maya 2016+ + "bakeComplexAnimation": int, + "bakeComplexStart": int, + "bakeComplexEnd": int, + "bakeComplexStep": int, + "bakeResampleAnimation": bool, + "animationOnly": bool, + "useSceneName": bool, + "quaternion": str, # "euler" + "shapes": bool, + "skins": bool, + "constraints": bool, + "lights": bool, + "embeddedTextures": bool, + "inputConnections": bool, + "upAxis": str, # x, y or z, + "triangulate": bool + } + + @property + def default_options(self): + """The default options for FBX extraction. + + This includes shapes, skins, constraints, lights and incoming + connections and exports with the Y-axis as up-axis. + + By default this uses the time sliders start and end time. + + """ + + start_frame = int(cmds.playbackOptions(query=True, + animationStartTime=True)) + end_frame = int(cmds.playbackOptions(query=True, + animationEndTime=True)) + + return { + "cameras": False, + "smoothingGroups": False, + "hardEdges": False, + "tangents": False, + "smoothMesh": False, + "instances": False, + "bakeComplexAnimation": True, + "bakeComplexStart": start_frame, + "bakeComplexEnd": end_frame, + "bakeComplexStep": 1, + "bakeResampleAnimation": True, + "animationOnly": False, + "useSceneName": False, + "quaternion": "euler", + "shapes": True, + "skins": True, + "constraints": False, + "lights": True, + "embeddedTextures": True, + "inputConnections": True, + "upAxis": "y", + "triangulate": False + } + + def __init__(self, log=None): + # Ensure FBX plug-in is loaded + self.log = log or logging.getLogger(__class__.__name__) + cmds.loadPlugin("fbxmaya", quiet=True) + + def parse_overrides(self, instance, options): + """Inspect data of instance to determine overridden options + + An instance may supply any of the overridable options + as data, the option is then added to the extraction. + + """ + + for key in instance.data: + if key not in self.options: + continue + + # Ensure the data is of correct type + value = instance.data[key] + if not isinstance(value, self.options[key]): + self.log.warning( + "Overridden attribute {key} was of " + "the wrong type: {invalid_type} " + "- should have been {valid_type}".format( + key=key, + invalid_type=type(value).__name__, + valid_type=self.options[key].__name__)) + continue + + options[key] = value + + return options + + def set_options_from_instance(self, instance): + # type: (Instance) -> None + """Sets FBX export options from data in the instance. + + Args: + instance (Instance): Instance data. + + """ + # Parse export options + options = self.default_options + options = self.parse_overrides(instance, options) + self.log.info("Export options: {0}".format(options)) + + # Collect the start and end including handles + # TODO: Move this to library function (pypeclub/OpenPype#2648) + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + handle_start = instance.data.get("handleStart", 0) + handle_end = instance.data.get("handleEnd", 0) + if handle_start: + start -= handle_start + if handle_end: + end += handle_end + + options['bakeComplexStart'] = start + options['bakeComplexEnd'] = end + + # First apply the default export settings to be fully consistent + # each time for successive publishes + mel.eval("FBXResetExport") + + # Apply the FBX overrides through MEL since the commands + # only work correctly in MEL according to online + # available discussions on the topic + _iteritems = getattr(options, "iteritems", options.items) + for option, value in _iteritems(): + key = option[0].upper() + option[1:] # uppercase first letter + + # Boolean must be passed as lower-case strings + # as to MEL standards + if isinstance(value, bool): + value = str(value).lower() + + template = "FBXExport{0} {1}" if key == "UpAxis" else \ + "FBXExport{0} -v {1}" # noqa + cmd = template.format(key, value) + self.log.info(cmd) + mel.eval(cmd) + + # Never show the UI or generate a log + mel.eval("FBXExportShowUI -v false") + mel.eval("FBXExportGenerateLog -v false") + + @staticmethod + def export(members, path): + # type: (list, str) -> None + """Export members as FBX with given path. + + Args: + members (list): List of members to export. + path (str): Path to use for export. + + """ + cmds.select(members, r=1, noExpand=True) + mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/plugins/publish/clean_nodes.py b/openpype/hosts/maya/plugins/publish/clean_nodes.py deleted file mode 100644 index 03995cdabe..0000000000 --- a/openpype/hosts/maya/plugins/publish/clean_nodes.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -"""Cleanup leftover nodes.""" -from maya import cmds # noqa -import pyblish.api - - -class CleanNodesUp(pyblish.api.InstancePlugin): - """Cleans up the staging directory after a successful publish. - - This will also clean published renders and delete their parent directories. - - """ - - order = pyblish.api.IntegratorOrder + 10 - label = "Clean Nodes" - optional = True - active = True - - def process(self, instance): - if not instance.data.get("cleanNodes"): - self.log.info("Nothing to clean.") - return - - nodes_to_clean = instance.data.pop("cleanNodes", []) - self.log.info("Removing {} nodes".format(len(nodes_to_clean))) - for node in nodes_to_clean: - try: - cmds.delete(node) - except ValueError: - # object might be already deleted, don't complain about it - pass diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 604aa58b50..1a0a561efd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -26,8 +26,6 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): ["CreateUnrealStaticMesh"] ["static_mesh_prefix"] ) - # add fbx family to trigger fbx extractor - instance.data["families"].append("fbx") # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py index 844084b9ab..fbbe8e06b0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py @@ -5,152 +5,29 @@ from maya import cmds # noqa import maya.mel as mel # noqa import pyblish.api import openpype.api -from openpype.hosts.maya.api.lib import ( - root_parent, - maintained_selection -) +from openpype.hosts.maya.api.lib import maintained_selection + +from openpype.hosts.maya.api import fbx class ExtractFBX(openpype.api.Extractor): """Extract FBX from Maya. - This extracts reproducible FBX exports ignoring any of the settings set - on the local machine in the FBX export options window. - - All export settings are applied with the `FBXExport*` commands prior - to the `FBXExport` call itself. The options can be overridden with their - nice names as seen in the "options" property on this class. - - For more information on FBX exports see: - - https://knowledge.autodesk.com/support/maya/learn-explore/caas - /CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4 - -9CB19C28F4E0-htm.html - - http://forums.cgsociety.org/archive/index.php?t-1032853.html - - https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE - /LKs9hakE28kJ + This extracts reproducible FBX exports ignoring any of the + settings set on the local machine in the FBX export options window. """ - order = pyblish.api.ExtractorOrder label = "Extract FBX" families = ["fbx"] - @property - def options(self): - """Overridable options for FBX Export - - Given in the following format - - {NAME: EXPECTED TYPE} - - If the overridden option's type does not match, - the option is not included and a warning is logged. - - """ - - return { - "cameras": bool, - "smoothingGroups": bool, - "hardEdges": bool, - "tangents": bool, - "smoothMesh": bool, - "instances": bool, - # "referencedContainersContent": bool, # deprecated in Maya 2016+ - "bakeComplexAnimation": int, - "bakeComplexStart": int, - "bakeComplexEnd": int, - "bakeComplexStep": int, - "bakeResampleAnimation": bool, - "animationOnly": bool, - "useSceneName": bool, - "quaternion": str, # "euler" - "shapes": bool, - "skins": bool, - "constraints": bool, - "lights": bool, - "embeddedTextures": bool, - "inputConnections": bool, - "upAxis": str, # x, y or z, - "triangulate": bool - } - - @property - def default_options(self): - """The default options for FBX extraction. - - This includes shapes, skins, constraints, lights and incoming - connections and exports with the Y-axis as up-axis. - - By default this uses the time sliders start and end time. - - """ - - start_frame = int(cmds.playbackOptions(query=True, - animationStartTime=True)) - end_frame = int(cmds.playbackOptions(query=True, - animationEndTime=True)) - - return { - "cameras": False, - "smoothingGroups": False, - "hardEdges": False, - "tangents": False, - "smoothMesh": False, - "instances": False, - "bakeComplexAnimation": True, - "bakeComplexStart": start_frame, - "bakeComplexEnd": end_frame, - "bakeComplexStep": 1, - "bakeResampleAnimation": True, - "animationOnly": False, - "useSceneName": False, - "quaternion": "euler", - "shapes": True, - "skins": True, - "constraints": False, - "lights": True, - "embeddedTextures": True, - "inputConnections": True, - "upAxis": "y", - "triangulate": False - } - - def parse_overrides(self, instance, options): - """Inspect data of instance to determine overridden options - - An instance may supply any of the overridable options - as data, the option is then added to the extraction. - - """ - - for key in instance.data: - if key not in self.options: - continue - - # Ensure the data is of correct type - value = instance.data[key] - if not isinstance(value, self.options[key]): - self.log.warning( - "Overridden attribute {key} was of " - "the wrong type: {invalid_type} " - "- should have been {valid_type}".format( - key=key, - invalid_type=type(value).__name__, - valid_type=self.options[key].__name__)) - continue - - options[key] = value - - return options - def process(self, instance): - - # Ensure FBX plug-in is loaded - cmds.loadPlugin("fbxmaya", quiet=True) + fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path - stagingDir = self.staging_dir(instance) + staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) - path = os.path.join(stagingDir, filename) + path = os.path.join(staging_dir, filename) # The export requires forward slashes because we need # to format it into a string in a mel expression @@ -162,58 +39,13 @@ class ExtractFBX(openpype.api.Extractor): self.log.info("Members: {0}".format(members)) self.log.info("Instance: {0}".format(instance[:])) - # Parse export options - options = self.default_options - options = self.parse_overrides(instance, options) - self.log.info("Export options: {0}".format(options)) - - # Collect the start and end including handles - start = instance.data["frameStart"] - end = instance.data["frameEnd"] - handles = instance.data.get("handles", 0) - if handles: - start -= handles - end += handles - - options['bakeComplexStart'] = start - options['bakeComplexEnd'] = end - - # First apply the default export settings to be fully consistent - # each time for successive publishes - mel.eval("FBXResetExport") - - # Apply the FBX overrides through MEL since the commands - # only work correctly in MEL according to online - # available discussions on the topic - _iteritems = getattr(options, "iteritems", options.items) - for option, value in _iteritems(): - key = option[0].upper() + option[1:] # uppercase first letter - - # Boolean must be passed as lower-case strings - # as to MEL standards - if isinstance(value, bool): - value = str(value).lower() - - template = "FBXExport{0} {1}" if key == "UpAxis" else "FBXExport{0} -v {1}" # noqa - cmd = template.format(key, value) - self.log.info(cmd) - mel.eval(cmd) - - # Never show the UI or generate a log - mel.eval("FBXExportShowUI -v false") - mel.eval("FBXExportGenerateLog -v false") + fbx_exporter.set_options_from_instance(instance) # Export - if "unrealStaticMesh" in instance.data["families"]: - with maintained_selection(): - with root_parent(members): - self.log.info("Un-parenting: {}".format(members)) - cmds.select(members, r=1, noExpand=True) - mel.eval('FBXExport -f "{}" -s'.format(path)) - else: - with maintained_selection(): - cmds.select(members, r=1, noExpand=True) - mel.eval('FBXExport -f "{}" -s'.format(path)) + with maintained_selection(): + fbx_exporter.export(members, path) + cmds.select(members, r=1, noExpand=True) + mel.eval('FBXExport -f "{}" -s'.format(path)) if "representations" not in instance.data: instance.data["representations"] = [] @@ -222,7 +54,7 @@ class ExtractFBX(openpype.api.Extractor): 'name': 'fbx', 'ext': 'fbx', 'files': filename, - "stagingDir": stagingDir, + "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 6153417de4..d3d491594a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -1,9 +1,18 @@ # -*- coding: utf-8 -*- """Create Unreal Static Mesh data to be extracted as FBX.""" -import openpype.api -import pyblish.api +import os + from maya import cmds # noqa +import pyblish.api +import openpype.api +from openpype.hosts.maya.api.lib import ( + root_parent, + maintained_selection, + delete_after +) +from openpype.hosts.maya.api import fbx + class ExtractUnrealStaticMesh(openpype.api.Extractor): """Extract FBX from Maya. """ @@ -13,32 +22,64 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): + fbx_exporter = fbx.FBXExtractor(log=self.log) to_combine = instance.data.get("membersToCombine") static_mesh_name = instance.data.get("staticMeshCombinedName") duplicates = [] - # if we have more objects, combine them into one - # or just duplicate the single one - if len(to_combine) > 1: - self.log.info( - "merging {} into {}".format( - " + ".join(to_combine), static_mesh_name)) - duplicates = cmds.duplicate(to_combine, ic=True) - cmds.polyUnite( - *duplicates, - n=static_mesh_name, ch=False) - else: - self.log.info( - "duplicating {} to {} for export".format( - to_combine[0], static_mesh_name) - ) - cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) + # delete created temporary nodes after extraction + with delete_after() as delete_bin: + # if we have more objects, combine them into one + # or just duplicate the single one + if len(to_combine) > 1: + self.log.info( + "merging {} into {}".format( + " + ".join(to_combine), static_mesh_name)) + duplicates = cmds.duplicate(to_combine, ic=True) + cmds.polyUnite( + *duplicates, + n=static_mesh_name, ch=False) + else: + self.log.info( + "duplicating {} to {} for export".format( + to_combine[0], static_mesh_name) + ) + cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) - if not instance.data.get("cleanNodes"): - instance.data["cleanNodes"] = [] + delete_bin.extend(static_mesh_name) + delete_bin.extend(duplicates) - instance.data["cleanNodes"].append(static_mesh_name) - instance.data["cleanNodes"] += duplicates + members = [static_mesh_name] + members += instance.data["collisionMembers"] - instance.data["setMembers"] = [static_mesh_name] - instance.data["setMembers"] += instance.data["collisionMembers"] + fbx_exporter = fbx.FBXExtractor() + + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.info("Extracting FBX to: {0}".format(path)) + self.log.info("Members: {0}".format(members)) + self.log.info("Instance: {0}".format(instance[:])) + + fbx_exporter.set_options_from_instance(instance) + + with maintained_selection(): + with root_parent(members): + self.log.info("Un-parenting: {}".format(members)) + fbx_exporter.export(members, path) + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extract FBX successful to: {0}".format(path)) \ No newline at end of file From 9604dca2593745380fc24741b7b7f5cf45f76ba8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 18:40:21 +0100 Subject: [PATCH 006/180] fix logging --- openpype/hosts/maya/api/fbx.py | 2 +- .../maya/plugins/publish/extract_unreal_staticmesh.py | 8 +++++--- .../plugins/publish/validate_unreal_staticmesh_naming.py | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 3a8ae19ff7..659f456e1a 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -110,7 +110,7 @@ class FBXExtractor: def __init__(self, log=None): # Ensure FBX plug-in is loaded - self.log = log or logging.getLogger(__class__.__name__) + self.log = log or logging.getLogger(self.__class__.__name__) cmds.loadPlugin("fbxmaya", quiet=True) def parse_overrides(self, instance, options): diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index d3d491594a..c5d2710dc2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -22,7 +22,6 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): - fbx_exporter = fbx.FBXExtractor(log=self.log) to_combine = instance.data.get("membersToCombine") static_mesh_name = instance.data.get("staticMeshCombinedName") duplicates = [] @@ -46,13 +45,13 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): ) cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) - delete_bin.extend(static_mesh_name) + delete_bin.extend([static_mesh_name]) delete_bin.extend(duplicates) members = [static_mesh_name] members += instance.data["collisionMembers"] - fbx_exporter = fbx.FBXExtractor() + fbx_exporter = fbx.FBXExtractor(log=self.log) # Define output path staging_dir = self.staging_dir(instance) @@ -74,6 +73,9 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): self.log.info("Un-parenting: {}".format(members)) fbx_exporter.export(members, path) + if "representations" not in instance.data: + instance.data["representations"] = [] + representation = { 'name': 'fbx', 'ext': 'fbx', diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 89769a3421..e233fd190c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -79,6 +79,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["static_mesh_prefix"] ) + to_combine = instance.data.get("membersToCombine") + if not to_combine: + raise ValueError("Missing geometry to export.") combined_geometry_name = instance.data.get( "staticMeshCombinedName", None) if cls.validate_mesh: From 3a42aa5c943e3a1a9a03a9f5c34cde0dbe8263d7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 18:42:19 +0100 Subject: [PATCH 007/180] fix family name in defaults --- openpype/settings/defaults/project_settings/global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 93aba808db..efed25287a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -298,7 +298,7 @@ }, { "families": [ - "unrealStaticMesh" + "staticMesh" ], "hosts": [ "maya" From f5087f4e47588d31bd335e51c648c8eb6bb33425 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 18 Feb 2022 19:04:24 +0100 Subject: [PATCH 008/180] =?UTF-8?q?fix=20hound=20=F0=9F=90=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/fbx.py | 1 - .../hosts/maya/plugins/publish/collect_unreal_staticmesh.py | 2 +- .../hosts/maya/plugins/publish/extract_unreal_staticmesh.py | 2 +- .../maya/plugins/publish/validate_unreal_staticmesh_naming.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 659f456e1a..00c58153af 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Tools to work with FBX.""" -import os import logging from pyblish.api import Instance diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 1a0a561efd..2c0bec2c1a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -29,7 +29,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, - instance.name[len(instance.data.get("family"))+3:] + instance.name[len(instance.data.get("family")) + 3:] ) geometry_set = [i for i in instance if i == "geometry_SET"] diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index c5d2710dc2..0c7d61f8f5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -84,4 +84,4 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) \ No newline at end of file + self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index e233fd190c..fd19e3d2af 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -117,7 +117,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): else: expected_collision = "{}_{}".format( cl_m.group("prefix"), - combined_geometry_name[len(static_mesh_prefix)+1:] + combined_geometry_name[len(static_mesh_prefix) + 1:] ) if not obj.startswith(expected_collision): From 1121bc8eaa42a0f8fcdda1001500908e42c7308e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 14:36:43 +0100 Subject: [PATCH 009/180] disable unnecessary plugins --- .../maya/plugins/publish/validate_unreal_mesh_triangulated.py | 1 + .../maya/plugins/publish/validate_unreal_staticmesh_naming.py | 4 ++-- .../hosts/maya/plugins/publish/validate_unreal_up_axis.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py index 737664ffd3..c05121a1b0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py @@ -14,6 +14,7 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): category = "geometry" label = "Mesh is Triangulated" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + active = False @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index fd19e3d2af..d15d52f3bd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -53,7 +53,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): order = openpype.api.ValidateContentsOrder hosts = ["maya"] families = ["staticMesh"] - label = "Unreal StaticMesh Name" + label = "Unreal Static Mesh Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] regex_mesh = r"(?P.*))" regex_collision = r"(?P.*)" @@ -101,7 +101,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cls.log.warning("No collision objects to validate.") return False - regex_collision = "{}{}".format( + regex_collision = "{}{}_(\\d+)".format( "(?P({}))_".format( "|".join("{0}".format(p) for p in collision_prefixes) ) or "", cls.regex_collision diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py index b3af643048..5e1b04889f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_up_axis.py @@ -9,6 +9,7 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin): """Validate if Z is set as up axis in Maya""" optional = True + active = False order = openpype.api.ValidateContentsOrder hosts = ["maya"] families = ["staticMesh"] From 691f23d72eab7ab0114efc6a9890ddcdce89f0da Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 14:38:48 +0100 Subject: [PATCH 010/180] unify handles --- openpype/hosts/maya/api/fbx.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 00c58153af..7980cd029c 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -154,15 +154,8 @@ class FBXExtractor: self.log.info("Export options: {0}".format(options)) # Collect the start and end including handles - # TODO: Move this to library function (pypeclub/OpenPype#2648) - start = instance.data["frameStart"] - end = instance.data["frameEnd"] - handle_start = instance.data.get("handleStart", 0) - handle_end = instance.data.get("handleEnd", 0) - if handle_start: - start -= handle_start - if handle_end: - end += handle_end + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] options['bakeComplexStart'] = start options['bakeComplexEnd'] = end From 901df528d4107e9423e7bc81aa80108024af143f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 14:42:36 +0100 Subject: [PATCH 011/180] remove ftrack submodules from old location --- .gitmodules | 8 +------- .../modules/default_modules/ftrack/python2_vendor/arrow | 1 - .../ftrack/python2_vendor/ftrack-python-api | 1 - 3 files changed, 1 insertion(+), 9 deletions(-) delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/arrow delete mode 160000 openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api diff --git a/.gitmodules b/.gitmodules index e1b0917e9d..67b820a247 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,10 +3,4 @@ url = https://github.com/pypeclub/avalon-core.git [submodule "repos/avalon-unreal-integration"] path = repos/avalon-unreal-integration - url = https://github.com/pypeclub/avalon-unreal-integration.git -[submodule "openpype/modules/default_modules/ftrack/python2_vendor/arrow"] - path = openpype/modules/default_modules/ftrack/python2_vendor/arrow - url = https://github.com/arrow-py/arrow.git -[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"] - path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api - url = https://bitbucket.org/ftrack/ftrack-python-api.git \ No newline at end of file + url = https://github.com/pypeclub/avalon-unreal-integration.git \ No newline at end of file diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/arrow b/openpype/modules/default_modules/ftrack/python2_vendor/arrow deleted file mode 160000 index b746fedf72..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/arrow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0 diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api deleted file mode 160000 index d277f474ab..0000000000 --- a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e From 28e11a5b2d847d0db971fe8a7d5c707064ac17fd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 15:18:38 +0100 Subject: [PATCH 012/180] fix frame handling and collision name determination --- openpype/hosts/maya/api/fbx.py | 6 ++++-- .../hosts/maya/plugins/publish/collect_unreal_staticmesh.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 7980cd029c..92683da51b 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -154,8 +154,10 @@ class FBXExtractor: self.log.info("Export options: {0}".format(options)) # Collect the start and end including handles - start = instance.data["frameStartHandle"] - end = instance.data["frameEndHandle"] + start = instance.data.get("frameStartHandle") or \ + instance.context.data.get("frameStartHandle") + end = instance.data.get("frameEndHandle") or \ + instance.context.data.get("frameEndHandle") options['bakeComplexStart'] = start options['bakeComplexEnd'] = end diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 2c0bec2c1a..ddcc3f691f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -29,7 +29,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, - instance.name[len(instance.data.get("family")) + 3:] + instance.name[len(instance.data.get("family")) + 1:] ) geometry_set = [i for i in instance if i == "geometry_SET"] From bd5478731cc6f6839d7dcbbeb1fd6d9d37e7cff0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 1 Mar 2022 22:39:53 +0100 Subject: [PATCH 013/180] change default templates --- .../defaults/project_anatomy/templates.json | 15 ++++++++++++--- .../defaults/project_settings/global.json | 9 ++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 2ab3ff5c54..7d01248653 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -28,9 +28,18 @@ }, "delivery": {}, "unreal": { - "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", - "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", + "folder": "{root[work]}/{project[name]}/unreal/{task[name]}", + "file": "{project[code]}_{asset}", "path": "{@folder}/{@file}" }, - "others": {} + "others": { + "maya2unreal": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", + "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", + "path": "{@folder}/{@file}" + }, + "__dynamic_keys_labels__": { + "maya2unreal": "Maya to Unreal" + } + } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index efed25287a..86786cc9ed 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -202,7 +202,7 @@ ], "task_types": [], "tasks": [], - "template_name": "unreal" + "template_name": "maya2unreal" } ], "subset_grouping_profiles": [ @@ -315,6 +315,13 @@ "task_types": [], "hosts": [], "workfile_template": "work" + }, + { + "task_types": [], + "hosts": [ + "unreal" + ], + "workfile_template": "unreal" } ], "last_workfile_on_startup": [ From 8c0d7ed1ee09c84b233cb7ec4068312afa27507a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 4 Mar 2022 18:38:26 +0100 Subject: [PATCH 014/180] initial work on skeletal mesh support --- .../create/create_unreal_skeletalmesh.py | 50 ++++++++++++++++ .../publish/collect_unreal_skeletalmesh.py | 23 ++++++++ .../publish/collect_unreal_staticmesh.py | 7 +-- .../publish/extract_unreal_skeletalmesh.py | 57 +++++++++++++++++++ .../publish/extract_unreal_staticmesh.py | 2 +- openpype/plugins/publish/integrate_new.py | 3 +- .../defaults/project_settings/global.json | 14 ++++- .../defaults/project_settings/maya.json | 5 ++ .../schemas/schema_maya_create.json | 26 +++++++++ 9 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py create mode 100644 openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py diff --git a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py new file mode 100644 index 0000000000..a6deeeee2e --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""Creator for Unreal Skeletal Meshes.""" +from openpype.hosts.maya.api import plugin, lib +from avalon.api import Session +from maya import cmds # noqa + + +class CreateUnrealSkeletalMesh(plugin.Creator): + """Unreal Static Meshes with collisions.""" + name = "staticMeshMain" + label = "Unreal - Skeletal Mesh" + family = "skeletalMesh" + icon = "thumbs-up" + dynamic_subset_keys = ["asset"] + + joint_hints = [] + + def __init__(self, *args, **kwargs): + """Constructor.""" + super(CreateUnrealSkeletalMesh, self).__init__(*args, **kwargs) + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + dynamic_data = super(CreateUnrealSkeletalMesh, cls).get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + dynamic_data["asset"] = Session.get("AVALON_ASSET") + return dynamic_data + + def process(self): + self.name = "{}_{}".format(self.family, self.name) + with lib.undo_chunk(): + instance = super(CreateUnrealSkeletalMesh, self).process() + content = cmds.sets(instance, query=True) + + # empty set and process its former content + cmds.sets(content, rm=instance) + geometry_set = cmds.sets(name="geometry_SET", empty=True) + joints_set = cmds.sets(name="joints_SET", empty=True) + + cmds.sets([geometry_set, joints_set], forceElement=instance) + members = cmds.ls(content) or [] + + for node in members: + if node in self.joint_hints: + cmds.sets(node, forceElement=joints_set) + else: + cmds.sets(node, forceElement=geometry_set) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py new file mode 100644 index 0000000000..4b1de865c5 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api +from avalon.api import Session +from openpype.api import get_project_settings + + +class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): + """Collect Unreal Skeletal Mesh.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Unreal Skeletal Meshes" + families = ["skeletalMesh"] + + def process(self, instance): + # set fbx overrides on instance + instance.data["smoothingGroups"] = True + instance.data["smoothMesh"] = True + instance.data["triangulate"] = True + + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index ddcc3f691f..59f8df1ef1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -6,12 +6,7 @@ from openpype.api import get_project_settings class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): - """Collect Unreal Static Mesh - - Ensures always only a single frame is extracted (current frame). This - also sets correct FBX options for later extraction. - - """ + """Collect Unreal Static Mesh.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Unreal Static Meshes" diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py new file mode 100644 index 0000000000..0ad1a92292 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Create Unreal Skeletal Mesh data to be extracted as FBX.""" +import os + +from maya import cmds # noqa + +import pyblish.api +import openpype.api +from openpype.hosts.maya.api.lib import ( + root_parent, + maintained_selection, + delete_after +) +from openpype.hosts.maya.api import fbx + + +class ExtractUnrealSkeletalMesh(openpype.api.Extractor): + """Extract Unreal Skeletal Mesh as FBX from Maya. """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Unreal Skeletal Mesh" + families = ["skeletalMesh"] + + def process(self, instance): + fbx_exporter = fbx.FBXExtractor(log=self.log) + + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.info("Extracting FBX to: {0}".format(path)) + self.log.info("Members: {0}".format(instance)) + self.log.info("Instance: {0}".format(instance[:])) + + fbx_exporter.set_options_from_instance(instance) + with maintained_selection(): + with root_parent(instance): + self.log.info("Un-parenting: {}".format(instance)) + fbx_exporter.export(instance, path) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.info("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 0c7d61f8f5..22a3af3059 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -15,7 +15,7 @@ from openpype.hosts.maya.api import fbx class ExtractUnrealStaticMesh(openpype.api.Extractor): - """Extract FBX from Maya. """ + """Extract Unreal Static Mesh as FBX from Maya. """ order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Unreal Static Mesh" diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 48b87c697b..fce8c73d1d 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -102,7 +102,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "xgen", "hda", "usd", - "staticMesh" + "staticMesh", + "skeletalMesh" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 86786cc9ed..b7f6414cfb 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -195,7 +195,8 @@ }, { "families": [ - "staticMesh" + "staticMesh", + "skeletalMesh" ], "hosts": [ "maya" @@ -306,6 +307,17 @@ "task_types": [], "tasks": [], "template": "S_{asset}{variant}" + }, + { + "families": [ + "skeletalMesh" + ], + "hosts": [ + "maya" + ], + "task_types": [], + "tasks": [], + "template": "SK_{asset}{variant}" } ] }, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 362835e558..6317d30b3f 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -60,6 +60,11 @@ "UCX" ] }, + "CreateUnrealSkeletalMesh": { + "enabled": true, + "defaults": [], + "joint_hints": "jnt_org" + }, "CreateAnimation": { "enabled": true, "defaults": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 0544b4bab7..6dc10ed2a5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -97,6 +97,32 @@ } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CreateUnrealSkeletalMesh", + "label": "Create Unreal - Skeletal Mesh", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + }, + { + "type": "text", + "key": "joint_hints", + "label": "Joint root hint" + } + ] + }, { "type": "schema_template", From 5e5f6e0879a470688072c3125145bdc544f10cb6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 8 Mar 2022 17:19:57 +0100 Subject: [PATCH 015/180] fix un-parenting --- .../publish/collect_unreal_skeletalmesh.py | 23 +++++++++++++++++++ .../publish/extract_unreal_skeletalmesh.py | 14 +++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py index 4b1de865c5..7d479b706c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -21,3 +21,26 @@ class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame + + geo_sets = [ + i for i in instance[:] + if i.lower().startswith("geometry_set") + ] + + joint_sets = [ + i for i in instance[:] + if i.lower().startswith("joints_set") + ] + + instance.data["geometry"] = [] + instance.data["joints"] = [] + + for geo_set in geo_sets: + geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True) + if geo_content: + instance.data["geometry"] += geo_content + + for join_set in joint_sets: + join_content = cmds.ls(cmds.sets(join_set, query=True), long=True) + if join_content: + instance.data["joints"] += join_content diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 0ad1a92292..c0b408c3f0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -29,19 +29,25 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) + geo = instance.data.get("geometry") + joints = instance.data.get("joints") + + to_extract = geo + joints + # The export requires forward slashes because we need # to format it into a string in a mel expression path = path.replace('\\', '/') self.log.info("Extracting FBX to: {0}".format(path)) - self.log.info("Members: {0}".format(instance)) + self.log.info("Members: {0}".format(to_extract)) self.log.info("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) with maintained_selection(): - with root_parent(instance): - self.log.info("Un-parenting: {}".format(instance)) - fbx_exporter.export(instance, path) + with root_parent(to_extract): + rooted = [i.split("|")[-1] for i in to_extract] + self.log.info("Un-parenting: {}".format(to_extract)) + fbx_exporter.export(rooted, path) if "representations" not in instance.data: instance.data["representations"] = [] From 592c95b1a66e0067b797b7a6d7458fa8127ac03a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 10 Mar 2022 01:14:21 +0100 Subject: [PATCH 016/180] fixes static mesh side of things --- .../plugins/publish/collect_unreal_staticmesh.py | 15 ++++++++++++--- .../plugins/publish/extract_unreal_staticmesh.py | 2 +- .../publish/validate_unreal_staticmesh_naming.py | 7 +++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 59f8df1ef1..02f21187b3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -from maya import cmds +from maya import cmds # noqa import pyblish.api from avalon.api import Session from openpype.api import get_project_settings +from pprint import pformat class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): @@ -24,17 +25,25 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): # take the name from instance (without the `staticMesh_` prefix) instance.data["staticMeshCombinedName"] = "{}_{}".format( sm_prefix, - instance.name[len(instance.data.get("family")) + 1:] - ) + instance.data.get("subset")[len(sm_prefix) + 1:]) + + self.log.info("joined mesh name: {}".format( + instance.data.get("staticMeshCombinedName"))) geometry_set = [i for i in instance if i == "geometry_SET"] instance.data["membersToCombine"] = cmds.sets( geometry_set, query=True) + self.log.info("joining meshes: {}".format( + pformat(instance.data.get("membersToCombine")))) + collision_set = [i for i in instance if i == "collisions_SET"] instance.data["collisionMembers"] = cmds.sets( collision_set, query=True) + self.log.info("collisions: {}".format( + pformat(instance.data.get("collisionMembers")))) + # set fbx overrides on instance instance.data["smoothingGroups"] = True instance.data["smoothMesh"] = True diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 22a3af3059..987370d395 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -46,7 +46,7 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) delete_bin.extend([static_mesh_name]) - delete_bin.extend(duplicates) + # delete_bin.extend(duplicates) members = [static_mesh_name] members += instance.data["collisionMembers"] diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index d15d52f3bd..1ecf436582 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -115,9 +115,12 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cls.log.error("{} is invalid".format(obj)) invalid.append(obj) else: + un_prefixed = combined_geometry_name[ + len(static_mesh_prefix) + 1: + ] expected_collision = "{}_{}".format( cl_m.group("prefix"), - combined_geometry_name[len(static_mesh_prefix) + 1:] + un_prefixed ) if not obj.startswith(expected_collision): @@ -130,7 +133,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), - combined_geometry_name, + un_prefixed, )) invalid.append(obj) From bc596899f5b96e27c9b1e3937384d18c4462ff3e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 10 Mar 2022 23:45:40 +0100 Subject: [PATCH 017/180] fixed fbx settings --- openpype/hosts/maya/api/fbx.py | 6 +++--- .../maya/plugins/publish/collect_unreal_skeletalmesh.py | 5 ----- .../hosts/maya/plugins/publish/collect_unreal_staticmesh.py | 5 ----- openpype/plugins/publish/collect_resources_path.py | 5 ++++- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 92683da51b..f8fe189589 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -84,10 +84,10 @@ class FBXExtractor: return { "cameras": False, - "smoothingGroups": False, + "smoothingGroups": True, "hardEdges": False, "tangents": False, - "smoothMesh": False, + "smoothMesh": True, "instances": False, "bakeComplexAnimation": True, "bakeComplexStart": start_frame, @@ -101,7 +101,7 @@ class FBXExtractor: "skins": True, "constraints": False, "lights": True, - "embeddedTextures": True, + "embeddedTextures": False, "inputConnections": True, "upAxis": "y", "triangulate": False diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py index 7d479b706c..2b176e3a6d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -13,11 +13,6 @@ class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): families = ["skeletalMesh"] def process(self, instance): - # set fbx overrides on instance - instance.data["smoothingGroups"] = True - instance.data["smoothMesh"] = True - instance.data["triangulate"] = True - frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 02f21187b3..faa5880e43 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -44,11 +44,6 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): self.log.info("collisions: {}".format( pformat(instance.data.get("collisionMembers")))) - # set fbx overrides on instance - instance.data["smoothingGroups"] = True - instance.data["smoothMesh"] = True - instance.data["triangulate"] = True - frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index fa181301ee..1f509365c7 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -53,7 +53,10 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "textures", "action", "background", - "effect" + "effect", + "staticMesh", + "skeletalMesh" + ] def process(self, instance): From 52dd76158fab048bd5ca8b9b1435b483aaa3c205 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Mar 2022 19:08:52 +0100 Subject: [PATCH 018/180] simplify whole process --- .../publish/collect_unreal_staticmesh.py | 24 +------ .../publish/extract_unreal_staticmesh.py | 62 ++++++------------- .../validate_unreal_staticmesh_naming.py | 14 +---- 3 files changed, 24 insertions(+), 76 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index faa5880e43..728a26931b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api -from avalon.api import Session -from openpype.api import get_project_settings from pprint import pformat @@ -14,28 +12,12 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): families = ["staticMesh"] def process(self, instance): - project_settings = get_project_settings(Session["AVALON_PROJECT"]) - sm_prefix = ( - project_settings - ["maya"] - ["create"] - ["CreateUnrealStaticMesh"] - ["static_mesh_prefix"] - ) - # take the name from instance (without the `staticMesh_` prefix) - instance.data["staticMeshCombinedName"] = "{}_{}".format( - sm_prefix, - instance.data.get("subset")[len(sm_prefix) + 1:]) - - self.log.info("joined mesh name: {}".format( - instance.data.get("staticMeshCombinedName"))) - geometry_set = [i for i in instance if i == "geometry_SET"] - instance.data["membersToCombine"] = cmds.sets( + instance.data["geometryMembers"] = cmds.sets( geometry_set, query=True) - self.log.info("joining meshes: {}".format( - pformat(instance.data.get("membersToCombine")))) + self.log.info("geometry: {}".format( + pformat(instance.data.get("geometryMembers")))) collision_set = [i for i in instance if i == "collisions_SET"] instance.data["collisionMembers"] = cmds.sets( diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 987370d395..02dd5dc572 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -22,56 +22,30 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): - to_combine = instance.data.get("membersToCombine") - static_mesh_name = instance.data.get("staticMeshCombinedName") - duplicates = [] + geo = instance.data.get("geometryMembers", []) + members = geo + instance.data.get("collisionMembers", []) - # delete created temporary nodes after extraction - with delete_after() as delete_bin: - # if we have more objects, combine them into one - # or just duplicate the single one - if len(to_combine) > 1: - self.log.info( - "merging {} into {}".format( - " + ".join(to_combine), static_mesh_name)) - duplicates = cmds.duplicate(to_combine, ic=True) - cmds.polyUnite( - *duplicates, - n=static_mesh_name, ch=False) - else: - self.log.info( - "duplicating {} to {} for export".format( - to_combine[0], static_mesh_name) - ) - cmds.duplicate(to_combine[0], name=static_mesh_name, ic=True) + fbx_exporter = fbx.FBXExtractor(log=self.log) - delete_bin.extend([static_mesh_name]) - # delete_bin.extend(duplicates) + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) - members = [static_mesh_name] - members += instance.data["collisionMembers"] + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') - fbx_exporter = fbx.FBXExtractor(log=self.log) + self.log.info("Extracting FBX to: {0}".format(path)) + self.log.info("Members: {0}".format(members)) + self.log.info("Instance: {0}".format(instance[:])) - # Define output path - staging_dir = self.staging_dir(instance) - filename = "{0}.fbx".format(instance.name) - path = os.path.join(staging_dir, filename) + fbx_exporter.set_options_from_instance(instance) - # The export requires forward slashes because we need - # to format it into a string in a mel expression - path = path.replace('\\', '/') - - self.log.info("Extracting FBX to: {0}".format(path)) - self.log.info("Members: {0}".format(members)) - self.log.info("Instance: {0}".format(instance[:])) - - fbx_exporter.set_options_from_instance(instance) - - with maintained_selection(): - with root_parent(members): - self.log.info("Un-parenting: {}".format(members)) - fbx_exporter.export(members, path) + with maintained_selection(): + with root_parent(members): + self.log.info("Un-parenting: {}".format(members)) + fbx_exporter.export(members, path) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 1ecf436582..920e0982dc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -79,18 +79,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["static_mesh_prefix"] ) - to_combine = instance.data.get("membersToCombine") - if not to_combine: - raise ValueError("Missing geometry to export.") - combined_geometry_name = instance.data.get( - "staticMeshCombinedName", None) if cls.validate_mesh: # compile regex for testing names regex_mesh = "{}{}".format( ("_" + cls.static_mesh_prefix) or "", cls.regex_mesh ) sm_r = re.compile(regex_mesh) - if not sm_r.match(combined_geometry_name): + if not sm_r.match(instance.data.get("subset")): cls.log.error("Mesh doesn't comply with name validation.") return True @@ -115,12 +110,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cls.log.error("{} is invalid".format(obj)) invalid.append(obj) else: - un_prefixed = combined_geometry_name[ - len(static_mesh_prefix) + 1: - ] expected_collision = "{}_{}".format( cl_m.group("prefix"), - un_prefixed + instance.data.get("subset") ) if not obj.startswith(expected_collision): @@ -133,7 +125,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), - un_prefixed, + instance.data.get("subset"), )) invalid.append(obj) From 87b256387d453ce760a13fd6151a67436e7b6ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 14 Mar 2022 22:30:26 +0100 Subject: [PATCH 019/180] fix for multiple subsets --- .../maya/plugins/publish/collect_unreal_staticmesh.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 728a26931b..79d0856fa0 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -12,14 +12,20 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): families = ["staticMesh"] def process(self, instance): - geometry_set = [i for i in instance if i == "geometry_SET"] + geometry_set = [ + i for i in instance + if i.startswith("geometry_SET") + ] instance.data["geometryMembers"] = cmds.sets( geometry_set, query=True) self.log.info("geometry: {}".format( pformat(instance.data.get("geometryMembers")))) - collision_set = [i for i in instance if i == "collisions_SET"] + collision_set = [ + i for i in instance + if i.startswith("collisions_SET") + ] instance.data["collisionMembers"] = cmds.sets( collision_set, query=True) From d31bee0c3e1a1c368dc099cc420a199c67919f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 15 Mar 2022 00:41:46 +0100 Subject: [PATCH 020/180] fix parenting for skeletal meshes --- openpype/hosts/maya/api/fbx.py | 2 +- openpype/hosts/maya/api/lib.py | 20 ++++++++++++++++--- .../publish/extract_unreal_skeletalmesh.py | 19 ++++++++++++------ .../publish/extract_unreal_staticmesh.py | 4 ++-- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index f8fe189589..260241f5fc 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -198,5 +198,5 @@ class FBXExtractor: path (str): Path to use for export. """ - cmds.select(members, r=1, noExpand=True) + cmds.select(members, r=True, noExpand=True) mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 41c67a6209..a5199d8443 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3085,11 +3085,20 @@ def set_colorspace(): @contextlib.contextmanager -def root_parent(nodes): - # type: (list) -> list +def parent_nodes(nodes, parent=None): + # type: (list, str) -> list """Context manager to un-parent provided nodes and return them back.""" import pymel.core as pm # noqa + parent_node = None + delete_parent = False + + if parent: + if not cmds.objExists(parent): + parent_node = pm.createNode("transform", n=parent, ss=False) + delete_parent = True + else: + parent_node = pm.PyNode(parent) node_parents = [] for node in nodes: n = pm.PyNode(node) @@ -3100,9 +3109,14 @@ def root_parent(nodes): node_parents.append((n, root)) try: for node in node_parents: - node[0].setParent(world=True) + if not parent: + node[0].setParent(world=True) + else: + node[0].setParent(parent_node) yield finally: for node in node_parents: if node[1]: node[0].setParent(node[1]) + if delete_parent: + pm.delete(parent_node) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index c0b408c3f0..6f4c70fc07 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -7,9 +7,8 @@ from maya import cmds # noqa import pyblish.api import openpype.api from openpype.hosts.maya.api.lib import ( - root_parent, - maintained_selection, - delete_after + parent_nodes, + maintained_selection ) from openpype.hosts.maya.api import fbx @@ -43,10 +42,18 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): self.log.info("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) + + parent = "{}{}".format( + instance.data["asset"], + instance.data.get("variant", "") + ) with maintained_selection(): - with root_parent(to_extract): - rooted = [i.split("|")[-1] for i in to_extract] - self.log.info("Un-parenting: {}".format(to_extract)) + with parent_nodes(to_extract, parent=parent): + rooted = [ + "{}|{}".format(parent, i.split("|")[-1]) + for i in to_extract + ] + self.log.info("Un-parenting: {}".format(rooted, path)) fbx_exporter.export(rooted, path) if "representations" not in instance.data: diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 02dd5dc572..c3cc322a29 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -7,7 +7,7 @@ from maya import cmds # noqa import pyblish.api import openpype.api from openpype.hosts.maya.api.lib import ( - root_parent, + parent_nodes, maintained_selection, delete_after ) @@ -43,7 +43,7 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): fbx_exporter.set_options_from_instance(instance) with maintained_selection(): - with root_parent(members): + with parent_nodes(members): self.log.info("Un-parenting: {}".format(members)) fbx_exporter.export(members, path) From 08370f53e564174c87f1dd9316ecb2dc53898f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 15 Mar 2022 01:01:14 +0100 Subject: [PATCH 021/180] fix getting correct name with prefix --- .../plugins/publish/validate_unreal_staticmesh_naming.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 920e0982dc..c0eeb82688 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -104,6 +104,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): cl_r = re.compile(regex_collision) + mesh_name = "{}{}".format(instance.data["asset"], + instance.data.get("variant", [])) + for obj in collision_set: cl_m = cl_r.match(obj) if not cl_m: @@ -112,7 +115,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): else: expected_collision = "{}_{}".format( cl_m.group("prefix"), - instance.data.get("subset") + mesh_name ) if not obj.startswith(expected_collision): @@ -121,11 +124,11 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): "Collision object name doesn't match " "static mesh name" ) - cls.log.error("{}_{} != {}_{}".format( + cls.log.error("{}_{} != {}_{}*".format( cl_m.group("prefix"), cl_m.group("renderName"), cl_m.group("prefix"), - instance.data.get("subset"), + mesh_name, )) invalid.append(obj) From 314f789bc91e30a4ddd646225a4442faf3725a8b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Mar 2022 11:22:10 +0100 Subject: [PATCH 022/180] initial commit of publishing development docs --- website/docs/dev_publishing.md | 390 +++++++++++++++++++++++++++++++++ website/sidebars.js | 9 +- 2 files changed, 398 insertions(+), 1 deletion(-) create mode 100644 website/docs/dev_publishing.md diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md new file mode 100644 index 0000000000..0808818456 --- /dev/null +++ b/website/docs/dev_publishing.md @@ -0,0 +1,390 @@ +--- +id: dev_publishing +title: Publishing +sidebar_label: Publishing +--- + +Publishing workflow consist of 2 parts: +- Creation - Mark what will be published and how. +- Publishing - Use data from creation to go through pyblish process. + +OpenPype is using `pyblish` for publishing process. It is a little bit extented and modified mainly for UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during creation part instead of in publishing part and has limited actions only for failed validator plugins. + +# Creation +Concept of creation does not have to "create" anything but prepare and store metadata about an "instance". Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach. Storing data to workfile gives ability to keep values so artist does not have to do create instances over and over. + +## Created instance +Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is dictionary like object with few immutable keys (maked with start `*`) that are defined by creator plugin or create context on initialization. Can have more arbitrary data but keep in mind that some keys are reserved. + +| Key | Type | Description | +|---|---|---| +| *id | str | Identifier of metadata type. ATM constant **"pyblish.avalon.instance"** | +| *instance_id | str | Unique ID of instance. Set automatically on instance creation using `str(uuid.uuid4())` | +| *family | str | Instance's family representing type defined by creator plugin. | +| *creator_identifier | str | Identifier of creator that collected/created the instance. | +| *creator_attributes | dict | Dictionary of attributes that are defined by creator plugin (`get_instance_attr_defs`). | +| *publish_attributes | dict | Dictionary of attributes that are defined by publish plugins. | +| variant | str | Variant is entered by artist on creation and may affect **subset**. | +| subset | str | Name of instance. This name will be used as subset name during publishing. | +| active | bool | Is instance active and will be published or not. | +| asset | str | Name of asset in which context was created. | +| task | str | Name of task in which context was created. Can be set to `None`. | + +Task should not be required until subset name template expect it. + +## Create plugin +Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. + +### BaseCreator +Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **AutoCreator** and **Creator** variants. + +**Abstractions** +- **`family`** (class attr) - Tells what kind of instance will be created. +```python +class WorkfileCreator(Creator): + family = "workfile" +``` + +- **`collect_instances`** (method) - Collect already existing instances from workfile and add them to create context. This method is called on initialization or reset of **CreateContext**. Each creator is responsible to find it's instances metadata, convert them to **CreatedInstance** object and add the to create context (`self._add_instance_to_context(instnace_obj)`). +```python +def collect_instances(self): + # Using 'pipeline.list_instances' is just example how to get existing instances from scene + # - getting existing instances is different per host implementation + for instance_data in pipeline.list_instances(): + # Process only instances that were created by this creator + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + # Create instance object from existing data + instance = CreatedInstance.from_existing( + instance_data, self + ) + # Add instance to create context + self._add_instance_to_context(instance) +``` + +- **`create`** (method) - Create new object of **CreatedInstance** store it's metadata to workfile and add the instance into create context. Failed creation should raise **CreatorError** if happens error that can artist fix or give him some useful information. Trigger and implementation differs for **Creator** and **AutoCreator**. + +- **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**. +```python +def update_instances(self, update_list): + # Loop over changed instances + for instance, changes in update_list: + # Example possible usage of 'changes' to use different node on change + # of node id in instance data (MADE UP) + node = None + if "node_id" in changes: + old_value, new_value = changes["node_id"] + if new_value is not None: + node = pipeline.get_node_by_id(new_value) + + if node is None: + node = pipeline.get_node_by_instance_id(instance.id) + # Get node in scene that represents the instance + # Imprind data to a node + pipeline.imprint(node, instance.data_to_store()) + + +# Most implementations will probably ignore 'changes' completely +def update_instances(self, update_list): + for instance, _ in update_list: + # Get node from scene + node = pipeline.get_node_by_instance_id(instance.id) + # Imprint data to node + pipeline.imprint(node, instance.data_to_store()) +``` + +- **`remove_instances`** (method) - Remove instance metadata from workfile and from create context. +```python +# Possible way how to remove instance +def remove_instances(self, instances): + for instance in instances: + # Remove instance metadata from workflle + pipeline.remove_instance(instance.id) + # Remove instance from create context + self._remove_instance_from_context(instance) + + +# Default implementation of `AutoCreator` +def remove_instances(self, instances): + pass +``` + +:::note +When host implementation use universal way how to store and load instances you should implement host specific creator plugin base class with implemented **collect_instances**, **update_instances** and **remove_instances**. +::: + +**Optional implementations** + +- **`enabled`** (attr) - Boolean if creator plugin is enabled and used. +- **`identifier`** (class attr) - Consistent unique string identifier of the creator plugin. Is used to identify source plugin of existing instances. There can't be 2 creator plugins with same identifier. Default implementation returns `family` attribute. +```python +class RenderLayerCreator(Creator): + family = "render" + identifier = "render_layer" + + +class RenderPassCreator(Creator): + family = "render" + identifier = "render_pass" +``` + +- **`label`** (attr) - String label of creator plugin which will showed in UI, `identifier` is used when not set. It should be possible to use html tags. +```python +class RenderLayerCreator(Creator): + label = "Render Layer" +``` + +- **`icon`** (attr) - Icon of creator and it's instances. Value can be a path to image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for used version in pyproject.toml). +- **`get_icon`** (method) - Default implementation returns `self.icon`. +```python +class RenderLayerCreator(Creator): + # Use font awesome 5 icon + icon = "fa5.building" +``` + + +- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic type of values for different cases e.g. boolean, number, string, enumerator, etc. Their advantage is that they can be created dynamically and +```python +from openpype.pipeline import attribute_definitions + + +class RenderLayerCreator(Creator): + def get_instance_attr_defs(self): + # Return empty list if '_allow_farm_render' is not enabled (can be set during initialization) + if not self._allow_farm_render: + return [] + # Give artist option to change if should be rendered on farm or locally + return [ + attribute_definitions.BoolDef( + "render_farm", + default=False, + label="Render on Farm" + ) + ] +``` + +- **`get_subset_name`** (method) - Calculate subset name based on passed data. Data can be extended using `get_dynamic_data` method. Default implementation is using `get_subset_name` from `openpype.lib` which is recommended. + +- **`get_dynamic_data`** (method) - Can be used to extend data for subset template which may be required in some cases. + + +### AutoCreator +Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting of all creators. + +:::important +**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would in most of cases lead to create new instance immediately. +::: + +```python +def __init__( + self, create_context, system_settings, project_settings, *args, **kwargs +): + super(MyCreator, self).__init__( + create_context, system_settings, project_settings, *args, **kwargs + ) + # Get variant value from settings + variant_name = ( + project_settings["my_host"][self.identifier]["variant"] + ).strip() + if not variant_name: + variant_name = "Main" + self._variant_name = variant_name + +# Create does not expect any arguments +def create(self): + # Look for existing instance in create context + existing_instance = None + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + existing_instance = instance + break + + # Collect current context information + # - variant can be filled from settings + variant = self._variant_name + # Only place where we can look for current context + project_name = io.Session["AVALON_PROJECT"] + asset_name = io.Session["AVALON_ASSET"] + task_name = io.Session["AVALON_TASK"] + host_name = io.Session["AVALON_APP"] + + # Create new instance if does not exist yet + if existing_instance is None: + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant + } + data.update(self.get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name + )) + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) + + # Update instance context if is not the same + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name +``` + +### Creator +Implementation of creator plugin that is triggered manually by artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments. + +- **`create_allow_context_change`** (class attr) - Allow to set context in UI before creation. Some creator may not allow it or their logic would not use the context selection (e.g. bulk creators). +```python +class BulkRenderCreator(Creator): + create_allow_context_change = False +``` +- **`get_default_variants`** (method) - Returns list of default variants that are showed in create dialog. Uses **default_variants** by default. +- **`default_variants`** (attr) - Attribute for default implementation of **get_default_variants**. + +- **`get_default_variant`** (method) - Return default variant that is prefilled in UI. By default returns `None`, in that case first item from **get_default_variants** is used if there is any or **"Main"**. + +- **`get_description`** (method) - Returns short string description of creator. Uses **description** by default. +- **`description`** (attr) - Attribute for default implementation of **get_description**. + +- **`get_detailed_description`** (method) - Returns detailed string description of creator. Can contain markdown. Uses **detailed_description** by default. +- **`detailed_description`** (attr) - Attribute for default implementation of **get_detailed_description**. + +- **`get_pre_create_attr_defs`** (method) - Similar to **get_instance_attr_defs** returns attribute definitions but they are filled before creation. When creation is called from UI the values are passed to **create** method. + +- **`create`** (method) - Code where creation of metadata + +```python +from openpype.pipeline import attribute_definitions + + +class RenderLayerCreator(Creator): + def __init__( + self, context, system_settings, project_settings, *args, **kwargs + ): + super(RenderLayerCreator, self).__init__( + context, system_settings, project_settings, *args, **kwargs + ) + plugin_settings = ( + project_settings["my_host"]["create"][self.__class__.__name__] + ) + self._allow_farm_render = plugin_settings["allow_farm_render"] + + def get_instance_attr_defs(self): + # Return empty list if '_allow_farm_render' is not enabled (can be set during initialization) + if not self._allow_farm_render: + return [] + # Give artist option to change if should be rendered on farm or locally + return [ + attribute_definitions.BoolDef( + "render_farm", + default=False, + label="Render on Farm" + ) + ] + + def get_pre_create_attr_defs(self): + return [ + # Give user option to use selection or not + attribute_definitions.BoolDef( + "use_selection", + default=False, + label="Use selection" + ), + # Set to render on farm in creator dialog + # - this value is not automatically passed to instance attributes + # creator must do that during creation + attribute_definitions.BoolDef( + "render_farm", + default=False, + label="Render on Farm" + ) + ] + + def create(self, subset_name, instance_data, pre_create_data): + # ARGS: + # - 'subset_name' - precalculated subset name + # - 'instance_data' - context data + # - 'asset' - asset name + # - 'task' - task name + # - 'variant' - variant + # - 'family' - instnace family + # Check if should use selection or not + if pre_create_data.get("use_selection"): + items = pipeline.get_selection() + else: + items = [pipeline.create_write()] + + # Validations related to selection + if len(items) > 1: + raise CreatorError("Please select only single item at time.") + + elif not items: + raise CreatorError("Nothing to create. Select at least one item.") + + # Create instence object + new_instance = CreatedInstance(self.family, subset_name, data, self) + # Pass value from pre create attribute to instance + # - use them only when pre create date contain the data + if "render_farm" in pre_create_data: + use_farm = pre_create_data["render_farm"] + new_instance.creator_attributes["render_farm"] = use_farm + + # Store metadata to workfile + pipeline.imprint(new_instance.id, new_instance.data_to_store()) + + # Add instance to context + self._add_instance_to_context(new_instance) +``` + +## Create context +Controller and wrapper around creation is `CreateContext` which cares about loading `CreatedInstance` + +# Publish +OpenPype is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. The main differences are that OpenPype's publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. + +## Exceptions +OpenPype define few specific exceptions that should be used in publish plugins. + +### Validation exception +Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. + +Exception `PublishValidationError` 3 arguments: +- **message** Which is not used in UI but for headless publishing. +- **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin. +- **description** Detailed description of happened issue where markdown and html can be used. + + +### Known errors +When there is a known error that can't be fixed by user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raise. The only difference is that it's message is shown in UI to artist otherwise a neutral message without context is shown. + +## Plugin extension +Publish plugins can be extended by additional logic when inherits from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). + +```python +import pyblish.api +from openpype.pipeline import OpenPypePyblishPluginMixin + + +# Example context plugin +class MyExtendedPlugin( + pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin +): + pass + +``` + +### Extensions +Currently only extension is ability to define attributes for instances during creation. Method `get_attribute_defs` returns attribute definitions for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be implemented `convert_attribute_values`. Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure. + +Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. diff --git a/website/sidebars.js b/website/sidebars.js index 16af1e1151..fe76336e7e 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -136,6 +136,13 @@ module.exports = { "dev_requirements", "dev_build", "dev_testing", - "dev_contribute" + "dev_contribute", + { + type: "category", + label: "Hosts development", + items: [ + "dev_publishing" + ] + } ] }; From b70a22e4734acefc842792fbeff5dcd11c35025d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Mar 2022 14:40:00 +0100 Subject: [PATCH 023/180] added attributes as default return value for methods --- openpype/pipeline/create/creator_plugins.py | 57 ++++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 1ac2c420a2..cae55431a9 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -46,6 +46,11 @@ class BaseCreator: # - may not be used if `get_icon` is reimplemented icon = None + # Instance attribute definitions that can be changed per instance + # - returns list of attribute definitions from + # `openpype.pipeline.attribute_definitions` + instance_attr_defs = [] + def __init__( self, create_context, system_settings, project_settings, headless=False ): @@ -56,10 +61,13 @@ class BaseCreator: # - we may use UI inside processing this attribute should be checked self.headless = headless - @abstractproperty + @property def identifier(self): - """Identifier of creator (must be unique).""" - pass + """Identifier of creator (must be unique). + + Default implementation returns plugin's family. + """ + return self.family @abstractproperty def family(self): @@ -90,11 +98,39 @@ class BaseCreator: pass @abstractmethod - def collect_instances(self, attr_plugins=None): + def collect_instances(self): + """Collect existing instances related to this creator plugin. + + The implementation differs on host abilities. The creator has to + collect metadata about instance and create 'CreatedInstance' object + which should be added to 'CreateContext'. + + Example: + ```python + def collect_instances(self): + # Getting existing instances is different per host implementation + for instance_data in pipeline.list_instances(): + # Process only instances that were created by this creator + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + # Create instance object from existing data + instance = CreatedInstance.from_existing( + instance_data, self + ) + # Add instance to create context + self._add_instance_to_context(instance) + ``` + """ pass @abstractmethod def update_instances(self, update_list): + """Store changes of existing instances so they can be recollected. + + Args: + update_list(list): Gets list of tuples. Each item + contain changed instance and it's changes. + """ pass @abstractmethod @@ -178,7 +214,7 @@ class BaseCreator: list: Attribute definitions that can be tweaked for created instance. """ - return [] + return self.instance_attr_defs class Creator(BaseCreator): @@ -191,6 +227,9 @@ class Creator(BaseCreator): # - default_variants may not be used if `get_default_variants` is overriden default_variants = [] + # Default variant used in 'get_default_variant' + default_variant = None + # Short description of family # - may not be used if `get_description` is overriden description = None @@ -204,6 +243,10 @@ class Creator(BaseCreator): # e.g. for buld creators create_allow_context_change = True + # Precreate attribute definitions showed before creation + # - similar to instance attribute definitions + pre_create_attr_defs = [] + @abstractmethod def create(self, subset_name, instance_data, pre_create_data): """Create new instance and store it. @@ -263,7 +306,7 @@ class Creator(BaseCreator): `get_default_variants` should be used. """ - return None + return self.default_variant def get_pre_create_attr_defs(self): """Plugin attribute definitions needed for creation. @@ -276,7 +319,7 @@ class Creator(BaseCreator): list: Attribute definitions that can be tweaked for created instance. """ - return [] + return self.pre_create_attr_defs class AutoCreator(BaseCreator): From 19edc98d4909b4e855cdc8f2d9a8fc7f5a155c4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Mar 2022 14:40:16 +0100 Subject: [PATCH 024/180] update list has named tuple 'UpdateData' --- openpype/pipeline/create/context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c2757a4502..15417a4ff8 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -18,6 +18,8 @@ from openpype.api import ( get_project_settings ) +UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) + class ImmutableKeyError(TypeError): """Accessed key is immutable so does not allow changes or removements.""" @@ -1080,7 +1082,7 @@ class CreateContext: for instance in cretor_instances: instance_changes = instance.changes() if instance_changes: - update_list.append((instance, instance_changes)) + update_list.append(UpdateData(instance, instance_changes)) creator = self.creators[identifier] if update_list: From d4abe6ea8788111ba77ff7dab5b64ef59a63182f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Mar 2022 15:39:41 +0100 Subject: [PATCH 025/180] added more information about publishing --- website/docs/dev_publishing.md | 203 +++++++++++++++++++++++++++------ 1 file changed, 165 insertions(+), 38 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 0808818456..b16a42bb43 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -8,7 +8,7 @@ Publishing workflow consist of 2 parts: - Creation - Mark what will be published and how. - Publishing - Use data from creation to go through pyblish process. -OpenPype is using `pyblish` for publishing process. It is a little bit extented and modified mainly for UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during creation part instead of in publishing part and has limited actions only for failed validator plugins. +OpenPype is using [pyblish](https://pyblish.com/) for publishing process. OpenPype a little bit extend and modify few functions mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during creation part instead of in publishing part and has limited plugin actions only for failed validation plugins. # Creation Concept of creation does not have to "create" anything but prepare and store metadata about an "instance". Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach. Storing data to workfile gives ability to keep values so artist does not have to do create instances over and over. @@ -30,7 +30,33 @@ Objected representation of created instance metadata defined by class **CreatedI | asset | str | Name of asset in which context was created. | | task | str | Name of task in which context was created. Can be set to `None`. | +:::note Task should not be required until subset name template expect it. +::: + +object of **CreatedInstance** has method **data_to_store** which returns dictionary that can be parsed to json string. This method will return all data related to instance so can be re-created using `CreatedInstance.from_existing(data)`. + +## Create context +Controller and wrapper around creation is `CreateContext` which cares about loading of plugins needed for creation. And validates required functions in host implementation. + +Context discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger creation itself. Also keeps in mind instance objects by their ids. + +Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instance but these methods are not meant to be called directly out of creator. The reason is that is creator's responsibility to remove metadata or decide if should remove the instance. + +### Required functions in host implementation +Host implementation **must** have implemented **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instane but are needed for creation and publishing process. Right now are there stored data about enabled/disabled optional publish plugins. When data are not stored and loaded properly reset of publishing will cause that they will be set to default value. Similar to instance data can be context data also parsed to json string. + +There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return string showed in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. + +Another optional function is **get_current_context**. This function is handy in hosts where is possible to open multiple workfiles in one process so using global context variables are not relevant because artist can switch between opened workfiles without being acknowledged. When function is not implemented or won't return right keys the global +```json +# Expected keys in output +{ + "project_name": "MyProject", + "asset_name": "sq01_sh0010", + "task_name": "Modeling" +} +``` ## Create plugin Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. @@ -134,16 +160,17 @@ class RenderLayerCreator(Creator): label = "Render Layer" ``` -- **`icon`** (attr) - Icon of creator and it's instances. Value can be a path to image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for used version in pyproject.toml). -- **`get_icon`** (method) - Default implementation returns `self.icon`. +- **`get_icon`** (attr) - Icon of creator and it's instances. Value can be a path to image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for used version in pyproject.toml). Default implementation return **icon** attribute. +- **`icon`** (method) - Attribute for default implementation of **get_icon**. ```python class RenderLayerCreator(Creator): # Use font awesome 5 icon icon = "fa5.building" ``` +- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic type of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementations returns **instance_attr_defs**. +- **`instance_attr_defs`** (attr) - Attribute for default implementation of **get_instance_attr_defs**. -- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic type of values for different cases e.g. boolean, number, string, enumerator, etc. Their advantage is that they can be created dynamically and ```python from openpype.pipeline import attribute_definitions @@ -172,7 +199,7 @@ class RenderLayerCreator(Creator): Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting of all creators. :::important -**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would in most of cases lead to create new instance immediately. +**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would lead to create new instance immediately or on refresh. ::: ```python @@ -244,41 +271,53 @@ def create(self): ### Creator Implementation of creator plugin that is triggered manually by artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments. -- **`create_allow_context_change`** (class attr) - Allow to set context in UI before creation. Some creator may not allow it or their logic would not use the context selection (e.g. bulk creators). +**Abstractions** +- **`create`** (method) - Code where creation of metadata + +**Optional implementations** +- **`create_allow_context_change`** (class attr) - Allow to set context in UI before creation. Some creator may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default. ```python class BulkRenderCreator(Creator): create_allow_context_change = False ``` -- **`get_default_variants`** (method) - Returns list of default variants that are showed in create dialog. Uses **default_variants** by default. +- **`get_default_variants`** (method) - Returns list of default variants that are listed in create dialog for user. Returns **default_variants** attribute by default. - **`default_variants`** (attr) - Attribute for default implementation of **get_default_variants**. -- **`get_default_variant`** (method) - Return default variant that is prefilled in UI. By default returns `None`, in that case first item from **get_default_variants** is used if there is any or **"Main"**. +- **`get_default_variant`** (method) - Returns default variant that is prefilled in UI (value does not have to be in default variants). By default returns **default_variant** attribute. If returns `None` then UI logic will take first item from **get_default_variants** if there is any otherwise **"Main"** is used. +- **`default_variant`** (attr) - Attribute for default implementation of **get_default_variant**. -- **`get_description`** (method) - Returns short string description of creator. Uses **description** by default. +- **`get_description`** (method) - Returns short string description of creator. Returns **description** attribute by default. - **`description`** (attr) - Attribute for default implementation of **get_description**. -- **`get_detailed_description`** (method) - Returns detailed string description of creator. Can contain markdown. Uses **detailed_description** by default. +- **`get_detailed_description`** (method) - Returns detailed string description of creator. Can contain markdown. Returns **detailed_description** attribute by default. - **`detailed_description`** (attr) - Attribute for default implementation of **get_detailed_description**. -- **`get_pre_create_attr_defs`** (method) - Similar to **get_instance_attr_defs** returns attribute definitions but they are filled before creation. When creation is called from UI the values are passed to **create** method. - -- **`create`** (method) - Code where creation of metadata +- **`get_pre_create_attr_defs`** (method) - Similar to **get_instance_attr_defs** returns attribute definitions but they are filled before creation. When creation is called from UI the values are passed to **create** method. Returns **pre_create_attr_defs** attribute by default. +- **`pre_create_attr_defs`** (attr) - Attribute for default implementation of **get_pre_create_attr_defs**. ```python -from openpype.pipeline import attribute_definitions +from openpype.pipeline import Creator, attribute_definitions -class RenderLayerCreator(Creator): +class CreateRender(Creator): + family = "render" + label = "Render" + icon = "fa.eye" + description = "Render scene viewport" + def __init__( self, context, system_settings, project_settings, *args, **kwargs ): - super(RenderLayerCreator, self).__init__( + super(CreateRender, self).__init__( context, system_settings, project_settings, *args, **kwargs ) plugin_settings = ( project_settings["my_host"]["create"][self.__class__.__name__] ) + # Get information if studio has enabled farm publishing self._allow_farm_render = plugin_settings["allow_farm_render"] + # Get default variants from settings + self.default_variants = plugin_settings["variants"] def get_instance_attr_defs(self): # Return empty list if '_allow_farm_render' is not enabled (can be set during initialization) @@ -294,22 +333,26 @@ class RenderLayerCreator(Creator): ] def get_pre_create_attr_defs(self): - return [ - # Give user option to use selection or not + # Give user option to use selection or not + attrs = [ attribute_definitions.BoolDef( "use_selection", default=False, label="Use selection" - ), + ) + ] + if self._allow_farm_render: # Set to render on farm in creator dialog # - this value is not automatically passed to instance attributes # creator must do that during creation - attribute_definitions.BoolDef( - "render_farm", - default=False, - label="Render on Farm" + attrs.append( + attribute_definitions.BoolDef( + "render_farm", + default=False, + label="Render on Farm" + ) ) - ] + return attrs def create(self, subset_name, instance_data, pre_create_data): # ARGS: @@ -319,6 +362,7 @@ class RenderLayerCreator(Creator): # - 'task' - task name # - 'variant' - variant # - 'family' - instnace family + # Check if should use selection or not if pre_create_data.get("use_selection"): items = pipeline.get_selection() @@ -347,44 +391,127 @@ class RenderLayerCreator(Creator): self._add_instance_to_context(new_instance) ``` -## Create context -Controller and wrapper around creation is `CreateContext` which cares about loading `CreatedInstance` - # Publish -OpenPype is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. The main differences are that OpenPype's publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. - ## Exceptions OpenPype define few specific exceptions that should be used in publish plugins. ### Validation exception Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. -Exception `PublishValidationError` 3 arguments: +Exception `PublishValidationError` expects 4 arguments: - **message** Which is not used in UI but for headless publishing. - **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin. - **description** Detailed description of happened issue where markdown and html can be used. +- **detail** Is optional to give even more detailed information for advanced users. At this moment is detail showed under description but it is in plan to have detail in collapsible widget. +Extended version is `PublishXmlValidationError` which uses xml files with stored descriptions. This helps to avoid having huge markdown texts inside code. The exception has 4 arguments: +- **plugin** The plugin object which raises the exception to find it's related xml file. +- **message** Exception message for publishing without UI or different pyblish UI. +- **key** Optional argument says which error from xml is used as validation plugin may raise error with different messages based on the current errors. Default is **"main"**. +- **formatting_data** Optional dictionary to format data in the error. This is used to fill detailed description with data from the publishing so artist can get more precise information. + +**Where and how to create xml file** + +Xml files for `PublishXmlValidationError` must be located in **./help** subfolder next to plugin and the filename must match the filename of plugin. +``` +# File location related to plugin file +└ publish + ├ help + │ ├ validate_scene.xml + │ └ ... + ├ validate_scene.py + └ ... +``` + +Xml file content has **<root>** node which may contain any amount of **<error>** nodes, but each of them must have **id** attribute with unique value. That is then used for **key**. Each error must have **<title>** and **<description>** and **<detail>**. Text content may contain python formatting keys that can be filled when exception is raised. +```xml + + + + Subset context + ## Invalid subset context + +Context of the given subset doesn't match your current scene. + +### How to repair? + +Yout can fix this with "Repair" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata. + +After that restart publishing with Reload button. + + +### How could this happen? + +The subset was created in different scene with different context +or the scene file was copy pasted from different context. + + + +``` ### Known errors When there is a known error that can't be fixed by user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raise. The only difference is that it's message is shown in UI to artist otherwise a neutral message without context is shown. ## Plugin extension -Publish plugins can be extended by additional logic when inherits from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). +Publish plugins can be extended by additional logic when inherits from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of most important usages is to be able turn on/off optional plugins. + +Attributes are defined by return value of `get_attribute_defs` method. Attribute definitions are for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be re-implemented `convert_attribute_values`. Default implementation just converts the values to right types. + +:::important +Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure. +::: + +Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. ```python import pyblish.api -from openpype.pipeline import OpenPypePyblishPluginMixin +from openpype.pipeline import ( + OpenPypePyblishPluginMixin, + attribute_definitions, +) # Example context plugin class MyExtendedPlugin( pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin ): - pass + optional = True + active = True + @classmethod + def get_attribute_defs(cls): + return [ + attribute_definitions.BoolDef( + # Key under which it will be stored + "process", + # Use 'active' as default value + default=cls.active, + # Use plugin label as label for attribute + label=cls.label + ) + ] + + def process_plugin(self, context): + # First check if plugin is optional + if not self.optional: + return True + + # Get 'process' key + process_value = ( + context.data + .get("publish_attributes", {}) + # Attribute values are stored by class names + .get(self.__class__.__name__, {}) + # Access the key + .get("process") + ) + if process_value or process_value is None: + return True + return False + + def process(self, context): + if not self.process_plugin(context): + return + # Do plugin logic + ... ``` - -### Extensions -Currently only extension is ability to define attributes for instances during creation. Method `get_attribute_defs` returns attribute definitions for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be implemented `convert_attribute_values`. Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure. - -Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. From bf08a0241991ac97ad1544bebe1bad5efe82754e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Mar 2022 15:53:31 +0100 Subject: [PATCH 026/180] few minor changes --- website/docs/dev_publishing.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index b16a42bb43..58f271641d 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -11,10 +11,10 @@ Publishing workflow consist of 2 parts: OpenPype is using [pyblish](https://pyblish.com/) for publishing process. OpenPype a little bit extend and modify few functions mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during creation part instead of in publishing part and has limited plugin actions only for failed validation plugins. # Creation -Concept of creation does not have to "create" anything but prepare and store metadata about an "instance". Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach. Storing data to workfile gives ability to keep values so artist does not have to do create instances over and over. +Concept of creation does not have to "create" anything but prepare and store metadata about an "instance" (becomes a subset after publish process). Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach that is the reason why it is creator plugin responsibility. Storing the metadata to workfile gives ability to keep values so artist does not have to do create and set what should be published and how over and over. ## Created instance -Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is dictionary like object with few immutable keys (maked with start `*`) that are defined by creator plugin or create context on initialization. Can have more arbitrary data but keep in mind that some keys are reserved. +Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is dictionary like object with few immutable keys (marked with start `*` in table). The immutable keys are set by creator plugin or create context on initialization and thei values can't change. Instance can have more arbitrary data, for example ids of nodes in scene but keep in mind that some keys are reserved. | Key | Type | Description | |---|---|---| @@ -25,7 +25,7 @@ Objected representation of created instance metadata defined by class **CreatedI | *creator_attributes | dict | Dictionary of attributes that are defined by creator plugin (`get_instance_attr_defs`). | | *publish_attributes | dict | Dictionary of attributes that are defined by publish plugins. | | variant | str | Variant is entered by artist on creation and may affect **subset**. | -| subset | str | Name of instance. This name will be used as subset name during publishing. | +| subset | str | Name of instance. This name will be used as subset name during publishing. Can be changed on context change or variant change. | | active | bool | Is instance active and will be published or not. | | asset | str | Name of asset in which context was created. | | task | str | Name of task in which context was created. Can be set to `None`. | @@ -48,7 +48,7 @@ Host implementation **must** have implemented **get_context_data** and **update_ There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return string showed in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. -Another optional function is **get_current_context**. This function is handy in hosts where is possible to open multiple workfiles in one process so using global context variables are not relevant because artist can switch between opened workfiles without being acknowledged. When function is not implemented or won't return right keys the global +Another optional function is **get_current_context**. This function is handy in hosts where is possible to open multiple workfiles in one process so using global context variables is not relevant because artist can switch between opened workfiles without being acknowledged. When function is not implemented or won't return right keys the global context is used. ```json # Expected keys in output { @@ -59,7 +59,7 @@ Another optional function is **get_current_context**. This function is handy in ``` ## Create plugin -Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. +Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. Create plugins have a lot of responsibility so it is recommended to implement common code per host. ### BaseCreator Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **AutoCreator** and **Creator** variants. From 0b9febaf3e215f45ecfba9c2a053db2cb9053ae4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Mar 2022 16:05:17 +0100 Subject: [PATCH 027/180] changed sidebar label --- website/sidebars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/sidebars.js b/website/sidebars.js index fe76336e7e..105afc30eb 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -139,7 +139,7 @@ module.exports = { "dev_contribute", { type: "category", - label: "Hosts development", + label: "Hosts integrations", items: [ "dev_publishing" ] From 5da139c957c79c5a51b1cff8c1973bf554f72853 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 17 Mar 2022 11:53:57 +0100 Subject: [PATCH 028/180] minor styling tweaks --- website/docs/dev_publishing.md | 74 +++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 58f271641d..49e720a7e0 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -2,18 +2,21 @@ id: dev_publishing title: Publishing sidebar_label: Publishing +toc_max_heading_level: 4 --- Publishing workflow consist of 2 parts: -- Creation - Mark what will be published and how. -- Publishing - Use data from creation to go through pyblish process. +- Creating - Mark what will be published and how. +- Publishing - Use data from Creating to go through pyblish process. -OpenPype is using [pyblish](https://pyblish.com/) for publishing process. OpenPype a little bit extend and modify few functions mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during creation part instead of in publishing part and has limited plugin actions only for failed validation plugins. +OpenPype is using [pyblish](https://pyblish.com/) for publishing process. OpenPype a little bit extend and modify few functions mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during Creating part instead of in publishing part and has limited plugin actions only for failed validation plugins. -# Creation -Concept of creation does not have to "create" anything but prepare and store metadata about an "instance" (becomes a subset after publish process). Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach that is the reason why it is creator plugin responsibility. Storing the metadata to workfile gives ability to keep values so artist does not have to do create and set what should be published and how over and over. +## **Creating** + +Concept of Creating does not have to "create" anything but prepare and store metadata about an "instance" (becomes a subset after publish process). Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach that is the reason why it is creator plugin responsibility. Storing the metadata to workfile gives ability to keep values so artist does not have to do create and set what should be published and how over and over. + +### Created instance -## Created instance Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is dictionary like object with few immutable keys (marked with start `*` in table). The immutable keys are set by creator plugin or create context on initialization and thei values can't change. Instance can have more arbitrary data, for example ids of nodes in scene but keep in mind that some keys are reserved. | Key | Type | Description | @@ -36,15 +39,16 @@ Task should not be required until subset name template expect it. object of **CreatedInstance** has method **data_to_store** which returns dictionary that can be parsed to json string. This method will return all data related to instance so can be re-created using `CreatedInstance.from_existing(data)`. -## Create context -Controller and wrapper around creation is `CreateContext` which cares about loading of plugins needed for creation. And validates required functions in host implementation. +#### *Create context* {#category-doc-link} -Context discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger creation itself. Also keeps in mind instance objects by their ids. +Controller and wrapper around Creating is `CreateContext` which cares about loading of plugins needed for Creating. And validates required functions in host implementation. + +Context discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger Creating itself. Also keeps in mind instance objects by their ids. Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instance but these methods are not meant to be called directly out of creator. The reason is that is creator's responsibility to remove metadata or decide if should remove the instance. -### Required functions in host implementation -Host implementation **must** have implemented **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instane but are needed for creation and publishing process. Right now are there stored data about enabled/disabled optional publish plugins. When data are not stored and loaded properly reset of publishing will cause that they will be set to default value. Similar to instance data can be context data also parsed to json string. +#### Required functions in host implementation +Host implementation **must** have implemented **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now are there stored data about enabled/disabled optional publish plugins. When data are not stored and loaded properly reset of publishing will cause that they will be set to default value. Similar to instance data can be context data also parsed to json string. There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return string showed in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. @@ -58,10 +62,10 @@ Another optional function is **get_current_context**. This function is handy in } ``` -## Create plugin +### Create plugin Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. Create plugins have a lot of responsibility so it is recommended to implement common code per host. -### BaseCreator +#### *BaseCreator* Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **AutoCreator** and **Creator** variants. **Abstractions** @@ -88,7 +92,7 @@ def collect_instances(self): self._add_instance_to_context(instance) ``` -- **`create`** (method) - Create new object of **CreatedInstance** store it's metadata to workfile and add the instance into create context. Failed creation should raise **CreatorError** if happens error that can artist fix or give him some useful information. Trigger and implementation differs for **Creator** and **AutoCreator**. +- **`create`** (method) - Create new object of **CreatedInstance** store it's metadata to workfile and add the instance into create context. Failed Creating should raise **CreatorError** if happens error that can artist fix or give him some useful information. Trigger and implementation differs for **Creator** and **AutoCreator**. - **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**. ```python @@ -195,7 +199,7 @@ class RenderLayerCreator(Creator): - **`get_dynamic_data`** (method) - Can be used to extend data for subset template which may be required in some cases. -### AutoCreator +#### *AutoCreator* Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting of all creators. :::important @@ -268,14 +272,11 @@ def create(self): existing_instance["task"] = task_name ``` -### Creator +#### *Creator* Implementation of creator plugin that is triggered manually by artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments. -**Abstractions** -- **`create`** (method) - Code where creation of metadata - **Optional implementations** -- **`create_allow_context_change`** (class attr) - Allow to set context in UI before creation. Some creator may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default. +- **`create_allow_context_change`** (class attr) - Allow to set context in UI before Creating. Some creator may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default. ```python class BulkRenderCreator(Creator): create_allow_context_change = False @@ -391,11 +392,11 @@ class CreateRender(Creator): self._add_instance_to_context(new_instance) ``` -# Publish -## Exceptions +## **Publish** +### Exceptions OpenPype define few specific exceptions that should be used in publish plugins. -### Validation exception +#### *Validation exception* Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. Exception `PublishValidationError` expects 4 arguments: @@ -449,10 +450,10 @@ or the scene file was copy pasted from different context. ``` -### Known errors +#### *Known errors* When there is a known error that can't be fixed by user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raise. The only difference is that it's message is shown in UI to artist otherwise a neutral message without context is shown. -## Plugin extension +### Plugin extension Publish plugins can be extended by additional logic when inherits from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of most important usages is to be able turn on/off optional plugins. Attributes are defined by return value of `get_attribute_defs` method. Attribute definitions are for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be re-implemented `convert_attribute_values`. Default implementation just converts the values to right types. @@ -463,18 +464,22 @@ Values of publish attributes from created instance are never removed automatical Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. -```python -import pyblish.api -from openpype.pipeline import ( +
+ Toggle me! + + ``` python + + import pyblish.api + from openpype.pipeline import ( OpenPypePyblishPluginMixin, attribute_definitions, -) + ) -# Example context plugin -class MyExtendedPlugin( + # Example context plugin + class MyExtendedPlugin( pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin -): + ): optional = True active = True @@ -514,4 +519,7 @@ class MyExtendedPlugin( return # Do plugin logic ... -``` + ``` +
+ + From 20e2bae01c06d009048c3386ce5145d6194c09a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Mar 2022 17:39:56 +0100 Subject: [PATCH 029/180] flame: wirtetap accepts also path like color policy --- openpype/hosts/flame/api/scripts/wiretap_com.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index ee906c2608..a85a85ae25 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -422,7 +422,7 @@ class WireTapCom(object): color_policy = color_policy or "Legacy" # check if the colour policy in custom dir - if not os.path.exists(color_policy): + if "/" not in color_policy: color_policy = "/syncolor/policies/Autodesk/{}".format( color_policy) From c6735927dcc62b2060bbac7c3d5bd52aee740783 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Mar 2022 17:41:23 +0100 Subject: [PATCH 030/180] flame: fixing head and tail could be string value --- openpype/hosts/flame/api/lib.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 74d9e7607a..aa2cfcb96d 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -18,6 +18,7 @@ log = Logger.get_logger(__name__) FRAME_PATTERN = re.compile(r"[\._](\d+)[\.]") + class CTX: # singleton used for passing data between api modules app_framework = None @@ -538,9 +539,17 @@ def get_segment_attributes(segment): # head and tail with forward compatibility if segment.head: - clip_data["segment_head"] = int(segment.head) + # `infinite` can be also returned + if isinstance(segment.head, str): + clip_data["segment_head"] = 0 + else: + clip_data["segment_head"] = int(segment.head) if segment.tail: - clip_data["segment_tail"] = int(segment.tail) + # `infinite` can be also returned + if isinstance(segment.tail, str): + clip_data["segment_tail"] = 0 + else: + clip_data["segment_tail"] = int(segment.tail) # add all available shot tokens shot_tokens = _get_shot_tokens_values(segment, [ From 7825da773636eeaa96a2ac377db90836c0f32727 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Mar 2022 17:43:04 +0100 Subject: [PATCH 031/180] flame: selected segments collected only once --- .../publish/collect_timeline_instances.py | 173 +++++++++--------- .../plugins/publish/collect_timeline_otio.py | 41 +++-- 2 files changed, 110 insertions(+), 104 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 70340ad7a2..390c55837c 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -34,119 +34,124 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): def process(self, context): project = context.data["flameProject"] sequence = context.data["flameSequence"] + selected_segments = context.data["flameSelectedSegments"] + self.log.debug("__ selected_segments: {}".format(selected_segments)) + self.otio_timeline = context.data["otioTimeline"] self.clips_in_reels = opfapi.get_clips_in_reels(project) self.fps = context.data["fps"] # process all sellected - with opfapi.maintained_segment_selection(sequence) as segments: - for segment in segments: - comment_attributes = self._get_comment_attributes(segment) - self.log.debug("_ comment_attributes: {}".format( - pformat(comment_attributes))) + for segment in selected_segments: + comment_attributes = self._get_comment_attributes(segment) + self.log.debug("__ segment.name: {}".format( + segment.name + )) + self.log.debug("_ comment_attributes: {}".format( + pformat(comment_attributes))) - clip_data = opfapi.get_segment_attributes(segment) - clip_name = clip_data["segment_name"] - self.log.debug("clip_name: {}".format(clip_name)) + clip_data = opfapi.get_segment_attributes(segment) + clip_name = clip_data["segment_name"] + self.log.debug("clip_name: {}".format(clip_name)) - # get openpype tag data - marker_data = opfapi.get_segment_data_marker(segment) - self.log.debug("__ marker_data: {}".format( - pformat(marker_data))) + # get openpype tag data + marker_data = opfapi.get_segment_data_marker(segment) + self.log.debug("__ marker_data: {}".format( + pformat(marker_data))) - if not marker_data: - continue + if not marker_data: + continue - if marker_data.get("id") != "pyblish.avalon.instance": - continue + if marker_data.get("id") != "pyblish.avalon.instance": + continue - # get file path - file_path = clip_data["fpath"] + # get file path + file_path = clip_data["fpath"] - # get source clip - source_clip = self._get_reel_clip(file_path) + # get source clip + source_clip = self._get_reel_clip(file_path) - first_frame = opfapi.get_frame_from_filename(file_path) or 0 + first_frame = opfapi.get_frame_from_filename(file_path) or 0 - head, tail = self._get_head_tail(clip_data, first_frame) + head, tail = self._get_head_tail(clip_data, first_frame) - # solve handles length - marker_data["handleStart"] = min( - marker_data["handleStart"], head) - marker_data["handleEnd"] = min( - marker_data["handleEnd"], tail) + # solve handles length + marker_data["handleStart"] = min( + marker_data["handleStart"], head) + marker_data["handleEnd"] = min( + marker_data["handleEnd"], tail) - with_audio = bool(marker_data.pop("audio")) + with_audio = bool(marker_data.pop("audio")) - # add marker data to instance data - inst_data = dict(marker_data.items()) + # add marker data to instance data + inst_data = dict(marker_data.items()) - asset = marker_data["asset"] - subset = marker_data["subset"] + asset = marker_data["asset"] + subset = marker_data["subset"] - # insert family into families - family = marker_data["family"] - families = [str(f) for f in marker_data["families"]] - families.insert(0, str(family)) + # insert family into families + family = marker_data["family"] + families = [str(f) for f in marker_data["families"]] + families.insert(0, str(family)) - # form label - label = asset - if asset != clip_name: - label += " ({})".format(clip_name) - label += " {}".format(subset) - label += " {}".format("[" + ", ".join(families) + "]") + # form label + label = asset + if asset != clip_name: + label += " ({})".format(clip_name) + label += " {}".format(subset) + label += " {}".format("[" + ", ".join(families) + "]") - inst_data.update({ - "name": "{}_{}".format(asset, subset), - "label": label, - "asset": asset, - "item": segment, - "families": families, - "publish": marker_data["publish"], - "fps": self.fps, - "flameSourceClip": source_clip, - "sourceFirstFrame": int(first_frame), - "path": file_path - }) + inst_data.update({ + "name": "{}_{}".format(asset, subset), + "label": label, + "asset": asset, + "item": segment, + "families": families, + "publish": marker_data["publish"], + "fps": self.fps, + "flameSourceClip": source_clip, + "sourceFirstFrame": int(first_frame), + "path": file_path + }) - # get otio clip data - otio_data = self._get_otio_clip_instance_data(clip_data) or {} - self.log.debug("__ otio_data: {}".format(pformat(otio_data))) + # get otio clip data + otio_data = self._get_otio_clip_instance_data(clip_data) or {} + self.log.debug("__ otio_data: {}".format(pformat(otio_data))) - # add to instance data - inst_data.update(otio_data) - self.log.debug("__ inst_data: {}".format(pformat(inst_data))) + # add to instance data + inst_data.update(otio_data) + self.log.debug("__ inst_data: {}".format(pformat(inst_data))) - # add resolution - self._get_resolution_to_data(inst_data, context) + # add resolution + self._get_resolution_to_data(inst_data, context) - # add comment attributes if any - inst_data.update(comment_attributes) + # add comment attributes if any + inst_data.update(comment_attributes) - # create instance - instance = context.create_instance(**inst_data) + # create instance + instance = context.create_instance(**inst_data) - # add colorspace data - instance.data.update({ - "versionData": { - "colorspace": clip_data["colour_space"], - } - }) + # add colorspace data + instance.data.update({ + "versionData": { + "colorspace": clip_data["colour_space"], + } + }) - # create shot instance for shot attributes create/update - self._create_shot_instance(context, clip_name, **inst_data) + # create shot instance for shot attributes create/update + self._create_shot_instance(context, clip_name, **inst_data) - self.log.info("Creating instance: {}".format(instance)) - self.log.info( - "_ instance.data: {}".format(pformat(instance.data))) + self.log.info("Creating instance: {}".format(instance)) + self.log.info( + "_ instance.data: {}".format(pformat(instance.data))) - if not with_audio: - continue + if not with_audio: + continue - # add audioReview attribute to plate instance data - # if reviewTrack is on - if marker_data.get("reviewTrack") is not None: - instance.data["reviewAudio"] = True + # add audioReview attribute to plate instance data + # if reviewTrack is on + if marker_data.get("reviewTrack") is not None: + instance.data["reviewAudio"] = True def _get_comment_attributes(self, segment): comment = segment.comment.get_value() @@ -188,7 +193,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # get pattern defined by type pattern = TXT_PATERN - if a_type in ("number" , "float"): + if a_type in ("number", "float"): pattern = NUM_PATERN res_goup = pattern.findall(value) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py index faa5be9d68..c6aeae7730 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -31,27 +31,28 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): ) # adding otio timeline to context - with opfapi.maintained_segment_selection(sequence): + with opfapi.maintained_segment_selection(sequence) as selected_seg: otio_timeline = flame_export.create_otio_timeline(sequence) - instance_data = { - "name": subset_name, - "asset": asset_doc["name"], - "subset": subset_name, - "family": "workfile" - } + instance_data = { + "name": subset_name, + "asset": asset_doc["name"], + "subset": subset_name, + "family": "workfile" + } - # create instance with workfile - instance = context.create_instance(**instance_data) - self.log.info("Creating instance: {}".format(instance)) + # create instance with workfile + instance = context.create_instance(**instance_data) + self.log.info("Creating instance: {}".format(instance)) - # update context with main project attributes - context.data.update({ - "flameProject": project, - "flameSequence": sequence, - "otioTimeline": otio_timeline, - "currentFile": "Flame/{}/{}".format( - project.name, sequence.name - ), - "fps": float(str(sequence.frame_rate)[:-4]) - }) + # update context with main project attributes + context.data.update({ + "flameProject": project, + "flameSequence": sequence, + "otioTimeline": otio_timeline, + "currentFile": "Flame/{}/{}".format( + project.name, sequence.name + ), + "flameSelectedSegments": selected_seg, + "fps": float(str(sequence.frame_rate)[:-4]) + }) From cc7a5e0a6fddfb7d0d55dae0f3445251f4d9c5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 22 Mar 2022 15:03:25 +0100 Subject: [PATCH 032/180] node renaming wip --- .../publish/extract_unreal_skeletalmesh.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 6f4c70fc07..58154638e6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Create Unreal Skeletal Mesh data to be extracted as FBX.""" import os +from contextlib import contextmanager from maya import cmds # noqa @@ -12,6 +13,16 @@ from openpype.hosts.maya.api.lib import ( ) from openpype.hosts.maya.api import fbx +@contextmanager +def renamed(original_name, renamed_name): + # type: (str, str) -> None + try: + cmds.rename(original_name, renamed_name) + yield + finally: + cmds.rename(renamed_name, original_name) + yield + class ExtractUnrealSkeletalMesh(openpype.api.Extractor): """Extract Unreal Skeletal Mesh as FBX from Maya. """ @@ -48,13 +59,14 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): instance.data.get("variant", "") ) with maintained_selection(): - with parent_nodes(to_extract, parent=parent): - rooted = [ - "{}|{}".format(parent, i.split("|")[-1]) - for i in to_extract - ] - self.log.info("Un-parenting: {}".format(rooted, path)) - fbx_exporter.export(rooted, path) + with renamed() + with parent_nodes(to_extract, parent=parent): + rooted = [ + "{}|{}".format(parent, i.split("|")[-1]) + for i in to_extract + ] + self.log.info("Un-parenting: {}".format(rooted, path)) + fbx_exporter.export(rooted, path) if "representations" not in instance.data: instance.data["representations"] = [] From 7651ebd8521fd7f2408aaebb262a877ca8840d81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Mar 2022 16:40:11 +0100 Subject: [PATCH 033/180] added option to save as to current context --- openpype/tools/workfiles/files_widget.py | 82 +++++++++++++++++------- 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index d2b8a76952..74729e5346 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -155,18 +155,33 @@ class FilesWidget(QtWidgets.QWidget): # Home Page # Build buttons widget for files widget btns_widget = QtWidgets.QWidget(self) - btn_save = QtWidgets.QPushButton("Save As", btns_widget) - btn_browse = QtWidgets.QPushButton("Browse", btns_widget) - btn_open = QtWidgets.QPushButton("Open", btns_widget) - btn_view_published = QtWidgets.QPushButton("View", btns_widget) + workarea_btns_widget = QtWidgets.QWidget(btns_widget) + btn_save = QtWidgets.QPushButton("Save As", workarea_btns_widget) + btn_browse = QtWidgets.QPushButton("Browse", workarea_btns_widget) + btn_open = QtWidgets.QPushButton("Open", workarea_btns_widget) + + workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget) + workarea_btns_layout.setContentsMargins(0, 0, 0, 0) + workarea_btns_layout.addWidget(btn_open, 1) + workarea_btns_layout.addWidget(btn_browse, 1) + workarea_btns_layout.addWidget(btn_save, 1) + + publish_btns_widget = QtWidgets.QWidget(btns_widget) + btn_view_published = QtWidgets.QPushButton("View", publish_btns_widget) + btn_save_as_published = QtWidgets.QPushButton( + "Save As", publish_btns_widget + ) + + publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget) + publish_btns_layout.setContentsMargins(0, 0, 0, 0) + publish_btns_layout.addWidget(btn_view_published, 1) + publish_btns_layout.addWidget(btn_save_as_published, 1) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addWidget(btn_open, 1) - btns_layout.addWidget(btn_browse, 1) - btns_layout.addWidget(btn_save, 1) - btns_layout.addWidget(btn_view_published, 1) + btns_layout.addWidget(workarea_btns_widget, 1) + btns_layout.addWidget(publish_btns_widget, 1) # Build files widgets for home page main_layout = QtWidgets.QVBoxLayout(self) @@ -189,13 +204,16 @@ class FilesWidget(QtWidgets.QWidget): self.on_file_select ) publish_files_view.doubleClickedLeft.connect( - self._on_view_published_pressed + self._on_published_view_pressed ) btn_open.pressed.connect(self._on_workarea_open_pressed) btn_browse.pressed.connect(self.on_browse_pressed) - btn_save.pressed.connect(self.on_save_as_pressed) - btn_view_published.pressed.connect(self._on_view_published_pressed) + btn_save.pressed.connect(self._on_save_as_pressed) + btn_view_published.pressed.connect(self._on_published_view_pressed) + btn_save_as_published.pressed.connect( + self._on_published_save_as_pressed + ) # Store attributes self._published_checkbox = published_checkbox @@ -211,7 +229,8 @@ class FilesWidget(QtWidgets.QWidget): self._publish_files_model = publish_files_model self._publish_proxy_model = publish_proxy_model - self._btns_widget = btns_widget + self._workarea_btns_widget = workarea_btns_widget + self._publish_btns_widget = publish_btns_widget self._btn_open = btn_open self._btn_browse = btn_browse self._btn_save = btn_save @@ -222,7 +241,7 @@ class FilesWidget(QtWidgets.QWidget): # Hide publish files widgets publish_files_view.setVisible(False) - btn_view_published.setVisible(False) + publish_btns_widget.setVisible(False) @property def published_enabled(self): @@ -232,12 +251,10 @@ class FilesWidget(QtWidgets.QWidget): published_enabled = self.published_enabled self._workarea_files_view.setVisible(not published_enabled) - self._btn_open.setVisible(not published_enabled) - self._btn_browse.setVisible(not published_enabled) - self._btn_save.setVisible(not published_enabled) + self._workarea_btns_widget.setVisible(not published_enabled) self._publish_files_view.setVisible(published_enabled) - self._btn_view_published.setVisible(published_enabled) + self._publish_btns_widget.setVisible(published_enabled) self._update_filtering() self._update_asset_task() @@ -462,11 +479,16 @@ class FilesWidget(QtWidgets.QWidget): if work_file: self.open_file(work_file) - def on_save_as_pressed(self): + def _on_save_as_pressed(self): + self._save_as_with_dialog() + + def _save_as_with_dialog(self): work_filename = self.get_filename() if not work_filename: return + src_path = self._get_selected_filepath() + # Trigger before save event emit_event( "workfile.save.before", @@ -486,13 +508,20 @@ class FilesWidget(QtWidgets.QWidget): log.debug("Initializing Work Directory: %s", self._workfiles_root) os.makedirs(self._workfiles_root) - # Update session if context has changed - self._enter_session() # Prepare full path to workfile and save it filepath = os.path.join( os.path.normpath(self._workfiles_root), work_filename ) - self.host.save_file(filepath) + + # Update session if context has changed + self._enter_session() + + if not self.published_enabled: + self.host.save_file(filepath) + else: + shutil.copy(src_path, filepath) + self.host.open_file(filepath) + # Create extra folders create_workdir_extra_folders( self._workdir_path, @@ -510,9 +539,12 @@ class FilesWidget(QtWidgets.QWidget): self.workfile_created.emit(filepath) # Refresh files model - self.refresh() + if self.published_enabled: + self._published_checkbox.setChecked(False) + else: + self.refresh() - def _on_view_published_pressed(self): + def _on_published_view_pressed(self): filepath = self._get_selected_filepath() if not filepath or not os.path.exists(filepath): return @@ -522,6 +554,10 @@ class FilesWidget(QtWidgets.QWidget): # Change state back to workarea self._published_checkbox.setChecked(False) + def _on_published_save_as_pressed(self): + self._save_as_with_dialog() + + def on_file_select(self): self.file_selected.emit(self._get_selected_filepath()) From bf0bc2436e1f0a085ca30381e2b5285cadf70320 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Mar 2022 16:46:38 +0100 Subject: [PATCH 034/180] added option to save as to context --- openpype/tools/workfiles/files_widget.py | 81 +++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 74729e5346..1faafe2bdb 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -172,11 +172,23 @@ class FilesWidget(QtWidgets.QWidget): btn_save_as_published = QtWidgets.QPushButton( "Save As", publish_btns_widget ) + btn_save_as_to_published = QtWidgets.QPushButton( + "Save As (to context)", publish_btns_widget + ) + btn_select_context_published = QtWidgets.QPushButton( + "Select context", publish_btns_widget + ) + btn_cancel_published = QtWidgets.QPushButton( + "Cancel", publish_btns_widget + ) publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget) publish_btns_layout.setContentsMargins(0, 0, 0, 0) publish_btns_layout.addWidget(btn_view_published, 1) publish_btns_layout.addWidget(btn_save_as_published, 1) + publish_btns_layout.addWidget(btn_save_as_to_published, 1) + publish_btns_layout.addWidget(btn_cancel_published, 1) + publish_btns_layout.addWidget(btn_select_context_published, 1) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) @@ -214,6 +226,15 @@ class FilesWidget(QtWidgets.QWidget): btn_save_as_published.pressed.connect( self._on_published_save_as_pressed ) + btn_save_as_to_published.pressed.connect( + self._on_publish_save_as_to_pressed + ) + btn_select_context_published.pressed.connect( + self._on_publish_select_context_pressed + ) + btn_cancel_published.pressed.connect( + self._on_publish_cancel_pressed + ) # Store attributes self._published_checkbox = published_checkbox @@ -234,7 +255,12 @@ class FilesWidget(QtWidgets.QWidget): self._btn_open = btn_open self._btn_browse = btn_browse self._btn_save = btn_save + self._btn_view_published = btn_view_published + self._btn_save_as_published = btn_save_as_published + self._btn_save_as_to_published = btn_save_as_to_published + self._btn_select_context_published = btn_select_context_published + self._btn_cancel_published = btn_cancel_published # Create a proxy widget for files widget self.setFocusProxy(btn_open) @@ -242,6 +268,10 @@ class FilesWidget(QtWidgets.QWidget): # Hide publish files widgets publish_files_view.setVisible(False) publish_btns_widget.setVisible(False) + btn_select_context_published.setVisible(False) + btn_cancel_published.setVisible(False) + + self._publish_context_select_mode = False @property def published_enabled(self): @@ -285,12 +315,15 @@ class FilesWidget(QtWidgets.QWidget): self._update_asset_task() def _update_asset_task(self): - if self.published_enabled: + if self.published_enabled and not self._publish_context_select_mode: self._publish_files_model.set_context( self._asset_id, self._task_name ) has_valid_items = self._publish_files_model.has_valid_items() self._btn_view_published.setEnabled(has_valid_items) + self._btn_save_as_published.setEnabled(has_valid_items) + self._btn_save_as_to_published.setEnabled(has_valid_items) + else: # Define a custom session so we can query the work root # for a "Work area" that is not our current Session. @@ -308,6 +341,13 @@ class FilesWidget(QtWidgets.QWidget): has_valid_items = self._workarea_files_model.has_valid_items() self._btn_browse.setEnabled(has_valid_items) self._btn_open.setEnabled(has_valid_items) + + if self._publish_context_select_mode: + self._btn_select_context_published.setEnabled( + bool(self._asset_id) and bool(self._task_name) + ) + return + # Manually trigger file selection if not has_valid_items: self.on_file_select() @@ -557,6 +597,45 @@ class FilesWidget(QtWidgets.QWidget): def _on_published_save_as_pressed(self): self._save_as_with_dialog() + def _set_publish_context_select_mode(self, enabled): + self._publish_context_select_mode = enabled + + # Show buttons related to context selection + self._btn_cancel_published.setVisible(enabled) + self._btn_select_context_published.setVisible(enabled) + # Change enabled state based on select context + self._btn_select_context_published.setEnabled( + bool(self._asset_id) and bool(self._task_name) + ) + + self._btn_view_published.setVisible(not enabled) + self._btn_save_as_published.setVisible(not enabled) + self._btn_save_as_to_published.setVisible(not enabled) + + # Change views and disable workarea view if enabled + self._workarea_files_view.setEnabled(not enabled) + if self.published_enabled: + self._workarea_files_view.setVisible(enabled) + self._publish_files_view.setVisible(not enabled) + else: + self._workarea_files_view.setVisible(True) + self._publish_files_view.setVisible(False) + + # Disable filter widgets + self._published_checkbox.setEnabled(not enabled) + self._filter_input.setEnabled(not enabled) + + def _on_publish_save_as_to_pressed(self): + self._set_publish_context_select_mode(True) + + def _on_publish_select_context_pressed(self): + self._save_as_with_dialog() + self._set_publish_context_select_mode(False) + self._update_asset_task() + + def _on_publish_cancel_pressed(self): + self._set_publish_context_select_mode(False) + self._update_asset_task() def on_file_select(self): self.file_selected.emit(self._get_selected_filepath()) From 8f77e92d6f456e06c98326c4d0c55b6cb4f70208 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Mar 2022 16:48:16 +0100 Subject: [PATCH 035/180] rename top node for variants --- .../publish/extract_unreal_skeletalmesh.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 58154638e6..5b0eb5a3bc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -13,6 +13,7 @@ from openpype.hosts.maya.api.lib import ( ) from openpype.hosts.maya.api import fbx + @contextmanager def renamed(original_name, renamed_name): # type: (str, str) -> None @@ -21,7 +22,6 @@ def renamed(original_name, renamed_name): yield finally: cmds.rename(renamed_name, original_name) - yield class ExtractUnrealSkeletalMesh(openpype.api.Extractor): @@ -42,6 +42,8 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): geo = instance.data.get("geometry") joints = instance.data.get("joints") + joints_parent = cmds.listRelatives(joints, p=True) + to_extract = geo + joints # The export requires forward slashes because we need @@ -54,19 +56,30 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): fbx_exporter.set_options_from_instance(instance) + # This magic is done for variants. To let Unreal merge correctly + # existing data, top node must have the same name. So for every + # variant we extract we need to rename top node of the rig correctly. + # It is finally done in context manager so it won't affect current + # scene. parent = "{}{}".format( instance.data["asset"], instance.data.get("variant", "") ) - with maintained_selection(): - with renamed() - with parent_nodes(to_extract, parent=parent): - rooted = [ - "{}|{}".format(parent, i.split("|")[-1]) - for i in to_extract - ] - self.log.info("Un-parenting: {}".format(rooted, path)) - fbx_exporter.export(rooted, path) + + renamed_to_extract = [] + for node in to_extract: + node_path = node.split("|") + node_path[1] = parent + renamed_to_extract.append("|".join(node_path)) + + with renamed(joints_parent, parent): + with parent_nodes(renamed_to_extract, parent=parent): + rooted = [ + "{}|{}".format(parent, i.split("|")[-1]) + for i in renamed_to_extract + ] + self.log.info("Un-parenting: {}".format(rooted, path)) + fbx_exporter.export(rooted, path) if "representations" not in instance.data: instance.data["representations"] = [] From 8956e70665926ee2c66fd3735bcf554d0f9137a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 14:33:53 +0100 Subject: [PATCH 036/180] added discover login into openpype pipeline --- openpype/pipeline/plugin_discover.py | 208 +++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 openpype/pipeline/plugin_discover.py diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py new file mode 100644 index 0000000000..305a8bab67 --- /dev/null +++ b/openpype/pipeline/plugin_discover.py @@ -0,0 +1,208 @@ +import os +import inspect +from openpype.lib.python_module_tools import ( + modules_from_path, + classes_from_module, +) + + +class DiscoverResult: + """Hold result of publish plugins discovery. + + Stores discovered plugins duplicated plugins and file paths which + crashed on execution of file. + """ + + def __init__(self): + self.plugins = [] + self.crashed_file_paths = {} + self.duplicated_plugins = [] + self.abstract_plugins = [] + self.ignored_plugins = set() + + def __iter__(self): + for plugin in self.plugins: + yield plugin + + def __getitem__(self, item): + return self.plugins[item] + + def __setitem__(self, item, value): + self.plugins[item] = value + + +class PluginDiscoverContext(object): + """Store and discover registered types nad registered paths to types. + + Keeps in memory all registered types and their paths. Paths are dynamically + loaded on discover so different discover calls won't return the same + class objects even if were loaded from same file. + """ + + def __init__(self): + self._registered_plugins = {} + self._registered_plugin_paths = {} + self._last_discovered_plugins = {} + + def get_last_discovered_plugins(self, superclass): + return self._last_discovered_plugins.get(superclass) + + def discover( + self, superclass, allow_duplicates=True, ignore_classes=None + ): + """Find and return subclasses of `superclass` + + Args: + superclass (type): Class which determines discovered subclasses. + allow_duplicates (bool): Validate class name duplications. + ignore_classes (list): List of classes that will be ignored + and not added to result. + + Returns: + DiscoverResult: Object holding succesfully discovered plugins, + ignored plugins, plugins with missing abstract implementation + and duplicated plugin. + """ + + if not ignore_classes: + ignore_classes = [] + + result = DiscoverResult() + plugin_names = set() + registered_classes = self._registered_plugins.get(superclass) or [] + registered_paths = self._registered_plugin_paths.get(superclass) or [] + for cls in registered_classes: + if cls is superclass or cls in ignore_classes: + result.ignored_plugins.add(cls) + continue + + if inspect.isabstract(cls): + result.abstract_plugins.append(cls) + continue + + class_name = cls.__name__ + if class_name in plugin_names: + result.duplicated_plugins.append(cls) + continue + plugin_names.add(class_name) + result.plugins.append(cls) + + # Include plug-ins from registered paths + for path in registered_paths: + modules, crashed = modules_from_path(path) + for item in crashed: + filepath, exc_info = item + result.crashed_file_paths[filepath] = exc_info + + for item in modules: + filepath, module = item + for cls in classes_from_module(superclass, module): + if cls is superclass or cls in ignore_classes: + result.ignored_plugins.add(cls) + continue + + if inspect.isabstract(cls): + result.abstract_plugins.append(cls) + continue + + if not allow_duplicates: + class_name = cls.__name__ + if class_name in plugin_names: + result.duplicated_plugins.append(cls) + continue + plugin_names.add(class_name) + + result.plugins.append(cls) + + self._last_discovered_plugins[superclass] = list( + result.plugins + ) + return result + + def register_plugin(self, superclass, cls): + """Register an individual `obj` of type `superclass` + + Arguments: + superclass (type): Superclass of plug-in + cls (object): Subclass of `superclass` + """ + + if superclass not in self._registered_plugins: + self._registered_plugins[superclass] = list() + + if cls not in self._registered_plugins[superclass]: + self._registered_plugins[superclass].append(cls) + + def register_plugin_path(self, superclass, path): + """Register a directory of one or more plug-ins + + Arguments: + superclass (type): Superclass of plug-ins to look for during discovery + path (str): Absolute path to directory in which to discover plug-ins + + """ + + if superclass not in self._registered_plugin_paths: + self._registered_plugin_paths[superclass] = list() + + path = os.path.normpath(path) + if path not in self._registered_plugin_paths[superclass]: + self._registered_plugin_paths[superclass].append(path) + + def registered_plugin_paths(self): + """Return all currently registered plug-in paths""" + # Prohibit editing in-place + duplicate = { + superclass: paths[:] + for superclass, paths in self._registered_plugin_paths.items() + } + return duplicate + + def deregister_plugin(self, superclass, plugin): + """Oppsite of `register_plugin()`""" + if superclass in self._registered_plugins: + self._registered_plugins[superclass].remove(plugin) + + def deregister_plugin_path(self, superclass, path): + """Oppsite of `register_plugin_path()`""" + self._registered_plugin_paths[superclass].remove(path) + + +class GlobalDiscover: + _context = None + + @classmethod + def get_context(cls): + if cls._context is None: + cls._context = PluginDiscoverContext() + return cls._context + + +def discover(superclass, allow_duplicates=True): + context = GlobalDiscover.get_context() + return context.discover(superclass, allow_duplicates) + + +def get_last_discovered_plugins(superclass): + context = GlobalDiscover.get_context() + return context.get_last_discovered_plugins(superclass) + + +def register_plugin(superclass, cls): + context = GlobalDiscover.get_context() + context.register_plugin(superclass, cls) + + +def register_plugin_path(superclass, path): + context = GlobalDiscover.get_context() + context.register_plugin_path(superclass, path) + + +def deregister_plugin(superclass, cls): + context = GlobalDiscover.get_context() + context.deregister_plugin(superclass, cls) + + +def deregister_plugin_path(superclass, path): + context = GlobalDiscover.get_context() + context.deregister_plugin_path(superclass, path) From 239f70a8f76df1536b19a0f07d6c070b0cf74b3d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 14:51:04 +0100 Subject: [PATCH 037/180] added better check of classes and subclasses --- openpype/lib/python_module_tools.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index f62c848e4a..4ef31b5579 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -5,8 +5,9 @@ import importlib import inspect import logging +import six + log = logging.getLogger(__name__) -PY3 = sys.version_info[0] == 3 def import_filepath(filepath, module_name=None): @@ -28,7 +29,7 @@ def import_filepath(filepath, module_name=None): # Prepare module object where content of file will be parsed module = types.ModuleType(module_name) - if PY3: + if six.PY3: # Use loader so module has full specs module_loader = importlib.machinery.SourceFileLoader( module_name, filepath @@ -38,7 +39,7 @@ def import_filepath(filepath, module_name=None): # Execute module code and store content to module with open(filepath) as _stream: # Execute content and store it to module object - exec(_stream.read(), module.__dict__) + six.exec_(_stream.read(), module.__dict__) module.__file__ = filepath return module @@ -129,20 +130,14 @@ def classes_from_module(superclass, module): for name in dir(module): # It could be anything at this point obj = getattr(module, name) - if not inspect.isclass(obj): - continue - - # These are subclassed from nothing, not even `object` - if not len(obj.__bases__) > 0: + if not inspect.isclass(obj) or obj is superclass: continue # Use string comparison rather than `issubclass` # in order to support reloading of this module. - bases = recursive_bases_from_class(obj) - if not any(base.__name__ == superclass.__name__ for base in bases): - continue + if issubclass(obj, superclass): + classes.append(obj) - classes.append(obj) return classes @@ -228,7 +223,7 @@ def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None): dst_module_name(str): Parent module name under which can be loaded module added. """ - if PY3: + if six.PY3: module = _import_module_from_dirpath_py3( dirpath, folder_name, dst_module_name ) From dc9852a0d6fde1c40b33bdb1508156738affae02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 14:53:14 +0100 Subject: [PATCH 038/180] use new register and discover functions for load, thumbnail and actions --- openpype/pipeline/actions.py | 43 ++++++++++++------------------- openpype/pipeline/load/plugins.py | 40 ++++++++++++++-------------- openpype/pipeline/thumbnail.py | 17 ++++++------ 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/openpype/pipeline/actions.py b/openpype/pipeline/actions.py index a045c92aa7..6cb2e9a5a4 100644 --- a/openpype/pipeline/actions.py +++ b/openpype/pipeline/actions.py @@ -1,4 +1,11 @@ import logging +from openpype.pipeline.plugin_discover import ( + discover, + register_plugin, + register_plugin_path, + deregister_plugin, + deregister_plugin_path +) class LauncherAction(object): @@ -90,57 +97,39 @@ class InventoryAction(object): # Launcher action def discover_launcher_actions(): - import avalon.api - - return avalon.api.discover(LauncherAction) + return discover(LauncherAction).plugins def register_launcher_action(plugin): - import avalon.api - - return avalon.api.register_plugin(LauncherAction, plugin) + return register_plugin(LauncherAction, plugin) def register_launcher_action_path(path): - import avalon.api - - return avalon.api.register_plugin_path(LauncherAction, path) + return register_plugin_path(LauncherAction, path) # Inventory action def discover_inventory_actions(): - import avalon.api - - actions = avalon.api.discover(InventoryAction) + actions = discover(InventoryAction).plugins filtered_actions = [] for action in actions: if action is not InventoryAction: - print("DISCOVERED", action) filtered_actions.append(action) - else: - print("GOT SOURCE") + return filtered_actions def register_inventory_action(plugin): - import avalon.api - - return avalon.api.register_plugin(InventoryAction, plugin) + return register_plugin(InventoryAction, plugin) def deregister_inventory_action(plugin): - import avalon.api - - avalon.api.deregister_plugin(InventoryAction, plugin) + deregister_plugin(InventoryAction, plugin) def register_inventory_action_path(path): - import avalon.api - - return avalon.api.register_plugin_path(InventoryAction, path) + return register_plugin_path(InventoryAction, path) def deregister_inventory_action_path(path): - import avalon.api - - return avalon.api.deregister_plugin_path(InventoryAction, path) + return deregister_plugin_path(InventoryAction, path) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index 9b2b6bb084..fb5d1df9b5 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -1,5 +1,13 @@ import logging +from openpype.lib import set_plugin_attributes_from_settings +from openpype.pipeline.plugin_discover import ( + discover, + register_plugin, + register_plugin_path, + deregister_plugin, + deregister_plugin_path +) from .utils import get_representation_path_from_context @@ -102,30 +110,22 @@ class SubsetLoaderPlugin(LoaderPlugin): def discover_loader_plugins(): - import avalon.api - - return avalon.api.discover(LoaderPlugin) + plugins = discover(LoaderPlugin).plugins + set_plugin_attributes_from_settings(plugins, LoaderPlugin) + return plugins def register_loader_plugin(plugin): - import avalon.api - - return avalon.api.register_plugin(LoaderPlugin, plugin) - - -def deregister_loader_plugin_path(path): - import avalon.api - - avalon.api.deregister_plugin_path(LoaderPlugin, path) - - -def register_loader_plugin_path(path): - import avalon.api - - return avalon.api.register_plugin_path(LoaderPlugin, path) + return register_plugin(LoaderPlugin, plugin) def deregister_loader_plugin(plugin): - import avalon.api + deregister_plugin(LoaderPlugin, plugin) - avalon.api.deregister_plugin(LoaderPlugin, plugin) + +def deregister_loader_plugin_path(path): + deregister_plugin_path(LoaderPlugin, path) + + +def register_loader_plugin_path(path): + return register_plugin_path(LoaderPlugin, path) diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 12bab83be6..47452b21e7 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -2,6 +2,11 @@ import os import copy import logging +from .plugin_discover import ( + discover, + register_plugin, + register_plugin_path, +) log = logging.getLogger(__name__) @@ -126,21 +131,15 @@ class BinaryThumbnail(ThumbnailResolver): # Thumbnail resolvers def discover_thumbnail_resolvers(): - import avalon.api - - return avalon.api.discover(ThumbnailResolver) + return discover(ThumbnailResolver).plugins def register_thumbnail_resolver(plugin): - import avalon.api - - return avalon.api.register_plugin(ThumbnailResolver, plugin) + register_plugin(ThumbnailResolver, plugin) def register_thumbnail_resolver_path(path): - import avalon.api - - return avalon.api.register_plugin_path(ThumbnailResolver, path) + register_plugin_path(ThumbnailResolver, path) register_thumbnail_resolver(TemplateResolver) From 645531f4025f7606c90f92f6ce846876e9e05c51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:05:10 +0100 Subject: [PATCH 039/180] creators have their specific functions --- openpype/pipeline/__init__.py | 14 ++++++ openpype/pipeline/create/__init__.py | 16 ++++++- openpype/pipeline/create/creator_plugins.py | 51 ++++++++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index d44fbad33e..eaee180619 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -15,6 +15,13 @@ from .create import ( LegacyCreator, legacy_create, + + discover_creator_plugins, + discover_legacy_creator_plugins, + register_creator_plugin, + deregister_creator_plugin, + register_creator_plugin_path, + deregister_creator_plugin_path, ) from .load import ( @@ -81,6 +88,13 @@ __all__ = ( "LegacyCreator", "legacy_create", + "discover_creator_plugins", + "discover_legacy_creator_plugins", + "register_creator_plugin", + "deregister_creator_plugin", + "register_creator_plugin_path", + "deregister_creator_plugin_path", + # --- Load --- "HeroVersionType", "IncompatibleLoaderError", diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 9571f56b8f..1beeb4267b 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -6,7 +6,14 @@ from .creator_plugins import ( BaseCreator, Creator, - AutoCreator + AutoCreator, + + discover_creator_plugins, + discover_legacy_creator_plugins, + register_creator_plugin, + deregister_creator_plugin, + register_creator_plugin_path, + deregister_creator_plugin_path, ) from .context import ( @@ -29,6 +36,13 @@ __all__ = ( "Creator", "AutoCreator", + "discover_creator_plugins", + "discover_legacy_creator_plugins", + "register_creator_plugin", + "deregister_creator_plugin", + "register_creator_plugin_path", + "deregister_creator_plugin_path", + "CreatedInstance", "CreateContext", diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 1ac2c420a2..dbeeb94050 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -8,7 +8,19 @@ from abc import ( ) import six -from openpype.lib import get_subset_name_with_asset_doc +from openpype.lib import ( + get_subset_name_with_asset_doc, + set_plugin_attributes_from_settings, +) +from openpype.pipeline.plugin_discover import ( + discover, + register_plugin, + register_plugin_path, + deregister_plugin, + deregister_plugin_path +) + +from .legacy_create import LegacyCreator class CreatorError(Exception): @@ -284,6 +296,43 @@ class AutoCreator(BaseCreator): Can be used e.g. for `workfile`. """ + def remove_instances(self, instances): """Skip removement.""" pass + + +def discover_creator_plugins(): + return discover(BaseCreator).plugins + + +def discover_legacy_creator_plugins(): + plugins = discover(LegacyCreator).plugins + set_plugin_attributes_from_settings(plugins, LegacyCreator) + return plugins + + +def register_creator_plugin(plugin): + if issubclass(plugin, BaseCreator): + register_plugin(BaseCreator, plugin) + + elif issubclass(plugin, LegacyCreator): + register_plugin(LegacyCreator, plugin) + + +def deregister_creator_plugin(plugin): + if issubclass(plugin, BaseCreator): + deregister_plugin(BaseCreator, plugin) + + elif issubclass(plugin, LegacyCreator): + deregister_plugin(LegacyCreator, plugin) + + +def register_creator_plugin_path(path): + register_plugin_path(BaseCreator, path) + register_plugin_path(LegacyCreator, path) + + +def deregister_creator_plugin_path(path): + deregister_plugin_path(BaseCreator, path) + deregister_plugin_path(LegacyCreator, path) From 77e2f6eb8d40315dd84cade6a40828e120f8811b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:11:43 +0100 Subject: [PATCH 040/180] use create register/discover functions in code --- openpype/hosts/aftereffects/api/pipeline.py | 8 ++++---- openpype/hosts/blender/api/pipeline.py | 7 ++++--- openpype/hosts/flame/api/pipeline.py | 8 ++++---- openpype/hosts/fusion/api/pipeline.py | 10 +++++----- openpype/hosts/harmony/api/pipeline.py | 7 ++++--- openpype/hosts/hiero/api/pipeline.py | 8 ++++---- openpype/hosts/houdini/api/pipeline.py | 4 ++-- openpype/hosts/maya/api/pipeline.py | 6 ++++-- openpype/hosts/nuke/api/lib.py | 3 ++- openpype/hosts/nuke/api/pipeline.py | 8 ++++---- .../nuke/plugins/publish/validate_write_legacy.py | 5 ++--- openpype/hosts/photoshop/api/pipeline.py | 7 ++++--- openpype/hosts/resolve/api/pipeline.py | 13 ++++++++----- openpype/hosts/tvpaint/api/pipeline.py | 7 ++++--- openpype/hosts/unreal/api/pipeline.py | 7 ++++--- openpype/lib/avalon_context.py | 4 ++-- openpype/pipeline/create/context.py | 5 +++-- openpype/tools/creator/model.py | 5 ++--- 18 files changed, 66 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index bb9affc9b6..94bc369856 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -5,15 +5,15 @@ from Qt import QtWidgets from bson.objectid import ObjectId import pyblish.api -import avalon.api from avalon import io from openpype import lib from openpype.api import Logger from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) import openpype.hosts.aftereffects @@ -73,7 +73,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -86,7 +86,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 8c580cf214..b9ec2cfea4 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -14,9 +14,10 @@ import avalon.api from avalon import io, schema from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) from openpype.api import Logger @@ -54,7 +55,7 @@ def install(): pyblish.api.register_plugin_path(str(PUBLISH_PATH)) register_loader_plugin_path(str(LOAD_PATH)) - avalon.api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) + register_creator_plugin_path(str(CREATE_PATH)) lib.append_user_scripts() @@ -76,7 +77,7 @@ def uninstall(): pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) deregister_loader_plugin_path(str(LOAD_PATH)) - avalon.api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH)) + deregister_creator_plugin_path(str(CREATE_PATH)) if not IS_HEADLESS: ops.unregister() diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index ca3f38c1bc..da44be1b15 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -3,14 +3,14 @@ Basic avalon integration """ import os import contextlib -from avalon import api as avalon from pyblish import api as pyblish from openpype.api import Logger from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) from .lib import ( @@ -37,7 +37,7 @@ def install(): pyblish.register_host("flame") pyblish.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) log.info("OpenPype Flame plug-ins registred ...") # register callback for switching publishable @@ -52,7 +52,7 @@ def uninstall(): log.info("Deregistering Flame plug-ins..") pyblish.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index c9cd76770a..0867b464d5 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -7,14 +7,14 @@ import logging import contextlib import pyblish.api -import avalon.api from openpype.api import Logger from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, - deregister_loader_plugin_path, + register_creator_plugin_path, register_inventory_action_path, + deregister_loader_plugin_path, + deregister_creator_plugin_path, deregister_inventory_action_path, AVALON_CONTAINER_ID, ) @@ -70,7 +70,7 @@ def install(): log.info("Registering Fusion plug-ins..") register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) pyblish.api.register_callback( @@ -94,7 +94,7 @@ def uninstall(): log.info("Deregistering Fusion plug-ins..") deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) deregister_inventory_action_path(INVENTORY_PATH) pyblish.api.deregister_callback( diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 420e9720db..b7d5941182 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -11,9 +11,10 @@ import avalon.api from openpype import lib from openpype.lib import register_event_callback from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) import openpype.hosts.harmony @@ -186,7 +187,7 @@ def install(): pyblish.api.register_host("harmony") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) log.info(PUBLISH_PATH) # Register callbacks. @@ -200,7 +201,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 0d3c8914ce..b334102129 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -5,13 +5,13 @@ import os import contextlib from collections import OrderedDict -from avalon import api as avalon from avalon import schema from pyblish import api as pyblish from openpype.api import Logger from openpype.pipeline import ( - LegacyCreator, + register_creator_plugin_path, register_loader_plugin_path, + deregister_creator_plugin_path, deregister_loader_plugin_path, AVALON_CONTAINER_ID, ) @@ -50,7 +50,7 @@ def install(): pyblish.register_host("hiero") pyblish.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) # register callback for switching publishable pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) @@ -71,7 +71,7 @@ def uninstall(): pyblish.deregister_host("hiero") pyblish.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index d079c9ea81..8e093a89bc 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -11,7 +11,7 @@ import avalon.api from avalon.lib import find_submodule from openpype.pipeline import ( - LegacyCreator, + register_creator_plugin_path, register_loader_plugin_path, AVALON_CONTAINER_ID, ) @@ -54,7 +54,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) log.info("Installing callbacks ... ") # register_event_callback("init", on_init) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index bb61128178..a8834d1ea3 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -23,8 +23,10 @@ from openpype.pipeline import ( LegacyCreator, register_loader_plugin_path, register_inventory_action_path, + register_creator_plugin_path, deregister_loader_plugin_path, deregister_inventory_action_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) from openpype.hosts.maya.lib import copy_workspace_mel @@ -60,7 +62,7 @@ def install(): pyblish.api.register_host("maya") register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) log.info(PUBLISH_PATH) @@ -189,7 +191,7 @@ def uninstall(): pyblish.api.deregister_host("maya") deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) deregister_inventory_action_path(INVENTORY_PATH) menu.uninstall() diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 3c8ba3e77c..c22488f728 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -26,6 +26,7 @@ from openpype.tools.utils import host_tools from openpype.lib.path_tools import HostDirmap from openpype.settings import get_project_settings from openpype.modules import ModulesManager +from openpype.pipeline import discover_legacy_creator_plugins from .workio import ( save_file, @@ -1902,7 +1903,7 @@ def recreate_instance(origin_node, avalon_data=None): # create new node # get appropriate plugin class creator_plugin = None - for Creator in api.discover(api.Creator): + for Creator in discover_legacy_creator_plugins(): if Creator.__name__ == data["creator"]: creator_plugin = Creator break diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 1d110cb94a..6ee3d2ce05 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -5,7 +5,6 @@ from collections import OrderedDict import nuke import pyblish.api -import avalon.api import openpype from openpype.api import ( @@ -15,10 +14,11 @@ from openpype.api import ( ) from openpype.lib import register_event_callback from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, register_inventory_action_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, deregister_inventory_action_path, AVALON_CONTAINER_ID, ) @@ -106,7 +106,7 @@ def install(): log.info("Registering Nuke plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) # Register Avalon event for workfiles loading. @@ -132,7 +132,7 @@ def uninstall(): pyblish.deregister_host("nuke") pyblish.api.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) deregister_inventory_action_path(INVENTORY_PATH) pyblish.api.deregister_callback( diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py index 08f09f8097..9fb57c1698 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_legacy.py @@ -1,11 +1,10 @@ -import os import toml import nuke -from avalon import api import pyblish.api import openpype.api +from openpype.pipeline import discover_creator_plugins from openpype.hosts.nuke.api.lib import get_avalon_knob_data @@ -79,7 +78,7 @@ class ValidateWriteLegacy(pyblish.api.InstancePlugin): # get appropriate plugin class creator_plugin = None - for Creator in api.discover(api.Creator): + for Creator in discover_creator_plugins(): if Creator.__name__ != Create_name: continue diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index c2ad0ac7b0..7fdaa61b40 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -9,9 +9,10 @@ from avalon import io from openpype.api import Logger from openpype.lib import register_event_callback from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) import openpype.hosts.photoshop @@ -75,7 +76,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -88,7 +89,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) def ls(): diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index e8b017ead5..636c826a11 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -4,14 +4,17 @@ Basic avalon integration import os import contextlib from collections import OrderedDict -from avalon import api as avalon -from avalon import schema + from pyblish import api as pyblish + +from avalon import schema + from openpype.api import Logger from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) from . import lib @@ -46,7 +49,7 @@ def install(): log.info("Registering DaVinci Resovle plug-ins..") register_loader_plugin_path(LOAD_PATH) - avalon.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) # register callback for switching publishable pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) @@ -70,7 +73,7 @@ def uninstall(): log.info("Deregistering DaVinci Resovle plug-ins..") deregister_loader_plugin_path(LOAD_PATH) - avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index ec880a1abc..cafdf0701d 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -15,9 +15,10 @@ from openpype.hosts import tvpaint from openpype.api import get_current_project_settings from openpype.lib import register_event_callback from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) @@ -82,7 +83,7 @@ def install(): pyblish.api.register_host("tvpaint") pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) registered_callbacks = ( pyblish.api.registered_callbacks().get("instanceToggled") or [] @@ -104,7 +105,7 @@ def uninstall(): pyblish.api.deregister_host("tvpaint") pyblish.api.deregister_plugin_path(PUBLISH_PATH) deregister_loader_plugin_path(LOAD_PATH) - avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) + deregister_creator_plugin_path(CREATE_PATH) def containerise( diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 713c588976..6d7a6ad1e2 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -7,9 +7,10 @@ import pyblish.api from avalon import api from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, + register_creator_plugin_path, deregister_loader_plugin_path, + deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) from openpype.tools.utils import host_tools @@ -49,7 +50,7 @@ def install(): logger.info("installing OpenPype for Unreal") pyblish.api.register_plugin_path(str(PUBLISH_PATH)) register_loader_plugin_path(str(LOAD_PATH)) - api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) + register_creator_plugin_path(str(CREATE_PATH)) _register_callbacks() _register_events() @@ -58,7 +59,7 @@ def uninstall(): """Uninstall Unreal configuration for Avalon.""" pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) deregister_loader_plugin_path(str(LOAD_PATH)) - api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH)) + deregister_creator_plugin_path(str(CREATE_PATH)) def _register_callbacks(): diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 05d2ffd821..26e05ecd63 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1604,13 +1604,13 @@ def get_creator_by_name(creator_name, case_sensitive=False): Returns: Creator: Return first matching plugin or `None`. """ - from openpype.pipeline import LegacyCreator + from openpype.pipeline import discover_legacy_creator_plugins # Lower input creator name if is not case sensitive if not case_sensitive: creator_name = creator_name.lower() - for creator_plugin in avalon.api.discover(LegacyCreator): + for creator_plugin in discover_legacy_creator_plugins(): _creator_name = creator_plugin.__name__ # Lower creator plugin name if is not case sensitive diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c2757a4502..21e726e060 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -10,7 +10,8 @@ from ..lib import UnknownDef from .creator_plugins import ( BaseCreator, Creator, - AutoCreator + AutoCreator, + discover_creator_plugins, ) from openpype.api import ( @@ -842,7 +843,7 @@ class CreateContext: creators = {} autocreators = {} manual_creators = {} - for creator_class in avalon.api.discover(BaseCreator): + for creator_class in discover_creator_plugins(BaseCreator): if inspect.isabstract(creator_class): self.log.info( "Skipping abstract Creator {}".format(str(creator_class)) diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py index ef61c6e0f0..d3d60b96f2 100644 --- a/openpype/tools/creator/model.py +++ b/openpype/tools/creator/model.py @@ -1,8 +1,7 @@ import uuid from Qt import QtGui, QtCore -from avalon import api -from openpype.pipeline import LegacyCreator +from openpype.pipeline import discover_legacy_creator_plugins from . constants import ( FAMILY_ROLE, @@ -22,7 +21,7 @@ class CreatorsModel(QtGui.QStandardItemModel): self._creators_by_id = {} items = [] - creators = api.discover(LegacyCreator) + creators = discover_legacy_creator_plugins() for creator in creators: item_id = str(uuid.uuid4()) self._creators_by_id[item_id] = creator From 1612ad0f96d7a9778df9130dece9a29dae6fae8e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:12:18 +0100 Subject: [PATCH 041/180] call 'ls' directly in harmony --- openpype/hosts/harmony/api/pipeline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index b7d5941182..88f11dd16f 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -6,7 +6,6 @@ from bson.objectid import ObjectId import pyblish.api from avalon import io -import avalon.api from openpype import lib from openpype.lib import register_event_callback @@ -109,9 +108,8 @@ def check_inventory(): if not lib.any_outdated(): return - host = avalon.api.registered_host() outdated_containers = [] - for container in host.ls(): + for container in ls(): representation = container['representation'] representation_doc = io.find_one( { From b767458bff9573e221303ce6c9d0e7d4137596e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:12:42 +0100 Subject: [PATCH 042/180] use direct imports of LegacyCreator --- .../hosts/aftereffects/plugins/create/create_render.py | 8 +++++--- openpype/hosts/fusion/plugins/create/create_exr_saver.py | 4 ++-- openpype/hosts/photoshop/plugins/create/create_image.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 41efb4b0ba..831085a5f1 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,12 +1,14 @@ -from openpype.pipeline import create -from openpype.pipeline import CreatorError +from openpype.pipeline import ( + CreatorError, + LegacyCreator +) from openpype.hosts.aftereffects.api import ( get_stub, list_instances ) -class CreateRender(create.LegacyCreator): +class CreateRender(LegacyCreator): """Render folder for publish. Creates subsets in format 'familyTaskSubsetname', diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index ff8bdb21ef..8bab5ee9b1 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,13 +1,13 @@ import os -from openpype.pipeline import create +from openpype.pipeline import LegacyCreator from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk ) -class CreateOpenEXRSaver(create.LegacyCreator): +class CreateOpenEXRSaver(LegacyCreator): name = "openexrDefault" label = "Create OpenEXR Saver" diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index a001b5f171..5078cbb587 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,9 +1,9 @@ from Qt import QtWidgets -from openpype.pipeline import create +from openpype.pipeline import LegacyCreator from openpype.hosts.photoshop import api as photoshop -class CreateImage(create.LegacyCreator): +class CreateImage(LegacyCreator): """Image folder for publish.""" name = "imageDefault" From ea79f0908b9070cc8a9301183a8a0432b272a508 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:14:22 +0100 Subject: [PATCH 043/180] fix test --- openpype/tests/test_avalon_plugin_presets.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/tests/test_avalon_plugin_presets.py b/openpype/tests/test_avalon_plugin_presets.py index f1b1a94713..c491be1c05 100644 --- a/openpype/tests/test_avalon_plugin_presets.py +++ b/openpype/tests/test_avalon_plugin_presets.py @@ -1,6 +1,10 @@ import avalon.api as api import openpype -from openpype.pipeline import LegacyCreator +from openpype.pipeline import ( + LegacyCreator, + register_creator_plugin, + discover_creator_plugins, +) class MyTestCreator(LegacyCreator): @@ -27,8 +31,8 @@ def test_avalon_plugin_presets(monkeypatch, printer): openpype.install() api.register_host(Test()) - api.register_plugin(LegacyCreator, MyTestCreator) - plugins = api.discover(LegacyCreator) + register_creator_plugin(MyTestCreator) + plugins = discover_creator_plugins() printer("Test if we got our test plugin") assert MyTestCreator in plugins for p in plugins: From aac580bda4d77b4862ca99e61366955e73d8bf3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:15:00 +0100 Subject: [PATCH 044/180] removed patched discover logic --- openpype/__init__.py | 75 +++++++------------------------------------- 1 file changed, 11 insertions(+), 64 deletions(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 8b94b2dc3f..2820091bcc 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -2,20 +2,16 @@ """Pype module.""" import os import platform -import functools import logging from .settings import get_project_settings from .lib import ( Anatomy, filter_pyblish_plugins, - set_plugin_attributes_from_settings, change_timer_to_current_context, register_event_callback, ) -pyblish = avalon = _original_discover = None - log = logging.getLogger(__name__) @@ -27,60 +23,17 @@ PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -def import_wrapper(func): - """Wrap module imports to specific functions.""" - @functools.wraps(func) - def decorated(*args, **kwargs): - global pyblish - global avalon - global _original_discover - if pyblish is None: - from pyblish import api as pyblish - from avalon import api as avalon - - # we are monkey patching `avalon.api.discover()` to allow us to - # load plugin presets on plugins being discovered by avalon. - # Little bit of hacking, but it allows us to add out own features - # without need to modify upstream code. - - _original_discover = avalon.discover - - return func(*args, **kwargs) - - return decorated - - -@import_wrapper -def patched_discover(superclass): - """Patch `avalon.api.discover()`. - - Monkey patched version of :func:`avalon.api.discover()`. It allows - us to load presets on plugins being discovered. - """ - # run original discover and get plugins - plugins = _original_discover(superclass) - filtered_plugins = [ - plugin - for plugin in plugins - if issubclass(plugin, superclass) - ] - - set_plugin_attributes_from_settings(filtered_plugins, superclass) - - return filtered_plugins - - -@import_wrapper def install(): """Install Pype to Avalon.""" + import avalon.api + import pyblish.api from pyblish.lib import MessageHandler from openpype.modules import load_modules from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, register_inventory_action, + register_creator_plugin_path, ) - from avalon import pipeline # Make sure modules are loaded load_modules() @@ -93,8 +46,8 @@ def install(): MessageHandler.emit = modified_emit log.info("Registering global plug-ins..") - pyblish.register_plugin_path(PUBLISH_PATH) - pyblish.register_discovery_filter(filter_pyblish_plugins) + pyblish.api.register_plugin_path(PUBLISH_PATH) + pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) project_name = os.environ.get("AVALON_PROJECT") @@ -103,7 +56,7 @@ def install(): if project_name: anatomy = Anatomy(project_name) anatomy.set_root_environments() - avalon.register_root(anatomy.roots) + avalon.api.register_root(anatomy.roots) project_settings = get_project_settings(project_name) platform_name = platform.system().lower() @@ -122,17 +75,14 @@ def install(): if not path or not os.path.exists(path): continue - pyblish.register_plugin_path(path) + pyblish.api.register_plugin_path(path) register_loader_plugin_path(path) - avalon.register_plugin_path(LegacyCreator, path) + register_creator_plugin_path(path) register_inventory_action(path) # apply monkey patched discover to original one log.info("Patching discovery") - avalon.discover = patched_discover - pipeline.discover = patched_discover - register_event_callback("taskChanged", _on_task_change) @@ -140,16 +90,13 @@ def _on_task_change(): change_timer_to_current_context() -@import_wrapper def uninstall(): """Uninstall Pype from Avalon.""" + import pyblish.api from openpype.pipeline import deregister_loader_plugin_path log.info("Deregistering global plug-ins..") - pyblish.deregister_plugin_path(PUBLISH_PATH) - pyblish.deregister_discovery_filter(filter_pyblish_plugins) + pyblish.api.deregister_plugin_path(PUBLISH_PATH) + pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) deregister_loader_plugin_path(LOAD_PATH) log.info("Global plug-ins unregistred") - - # restore original discover - avalon.discover = _original_discover From cd4e01f400344a0351246f70a7ef09b9e0b1cbba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 15:54:43 +0100 Subject: [PATCH 045/180] print report of failed parts of discover --- openpype/pipeline/plugin_discover.py | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py index 305a8bab67..f4a133b0aa 100644 --- a/openpype/pipeline/plugin_discover.py +++ b/openpype/pipeline/plugin_discover.py @@ -1,5 +1,7 @@ import os import inspect +import traceback + from openpype.lib.python_module_tools import ( modules_from_path, classes_from_module, @@ -30,6 +32,59 @@ class DiscoverResult: def __setitem__(self, item, value): self.plugins[item] = value + def get_report(self, only_errors=True, exc_info=True, full_report=False): + lines = [] + if not only_errors: + # Successfully discovered plugins + if self.plugins or full_report: + lines.append( + "*** Discovered {} plugins".format(len(self.plugins)) + ) + for cls in self.plugins: + lines.append("- {}".format(cls.__class__.__name__)) + + # Plugin that were defined to be ignored + if self.ignored_plugins or full_report: + lines.append("*** Ignored plugins {}".format(len( + self.ignored_plugins + ))) + for cls in self.ignored_plugins: + lines.append("- {}".format(cls.__class__.__name__)) + + # Abstract classes + if self.abstract_plugins or full_report: + lines.append("*** Discovered {} abstract plugins".format(len( + self.abstract_plugins + ))) + for cls in self.abstract_plugins: + lines.append("- {}".format(cls.__class__.__name__)) + + # Abstract classes + if self.duplicated_plugins or full_report: + lines.append("*** There were {} duplicated plugins".format(len( + self.duplicated_plugins + ))) + for cls in self.duplicated_plugins: + lines.append("- {}".format(cls.__class__.__name__)) + + if self.crashed_file_paths or full_report: + lines.append("*** Failed to load {} files".format(len( + self.crashed_file_paths + ))) + for path, exc_info_args in self.crashed_file_paths.items(): + lines.append("- {}".format(path)) + if exc_info: + lines.append(10 * "*") + lines.extend(traceback.format_exception(*exc_info_args)) + lines.append(10 * "*") + + return "\n".join(lines) + + def print_report(self, only_errors=True, exc_info=True): + report = self.get_report(only_errors, exc_info) + if report: + print(report) + class PluginDiscoverContext(object): """Store and discover registered types nad registered paths to types. @@ -117,6 +172,7 @@ class PluginDiscoverContext(object): self._last_discovered_plugins[superclass] = list( result.plugins ) + result.print_report() return result def register_plugin(self, superclass, cls): From 6bb10852f5aaee9a85a66c83d5a66aeab439d6d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Mar 2022 18:18:59 +0100 Subject: [PATCH 046/180] Fix creator discover Co-authored-by: Roy Nieterau --- openpype/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 21e726e060..d833e6f686 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -843,7 +843,7 @@ class CreateContext: creators = {} autocreators = {} manual_creators = {} - for creator_class in discover_creator_plugins(BaseCreator): + for creator_class in discover_creator_plugins(): if inspect.isabstract(creator_class): self.log.info( "Skipping abstract Creator {}".format(str(creator_class)) From e3196a40236e4eb1540019333fc30c3a2b5b0cc7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Mar 2022 18:30:28 +0100 Subject: [PATCH 047/180] Fix docstring Co-authored-by: Roy Nieterau --- openpype/pipeline/plugin_discover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py index f4a133b0aa..a657df7994 100644 --- a/openpype/pipeline/plugin_discover.py +++ b/openpype/pipeline/plugin_discover.py @@ -190,7 +190,7 @@ class PluginDiscoverContext(object): self._registered_plugins[superclass].append(cls) def register_plugin_path(self, superclass, path): - """Register a directory of one or more plug-ins + """Register a directory containing plug-ins of type `superclass` Arguments: superclass (type): Superclass of plug-ins to look for during discovery From 89d61723de876c02b061198e9462feb7f52d98ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Mar 2022 19:20:37 +0100 Subject: [PATCH 048/180] applied comments --- openpype/lib/python_module_tools.py | 2 - openpype/pipeline/actions.py | 4 +- openpype/pipeline/create/creator_plugins.py | 4 +- openpype/pipeline/load/plugins.py | 2 +- openpype/pipeline/plugin_discover.py | 88 ++++++++++++++------- 5 files changed, 66 insertions(+), 34 deletions(-) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 4ef31b5579..6fad3b547f 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -133,8 +133,6 @@ def classes_from_module(superclass, module): if not inspect.isclass(obj) or obj is superclass: continue - # Use string comparison rather than `issubclass` - # in order to support reloading of this module. if issubclass(obj, superclass): classes.append(obj) diff --git a/openpype/pipeline/actions.py b/openpype/pipeline/actions.py index 6cb2e9a5a4..b488fe3e1f 100644 --- a/openpype/pipeline/actions.py +++ b/openpype/pipeline/actions.py @@ -97,7 +97,7 @@ class InventoryAction(object): # Launcher action def discover_launcher_actions(): - return discover(LauncherAction).plugins + return discover(LauncherAction) def register_launcher_action(plugin): @@ -110,7 +110,7 @@ def register_launcher_action_path(path): # Inventory action def discover_inventory_actions(): - actions = discover(InventoryAction).plugins + actions = discover(InventoryAction) filtered_actions = [] for action in actions: if action is not InventoryAction: diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index dbeeb94050..c3ba8b1d1c 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -303,11 +303,11 @@ class AutoCreator(BaseCreator): def discover_creator_plugins(): - return discover(BaseCreator).plugins + return discover(BaseCreator) def discover_legacy_creator_plugins(): - plugins = discover(LegacyCreator).plugins + plugins = discover(LegacyCreator) set_plugin_attributes_from_settings(plugins, LegacyCreator) return plugins diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index fb5d1df9b5..d60aed0083 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -110,7 +110,7 @@ class SubsetLoaderPlugin(LoaderPlugin): def discover_loader_plugins(): - plugins = discover(LoaderPlugin).plugins + plugins = discover(LoaderPlugin) set_plugin_attributes_from_settings(plugins, LoaderPlugin) return plugins diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py index a657df7994..b5edda7e9d 100644 --- a/openpype/pipeline/plugin_discover.py +++ b/openpype/pipeline/plugin_discover.py @@ -2,25 +2,31 @@ import os import inspect import traceback +from openpype.lib import Logger from openpype.lib.python_module_tools import ( modules_from_path, classes_from_module, ) +log = Logger.get_logger(__name__) + class DiscoverResult: - """Hold result of publish plugins discovery. + """Result of Plug-ins discovery of a single superclass type. - Stores discovered plugins duplicated plugins and file paths which - crashed on execution of file. + Stores discovered, duplicated, ignored and abstract plugins and file paths + which crashed on execution of file. """ - def __init__(self): + def __init__(self, superclass): + self.superclass = superclass self.plugins = [] self.crashed_file_paths = {} self.duplicated_plugins = [] self.abstract_plugins = [] self.ignored_plugins = set() + # Store loaded modules to keep them in memory + self._modules = set() def __iter__(self): for plugin in self.plugins: @@ -32,6 +38,10 @@ class DiscoverResult: def __setitem__(self, item, value): self.plugins[item] = value + def add_module(self, module): + """Add dynamically loaded python module to keep it in memory.""" + self._modules.add(module) + def get_report(self, only_errors=True, exc_info=True, full_report=False): lines = [] if not only_errors: @@ -80,10 +90,10 @@ class DiscoverResult: return "\n".join(lines) - def print_report(self, only_errors=True, exc_info=True): + def log_report(self, only_errors=True, exc_info=True): report = self.get_report(only_errors, exc_info) if report: - print(report) + log.info(report) class PluginDiscoverContext(object): @@ -98,12 +108,25 @@ class PluginDiscoverContext(object): self._registered_plugins = {} self._registered_plugin_paths = {} self._last_discovered_plugins = {} + # Store the last result to memory + self._last_discovered_results = {} def get_last_discovered_plugins(self, superclass): + """Access last discovered plugin by a subperclass. + + Returns: + None: When superclass was not discovered yet. + list: Lastly discovered plugins of the superclass. + """ + return self._last_discovered_plugins.get(superclass) def discover( - self, superclass, allow_duplicates=True, ignore_classes=None + self, + superclass, + allow_duplicates=True, + ignore_classes=None, + return_report=False ): """Find and return subclasses of `superclass` @@ -122,7 +145,7 @@ class PluginDiscoverContext(object): if not ignore_classes: ignore_classes = [] - result = DiscoverResult() + result = DiscoverResult(superclass) plugin_names = set() registered_classes = self._registered_plugins.get(superclass) or [] registered_paths = self._registered_plugin_paths.get(superclass) or [] @@ -151,6 +174,7 @@ class PluginDiscoverContext(object): for item in modules: filepath, module = item + result.add_module(module) for cls in classes_from_module(superclass, module): if cls is superclass or cls in ignore_classes: result.ignored_plugins.add(cls) @@ -169,14 +193,18 @@ class PluginDiscoverContext(object): result.plugins.append(cls) + # Store in memory last result to keep in memory loaded modules + self._last_discovered_results[superclass] = result self._last_discovered_plugins[superclass] = list( result.plugins ) - result.print_report() - return result + result.log_report() + if return_report: + return result + return result.plugins def register_plugin(self, superclass, cls): - """Register an individual `obj` of type `superclass` + """Register a directory containing plug-ins of type `superclass` Arguments: superclass (type): Superclass of plug-in @@ -190,12 +218,13 @@ class PluginDiscoverContext(object): self._registered_plugins[superclass].append(cls) def register_plugin_path(self, superclass, path): - """Register a directory containing plug-ins of type `superclass` + """Register a directory of one or more plug-ins Arguments: - superclass (type): Superclass of plug-ins to look for during discovery - path (str): Absolute path to directory in which to discover plug-ins - + superclass (type): Superclass of plug-ins to look for during + discovery + path (str): Absolute path to directory in which to discover + plug-ins """ if superclass not in self._registered_plugin_paths: @@ -207,24 +236,29 @@ class PluginDiscoverContext(object): def registered_plugin_paths(self): """Return all currently registered plug-in paths""" - # Prohibit editing in-place - duplicate = { + # Return shallow copy so we the original data can't be changed + return { superclass: paths[:] for superclass, paths in self._registered_plugin_paths.items() } - return duplicate def deregister_plugin(self, superclass, plugin): - """Oppsite of `register_plugin()`""" + """Opposite of `register_plugin()`""" if superclass in self._registered_plugins: self._registered_plugins[superclass].remove(plugin) def deregister_plugin_path(self, superclass, path): - """Oppsite of `register_plugin_path()`""" + """Opposite of `register_plugin_path()`""" self._registered_plugin_paths[superclass].remove(path) -class GlobalDiscover: +class _GlobalDiscover: + """Access to global object of PluginDiscoverContext. + + Using singleton object to register/deregister plugins and plugin paths + and then discover them by superclass. + """ + _context = None @classmethod @@ -235,30 +269,30 @@ class GlobalDiscover: def discover(superclass, allow_duplicates=True): - context = GlobalDiscover.get_context() + context = _GlobalDiscover.get_context() return context.discover(superclass, allow_duplicates) def get_last_discovered_plugins(superclass): - context = GlobalDiscover.get_context() + context = _GlobalDiscover.get_context() return context.get_last_discovered_plugins(superclass) def register_plugin(superclass, cls): - context = GlobalDiscover.get_context() + context = _GlobalDiscover.get_context() context.register_plugin(superclass, cls) def register_plugin_path(superclass, path): - context = GlobalDiscover.get_context() + context = _GlobalDiscover.get_context() context.register_plugin_path(superclass, path) def deregister_plugin(superclass, cls): - context = GlobalDiscover.get_context() + context = _GlobalDiscover.get_context() context.deregister_plugin(superclass, cls) def deregister_plugin_path(superclass, path): - context = GlobalDiscover.get_context() + context = _GlobalDiscover.get_context() context.deregister_plugin_path(superclass, path) From 2621225046ac8b60942834b03fd6c68655d48c31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 10:18:31 +0100 Subject: [PATCH 049/180] fix logger import --- openpype/pipeline/plugin_discover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/plugin_discover.py b/openpype/pipeline/plugin_discover.py index b5edda7e9d..fb860fe5f2 100644 --- a/openpype/pipeline/plugin_discover.py +++ b/openpype/pipeline/plugin_discover.py @@ -2,7 +2,7 @@ import os import inspect import traceback -from openpype.lib import Logger +from openpype.api import Logger from openpype.lib.python_module_tools import ( modules_from_path, classes_from_module, From be652995e8d4778f4c4997a6792e4510511a3c89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 11:21:41 +0100 Subject: [PATCH 050/180] added extractor converting pngs to exr from tvpaint --- .../plugins/publish/extract_convert_to_exr.py | 96 +++++++++++++++++++ .../plugins/publish/extract_sequence.py | 1 - 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py new file mode 100644 index 0000000000..5fc9be9f42 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py @@ -0,0 +1,96 @@ +"""Plugin converting png files from ExtractSequence into exrs. + +Requires: + ExtractSequence - source of PNG + ExtractReview - review was already created so we can convert to any exr +""" +import os +import json + +import pyblish.api +from openpype.lib import ( + get_oiio_tools_path, + run_subprocess, +) +from openpype.pipeline import KnownPublishError + + +class ExtractConvertToEXR(pyblish.api.InstancePlugin): + # Offset to get after ExtractSequence plugin. + order = pyblish.api.ExtractorOrder + 0.1 + label = "Extract Sequence EXR" + hosts = ["tvpaint"] + families = ["render"] + + active = False + + replace_pngs = True + exr_compression = "DWAA" + + def process(self, instance): + repres = instance.data.get("representations") + if not repres: + return + + oiio_path = get_oiio_tools_path() + # Raise an exception when oiiotool is not available + # - this can currently happen on MacOS machines + if not os.path.exists(oiio_path): + KnownPublishError( + "OpenImageIO tool is not available on this machine." + ) + + new_repres = [] + for repre in repres: + if repre["name"] != "png": + continue + + self.log.info( + "Processing representation: {}".format( + json.dumps(repre, sort_keys=True, indent=4) + ) + ) + + src_filepaths = set() + new_filenames = [] + for src_filename in repre["files"]: + dst_filename = os.path.splitext(src_filename)[0] + ".exr" + new_filenames.append(dst_filename) + + src_filepath = os.path.join(repre["stagingDir"], src_filename) + dst_filepath = os.path.join(repre["stagingDir"], dst_filename) + + src_filepaths.add(src_filepath) + + args = [ + oiio_path, src_filepath, + "--compression", self.exr_compression, + # TODO how to define color conversion? + "--colorconvert", "sRGB", "linear", + "-o", dst_filepath + ] + run_subprocess(args) + + new_repres.append( + { + "name": "exr", + "ext": "exr", + "files": new_filenames, + "stagingDir": repre["stagingDir"], + "tags": list(repre["tags"]) + } + ) + + if self.replace_pngs: + instance.data["representations"].remove(repre) + for filepath in src_filepaths: + os.remove(filepath) + + instance.data["representations"].extend(new_repres) + self.log.info( + "Representations: {}".format( + json.dumps( + instance.data["representations"], sort_keys=True, indent=4 + ) + ) + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 729c545545..139dabadee 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -12,7 +12,6 @@ from openpype.hosts.tvpaint.lib import ( fill_reference_frames, composite_rendered_layers, rename_filepaths_by_frame_start, - composite_images ) From 41a66e86775637a9215e6887a24627755d508a4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 11:22:14 +0100 Subject: [PATCH 051/180] added settings for new plugin --- .../defaults/project_settings/tvpaint.json | 5 +++ .../schema_project_tvpaint.json | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 528bf6de8e..46beeb85b9 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -28,6 +28,11 @@ "enabled": true, "optional": true, "active": true + }, + "ExtractConvertToEXR": { + "enabled": false, + "replace_pngs": true, + "exr_compression": "ZIP" } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 8286ed1193..97462a8b62 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -78,6 +78,47 @@ "docstring": "Validate if shot on instances metadata is same as workfiles shot" } ] + }, + { + "type": "dict", + "key": "ExtractConvertToEXR", + "label": "Extract Convert To EXR", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "WARNING: This plugin does not work on MacOS (using OIIO tool)." + }, + { + "type": "boolean", + "key": "replace_pngs", + "label": "Replace source PNG" + }, + { + "type": "enum", + "key": "exr_compression", + "label": "EXR Compression", + "multiselection": false, + "enum_items": [ + {"ZIP": "ZIP"}, + {"ZIPS": "ZIPS"}, + {"DWAA": "DWAA"}, + {"DWAB": "DWAB"}, + {"PIZ": "PIZ"}, + {"RLE": "RLE"}, + {"PXR24": "PXR24"}, + {"B44": "B44"}, + {"B44A": "B44A"}, + {"none": "None"} + ] + } + ] } ] }, From c68a93b467135fa02f48e41db10b3464ed9379bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 12:10:43 +0100 Subject: [PATCH 052/180] added plugin which prepare keys for ExplicitCleanUp plugin --- .../plugins/publish/collect_cleanup_keys.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 openpype/plugins/publish/collect_cleanup_keys.py diff --git a/openpype/plugins/publish/collect_cleanup_keys.py b/openpype/plugins/publish/collect_cleanup_keys.py new file mode 100644 index 0000000000..635b038387 --- /dev/null +++ b/openpype/plugins/publish/collect_cleanup_keys.py @@ -0,0 +1,21 @@ +""" +Requires: + None +Provides: + context + - cleanupFullPaths (list) + - cleanupEmptyDirs (list) +""" + +import pyblish.api + + +class CollectCleanupKeys(pyblish.api.ContextPlugin): + """Prepare keys for 'ExplicitCleanUp' plugin.""" + + label = "Collect Cleanup Keys" + order = pyblish.api.CollectorOrder + + def process(self, context): + context.data["cleanupFullPaths"] = [] + context.data["cleanupEmptyDirs"] = [] From 62d7bcc0d027f26fc62620a0f670ec32f5fbd399 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 12:11:01 +0100 Subject: [PATCH 053/180] changed default attributes --- .../hosts/tvpaint/plugins/publish/extract_convert_to_exr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py index 5fc9be9f42..522173803b 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py @@ -22,10 +22,12 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["render"] - active = False + enabled = False + # Replace source PNG files or just add replace_pngs = True - exr_compression = "DWAA" + # EXR compression + exr_compression = "ZIP" def process(self, instance): repres = instance.data.get("representations") From 54b0e2ec9cf0e37228c1ece66f4728e2cb53ac79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 12:11:19 +0100 Subject: [PATCH 054/180] don't remove the files but add to cleanup data --- .../hosts/tvpaint/plugins/publish/extract_convert_to_exr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py index 522173803b..ab5bbc5e2c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_convert_to_exr.py @@ -85,8 +85,9 @@ class ExtractConvertToEXR(pyblish.api.InstancePlugin): if self.replace_pngs: instance.data["representations"].remove(repre) + for filepath in src_filepaths: - os.remove(filepath) + instance.context.data["cleanupFullPaths"].append(filepath) instance.data["representations"].extend(new_repres) self.log.info( From 7d3053c6c358539795626db69f8a463283356fbc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 14:22:11 +0100 Subject: [PATCH 055/180] removed deprecated argument 'options' --- openpype/hosts/testhost/plugins/create/auto_creator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/testhost/plugins/create/auto_creator.py b/openpype/hosts/testhost/plugins/create/auto_creator.py index d5935602a0..4c22eea9dd 100644 --- a/openpype/hosts/testhost/plugins/create/auto_creator.py +++ b/openpype/hosts/testhost/plugins/create/auto_creator.py @@ -30,7 +30,7 @@ class MyAutoCreator(AutoCreator): def update_instances(self, update_list): pipeline.update_instances(update_list) - def create(self, options=None): + def create(self): existing_instance = None for instance in self.create_context.instances: if instance.family == self.family: From 4767bd7f5e75102cea504cc7b66600430bf1420b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 15:41:04 +0100 Subject: [PATCH 056/180] fix import in tray publisher --- .../hosts/traypublisher/plugins/create/create_workfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_workfile.py b/openpype/hosts/traypublisher/plugins/create/create_workfile.py index 2db4770bbc..5e0af350f0 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_workfile.py +++ b/openpype/hosts/traypublisher/plugins/create/create_workfile.py @@ -1,8 +1,8 @@ from openpype.hosts.traypublisher.api import pipeline +from openpype.lib import FileDef from openpype.pipeline import ( Creator, - CreatedInstance, - lib + CreatedInstance ) @@ -80,7 +80,7 @@ class WorkfileCreator(Creator): def get_instance_attr_defs(self): output = [ - lib.FileDef( + FileDef( "filepath", folders=False, extensions=self.extensions, From abf26fcaa47e067e4437bc2c11ac35bd1508e417 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 15:41:18 +0100 Subject: [PATCH 057/180] changed Name to Variant in creator dialog --- openpype/tools/publisher/widgets/create_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 27ce97955a..7d98609c2c 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -271,7 +271,7 @@ class CreateDialog(QtWidgets.QDialog): create_btn.setEnabled(False) form_layout = QtWidgets.QFormLayout() - form_layout.addRow("Name:", variant_layout) + form_layout.addRow("Variant:", variant_layout) form_layout.addRow("Subset:", subset_name_input) mid_widget = QtWidgets.QWidget(self) From db66fe98fd0fc7bfdc9096a0e40fc380f8479aa4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 15:41:43 +0100 Subject: [PATCH 058/180] fixed typo --- openpype/modules/log_viewer/tray/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py index 71827fcac9..def319e0e3 100644 --- a/openpype/modules/log_viewer/tray/app.py +++ b/openpype/modules/log_viewer/tray/app.py @@ -27,11 +27,11 @@ class LogsWindow(QtWidgets.QWidget): self.setStyleSheet(style.load_stylesheet()) - self._frist_show = True + self._first_show = True def showEvent(self, event): super(LogsWindow, self).showEvent(event) - if self._frist_show: - self._frist_show = False + if self._first_show: + self._first_show = False self.logs_widget.refresh() From 57feaee44b8bff1eb7156b25bad70c978b715fec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 15:44:50 +0100 Subject: [PATCH 059/180] added informative logs about not found icons --- openpype/tools/utils/lib.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 93b156bef8..f4ba2106f2 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -17,6 +17,8 @@ from openpype.lib import filter_profiles from openpype.style import get_objected_colors from openpype.resources import get_image_path +log = Logger.get_logger(__name__) + def center_window(window): """Move window to center of it's screen.""" @@ -111,13 +113,23 @@ def get_qta_icon_by_name_and_color(icon_name, icon_color): variants.append("{0}.{1}".format(key, icon_name)) icon = None + used_variant = None for variant in variants: try: icon = qtawesome.icon(variant, color=icon_color) + used_variant = variant break except Exception: pass + if used_variant is None: + log.info("Didn't find icon \"{}\"".format(icon_name)) + + elif used_variant != icon_name: + log.info("Icon \"{}\" was not found \"{}\" is used instead".format( + icon_name, used_variant + )) + SharedObjects.icons[full_icon_name] = icon return icon From d772236278b643e748e446db35b4200eb4b9eb48 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 16:07:11 +0100 Subject: [PATCH 060/180] changed info log level to debug --- openpype/tools/utils/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index f4ba2106f2..d069088932 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -126,7 +126,7 @@ def get_qta_icon_by_name_and_color(icon_name, icon_color): log.info("Didn't find icon \"{}\"".format(icon_name)) elif used_variant != icon_name: - log.info("Icon \"{}\" was not found \"{}\" is used instead".format( + log.debug("Icon \"{}\" was not found \"{}\" is used instead".format( icon_name, used_variant )) From 1c775b382fdb9d864e97bcfb557d2d2ad2383799 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 16:24:40 +0100 Subject: [PATCH 061/180] use explicit icon names for asset --- openpype/tools/utils/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index d069088932..422d0f5389 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -152,8 +152,8 @@ def get_asset_icon_name(asset_doc, has_children=True): return icon_name if has_children: - return "folder" - return "folder-o" + return "fa.folder" + return "fa.folder-o" def get_asset_icon_color(asset_doc): From 9ae5474a929bba3553af854fef332ef623600133 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 16:47:37 +0100 Subject: [PATCH 062/180] fix drop files in files widget --- openpype/widgets/attribute_defs/files_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 87b98e2378..34f7d159ad 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -641,5 +641,6 @@ class SingleFileWidget(QtWidgets.QWidget): filepaths.append(filepath) # TODO filter check if len(filepaths) == 1: - self.set_value(filepaths[0], False) + self._filepath_input.setText(filepaths[0]) + event.accept() From 3534e9c448f30bc8c04f8bfce0837fb5f5ea80f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 24 Mar 2022 17:17:38 +0100 Subject: [PATCH 063/180] Fix plugin label --- .../hosts/traypublisher/plugins/publish/validate_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py index 88339d2aac..e8eeb46065 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py @@ -6,7 +6,7 @@ from openpype.pipeline import PublishValidationError class ValidateWorkfilePath(pyblish.api.InstancePlugin): """Validate existence of workfile instance existence.""" - label = "Collect Workfile" + label = "Validate Workfile" order = pyblish.api.ValidatorOrder - 0.49 families = ["workfile"] hosts = ["traypublisher"] From 76bc7799f1dbcc993b5dbd2868f9309bd3e7e234 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 25 Mar 2022 10:52:54 +0100 Subject: [PATCH 064/180] add top node validator --- .../publish/extract_unreal_staticmesh.py | 5 +-- .../help/validate_skeletalmesh_hierarchy.xml | 14 ++++++++ .../validate_skeletalmesh_hierarchy.py | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index c3cc322a29..92fa1b5933 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -22,8 +22,9 @@ class ExtractUnrealStaticMesh(openpype.api.Extractor): families = ["staticMesh"] def process(self, instance): - geo = instance.data.get("geometryMembers", []) - members = geo + instance.data.get("collisionMembers", []) + members = instance.data.get("geometryMembers", []) + if instance.data.get("collisionMembers"): + members = members + instance.data.get("collisionMembers") fbx_exporter = fbx.FBXExtractor(log=self.log) diff --git a/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml b/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml new file mode 100644 index 0000000000..d30c4cb69d --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/help/validate_skeletalmesh_hierarchy.xml @@ -0,0 +1,14 @@ + + + +Skeletal Mesh Top Node +## Skeletal meshes needs common root + +Skeletal meshes and their joints must be under one common root. + +### How to repair? + +Make sure all geometry and joints resides under same root. + + + diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py new file mode 100644 index 0000000000..dda7e063f6 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import pyblish.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError + +from maya import cmds + + +class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): + """Adheres to the content of 'model' family + + - Must have one top group. (configurable) + - Must only contain: transforms, meshes and groups + + """ + + order = openpype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["skeletalMesh"] + label = "Skeletal Mesh Top Node" + + def process(self, instance): + geo = instance.data.get("geometry") + joints = instance.data.get("joints") + joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] + geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] + + self.log.info(joints_parents) + self.log.info(geo_parents) + self.log.info(set(joints_parents + geo_parents)) + + if len(set(joints_parents + geo_parents)) != 1: + raise PublishXmlValidationError( + self, + "Multiple roots on geometry or joints." + ) From dc86a0d2bc452546e9f99662090c78321222c0ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Mar 2022 01:37:37 +0000 Subject: [PATCH 065/180] Bump node-forge from 1.2.1 to 1.3.0 in /website Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.0. - [Release notes](https://github.com/digitalbazaar/forge/releases) - [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md) - [Commits](https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.0) --- updated-dependencies: - dependency-name: node-forge dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 7f677aaed7..0dd96f63ba 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -5207,9 +5207,9 @@ node-fetch@2.6.7: whatwg-url "^5.0.0" node-forge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" - integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2" + integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== node-releases@^2.0.1: version "2.0.2" From 4a68b1b00aa6c3c3476e97cd99badec073967799 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Mar 2022 03:37:21 +0000 Subject: [PATCH 066/180] Bump minimist from 1.2.5 to 1.2.6 in /website Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 7f677aaed7..ed416a5cc4 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -5125,9 +5125,9 @@ minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@^0.5.5: version "0.5.5" From b711ba51745768809cdbc0c13c8fad0d67da71b9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sun, 27 Mar 2022 23:48:24 +0200 Subject: [PATCH 067/180] fix validator --- .../publish/validate_skeletalmesh_hierarchy.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py index dda7e063f6..cffbd13834 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -22,14 +22,17 @@ class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): def process(self, instance): geo = instance.data.get("geometry") joints = instance.data.get("joints") - joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] - geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] + # joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] + # geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] - self.log.info(joints_parents) - self.log.info(geo_parents) - self.log.info(set(joints_parents + geo_parents)) + joints_parents = cmds.ls(joints, long=True) + geo_parents = cmds.ls(geo, long=True) - if len(set(joints_parents + geo_parents)) != 1: + parents_set = { + parent.split("|")[1] for parent in (joints_parents + geo_parents) + } + + if len(set(parents_set)) != 1: raise PublishXmlValidationError( self, "Multiple roots on geometry or joints." From 3a55b806345ec823177f30796ba018d860aea33d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 28 Mar 2022 00:07:15 +0200 Subject: [PATCH 068/180] fix docstring and remove unused code --- .../plugins/publish/validate_skeletalmesh_hierarchy.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py index cffbd13834..54a86d27cf 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -7,12 +7,7 @@ from maya import cmds class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): - """Adheres to the content of 'model' family - - - Must have one top group. (configurable) - - Must only contain: transforms, meshes and groups - - """ + """Validates that nodes has common root.""" order = openpype.api.ValidateContentsOrder hosts = ["maya"] @@ -22,8 +17,6 @@ class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): def process(self, instance): geo = instance.data.get("geometry") joints = instance.data.get("joints") - # joints_parents = cmds.ls(joints, long=True)[0].split("|")[1:-1] - # geo_parents = cmds.ls(geo, long=True)[0].split("|")[1:-1] joints_parents = cmds.ls(joints, long=True) geo_parents = cmds.ls(geo, long=True) From 228d3cfc004b21398ee0fb381ed8ea5fa28e6826 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 28 Mar 2022 01:54:40 +0200 Subject: [PATCH 069/180] fix hierarchy --- .../publish/extract_unreal_skeletalmesh.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 5b0eb5a3bc..98dbf117dc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -66,20 +66,22 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): instance.data.get("variant", "") ) + joints_parents = cmds.ls(joints, long=True) + geo_parents = cmds.ls(geo, long=True) + + parent_node = { + parent.split("|")[1] for parent in (joints_parents + geo_parents) + }.pop() + renamed_to_extract = [] for node in to_extract: node_path = node.split("|") node_path[1] = parent renamed_to_extract.append("|".join(node_path)) - with renamed(joints_parent, parent): - with parent_nodes(renamed_to_extract, parent=parent): - rooted = [ - "{}|{}".format(parent, i.split("|")[-1]) - for i in renamed_to_extract - ] - self.log.info("Un-parenting: {}".format(rooted, path)) - fbx_exporter.export(rooted, path) + with renamed(parent_node, parent): + self.log.info("Extracting: {}".format(renamed_to_extract, path)) + fbx_exporter.export(renamed_to_extract, path) if "representations" not in instance.data: instance.data["representations"] = [] From 9b097d40e2d3a9830d9fa49bd587d5b363ae8be2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 28 Mar 2022 12:10:36 +0200 Subject: [PATCH 070/180] create a sequence output when converting sequence --- openpype/lib/transcoding.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 6bab6a8160..8e79aba0ae 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -478,8 +478,14 @@ def convert_for_ffmpeg( oiio_cmd.extend(["--eraseattrib", attr_name]) # Add last argument - path to output - base_file_name = os.path.basename(first_input_path) - output_path = os.path.join(output_dir, base_file_name) + if is_sequence: + ext = os.path.splitext(first_input_path)[1] + base_filename = "tmp.%{:0>2}d{}".format( + len(str(input_frame_end)), ext + ) + else: + base_filename = os.path.basename(first_input_path) + output_path = os.path.join(output_dir, base_filename) oiio_cmd.extend([ "-o", output_path ]) From 000461243200f36d654a639c12d35314007878da Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 11:25:57 +0200 Subject: [PATCH 071/180] added settings of template name into hero integrator --- .../defaults/project_settings/global.json | 30 +++--- .../schemas/schema_global_publish.json | 100 +++++++++++++----- 2 files changed, 90 insertions(+), 40 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 30a71b044a..24334b0045 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -33,20 +33,6 @@ "enabled": false, "profiles": [] }, - "IntegrateHeroVersion": { - "enabled": true, - "optional": true, - "families": [ - "model", - "rig", - "look", - "pointcache", - "animation", - "setdress", - "layout", - "mayaScene" - ] - }, "ExtractJpegEXR": { "enabled": true, "ffmpeg_args": { @@ -204,6 +190,22 @@ } ] }, + "IntegrateHeroVersion": { + "enabled": true, + "optional": true, + "active": true, + "families": [ + "model", + "rig", + "look", + "pointcache", + "animation", + "setdress", + "layout", + "mayaScene" + ], + "template_name_profiles": [] + }, "CleanUp": { "paterns": [], "remove_temp_renders": false diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 12043d4205..061874e31c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -122,32 +122,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "checkbox_key": "enabled", - "key": "IntegrateHeroVersion", - "label": "IntegrateHeroVersion", - "is_group": true, - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "boolean", - "key": "optional", - "label": "Optional" - }, - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - } - ] - }, { "type": "dict", "collapsible": true, @@ -652,6 +626,80 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "IntegrateHeroVersion", + "label": "IntegrateHeroVersion", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "list", + "key": "template_name_profiles", + "label": "Template name profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "template_name", + "label": "Template name", + "tooltip": "Name of template from Anatomy templates" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 160247ab61d6c3e1a1e6eeab042a58d9d1b0933b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 11:26:29 +0200 Subject: [PATCH 072/180] remove unused template color --- .../schemas/template_color.json | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_color.json diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json deleted file mode 100644 index af8fd9dae4..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_color.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "type": "list-strict", - "key": "{name}", - "label": "{label}", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - } -] From 0114bbd965ec0975d179faf8d8f854b5a82004c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 12:17:50 +0200 Subject: [PATCH 073/180] use new settings to define which template is used for her opublishing --- .../plugins/publish/integrate_hero_version.py | 105 ++++++++++++------ 1 file changed, 70 insertions(+), 35 deletions(-) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 466606d08b..9a50257a8b 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -7,8 +7,12 @@ import shutil from bson.objectid import ObjectId from pymongo import InsertOne, ReplaceOne import pyblish.api + from avalon import api, io, schema -from openpype.lib import create_hard_link +from openpype.lib import ( + create_hard_link, + filter_profiles +) class IntegrateHeroVersion(pyblish.api.InstancePlugin): @@ -17,7 +21,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.1 optional = True + active = True + # Families are modified using settings families = [ "model", "rig", @@ -33,11 +39,13 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): "project", "asset", "task", "subset", "representation", "family", "hierarchy", "task", "username" ] - # TODO add family filtering # QUESTION/TODO this process should happen on server if crashed due to # permissions error on files (files were used or user didn't have perms) # *but all other plugins must be sucessfully completed + template_name_profiles = [] + _default_template_name = "hero" + def process(self, instance): self.log.debug( "--- Integration of Hero version for subset `{}` begins.".format( @@ -51,26 +59,34 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): ) return - project_name = api.Session["AVALON_PROJECT"] + template_key = self._get_template_key(instance) - # TODO raise error if Hero not set? anatomy = instance.context.data["anatomy"] - if "hero" not in anatomy.templates: - self.log.warning("!!! Anatomy does not have set `hero` key!") - return - - if "path" not in anatomy.templates["hero"]: + project_name = api.Session["AVALON_PROJECT"] + if template_key not in anatomy.templates: self.log.warning(( - "!!! There is not set `path` template in `hero` anatomy" - " for project \"{}\"." - ).format(project_name)) + "!!! Anatomy of project \"{}\" does not have set" + " \"{}\" template key!" + ).format(project_name, template_key)) return - hero_template = anatomy.templates["hero"]["path"] + if "path" not in anatomy.templates[template_key]: + self.log.warning(( + "!!! There is not set \"path\" template in \"{}\" anatomy" + " for project \"{}\"." + ).format(template_key, project_name)) + return + + hero_template = anatomy.templates[template_key]["path"] self.log.debug("`hero` template check was successful. `{}`".format( hero_template )) + self.process_instance(instance, template_key, hero_template) + + def process_instance(self, instance, template_key, hero_template): + anatomy = instance.context.data["anatomy"] + published_repres = instance.data["published_representations"] hero_publish_dir = self.get_publish_dir(instance) src_version_entity = instance.data.get("versionEntity") @@ -271,12 +287,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): continue # Prepare anatomy data - anatomy_data = repre_info["anatomy_data"] + anatomy_data = copy.deepcopy(repre_info["anatomy_data"]) anatomy_data.pop("version", None) # Get filled path to repre context anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled["hero"]["path"] + template_filled = anatomy_filled[template_key]["path"] repre_data = { "path": str(template_filled), @@ -308,11 +324,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): collections, remainders = clique.assemble(published_files) if remainders or not collections or len(collections) > 1: raise Exception(( - "Integrity error. Files of published representation " - "is combination of frame collections and single files." - "Collections: `{}` Single files: `{}`" - ).format(str(collections), - str(remainders))) + "Integrity error. Files of published" + " representation is combination of frame" + " collections and single files. Collections:" + " `{}` Single files: `{}`" + ).format(str(collections), str(remainders))) src_col = collections[0] @@ -320,7 +336,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): frame_splitter = "_-_FRAME_SPLIT_-_" anatomy_data["frame"] = frame_splitter _anatomy_filled = anatomy.format(anatomy_data) - _template_filled = _anatomy_filled["hero"]["path"] + _template_filled = _anatomy_filled[template_key]["path"] head, tail = _template_filled.split(frame_splitter) padding = int( anatomy.templates["render"].get( @@ -466,13 +482,13 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): files.append(_path) return files - def get_publish_dir(self, instance): + def get_publish_dir(self, instance, template_key): anatomy = instance.context.data["anatomy"] template_data = copy.deepcopy(instance.data["anatomyData"]) - if "folder" in anatomy.templates["hero"]: + if "folder" in anatomy.templates[template_key]: anatomy_filled = anatomy.format(template_data) - publish_folder = anatomy_filled["hero"]["folder"] + publish_folder = anatomy_filled[template_key]["folder"] else: # This is for cases of Deprecated anatomy without `folder` # TODO remove when all clients have solved this issue @@ -489,7 +505,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): " key underneath `publish` (in global of for project `{}`)." ).format(project_name)) - file_path = anatomy_filled["hero"]["path"] + file_path = anatomy_filled[template_key]["path"] # Directory publish_folder = os.path.dirname(file_path) @@ -499,6 +515,31 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): return publish_folder + def _get_template_key(self, instance): + anatomy_data = instance.data["anatomyData"] + task_data = anatomy_data.get("task") or {} + task_name = task_data.get("name") + task_type = task_data.get("type") + host_name = instance.context.data["hostName"] + # TODO raise error if Hero not set? + family = self.main_family_from_instance(instance) + key_values = { + "families": family, + "task_names": task_name, + "task_types": task_type, + "hosts": host_name + } + profile = filter_profiles( + self.template_name_profiles, + key_values, + logger=self.log + ) + if profile: + template_name = profile["template_name"] + else: + template_name = self._default_template_name + return template_name + def copy_file(self, src_path, dst_path): # TODO check drives if are the same to check if cas hardlink dirname = os.path.dirname(dst_path) @@ -564,22 +605,16 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): src_file (string) - original file path dst_file (string) - hero file path """ - _, rootless = anatomy.find_root_template_from_path( - dst_file - ) - _, rtls_src = anatomy.find_root_template_from_path( - src_file - ) + _, rootless = anatomy.find_root_template_from_path(dst_file) + _, rtls_src = anatomy.find_root_template_from_path(src_file) return path.replace(rtls_src, rootless) def _update_hash(self, hash, src_file_name, dst_file): """ Updates hash value with proper hero name """ - src_file_name = self._get_name_without_ext( - src_file_name) - hero_file_name = self._get_name_without_ext( - dst_file) + src_file_name = self._get_name_without_ext(src_file_name) + hero_file_name = self._get_name_without_ext(dst_file) return hash.replace(src_file_name, hero_file_name) def _get_name_without_ext(self, value): From 952717440d550ed602b424b3a4f43a5aaa01f350 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 13:46:38 +0200 Subject: [PATCH 074/180] bug fixes and implementing missing method --- openpype/plugins/publish/integrate_hero_version.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 9a50257a8b..67c91634d2 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -82,12 +82,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): hero_template )) - self.process_instance(instance, template_key, hero_template) + self.integrate_instance(instance, template_key, hero_template) - def process_instance(self, instance, template_key, hero_template): + def integrate_instance(self, instance, template_key, hero_template): anatomy = instance.context.data["anatomy"] published_repres = instance.data["published_representations"] - hero_publish_dir = self.get_publish_dir(instance) + hero_publish_dir = self.get_publish_dir(instance, template_key) src_version_entity = instance.data.get("versionEntity") filtered_repre_ids = [] @@ -540,6 +540,13 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): template_name = self._default_template_name return template_name + def main_family_from_instance(self, instance): + """Returns main family of entered instance.""" + family = instance.data.get("family") + if not family: + family = instance.data["families"][0] + return family + def copy_file(self, src_path, dst_path): # TODO check drives if are the same to check if cas hardlink dirname = os.path.dirname(dst_path) From 059020cabd21cf656bceadf76d63ab719ff59a58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 13:53:22 +0200 Subject: [PATCH 075/180] change frame padding access --- openpype/plugins/publish/integrate_hero_version.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 67c91634d2..d50f2a4712 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -339,10 +339,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): _template_filled = _anatomy_filled[template_key]["path"] head, tail = _template_filled.split(frame_splitter) padding = int( - anatomy.templates["render"].get( - "frame_padding", - anatomy.templates["render"].get("padding") - ) + anatomy.templates[template_key]["frame_padding"] ) dst_col = clique.Collection( From e76c317d7700d0950e323a69ae33650889815543 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 13:56:28 +0200 Subject: [PATCH 076/180] remove her publish dir on error before renaming it back --- openpype/plugins/publish/integrate_hero_version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index d50f2a4712..d6df6535d8 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -457,6 +457,8 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): backup_hero_publish_dir is not None and os.path.exists(backup_hero_publish_dir) ): + if os.path.exists(hero_publish_dir): + shutil.rmtree(hero_publish_dir) os.rename(backup_hero_publish_dir, hero_publish_dir) self.log.error(( "!!! Creating of hero version failed." From dfa5555535b06d9fadf7118858574d79f41d0155 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 16:28:13 +0200 Subject: [PATCH 077/180] Fix creation of subset names in PS review and workfile --- .../hosts/photoshop/plugins/publish/collect_review.py | 10 +++++++++- .../photoshop/plugins/publish/collect_workfile.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 5ab48b76da..df069efd1f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -2,6 +2,8 @@ import os import pyblish.api +from openpype.lib import get_subset_name + class CollectReview(pyblish.api.ContextPlugin): """Gather the active document as review instance.""" @@ -13,7 +15,13 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" task = os.getenv("AVALON_TASK", None) - subset = family + task.capitalize() + subset = get_subset_name( + family, + "", + task, + context.data["assetEntity"]["_id"], + host_name="photoshop" + ) file_path = context.data["currentFile"] base_name = os.path.basename(file_path) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index db1ede14d5..841db72cde 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,6 +1,8 @@ import os import pyblish.api +from openpype.lib import get_subset_name + class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" @@ -12,7 +14,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): family = "workfile" task = os.getenv("AVALON_TASK", None) - subset = family + task.capitalize() + subset = get_subset_name( + family, + "", + task, + context.data["assetEntity"]["_id"], + host_name="photoshop" + ) file_path = context.data["currentFile"] staging_dir = os.path.dirname(file_path) From a19983313c5c509cdb5cc1e02cfbb5aa0c918c88 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 16:38:59 +0200 Subject: [PATCH 078/180] OP-2766 - Fix order of collector --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index df069efd1f..6c299a60f5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -11,6 +11,7 @@ class CollectReview(pyblish.api.ContextPlugin): label = "Review" order = pyblish.api.CollectorOrder hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.1 def process(self, context): family = "review" From 653d0c1366f5d976bcd4458bfd1ceb903f216e83 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 17:21:49 +0200 Subject: [PATCH 079/180] OP-2766 - Fix pulling task and project from context --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 4 ++-- openpype/hosts/photoshop/plugins/publish/collect_workfile.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 6c299a60f5..e219326d64 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -15,12 +15,12 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" - task = os.getenv("AVALON_TASK", None) subset = get_subset_name( family, "", - task, + context.data["anatomyData"]["task"]["name"], context.data["assetEntity"]["_id"], + context.data["anatomyData"]["project"]["name"], host_name="photoshop" ) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 841db72cde..ca1124fd24 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -13,12 +13,12 @@ class CollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): family = "workfile" - task = os.getenv("AVALON_TASK", None) subset = get_subset_name( family, "", - task, + context.data["anatomyData"]["task"]["name"], context.data["assetEntity"]["_id"], + context.data["anatomyData"]["project"]["name"], host_name="photoshop" ) From 9d8df33d4af634614f0ec9c2cbf716ef014c7698 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 16:38:59 +0200 Subject: [PATCH 080/180] Fix order of collector --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index df069efd1f..6c299a60f5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -11,6 +11,7 @@ class CollectReview(pyblish.api.ContextPlugin): label = "Review" order = pyblish.api.CollectorOrder hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.1 def process(self, context): family = "review" From 8b1cfa7d19e476c3a6579fa63de89ef9c8c1e855 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 17:21:49 +0200 Subject: [PATCH 081/180] Fix pulling task and project from context --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 4 ++-- openpype/hosts/photoshop/plugins/publish/collect_workfile.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 6c299a60f5..e219326d64 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -15,12 +15,12 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" - task = os.getenv("AVALON_TASK", None) subset = get_subset_name( family, "", - task, + context.data["anatomyData"]["task"]["name"], context.data["assetEntity"]["_id"], + context.data["anatomyData"]["project"]["name"], host_name="photoshop" ) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 841db72cde..ca1124fd24 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -13,12 +13,12 @@ class CollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): family = "workfile" - task = os.getenv("AVALON_TASK", None) subset = get_subset_name( family, "", - task, + context.data["anatomyData"]["task"]["name"], context.data["assetEntity"]["_id"], + context.data["anatomyData"]["project"]["name"], host_name="photoshop" ) From 3baee04ada9b7ea11b4877ac0e30d29453ccb67f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 17:32:12 +0200 Subject: [PATCH 082/180] Fix order --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index e219326d64..8b7508cf5b 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -9,9 +9,8 @@ class CollectReview(pyblish.api.ContextPlugin): """Gather the active document as review instance.""" label = "Review" - order = pyblish.api.CollectorOrder - hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.1 + hosts = ["photoshop"] def process(self, context): family = "review" From b3391ebe7151ae540dc01201d43cf75651cd7979 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 17:42:53 +0200 Subject: [PATCH 083/180] Fix order --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 2f87af5d72..8b7508cf5b 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -11,7 +11,6 @@ class CollectReview(pyblish.api.ContextPlugin): label = "Review" order = pyblish.api.CollectorOrder + 0.1 hosts = ["photoshop"] - order = pyblish.api.CollectorOrder + 0.1 def process(self, context): family = "review" From 505d73145c34a3379dae4e699f1d3b825ce0567c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 18:11:14 +0200 Subject: [PATCH 084/180] enhanced tool settings to have also filters --- .../defaults/system_settings/tools.json | 12 +++++++-- .../schemas/system_schema/schema_tools.json | 25 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/system_settings/tools.json b/openpype/settings/defaults/system_settings/tools.json index 181236abe8..9e08465195 100644 --- a/openpype/settings/defaults/system_settings/tools.json +++ b/openpype/settings/defaults/system_settings/tools.json @@ -25,10 +25,18 @@ }, "variants": { "3-2": { - "MTOA_VERSION": "3.2" + "host_names": [], + "app_variants": [], + "environment": { + "MTOA_VERSION": "3.2" + } }, "3-1": { - "MTOA_VERSION": "3.1" + "host_names": [], + "app_variants": [], + "environment": { + "MTOA_VERSION": "3.1" + } }, "__dynamic_keys_labels__": { "3-2": "3.2", diff --git a/openpype/settings/entities/schemas/system_schema/schema_tools.json b/openpype/settings/entities/schemas/system_schema/schema_tools.json index 2346bef36d..7962fdd465 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_tools.json +++ b/openpype/settings/entities/schemas/system_schema/schema_tools.json @@ -25,7 +25,30 @@ "key": "variants", "collapsible_key": true, "object_type": { - "type": "raw-json" + "type": "dict", + "children": [ + { + "key": "host_names", + "label": "Hosts", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "app_variants", + "label": "Applications", + "type": "apps-enum", + "multiselection": true, + "tooltip": "Applications are not \"live\" and may require to Save and refresh settings UI to update values." + }, + { + "type": "separator" + }, + { + "key": "environment", + "label": "Environments", + "type": "raw-json" + } + ] } } ] From 3fd989671acb44e9df400be7217243bd12d23ae2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 18:11:59 +0200 Subject: [PATCH 085/180] added backwards compatibility of tools right into settings lib function --- openpype/settings/lib.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 1d303564d5..54502292dc 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -265,11 +265,43 @@ def save_project_anatomy(project_name, anatomy_data): raise SaveWarningExc(warnings) +def _system_settings_backwards_compatible_conversion(studio_overrides): + # Backwards compatibility of tools 3.9.1 - 3.9.2 to keep + # "tools" environments + if ( + "tools" in studio_overrides + and "tool_groups" in studio_overrides["tools"] + ): + tool_groups = studio_overrides["tools"]["tool_groups"] + for tool_group, group_value in tool_groups.items(): + if tool_group in METADATA_KEYS: + continue + + variants = group_value.get("variants") + if not variants: + continue + + for key in set(variants.keys()): + if key in METADATA_KEYS: + continue + + variant_value = variants[key] + if "environment" not in variant_value: + variants[key] = { + "environment": variant_value + } + + @require_handler def get_studio_system_settings_overrides(return_version=False): - return _SETTINGS_HANDLER.get_studio_system_settings_overrides( + output = _SETTINGS_HANDLER.get_studio_system_settings_overrides( return_version ) + value = output + if return_version: + value, version = output + _system_settings_backwards_compatible_conversion(value) + return output @require_handler From b3463afa38d2ed80c3ccffe8f3f757ace44955ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 18:14:09 +0200 Subject: [PATCH 086/180] load settings data in EnvironmentTool with backwards comatible way --- openpype/lib/applications.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index ad59ae0dbc..e415120ac4 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -211,6 +211,7 @@ class ApplicationGroup: data (dict): Group defying data loaded from settings. manager (ApplicationManager): Manager that created the group. """ + def __init__(self, name, data, manager): self.name = name self.manager = manager @@ -374,6 +375,7 @@ class ApplicationManager: will always use these values. Gives ability to create manager using different settings. """ + def __init__(self, system_settings=None): self.log = PypeLogger.get_logger(self.__class__.__name__) @@ -530,13 +532,13 @@ class EnvironmentToolGroup: variants = data.get("variants") or {} label_by_key = variants.pop(M_DYNAMIC_KEY_LABEL, {}) variants_by_name = {} - for variant_name, variant_env in variants.items(): + for variant_name, variant_data in variants.items(): if variant_name in METADATA_KEYS: continue variant_label = label_by_key.get(variant_name) or variant_name tool = EnvironmentTool( - variant_name, variant_label, variant_env, self + variant_name, variant_label, variant_data, self ) variants_by_name[variant_name] = tool self.variants = variants_by_name @@ -560,15 +562,35 @@ class EnvironmentTool: Args: name (str): Name of the tool. - environment (dict): Variant environments. + variant_data (dict): Variant data with environments and + host and app variant filters. group (str): Name of group which wraps tool. """ - def __init__(self, name, label, environment, group): + def __init__(self, name, label, variant_data, group): + # Backwards compatibility 3.9.1 - 3.9.2 + # - 'variant_data' contained only environments but contain also host + # and application variant filters + host_names = [] + app_variants = [] + if "host_names" in variant_data: + host_names = variant_data["host_names"] + + if "app_variants" in variant_data: + app_variants = variant_data["app_variants"] + + if "environment" in variant_data: + environment = variant_data["environemnt"] + else: + environment = variant_data + + self.host_names = host_names + self.app_variants = app_variants self.name = name self.variant_label = label self.label = " ".join((group.label, label)) self.group = group + self._environment = environment self.full_name = "/".join((group.name, name)) From 39f84f8dfdb4f2ec7f369acf3b2e80971befea70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 18:14:24 +0200 Subject: [PATCH 087/180] added option to validate if tool is valid for application --- openpype/lib/applications.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e415120ac4..176a9a2391 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -601,6 +601,19 @@ class EnvironmentTool: def environment(self): return copy.deepcopy(self._environment) + def is_valid_for_app(self, app): + """Is tool valid for application. + + Args: + app (Application): Application for which are prepared environments. + """ + if self.app_variants and app.full_name not in self.app_variants: + return False + + if self.host_names and app.host_name not in self.host_names: + return False + return True + class ApplicationExecutable: """Representation of executable loaded from settings.""" @@ -1406,7 +1419,7 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): # Make sure each tool group can be added only once for key in asset_doc["data"].get("tools_env") or []: tool = app.manager.tools.get(key) - if not tool: + if not tool or not tool.is_valid_for_app(app): continue groups_by_name[tool.group.name] = tool.group tool_by_group_name[tool.group.name][tool.name] = tool From 3bda53a54138b9e211f1d88d0e7e01a48fef9ac7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 29 Mar 2022 18:21:10 +0200 Subject: [PATCH 088/180] fix typo --- openpype/lib/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 176a9a2391..b496bd74e2 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -580,7 +580,7 @@ class EnvironmentTool: app_variants = variant_data["app_variants"] if "environment" in variant_data: - environment = variant_data["environemnt"] + environment = variant_data["environment"] else: environment = variant_data From 461bf75d660cc60aeae3401bf413496cd71b7e17 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Mar 2022 23:11:29 +0200 Subject: [PATCH 089/180] texture publishing initial commit --- .../validate_simple_unreal_texture_naming.py | 15 +++++++++++++++ openpype/plugins/publish/integrate_new.py | 3 ++- .../defaults/project_anatomy/templates.json | 11 ++++++++++- .../defaults/project_settings/global.json | 11 +++++++++++ .../project_settings/standalonepublisher.json | 11 ++++++++++- 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py new file mode 100644 index 0000000000..158a749075 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Validator for correct file naming.""" +import pyblish.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError + + +class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin): + label = "Validate Unreal Texture Names" + hosts = ["standalonepublisher"] + families = ["simpleUnrealTexture"] + order = openpype.api.ValidateContentsOrder + + def process(self, instance): + ... \ No newline at end of file diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 2304f98713..4025f18cb2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -107,7 +107,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hda", "usd", "usdComposition", - "usdOverride" + "usdOverride", + "simpleUnrealTexture" ] exclude_families = ["clip"] db_representation_context_keys = [ diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index d46d449c77..45611f55b1 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -32,5 +32,14 @@ "file": "{subset}_{@version}<_{output}><.{@frame}>.{ext}", "path": "{@folder}/{@file}" }, - "others": {} + "others": { + "simpleUnrealTexture": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", + "file": "{original_file}", + "path": "{@folder}/{@file}" + }, + "__dynamic_keys_labels__": { + "simpleUnrealTexture": "Simple Unreal Texture" + } + } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 30a71b044a..f0fa09957e 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -192,6 +192,17 @@ "task_types": [], "tasks": [], "template_name": "render" + }, + { + "families": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "tasks": [], + "template_name": "simpleUnrealTexture" } ], "subset_grouping_profiles": [ diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 6858c4f34d..bc91a5ea8a 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -133,6 +133,14 @@ ], "help": "Texture files with UDIM together with worfile" }, + "create_simple_unreal_texture": { + "name": "simple_unreal_texture", + "label": "Simple Unreal Texture", + "family": "simpleUnrealTexture", + "icon": "Image", + "defaults": [], + "help": "Texture files with Unreal naming convention" + }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", "create_model": "Model", @@ -145,7 +153,8 @@ "create_matchmove": "Matchmove", "create_render": "Render", "create_mov_batch": "Batch Mov", - "create_texture_batch": "Batch Texture" + "create_texture_batch": "Batch Texture", + "create_simple_unreal_texture": "Simple Unreal Texture" } }, "publish": { From d26143198c59619a879ace67930f05cd0b792f61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 22:21:09 +0000 Subject: [PATCH 090/180] Bump paramiko from 2.9.2 to 2.10.1 Bumps [paramiko](https://github.com/paramiko/paramiko) from 2.9.2 to 2.10.1. - [Release notes](https://github.com/paramiko/paramiko/releases) - [Changelog](https://github.com/paramiko/paramiko/blob/main/NEWS) - [Commits](https://github.com/paramiko/paramiko/compare/2.9.2...2.10.1) --- updated-dependencies: - dependency-name: paramiko dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 84 ++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/poetry.lock b/poetry.lock index ed2b0dd3c2..7998ede693 100644 --- a/poetry.lock +++ b/poetry.lock @@ -680,15 +680,8 @@ category = "main" optional = false python-versions = "*" -[package.dependencies] -attrs = ">=17.4.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -pyrsistent = ">=0.14.0" -six = ">=1.11.0" - [package.extras] -format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +format = ["rfc3987", "strict-rfc3339", "webcolors"] [[package]] name = "keyring" @@ -784,7 +777,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "paramiko" -version = "2.9.2" +version = "2.10.1" description = "SSH2 protocol library" category = "main" optional = false @@ -794,6 +787,7 @@ python-versions = "*" bcrypt = ">=3.1.3" cryptography = ">=2.5" pynacl = ">=1.0.1" +six = "*" [package.extras] all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] @@ -1087,14 +1081,6 @@ category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "pyrsistent" -version = "0.18.1" -description = "Persistent/Functional/Immutable data structures" -category = "main" -optional = false -python-versions = ">=3.7" - [[package]] name = "pysftp" version = "0.2.9" @@ -1633,7 +1619,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "2f78d48a6aad2d8a88b7dd7f31a76d907bec9fb65f0086fba6b6d2e1605f0f88" +content-hash = "b02313c8255a1897b0f0617ad4884a5943696c363512921aab1cb2dd8f4fdbe0" [metadata.files] acre = [] @@ -2171,12 +2157,28 @@ log4mongo = [ {file = "log4mongo-1.7.0.tar.gz", hash = "sha256:dc374617206162a0b14167fbb5feac01dbef587539a235dadba6200362984a68"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -2185,14 +2187,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -2202,6 +2217,12 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -2277,8 +2298,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] paramiko = [ - {file = "paramiko-2.9.2-py2.py3-none-any.whl", hash = "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603"}, - {file = "paramiko-2.9.2.tar.gz", hash = "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b"}, + {file = "paramiko-2.10.1-py2.py3-none-any.whl", hash = "sha256:f6cbd3e1204abfdbcd40b3ecbc9d32f04027cd3080fe666245e21e7540ccfc1b"}, + {file = "paramiko-2.10.1.tar.gz", hash = "sha256:443f4da23ec24e9a9c0ea54017829c282abdda1d57110bf229360775ccd27a31"}, ] parso = [ {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, @@ -2598,29 +2619,6 @@ pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] -pyrsistent = [ - {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, - {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, - {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, - {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, - {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, - {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, - {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, - {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, - {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, - {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, - {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, -] pysftp = [ {file = "pysftp-0.2.9.tar.gz", hash = "sha256:fbf55a802e74d663673400acd92d5373c1c7ee94d765b428d9f977567ac4854a"}, ] From d6a7beb16b056f24114aec5b27ecc6b5e60497cd Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 30 Mar 2022 03:40:40 +0000 Subject: [PATCH 091/180] [Automated] Bump version --- CHANGELOG.md | 19 ++++++++++++------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abe9eaa3ce..f767bc71d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.9.2-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...HEAD) @@ -8,11 +8,16 @@ - Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951) +**🆕 New features** + +- Multiverse: First PR [\#2908](https://github.com/pypeclub/OpenPype/pull/2908) + **🚀 Enhancements** - Slack: Added configurable maximum file size of review upload to Slack [\#2945](https://github.com/pypeclub/OpenPype/pull/2945) - NewPublisher: Prepared implementation of optional pyblish plugin [\#2943](https://github.com/pypeclub/OpenPype/pull/2943) - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) +- General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) - CI: change the version bump logic [\#2919](https://github.com/pypeclub/OpenPype/pull/2919) - Deadline: Add headless argument [\#2916](https://github.com/pypeclub/OpenPype/pull/2916) - Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) @@ -22,16 +27,21 @@ **🐛 Bug fixes** +- Slack: Added default for review\_upload\_limit for Slack [\#2965](https://github.com/pypeclub/OpenPype/pull/2965) +- Settings: Conditional dictionary avoid invalid logs [\#2956](https://github.com/pypeclub/OpenPype/pull/2956) - LogViewer: Don't refresh on initialization [\#2949](https://github.com/pypeclub/OpenPype/pull/2949) +- nuke: python3 compatibility issue with `iteritems` [\#2948](https://github.com/pypeclub/OpenPype/pull/2948) - General: anatomy data with correct task short key [\#2947](https://github.com/pypeclub/OpenPype/pull/2947) - SceneInventory: Fix imports in UI [\#2944](https://github.com/pypeclub/OpenPype/pull/2944) - Slack: add generic exception [\#2941](https://github.com/pypeclub/OpenPype/pull/2941) - General: Python specific vendor paths on env injection [\#2939](https://github.com/pypeclub/OpenPype/pull/2939) - General: More fail safe delete old versions [\#2936](https://github.com/pypeclub/OpenPype/pull/2936) - Settings UI: Collapsed of collapsible wrapper works as expected [\#2934](https://github.com/pypeclub/OpenPype/pull/2934) +- Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932) - General: Don't print log record on OSError [\#2926](https://github.com/pypeclub/OpenPype/pull/2926) - Hiero: Fix import of 'register\_event\_callback' [\#2924](https://github.com/pypeclub/OpenPype/pull/2924) - Ftrack: Missing Ftrack id after editorial publish [\#2905](https://github.com/pypeclub/OpenPype/pull/2905) +- AfterEffects: Fix rendering for single frame in DL [\#2875](https://github.com/pypeclub/OpenPype/pull/2875) **🔀 Refactored code** @@ -43,7 +53,7 @@ **Merged pull requests:** -- Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932) +- Maya - added transparency into review creator [\#2952](https://github.com/pypeclub/OpenPype/pull/2952) ## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18) @@ -96,14 +106,10 @@ - Maya: add loaded containers to published instance [\#2837](https://github.com/pypeclub/OpenPype/pull/2837) - Ftrack: Can sync fps as string [\#2836](https://github.com/pypeclub/OpenPype/pull/2836) - General: Custom function for find executable [\#2822](https://github.com/pypeclub/OpenPype/pull/2822) -- General: Color dialog UI fixes [\#2817](https://github.com/pypeclub/OpenPype/pull/2817) -- global: letter box calculated on output as last process [\#2812](https://github.com/pypeclub/OpenPype/pull/2812) -- Nuke: adding Reformat to baking mov plugin [\#2811](https://github.com/pypeclub/OpenPype/pull/2811) **🐛 Bug fixes** - General: Missing time function [\#2877](https://github.com/pypeclub/OpenPype/pull/2877) -- AfterEffects: Fix rendering for single frame in DL [\#2875](https://github.com/pypeclub/OpenPype/pull/2875) - Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868) - Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866) - General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864) @@ -126,7 +132,6 @@ - Settings UI: Fix "Apply from" action [\#2820](https://github.com/pypeclub/OpenPype/pull/2820) - Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819) - Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818) -- StandalonePublisher: use dynamic groups in subset names [\#2816](https://github.com/pypeclub/OpenPype/pull/2816) **🔀 Refactored code** diff --git a/openpype/version.py b/openpype/version.py index 84ea02fd08..6d55672aca 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.2-nightly.2" +__version__ = "3.9.2-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 46515b4785..479cd731fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.2-nightly.2" # OpenPype +version = "3.9.2-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 3f8f9b9008238f7220b2b101bcceedefb70cc9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 30 Mar 2022 10:47:55 +0200 Subject: [PATCH 092/180] Update openpype/hosts/flame/plugins/publish/collect_timeline_instances.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/flame/plugins/publish/collect_timeline_instances.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 390c55837c..bc5c60a97d 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -98,8 +98,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): label = asset if asset != clip_name: label += " ({})".format(clip_name) - label += " {}".format(subset) - label += " {}".format("[" + ", ".join(families) + "]") + label += " {} [{}]".format(subset, ", ".join(families)) inst_data.update({ "name": "{}_{}".format(asset, subset), From fbe34f74a3c18901c31b5735ddbdec38093f156d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Mar 2022 10:49:53 +0200 Subject: [PATCH 093/180] flame: flip order of processing --- .../publish/collect_timeline_instances.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index bc5c60a97d..2482abd9c7 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -43,17 +43,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # process all sellected for segment in selected_segments: - comment_attributes = self._get_comment_attributes(segment) - self.log.debug("__ segment.name: {}".format( - segment.name - )) - self.log.debug("_ comment_attributes: {}".format( - pformat(comment_attributes))) - - clip_data = opfapi.get_segment_attributes(segment) - clip_name = clip_data["segment_name"] - self.log.debug("clip_name: {}".format(clip_name)) - # get openpype tag data marker_data = opfapi.get_segment_data_marker(segment) self.log.debug("__ marker_data: {}".format( @@ -65,6 +54,19 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): if marker_data.get("id") != "pyblish.avalon.instance": continue + self.log.debug("__ segment.name: {}".format( + segment.name + )) + + comment_attributes = self._get_comment_attributes(segment) + + self.log.debug("_ comment_attributes: {}".format( + pformat(comment_attributes))) + + clip_data = opfapi.get_segment_attributes(segment) + clip_name = clip_data["segment_name"] + self.log.debug("clip_name: {}".format(clip_name)) + # get file path file_path = clip_data["fpath"] From ee96827ff225fb1dff1419074acc89103b0cdd08 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Mar 2022 10:59:01 +0200 Subject: [PATCH 094/180] flame: improving handling color policy input --- openpype/hosts/flame/api/scripts/wiretap_com.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index a85a85ae25..54993d34eb 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -422,7 +422,13 @@ class WireTapCom(object): color_policy = color_policy or "Legacy" # check if the colour policy in custom dir - if "/" not in color_policy: + if "/" in color_policy: + # if unlikelly full path was used make it redundant + color_policy = color_policy.replace("/syncolor/policies/", "") + # expecting input is `Shared/NameOfPolicy` + color_policy = "/syncolor/policies/{}".format( + color_policy) + else: color_policy = "/syncolor/policies/Autodesk/{}".format( color_policy) From 36a43d402012e0115a856f1d6058397ab29e4fd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Mar 2022 11:11:37 +0200 Subject: [PATCH 095/180] Simplified getting host and app filter acess. Co-authored-by: Roy Nieterau --- openpype/lib/applications.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index b496bd74e2..5821c863d7 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -571,13 +571,8 @@ class EnvironmentTool: # Backwards compatibility 3.9.1 - 3.9.2 # - 'variant_data' contained only environments but contain also host # and application variant filters - host_names = [] - app_variants = [] - if "host_names" in variant_data: - host_names = variant_data["host_names"] - - if "app_variants" in variant_data: - app_variants = variant_data["app_variants"] + host_names = variant_data.get("host_names", []) + app_variants = variant_data.get("app_variants", []) if "environment" in variant_data: environment = variant_data["environment"] From a62d98235c9d316a5c878c9376b254472d37b3de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 30 Mar 2022 11:50:05 +0200 Subject: [PATCH 096/180] force completer to be shown inactive --- openpype/tools/settings/settings/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 577c2630ab..b3518cffd9 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -97,6 +97,9 @@ class CompleterView(QtWidgets.QListView): QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool ) + + # Open the widget unactivated + self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) From 10673544a481e34ebde96dccc3480fb03ff47452 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 11:51:32 +0200 Subject: [PATCH 097/180] Fix broken links --- website/docs/manager_ftrack.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/manager_ftrack.md b/website/docs/manager_ftrack.md index defbb4b48f..1b1c220c18 100644 --- a/website/docs/manager_ftrack.md +++ b/website/docs/manager_ftrack.md @@ -4,7 +4,7 @@ title: Ftrack sidebar_label: Project Manager --- -Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://ftrack.rtd.ftrack.com/en/stable/). +Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://help.ftrack.com/en/articles/1040483-creating-a-new-project). ## Project management Setting project attributes is the key to properly working pipeline. @@ -31,7 +31,7 @@ This process describes how data from Ftrack will get into Avalon database. ### How to synchronize You can trigger synchronization manually using [Sync To Avalon](manager_ftrack_actions.md#sync-to-avalon) action. -Synchronization can also be automated with OpenPype's [event server](#event-server) and synchronization events. If your Ftrack is [prepared for OpenPype](#prepare-ftrack-for-openpype), the project should have custom attribute `Avalon auto-sync`. Check the custom attribute to allow auto-updates with event server. +Synchronization can also be automated with OpenPype's [event server](#event-server) and synchronization events. If your Ftrack is [prepared for OpenPype](module_ftrack.md#prepare-ftrack-for-openpype), the project should have custom attribute `Avalon auto-sync`. Check the custom attribute to allow auto-updates with event server. :::tip Always use `Sync To Avalon` action before you enable `Avalon auto-sync`! From ccf563d60ab7a11dc3dcd7ab81c45134d0a5c387 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 11:54:38 +0200 Subject: [PATCH 098/180] `METADATA_KEYS` constant as `frozenset` for optimal immutable lookup --- openpype/settings/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/constants.py b/openpype/settings/constants.py index 8b8acf5714..19ff953eb4 100644 --- a/openpype/settings/constants.py +++ b/openpype/settings/constants.py @@ -8,11 +8,11 @@ M_ENVIRONMENT_KEY = "__environment_keys__" # Metadata key for storing dynamic created labels M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__" -METADATA_KEYS = ( +METADATA_KEYS = frozenset([ M_OVERRIDDEN_KEY, M_ENVIRONMENT_KEY, M_DYNAMIC_KEY_LABEL -) +]) # Keys where studio's system overrides are stored GLOBAL_SETTINGS_KEY = "global_settings" From a6a98ae104c3747bec65ebd19c2d81110021acc1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 30 Mar 2022 13:27:59 +0200 Subject: [PATCH 099/180] Switched subset function according to review comments --- .../hosts/photoshop/plugins/publish/collect_review.py | 8 ++++---- .../hosts/photoshop/plugins/publish/collect_workfile.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 8b7508cf5b..f3842b9ee5 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -2,7 +2,7 @@ import os import pyblish.api -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectReview(pyblish.api.ContextPlugin): @@ -14,13 +14,13 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" - subset = get_subset_name( + subset = get_subset_name_with_asset_doc( family, "", context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"]["_id"], + context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name="photoshop" + host_name=context.data["hostName"] ) file_path = context.data["currentFile"] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index ca1124fd24..0dbe2c6609 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): @@ -13,13 +13,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): def process(self, context): family = "workfile" - subset = get_subset_name( + subset = get_subset_name_with_asset_doc( family, "", context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"]["_id"], + context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name="photoshop" + host_name=context.data["hostName"] ) file_path = context.data["currentFile"] From 57ba944ad3759136d000e7d946a6c438e56d6faa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Mar 2022 14:10:09 +0200 Subject: [PATCH 100/180] nuke: settings for read raw reordering ui attributes for better logic --- .../defaults/project_settings/nuke.json | 7 +++-- .../schemas/schema_nuke_publish.json | 29 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 6992fb6e3e..2fb6a372e4 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -119,11 +119,10 @@ "families": [], "sebsets": [] }, - "extension": "mov", + "read_raw": false, "viewer_process_override": "", "bake_viewer_process": true, "bake_viewer_input_process": true, - "add_tags": [], "reformat_node_add": false, "reformat_node_config": [ { @@ -151,7 +150,9 @@ "name": "pbb", "value": false } - ] + ], + "extension": "mov", + "add_tags": [] } } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 1636a8d700..673e12d54b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -208,9 +208,10 @@ "type": "separator" }, { - "type": "text", - "key": "extension", - "label": "File extension" + "type": "boolean", + "key": "read_raw", + "label": "Read colorspace RAW", + "default": false }, { "type": "text", @@ -227,12 +228,6 @@ "key": "bake_viewer_input_process", "label": "Bake Viewer Input Process (LUTs)" }, - { - "key": "add_tags", - "label": "Add additional tags to representations", - "type": "list", - "object_type": "text" - }, { "type": "separator" }, @@ -246,7 +241,7 @@ "type": "collapsible-wrap", "label": "Reformat Node Knobs", "collapsible": true, - "collapsed": false, + "collapsed": true, "children": [ { "type": "list", @@ -347,6 +342,20 @@ } } ] + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "extension", + "label": "Write node file type" + }, + { + "key": "add_tags", + "label": "Add additional tags to representations", + "type": "list", + "object_type": "text" } ] } From 65d93ca9f109ae9dad81d2c9058dee423feda6fb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Mar 2022 14:15:22 +0200 Subject: [PATCH 101/180] nuke: adding read raw baking preset attr --- openpype/hosts/nuke/api/plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index d0bb45a05d..3ac750a48f 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -450,6 +450,7 @@ class ExporterReviewMov(ExporterReview): def generate_mov(self, farm=False, **kwargs): self.publish_on_farm = farm + read_raw = kwargs["read_raw"] reformat_node_add = kwargs["reformat_node_add"] reformat_node_config = kwargs["reformat_node_config"] bake_viewer_process = kwargs["bake_viewer_process"] @@ -484,6 +485,9 @@ class ExporterReviewMov(ExporterReview): r_node["origlast"].setValue(self.last_frame) r_node["colorspace"].setValue(self.write_colorspace) + if read_raw: + r_node["raw"].setValue(1) + # connect self._temp_nodes[subset].append(r_node) self.previous_node = r_node From 215da09d3dcf92138eeddfc97bf48a9fd5728729 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 30 Mar 2022 15:24:23 +0200 Subject: [PATCH 102/180] Change default value of force copy to True --- openpype/hosts/maya/plugins/create/create_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_look.py b/openpype/hosts/maya/plugins/create/create_look.py index 56e2640919..d9521d1860 100644 --- a/openpype/hosts/maya/plugins/create/create_look.py +++ b/openpype/hosts/maya/plugins/create/create_look.py @@ -22,4 +22,4 @@ class CreateLook(plugin.Creator): self.data["maketx"] = self.make_tx # Enable users to force a copy. - self.data["forceCopy"] = False + self.data["forceCopy"] = True From 62e98546e1826bb96bac90c365bb92ffadfd1d43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Mar 2022 16:03:16 +0200 Subject: [PATCH 103/180] Update website/docs/manager_ftrack.md - add https to url Co-authored-by: Petr Kalis --- website/docs/manager_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/manager_ftrack.md b/website/docs/manager_ftrack.md index 1b1c220c18..730c57d1f9 100644 --- a/website/docs/manager_ftrack.md +++ b/website/docs/manager_ftrack.md @@ -4,7 +4,7 @@ title: Ftrack sidebar_label: Project Manager --- -Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](http://help.ftrack.com/en/articles/1040483-creating-a-new-project). +Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/articles/1040483-creating-a-new-project). ## Project management Setting project attributes is the key to properly working pipeline. From 8eab6ae180fe308e03bc4d3df7cd53bd030b59ce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 30 Mar 2022 16:25:37 +0200 Subject: [PATCH 104/180] disable hardlink on windows for look publishing --- openpype/hosts/maya/plugins/create/create_look.py | 4 +++- openpype/hosts/maya/plugins/publish/extract_look.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_look.py b/openpype/hosts/maya/plugins/create/create_look.py index d9521d1860..44e439fe1f 100644 --- a/openpype/hosts/maya/plugins/create/create_look.py +++ b/openpype/hosts/maya/plugins/create/create_look.py @@ -22,4 +22,6 @@ class CreateLook(plugin.Creator): self.data["maketx"] = self.make_tx # Enable users to force a copy. - self.data["forceCopy"] = True + # - on Windows is "forceCopy" always changed to `True` because of + # windows implementation of hardlinks + self.data["forceCopy"] = False diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index a8893072d0..137683ca6d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -4,6 +4,7 @@ import os import sys import json import tempfile +import platform import contextlib import subprocess from collections import OrderedDict @@ -334,7 +335,11 @@ class ExtractLook(openpype.api.Extractor): transfers = [] hardlinks = [] hashes = {} - force_copy = instance.data.get("forceCopy", False) + # Temporary fix to NOT create hardlinks on windows machines + if platform.system().lower() == "windows": + force_copy = True + else: + force_copy = instance.data.get("forceCopy", False) for filepath in files_metadata: From 58085d3b4ed15ac8506b2a86ae402e68be176f51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 30 Mar 2022 16:31:41 +0200 Subject: [PATCH 105/180] removed unused function --- openpype/modules/ftrack/lib/avalon_sync.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 5301ec568e..c5b58ca94d 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -286,21 +286,6 @@ def from_dict_to_set(data, is_project): return result -def get_avalon_project_template(project_name): - """Get avalon template - Args: - project_name: (string) - Returns: - dictionary with templates - """ - templates = Anatomy(project_name).templates - return { - "workfile": templates["avalon"]["workfile"], - "work": templates["avalon"]["work"], - "publish": templates["avalon"]["publish"] - } - - def get_project_apps(in_app_list): """ Application definitions for app name. From 8940830787fc68d043febecd26040d944e5d91d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 30 Mar 2022 16:38:45 +0200 Subject: [PATCH 106/180] added info log about changing hardlink --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 137683ca6d..6fcc308f78 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -337,6 +337,9 @@ class ExtractLook(openpype.api.Extractor): hashes = {} # Temporary fix to NOT create hardlinks on windows machines if platform.system().lower() == "windows": + self.log.info( + "Forcing copy instead of hardlink due to issues on Windows..." + ) force_copy = True else: force_copy = instance.data.get("forceCopy", False) From 053748ac2319468a4071db41e3818d4923e105c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 30 Mar 2022 18:54:05 +0200 Subject: [PATCH 107/180] handling validation and hero versions --- .../publish/collect_original_basename.py | 18 ++++++++++++++++++ .../help/validate_simple_texture_naming.xml | 17 +++++++++++++++++ .../validate_simple_unreal_texture_naming.py | 10 +++++++++- .../plugins/publish/integrate_hero_version.py | 5 +++++ openpype/plugins/publish/integrate_new.py | 13 +++++++++++++ .../defaults/project_anatomy/templates.json | 10 ++++++++-- .../defaults/project_settings/global.json | 17 +++++++++++++++-- 7 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py new file mode 100644 index 0000000000..b83a924d33 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_original_basename.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Collect original base name for use in templates.""" +from pathlib import Path + +import pyblish.api + + +class CollectOriginalBasename(pyblish.api.InstancePlugin): + """Collect original file base name.""" + + order = pyblish.api.CollectorOrder + 0.498 + label = "Collect Base Name" + hosts = ["standalonepublisher"] + families = ["simpleUnrealTexture"] + + def process(self, instance): + file_name = Path(instance.data["representations"][0]["files"]) + instance.data["originalBasename"] = file_name.stem diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml new file mode 100644 index 0000000000..1818748407 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml @@ -0,0 +1,17 @@ + + + +Invalid texture name + +## Source files not found + +Submitted file has invalid name: +'{invalid_file}' + +### How to repair? + + Texture file must adhere to naming conventions for Unreal: + T_[ASSET_NAME}_*.ext + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py index 158a749075..05f38159c1 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py @@ -2,6 +2,7 @@ """Validator for correct file naming.""" import pyblish.api import openpype.api +import re from openpype.pipeline import PublishXmlValidationError @@ -10,6 +11,13 @@ class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin): hosts = ["standalonepublisher"] families = ["simpleUnrealTexture"] order = openpype.api.ValidateContentsOrder + regex = "^T_{asset}.*" def process(self, instance): - ... \ No newline at end of file + file_name = instance.data.get("originalBasename") + self.log.info(file_name) + pattern = self.regex.format(asset=instance.data.get("asset")) + if not re.match(pattern, file_name): + msg = f"Invalid file name {file_name}" + raise PublishXmlValidationError( + self, msg, formatting_data={"invalid_file": file_name}) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index d6df6535d8..ded149bdd0 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -485,6 +485,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] template_data = copy.deepcopy(instance.data["anatomyData"]) + if "originalBasename" in instance.data: + template_data.update({ + "originalBasename": instance.data.get("originalBasename") + }) + if "folder" in anatomy.templates[template_key]: anatomy_filled = anatomy.format(template_data) publish_folder = anatomy_filled[template_key]["folder"] diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 4025f18cb2..a3b9f0ef4a 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -356,6 +356,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if profile: template_name = profile["template_name"] + + published_representations = {} for idx, repre in enumerate(instance.data["representations"]): # reset transfers for next representation @@ -384,6 +386,11 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if resolution_width: template_data["fps"] = fps + if "originalBasename" in instance.data: + template_data.update({ + "originalBasename": instance.data.get("originalBasename") + }) + files = repre['files'] if repre.get('stagingDir'): stagingdir = repre['stagingDir'] @@ -555,6 +562,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre['published_path'] = dst self.log.debug("__ dst: {}".format(dst)) + if not instance.data.get("publishDir"): + instance.data["publishDir"] = ( + anatomy_filled + [template_name] + ["folder"] + ) if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 45611f55b1..4be923da11 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -33,12 +33,18 @@ "path": "{@folder}/{@file}" }, "others": { + "simpleUnrealTextureHero": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/hero", + "file": "{originalBasename}.{ext}", + "path": "{@folder}/{@file}" + }, "simpleUnrealTexture": { - "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}", - "file": "{original_file}", + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{@version}", + "file": "{originalBasename}_{@version}.{ext}", "path": "{@folder}/{@file}" }, "__dynamic_keys_labels__": { + "simpleUnrealTextureHero": "Simple Unreal Texture - Hero", "simpleUnrealTexture": "Simple Unreal Texture" } } diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0bfd571a79..5443293c93 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -213,9 +213,22 @@ "animation", "setdress", "layout", - "mayaScene" + "mayaScene", + "simpleUnrealTexture" ], - "template_name_profiles": [] + "template_name_profiles": [ + { + "families": [ + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTextureHero" + } + ] }, "CleanUp": { "paterns": [], From 34f5f7d60c2692d99448d535ba0c4a688b844b59 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 31 Mar 2022 10:13:13 +0200 Subject: [PATCH 108/180] fix validator message --- .../plugins/publish/help/validate_simple_texture_naming.xml | 4 ++-- .../plugins/publish/validate_simple_unreal_texture_naming.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml index 1818748407..b65d274fe5 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_simple_texture_naming.xml @@ -3,7 +3,7 @@ Invalid texture name -## Source files not found +## Invalid file name Submitted file has invalid name: '{invalid_file}' @@ -11,7 +11,7 @@ Submitted file has invalid name: ### How to repair? Texture file must adhere to naming conventions for Unreal: - T_[ASSET_NAME}_*.ext + T_{asset}_*.ext \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py index 05f38159c1..ef8da9f280 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_simple_unreal_texture_naming.py @@ -20,4 +20,7 @@ class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin): if not re.match(pattern, file_name): msg = f"Invalid file name {file_name}" raise PublishXmlValidationError( - self, msg, formatting_data={"invalid_file": file_name}) + self, msg, formatting_data={ + "invalid_file": file_name, + "asset": instance.data.get("asset") + }) From 1f1b15e2fcf07b117c34d6e429e7d92677d8dd66 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 11:03:42 +0200 Subject: [PATCH 109/180] hide completer using timer --- openpype/tools/settings/settings/widgets.py | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b3518cffd9..6db001f2f6 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -228,10 +228,18 @@ class SettingsLineEdit(PlaceholderLineEdit): def __init__(self, *args, **kwargs): super(SettingsLineEdit, self).__init__(*args, **kwargs) - self._completer = None + # Timer which will get started on focus in and stopped on focus out + # - callback checks if line edit or completer have focus + # and hide completer if not + focus_timer = QtCore.QTimer() + focus_timer.setInterval(50) + focus_timer.timeout.connect(self._on_focus_timer) self.textChanged.connect(self._on_text_change) + self._completer = None + self._focus_timer = focus_timer + def _on_text_change(self, text): if self._completer is not None: self._completer.set_text_filter(text) @@ -243,19 +251,19 @@ class SettingsLineEdit(PlaceholderLineEdit): new_point = self.mapToGlobal(point) self._completer.move(new_point) + def _on_focus_timer(self): + if not self.hasFocus() and not self._completer.hasFocus(): + self._completer.hide() + self._focus_timer.stop() + def focusInEvent(self, event): super(SettingsLineEdit, self).focusInEvent(event) self.focused_in.emit() - if self._completer is None: - return - self._completer.show() - self._update_completer() - - def focusOutEvent(self, event): - super(SettingsLineEdit, self).focusOutEvent(event) if self._completer is not None: - self._completer.hide() + self._focus_timer.start() + self._completer.show() + self._update_completer() def paintEvent(self, event): super(SettingsLineEdit, self).paintEvent(event) From 4891f336ed50ec51ab8d3736c5af483f08334acc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 11:09:57 +0200 Subject: [PATCH 110/180] Fix - handle better exception when no file opened in AE Occured when no scene was opened and Workfile wanted to open existing workfile --- openpype/hosts/aftereffects/api/workio.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/aftereffects/api/workio.py b/openpype/hosts/aftereffects/api/workio.py index 5a8f86ead5..70815bda6b 100644 --- a/openpype/hosts/aftereffects/api/workio.py +++ b/openpype/hosts/aftereffects/api/workio.py @@ -5,14 +5,6 @@ from openpype.pipeline import HOST_WORKFILE_EXTENSIONS from .launch_logic import get_stub -def _active_document(): - document_name = get_stub().get_active_document_name() - if not document_name: - return None - - return document_name - - def file_extensions(): return HOST_WORKFILE_EXTENSIONS["aftereffects"] @@ -39,7 +31,8 @@ def current_file(): full_name = get_stub().get_active_document_full_name() if full_name and full_name != "null": return os.path.normpath(full_name).replace("\\", "/") - except Exception: + except ValueError: + print("Nothing opened") pass return None @@ -47,3 +40,15 @@ def current_file(): def work_root(session): return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") + + +def _active_document(): + # TODO merge with current_file - even in extension + document_name = None + try: + document_name = get_stub().get_active_document_name() + except ValueError: + print("Nothing opened") + pass + + return document_name \ No newline at end of file From d6b423e2082f0cc3e0cff7a4b3d8730ca44533d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Mar 2022 11:15:34 +0200 Subject: [PATCH 111/180] Change Pype to OpenPype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 2820091bcc..7fc7e63e61 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -24,7 +24,7 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") def install(): - """Install Pype to Avalon.""" + """Install OpenPype to Avalon.""" import avalon.api import pyblish.api from pyblish.lib import MessageHandler From 1499fecc73af170f095b1bdd3bd244e63413a54d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 11:18:32 +0200 Subject: [PATCH 112/180] fix thumbnail discovery --- openpype/pipeline/thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 47452b21e7..c09dab70eb 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -131,7 +131,7 @@ class BinaryThumbnail(ThumbnailResolver): # Thumbnail resolvers def discover_thumbnail_resolvers(): - return discover(ThumbnailResolver).plugins + return discover(ThumbnailResolver) def register_thumbnail_resolver(plugin): From f486cef9251f18dcdfb390e8cfbaa840c6cda461 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Mar 2022 12:03:48 +0200 Subject: [PATCH 113/180] Update manager_ftrack.md Update Ftrack Documentation URL --- website/docs/manager_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/manager_ftrack.md b/website/docs/manager_ftrack.md index 730c57d1f9..b5ca167838 100644 --- a/website/docs/manager_ftrack.md +++ b/website/docs/manager_ftrack.md @@ -4,7 +4,7 @@ title: Ftrack sidebar_label: Project Manager --- -Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/articles/1040483-creating-a-new-project). +Ftrack is currently the main project management option for OpenPype. This documentation assumes that you are familiar with Ftrack and it's basic principles. If you're new to Ftrack, we recommend having a thorough look at [Ftrack Official Documentation](https://help.ftrack.com/en/). ## Project management Setting project attributes is the key to properly working pipeline. From eac1394be8b1644468f8b6427c5abe37bedd3f97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 12:12:56 +0200 Subject: [PATCH 114/180] hide published checkbox if save is not enabled --- openpype/tools/workfiles/files_widget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 1faafe2bdb..1f93d15e22 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -305,6 +305,9 @@ class FilesWidget(QtWidgets.QWidget): def set_save_enabled(self, enabled): self._btn_save.setEnabled(enabled) + if not enabled and self._published_checkbox.isChecked(): + self._published_checkbox.setChecked(False) + self._published_checkbox.setVisible(enabled) def set_asset_task(self, asset_id, task_name, task_type): if asset_id != self._asset_id: From 8e382b9c52feb0bcc45c235a5121088552e7d01c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 12:17:19 +0200 Subject: [PATCH 115/180] removed View option from published files --- openpype/tools/workfiles/files_widget.py | 46 +--- openpype/tools/workfiles/lib.py | 272 ----------------------- openpype/tools/workfiles/window.py | 65 ++---- 3 files changed, 27 insertions(+), 356 deletions(-) delete mode 100644 openpype/tools/workfiles/lib.py diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 1f93d15e22..80a94cc1bd 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -26,7 +26,6 @@ from .model import ( DATE_MODIFIED_ROLE, ) from .save_as_dialog import SaveAsDialog -from .lib import TempPublishFiles log = logging.getLogger(__name__) @@ -49,7 +48,6 @@ class FilesWidget(QtWidgets.QWidget): """A widget displaying files that allows to save and open files.""" file_selected = QtCore.Signal(str) file_opened = QtCore.Signal() - publish_file_viewed = QtCore.Signal() workfile_created = QtCore.Signal(str) published_visible_changed = QtCore.Signal(bool) @@ -71,9 +69,6 @@ class FilesWidget(QtWidgets.QWidget): self._workfiles_root = None self._workdir_path = None self.host = api.registered_host() - temp_publish_files = TempPublishFiles() - temp_publish_files.cleanup() - self._temp_publish_files = temp_publish_files # Whether to automatically select the latest modified # file on a refresh of the files model. @@ -168,15 +163,14 @@ class FilesWidget(QtWidgets.QWidget): workarea_btns_layout.addWidget(btn_save, 1) publish_btns_widget = QtWidgets.QWidget(btns_widget) - btn_view_published = QtWidgets.QPushButton("View", publish_btns_widget) btn_save_as_published = QtWidgets.QPushButton( - "Save As", publish_btns_widget + "Copy & Open", publish_btns_widget ) - btn_save_as_to_published = QtWidgets.QPushButton( - "Save As (to context)", publish_btns_widget + btn_change_context = QtWidgets.QPushButton( + "Choose different context", publish_btns_widget ) btn_select_context_published = QtWidgets.QPushButton( - "Select context", publish_btns_widget + "Copy & Open", publish_btns_widget ) btn_cancel_published = QtWidgets.QPushButton( "Cancel", publish_btns_widget @@ -184,9 +178,8 @@ class FilesWidget(QtWidgets.QWidget): publish_btns_layout = QtWidgets.QHBoxLayout(publish_btns_widget) publish_btns_layout.setContentsMargins(0, 0, 0, 0) - publish_btns_layout.addWidget(btn_view_published, 1) publish_btns_layout.addWidget(btn_save_as_published, 1) - publish_btns_layout.addWidget(btn_save_as_to_published, 1) + publish_btns_layout.addWidget(btn_change_context, 1) publish_btns_layout.addWidget(btn_cancel_published, 1) publish_btns_layout.addWidget(btn_select_context_published, 1) @@ -215,19 +208,15 @@ class FilesWidget(QtWidgets.QWidget): workarea_files_view.selectionModel().selectionChanged.connect( self.on_file_select ) - publish_files_view.doubleClickedLeft.connect( - self._on_published_view_pressed - ) btn_open.pressed.connect(self._on_workarea_open_pressed) btn_browse.pressed.connect(self.on_browse_pressed) btn_save.pressed.connect(self._on_save_as_pressed) - btn_view_published.pressed.connect(self._on_published_view_pressed) btn_save_as_published.pressed.connect( self._on_published_save_as_pressed ) - btn_save_as_to_published.pressed.connect( - self._on_publish_save_as_to_pressed + btn_change_context.pressed.connect( + self._on_publish_change_context_pressed ) btn_select_context_published.pressed.connect( self._on_publish_select_context_pressed @@ -256,9 +245,8 @@ class FilesWidget(QtWidgets.QWidget): self._btn_browse = btn_browse self._btn_save = btn_save - self._btn_view_published = btn_view_published self._btn_save_as_published = btn_save_as_published - self._btn_save_as_to_published = btn_save_as_to_published + self._btn_change_context = btn_change_context self._btn_select_context_published = btn_select_context_published self._btn_cancel_published = btn_cancel_published @@ -323,9 +311,8 @@ class FilesWidget(QtWidgets.QWidget): self._asset_id, self._task_name ) has_valid_items = self._publish_files_model.has_valid_items() - self._btn_view_published.setEnabled(has_valid_items) self._btn_save_as_published.setEnabled(has_valid_items) - self._btn_save_as_to_published.setEnabled(has_valid_items) + self._btn_change_context.setEnabled(has_valid_items) else: # Define a custom session so we can query the work root @@ -587,16 +574,6 @@ class FilesWidget(QtWidgets.QWidget): else: self.refresh() - def _on_published_view_pressed(self): - filepath = self._get_selected_filepath() - if not filepath or not os.path.exists(filepath): - return - item = self._temp_publish_files.add_file(filepath) - self.host.open_file(item.filepath) - self.publish_file_viewed.emit() - # Change state back to workarea - self._published_checkbox.setChecked(False) - def _on_published_save_as_pressed(self): self._save_as_with_dialog() @@ -611,9 +588,8 @@ class FilesWidget(QtWidgets.QWidget): bool(self._asset_id) and bool(self._task_name) ) - self._btn_view_published.setVisible(not enabled) self._btn_save_as_published.setVisible(not enabled) - self._btn_save_as_to_published.setVisible(not enabled) + self._btn_change_context.setVisible(not enabled) # Change views and disable workarea view if enabled self._workarea_files_view.setEnabled(not enabled) @@ -628,7 +604,7 @@ class FilesWidget(QtWidgets.QWidget): self._published_checkbox.setEnabled(not enabled) self._filter_input.setEnabled(not enabled) - def _on_publish_save_as_to_pressed(self): + def _on_publish_change_context_pressed(self): self._set_publish_context_select_mode(True) def _on_publish_select_context_pressed(self): diff --git a/openpype/tools/workfiles/lib.py b/openpype/tools/workfiles/lib.py deleted file mode 100644 index 21a7485b7b..0000000000 --- a/openpype/tools/workfiles/lib.py +++ /dev/null @@ -1,272 +0,0 @@ -import os -import shutil -import uuid -import time -import json -import logging -import contextlib - -import appdirs - - -class TempPublishFilesItem(object): - """Object representing copied workfile in app temp folder. - - Args: - item_id (str): Id of item used as subfolder. - data (dict): Metadata about temp files. - directory (str): Path to directory where files are copied to. - """ - - def __init__(self, item_id, data, directory): - self._id = item_id - self._directory = directory - self._filepath = os.path.join(directory, data["filename"]) - - @property - def directory(self): - return self._directory - - @property - def filepath(self): - return self._filepath - - @property - def id(self): - return self._id - - @property - def size(self): - if os.path.exists(self.filepath): - s = os.stat(self.filepath) - return s.st_size - return 0 - - -class TempPublishFiles(object): - """Directory where published workfiles are copied when opened. - - Directory is located in appdirs on the machine. Folder contains file - with metadata about stored files. Each item in metadata has id, filename - and expiration time. When expiration time is higher then current time the - item is removed from metadata and it's files are deleted. Files of items - are stored in subfolder named by item's id. - - Metadata file can be in theory opened and modified by multiple processes, - threads at one time. For those cases is created simple lock file which - is created before modification begins and is removed when modification - ends. Existence of the file means that it should not be modified by - any other process at the same time. - - Metadata example: - ``` - { - "96050b4a-8974-4fca-8179-7c446c478d54": { - "created": 1647880725.555, - "expiration": 1647884325.555, - "filename": "cg_pigeon_workfileModeling_v025.ma" - }, - ... - } - ``` - - ## Why is this needed - Combination of more issues. Temp files are not automatically removed by - OS on windows so using tempfiles in TEMP would lead to kill disk space of - machine. There are also cases when someone wants to open multiple files - in short period of time and want to manually remove those files so keeping - track of temporary copied files in pre-defined structure is needed. - """ - minute_in_seconds = 60 - hour_in_seconds = 60 * minute_in_seconds - day_in_seconds = 24 * hour_in_seconds - - def __init__(self): - root_dir = appdirs.user_data_dir( - "published_workfiles_temp", "openpype" - ) - if not os.path.exists(root_dir): - os.makedirs(root_dir) - - metadata_path = os.path.join(root_dir, "metadata.json") - lock_path = os.path.join(root_dir, "lock.json") - - self._root_dir = root_dir - self._metadata_path = metadata_path - self._lock_path = lock_path - self._log = None - - @property - def log(self): - if self._log is None: - self._log = logging.getLogger(self.__class__.__name__) - return self._log - - @property - def life_time(self): - """How long will be new item kept in temp in seconds. - - Returns: - int: Lifetime of temp item. - """ - return int(self.hour_in_seconds) - - @property - def size(self): - """File size of existing items.""" - size = 0 - for item in self.get_items(): - size += item.size - return size - - def add_file(self, src_path): - """Add workfile to temp directory. - - This will create new item and source path is copied to it's directory. - """ - filename = os.path.basename(src_path) - - item_id = str(uuid.uuid4()) - dst_dirpath = os.path.join(self._root_dir, item_id) - if not os.path.exists(dst_dirpath): - os.makedirs(dst_dirpath) - - dst_path = os.path.join(dst_dirpath, filename) - shutil.copy(src_path, dst_path) - - now = time.time() - item_data = { - "filename": filename, - "expiration": now + self.life_time, - "created": now - } - with self._modify_data() as data: - data[item_id] = item_data - - return TempPublishFilesItem(item_id, item_data, dst_dirpath) - - @contextlib.contextmanager - def _modify_data(self): - """Create lock file when data in metadata file are modified.""" - start_time = time.time() - timeout = 3 - while os.path.exists(self._lock_path): - time.sleep(0.01) - if start_time > timeout: - self.log.warning(( - "Waited for {} seconds to free lock file. Overriding lock." - ).format(timeout)) - - with open(self._lock_path, "w") as stream: - json.dump({"pid": os.getpid()}, stream) - - try: - data = self._get_data() - yield data - with open(self._metadata_path, "w") as stream: - json.dump(data, stream) - - finally: - os.remove(self._lock_path) - - def _get_data(self): - output = {} - if not os.path.exists(self._metadata_path): - return output - - try: - with open(self._metadata_path, "r") as stream: - output = json.load(stream) - except Exception: - self.log.warning("Failed to read metadata file.", exc_info=True) - return output - - def cleanup(self, check_expiration=True): - """Cleanup files based on metadata. - - Items that passed expiration are removed when this is called. Or all - files are removed when `check_expiration` is set to False. - - Args: - check_expiration (bool): All items and files are removed when set - to True. - """ - data = self._get_data() - now = time.time() - remove_ids = set() - all_ids = set() - for item_id, item_data in data.items(): - all_ids.add(item_id) - if check_expiration and now < item_data["expiration"]: - continue - - remove_ids.add(item_id) - - for item_id in remove_ids: - try: - self.remove_id(item_id) - except Exception: - self.log.warning( - "Failed to remove temp publish item \"{}\"".format( - item_id - ), - exc_info=True - ) - - # Remove unknown folders/files - for filename in os.listdir(self._root_dir): - if filename in all_ids: - continue - - full_path = os.path.join(self._root_dir, filename) - if full_path in (self._metadata_path, self._lock_path): - continue - - try: - shutil.rmtree(full_path) - except Exception: - self.log.warning( - "Couldn't remove arbitrary path \"{}\"".format(full_path), - exc_info=True - ) - - def clear(self): - self.cleanup(False) - - def get_items(self): - """Receive all items from metadata file. - - Returns: - list: Info about each item in metadata. - """ - output = [] - data = self._get_data() - for item_id, item_data in data.items(): - item_path = os.path.join(self._root_dir, item_id) - output.append(TempPublishFilesItem(item_id, item_data, item_path)) - return output - - def remove_id(self, item_id): - """Remove files of item and then remove the item from metadata.""" - filepath = os.path.join(self._root_dir, item_id) - if os.path.exists(filepath): - shutil.rmtree(filepath) - - with self._modify_data() as data: - data.pop(item_id, None) - - -def file_size_to_string(file_size): - size = 0 - size_ending_mapping = { - "KB": 1024 ** 1, - "MB": 1024 ** 2, - "GB": 1024 ** 3 - } - ending = "B" - for _ending, _size in size_ending_mapping.items(): - if file_size < _size: - break - size = file_size / _size - ending = _ending - return "{:.2f} {}".format(size, ending) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 8654a18036..73e63d30b5 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -14,7 +14,22 @@ from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from openpype.tools.utils.tasks_widget import TasksWidget from .files_widget import FilesWidget -from .lib import TempPublishFiles, file_size_to_string + + +def file_size_to_string(file_size): + size = 0 + size_ending_mapping = { + "KB": 1024 ** 1, + "MB": 1024 ** 2, + "GB": 1024 ** 3 + } + ending = "B" + for _ending, _size in size_ending_mapping.items(): + if file_size < _size: + break + size = file_size / _size + ending = _ending + return "{:.2f} {}".format(size, ending) class SidePanelWidget(QtWidgets.QWidget): @@ -44,67 +59,25 @@ class SidePanelWidget(QtWidgets.QWidget): btn_note_save, 0, alignment=QtCore.Qt.AlignRight ) - publish_temp_widget = QtWidgets.QWidget(self) - publish_temp_info_label = QtWidgets.QLabel( - self.published_workfile_message.format( - file_size_to_string(0) - ), - publish_temp_widget - ) - publish_temp_info_label.setWordWrap(True) - - btn_clear_temp = QtWidgets.QPushButton( - "Clear temp", publish_temp_widget - ) - - publish_temp_layout = QtWidgets.QVBoxLayout(publish_temp_widget) - publish_temp_layout.setContentsMargins(0, 0, 0, 0) - publish_temp_layout.addWidget(publish_temp_info_label, 0) - publish_temp_layout.addWidget( - btn_clear_temp, 0, alignment=QtCore.Qt.AlignRight - ) - main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(details_label, 0) main_layout.addWidget(details_input, 1) main_layout.addWidget(artist_note_widget, 1) - main_layout.addWidget(publish_temp_widget, 0) note_input.textChanged.connect(self._on_note_change) btn_note_save.clicked.connect(self._on_save_click) - btn_clear_temp.clicked.connect(self._on_clear_temp_click) self._details_input = details_input self._artist_note_widget = artist_note_widget self._note_input = note_input self._btn_note_save = btn_note_save - self._publish_temp_info_label = publish_temp_info_label - self._publish_temp_widget = publish_temp_widget - self._orig_note = "" self._workfile_doc = None - publish_temp_widget.setVisible(False) - def set_published_visible(self, published_visible): self._artist_note_widget.setVisible(not published_visible) - self._publish_temp_widget.setVisible(published_visible) - if published_visible: - self.refresh_publish_temp_sizes() - - def refresh_publish_temp_sizes(self): - temp_publish_files = TempPublishFiles() - text = self.published_workfile_message.format( - file_size_to_string(temp_publish_files.size) - ) - self._publish_temp_info_label.setText(text) - - def _on_clear_temp_click(self): - temp_publish_files = TempPublishFiles() - temp_publish_files.clear() - self.refresh_publish_temp_sizes() def _on_note_change(self): text = self._note_input.toPlainText() @@ -225,9 +198,6 @@ class Window(QtWidgets.QMainWindow): files_widget.file_selected.connect(self.on_file_select) files_widget.workfile_created.connect(self.on_workfile_create) files_widget.file_opened.connect(self._on_file_opened) - files_widget.publish_file_viewed.connect( - self._on_publish_file_viewed - ) files_widget.published_visible_changed.connect( self._on_published_change ) @@ -292,9 +262,6 @@ class Window(QtWidgets.QMainWindow): def _on_file_opened(self): self.close() - def _on_publish_file_viewed(self): - self.side_panel.refresh_publish_temp_sizes() - def _on_published_change(self, visible): self.side_panel.set_published_visible(visible) From 34656d9b79536155eca3a5729cf6d0b449c6633d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 12:29:29 +0200 Subject: [PATCH 116/180] fix ampresand in button label --- openpype/tools/workfiles/files_widget.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 80a94cc1bd..f29223b321 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -95,7 +95,7 @@ class FilesWidget(QtWidgets.QWidget): extensions = set(self.host.file_extensions()) views_widget = QtWidgets.QWidget(self) - # Workarea view + # --- Workarea view --- workarea_files_model = WorkAreaFilesModel(extensions) # Create proxy model for files to be able sort and filter @@ -113,13 +113,14 @@ class FilesWidget(QtWidgets.QWidget): # Date modified delegate workarea_time_delegate = PrettyTimeDelegate() workarea_files_view.setItemDelegateForColumn(1, workarea_time_delegate) - workarea_files_view.setIndentation(3) # smaller indentation + # smaller indentation + workarea_files_view.setIndentation(3) # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. workarea_files_view.setColumnWidth(0, 330) - # Publish files view + # --- Publish files view --- publish_files_model = PublishFilesModel(extensions, io, self.anatomy) publish_proxy_model = QtCore.QSortFilterProxyModel() @@ -136,7 +137,8 @@ class FilesWidget(QtWidgets.QWidget): # Date modified delegate publish_time_delegate = PrettyTimeDelegate() publish_files_view.setItemDelegateForColumn(1, publish_time_delegate) - publish_files_view.setIndentation(3) # smaller indentation + # smaller indentation + publish_files_view.setIndentation(3) # Default to a wider first filename column it is what we mostly care # about and the date modified is relatively small anyway. @@ -164,13 +166,13 @@ class FilesWidget(QtWidgets.QWidget): publish_btns_widget = QtWidgets.QWidget(btns_widget) btn_save_as_published = QtWidgets.QPushButton( - "Copy & Open", publish_btns_widget + "Copy && Open", publish_btns_widget ) btn_change_context = QtWidgets.QPushButton( "Choose different context", publish_btns_widget ) btn_select_context_published = QtWidgets.QPushButton( - "Copy & Open", publish_btns_widget + "Copy && Open", publish_btns_widget ) btn_cancel_published = QtWidgets.QPushButton( "Cancel", publish_btns_widget From aa8438a6ad2e947038af43bef93d1dd686267aea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 12:29:50 +0200 Subject: [PATCH 117/180] define extensions of save as dialog with argument --- openpype/tools/workfiles/files_widget.py | 7 +++++++ openpype/tools/workfiles/save_as_dialog.py | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index f29223b321..6e90dea982 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -449,11 +449,18 @@ class FilesWidget(QtWidgets.QWidget): """ session = self._get_session() + if self.published_enabled: + filepath = self._get_selected_filepath() + extensions = [os.path.splitext(filepath)[1]] + else: + extensions = self.host.file_extensions() + window = SaveAsDialog( parent=self, root=self._workfiles_root, anatomy=self.anatomy, template_key=self.template_key, + extensions=extensions, session=session ) window.exec_() diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index e616a325cc..f5ae393d0f 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -193,7 +193,9 @@ class SaveAsDialog(QtWidgets.QDialog): """ - def __init__(self, parent, root, anatomy, template_key, session=None): + def __init__( + self, parent, root, anatomy, template_key, extensions, session=None + ): super(SaveAsDialog, self).__init__(parent=parent) self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) @@ -201,6 +203,7 @@ class SaveAsDialog(QtWidgets.QDialog): self.host = api.registered_host() self.root = root self.work_file = None + self._extensions = extensions if not session: # Fallback to active session @@ -257,7 +260,7 @@ class SaveAsDialog(QtWidgets.QDialog): # Add styled delegate to use stylesheets ext_delegate = QtWidgets.QStyledItemDelegate() ext_combo.setItemDelegate(ext_delegate) - ext_combo.addItems(self.host.file_extensions()) + ext_combo.addItems(self._extensions) # Build inputs inputs_layout = QtWidgets.QFormLayout(inputs_widget) @@ -336,7 +339,7 @@ class SaveAsDialog(QtWidgets.QDialog): def get_existing_comments(self): matcher = CommentMatcher(self.anatomy, self.template_key, self.data) - host_extensions = set(self.host.file_extensions()) + host_extensions = set(self._extensions) comments = set() if os.path.isdir(self.root): for fname in os.listdir(self.root): @@ -392,7 +395,7 @@ class SaveAsDialog(QtWidgets.QDialog): return anatomy_filled[self.template_key]["file"] def refresh(self): - extensions = self.host.file_extensions() + extensions = list(self._extensions) extension = self.data["ext"] if extension is None: # Define saving file extension From f630b8cb7d2c76d512c9dde35ba0a620cb7d1237 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 31 Mar 2022 12:46:24 +0200 Subject: [PATCH 118/180] fix setuptools --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3ee6ad43ea..5739356084 100644 --- a/setup.py +++ b/setup.py @@ -187,5 +187,6 @@ setup( "build_dir": (openpype_root / "docs" / "build").as_posix() } }, - executables=executables + executables=executables, + package_dir=[] ) From 790850697f4742081779bccdc8230c1c04100f39 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 13:16:52 +0200 Subject: [PATCH 119/180] OP-2517 - fix renaming subset incorrectly in PS --- .../hosts/photoshop/plugins/publish/validate_naming.py | 9 +++++---- .../settings/defaults/project_settings/photoshop.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index b40e44d016..583e9c7a4e 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -29,7 +29,8 @@ class ValidateNamingRepair(pyblish.api.Action): stub = photoshop.stub() for instance in instances: self.log.info("validate_naming instance {}".format(instance)) - metadata = stub.read(instance[0]) + layer_item = instance.data["layer"] + metadata = stub.read(layer_item) self.log.info("metadata instance {}".format(metadata)) layer_name = None if metadata.get("uuid"): @@ -43,11 +44,11 @@ class ValidateNamingRepair(pyblish.api.Action): stub.rename_layer(instance.data["uuid"], layer_name) subset_name = re.sub(invalid_chars, replace_char, - instance.data["name"]) + instance.data["subset"]) - instance[0].Name = layer_name or subset_name + layer_item.name = layer_name or subset_name metadata["subset"] = subset_name - stub.imprint(instance[0], metadata) + stub.imprint(layer_item, metadata) return True diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 118b9c721e..822a94a8eb 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -18,7 +18,7 @@ "active": true }, "ValidateNaming": { - "invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,]", + "invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,;]", "replace_char": "_" }, "ExtractImage": { From 5b81a43c7c7db30171d8f4110018f3a17e77f2b8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 31 Mar 2022 13:31:18 +0200 Subject: [PATCH 120/180] print log if build fails --- tools/build.ps1 | 3 +++ tools/build.sh | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 10da3d0b83..ff28544954 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -180,6 +180,9 @@ $out = & "$($env:POETRY_HOME)\bin\poetry" run python setup.py build 2>&1 Set-Content -Path "$($openpype_root)\build\build.log" -Value $out if ($LASTEXITCODE -ne 0) { + Write-Host "------------------------------------------" -ForegroundColor Red + Get-Content "$($openpype_root)\build\build.log" + Write-Host "------------------------------------------" -ForegroundColor Red Write-Host "!!! " -NoNewLine -ForegroundColor Red Write-Host "Build failed. Check the log: " -NoNewline Write-Host ".\build\build.log" -ForegroundColor Yellow diff --git a/tools/build.sh b/tools/build.sh index 301f26023a..79fb748cd5 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -185,9 +185,9 @@ if [ "$disable_submodule_update" == 1 ]; then fi echo -e "${BIGreen}>>>${RST} Building ..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then - "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build &> "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build &> "$openpype_root/build/build.log" || { echo -e "${BIRed}------------------------------------------${RST}"; cat "$openpype_root/build/build.log"; echo -e "${BIRed}------------------------------------------${RST}"; echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; } elif [[ "$OSTYPE" == "darwin"* ]]; then - "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac &> "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac &> "$openpype_root/build/build.log" || { echo -e "${BIRed}------------------------------------------${RST}"; cat "$openpype_root/build/build.log"; echo -e "${BIRed}------------------------------------------${RST}"; echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; } fi "$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py" From 67bc56c8bca8dde59b7b56e4823b437d3c547e5d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 31 Mar 2022 14:12:38 +0200 Subject: [PATCH 121/180] fix argument --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5739356084..bf42602b52 100644 --- a/setup.py +++ b/setup.py @@ -188,5 +188,5 @@ setup( } }, executables=executables, - package_dir=[] + packages=[] ) From 37900da59d1c5ba718c20cc3266c3bb62c9ee987 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 14:32:40 +0200 Subject: [PATCH 122/180] added overlay guiding to select context --- openpype/style/style.css | 8 ++++++ openpype/tools/workfiles/files_widget.py | 31 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index df83600973..b5f6962eee 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1269,6 +1269,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: #21252B; } +/* Workfiles */ +#WorkfilesPublishedContextSelect { + background: rgba(0, 0, 0, 127); +} +#WorkfilesPublishedContextSelect QLabel { + font-size: 17pt; +} + /* Tray */ #TrayRestartButton { background: {color:restart-btn-bg}; diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 6e90dea982..55abd39b36 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -44,6 +44,31 @@ class FilesView(QtWidgets.QTreeView): return super(FilesView, self).mouseDoubleClickEvent(event) +class SelectContextOverlay(QtWidgets.QFrame): + def __init__(self, parent): + super(SelectContextOverlay, self).__init__(parent) + + self.setObjectName("WorkfilesPublishedContextSelect") + label_widget = QtWidgets.QLabel( + "Please select context on Left side
<", + self + ) + label_widget.setAlignment(QtCore.Qt.AlignCenter) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + parent.installEventFilter(self) + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.Resize: + self.resize(obj.size()) + + return super(SelectContextOverlay, self).eventFilter(obj, event) + + class FilesWidget(QtWidgets.QWidget): """A widget displaying files that allows to save and open files.""" file_selected = QtCore.Signal(str) @@ -144,6 +169,9 @@ class FilesWidget(QtWidgets.QWidget): # about and the date modified is relatively small anyway. publish_files_view.setColumnWidth(0, 330) + publish_context_overlay = SelectContextOverlay(views_widget) + publish_context_overlay.setVisible(False) + views_layout = QtWidgets.QHBoxLayout(views_widget) views_layout.setContentsMargins(0, 0, 0, 0) views_layout.addWidget(workarea_files_view, 1) @@ -241,6 +269,8 @@ class FilesWidget(QtWidgets.QWidget): self._publish_files_model = publish_files_model self._publish_proxy_model = publish_proxy_model + self._publish_context_overlay = publish_context_overlay + self._workarea_btns_widget = workarea_btns_widget self._publish_btns_widget = publish_btns_widget self._btn_open = btn_open @@ -590,6 +620,7 @@ class FilesWidget(QtWidgets.QWidget): self._publish_context_select_mode = enabled # Show buttons related to context selection + self._publish_context_overlay.setVisible(enabled) self._btn_cancel_published.setVisible(enabled) self._btn_select_context_published.setVisible(enabled) # Change enabled state based on select context From 14da7f74c6ddedc4eeaa9fc3fb5b093bf630b2e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 14:33:02 +0200 Subject: [PATCH 123/180] don't cancel save as publishing on cancel save as dialog --- openpype/tools/workfiles/files_widget.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 55abd39b36..6cfd3dd651 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -554,7 +554,7 @@ class FilesWidget(QtWidgets.QWidget): def _save_as_with_dialog(self): work_filename = self.get_filename() if not work_filename: - return + return None src_path = self._get_selected_filepath() @@ -612,6 +612,7 @@ class FilesWidget(QtWidgets.QWidget): self._published_checkbox.setChecked(False) else: self.refresh() + return filepath def _on_published_save_as_pressed(self): self._save_as_with_dialog() @@ -648,9 +649,10 @@ class FilesWidget(QtWidgets.QWidget): self._set_publish_context_select_mode(True) def _on_publish_select_context_pressed(self): - self._save_as_with_dialog() - self._set_publish_context_select_mode(False) - self._update_asset_task() + result = self._save_as_with_dialog() + if result is not None: + self._set_publish_context_select_mode(False) + self._update_asset_task() def _on_publish_cancel_pressed(self): self._set_publish_context_select_mode(False) From 6e592e4df62dd5f8969fbe8e29a730ce999a8d57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 14:56:07 +0200 Subject: [PATCH 124/180] changed label --- openpype/tools/workfiles/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 6cfd3dd651..edfcb17722 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -50,7 +50,7 @@ class SelectContextOverlay(QtWidgets.QFrame): self.setObjectName("WorkfilesPublishedContextSelect") label_widget = QtWidgets.QLabel( - "Please select context on Left side
<", + "Please choose context on the left
<", self ) label_widget.setAlignment(QtCore.Qt.AlignCenter) From 8c213809effe28eaf92f09e876adae71900d8922 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Mar 2022 15:47:37 +0200 Subject: [PATCH 125/180] flame: adding extract review data to bypass baking --- .../plugins/publish/extract_review_data.py | 41 +++++++++++++++++++ .../defaults/project_settings/nuke.json | 3 ++ .../schemas/schema_nuke_publish.json | 15 +++++++ 3 files changed, 59 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/publish/extract_review_data.py diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data.py b/openpype/hosts/nuke/plugins/publish/extract_review_data.py new file mode 100644 index 0000000000..19c894448d --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +import openpype +from pprint import pformat + + +class ExtractReviewData(openpype.api.Extractor): + """Extracts review tag into available representation + """ + + order = pyblish.api.ExtractorOrder + 0.01 + # order = pyblish.api.CollectorOrder + 0.499 + label = "Extract Review Data" + + families = ["review"] + hosts = ["nuke"] + + def process(self, instance): + fpath = instance.data["path"] + ext = os.path.splitext(fpath)[-1][1:] + + representations = instance.data.get("representations", []) + + if "render.farm" in instance.data["families"]: + instance.data["families"].remove("review") + + for repre in representations: + if ext not in repre["ext"]: + continue + + if not repre.get("tags"): + repre["tags"] = [] + + if "review" not in repre["tags"]: + repre["tags"].append("review") + + self.log.debug("Matching representation: {}".format( + pformat(repre) + )) + + instance.data["representations"] = representations diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 6992fb6e3e..d3d6252be7 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -106,6 +106,9 @@ ] } }, + "ExtractReviewData": { + "enabled": false + }, "ExtractReviewDataLut": { "enabled": false }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 1636a8d700..6776f316d9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -138,6 +138,21 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ExtractReviewData", + "label": "ExtractReviewData", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "dict", "collapsible": true, From 15f4d110699249a540f4889b08b319056f42f29b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Mar 2022 15:47:37 +0200 Subject: [PATCH 126/180] nuke: adding extract review data to bypass baking --- .../plugins/publish/extract_review_data.py | 41 +++++++++++++++++++ .../defaults/project_settings/nuke.json | 3 ++ .../schemas/schema_nuke_publish.json | 15 +++++++ 3 files changed, 59 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/publish/extract_review_data.py diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data.py b/openpype/hosts/nuke/plugins/publish/extract_review_data.py new file mode 100644 index 0000000000..19c894448d --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +import openpype +from pprint import pformat + + +class ExtractReviewData(openpype.api.Extractor): + """Extracts review tag into available representation + """ + + order = pyblish.api.ExtractorOrder + 0.01 + # order = pyblish.api.CollectorOrder + 0.499 + label = "Extract Review Data" + + families = ["review"] + hosts = ["nuke"] + + def process(self, instance): + fpath = instance.data["path"] + ext = os.path.splitext(fpath)[-1][1:] + + representations = instance.data.get("representations", []) + + if "render.farm" in instance.data["families"]: + instance.data["families"].remove("review") + + for repre in representations: + if ext not in repre["ext"]: + continue + + if not repre.get("tags"): + repre["tags"] = [] + + if "review" not in repre["tags"]: + repre["tags"].append("review") + + self.log.debug("Matching representation: {}".format( + pformat(repre) + )) + + instance.data["representations"] = representations diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 6992fb6e3e..d3d6252be7 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -106,6 +106,9 @@ ] } }, + "ExtractReviewData": { + "enabled": false + }, "ExtractReviewDataLut": { "enabled": false }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 1636a8d700..6776f316d9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -138,6 +138,21 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ExtractReviewData", + "label": "ExtractReviewData", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "dict", "collapsible": true, From dc2da01f52be73061de77e868f19e6252f8c2510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 31 Mar 2022 16:03:22 +0200 Subject: [PATCH 127/180] Update openpype/hosts/nuke/plugins/publish/extract_review_data.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_review_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data.py b/openpype/hosts/nuke/plugins/publish/extract_review_data.py index 19c894448d..d973e6accd 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data.py @@ -25,7 +25,7 @@ class ExtractReviewData(openpype.api.Extractor): instance.data["families"].remove("review") for repre in representations: - if ext not in repre["ext"]: + if ext != repre["ext"]: continue if not repre.get("tags"): From 3ee7c213c38f08a15dd7ca4e3d9edd8a0ec1072f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 19:16:16 +0200 Subject: [PATCH 128/180] added some ui examples --- website/docs/assets/publisher_card_view.png | Bin 0 -> 103961 bytes .../docs/assets/publisher_create_dialog.png | Bin 0 -> 59900 bytes website/docs/assets/publisher_list_view.png | Bin 0 -> 29456 bytes website/docs/dev_publishing.md | 25 ++++++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 website/docs/assets/publisher_card_view.png create mode 100644 website/docs/assets/publisher_create_dialog.png create mode 100644 website/docs/assets/publisher_list_view.png diff --git a/website/docs/assets/publisher_card_view.png b/website/docs/assets/publisher_card_view.png new file mode 100644 index 0000000000000000000000000000000000000000..57b012cb6ddd1399bf89fe766a536a9fa13ae5d0 GIT binary patch literal 103961 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/publisher_create_dialog.png b/website/docs/assets/publisher_create_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9275062d5bcf688a0466e7893ad157a2e8a581 GIT binary patch literal 59900 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 Cxfq`S literal 0 HcmV?d00001 diff --git a/website/docs/assets/publisher_list_view.png b/website/docs/assets/publisher_list_view.png new file mode 100644 index 0000000000000000000000000000000000000000..e9dc8a607af520f37ae403b775c5800af071894f GIT binary patch literal 29456 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 literal 0 HcmV?d00001 diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 49e720a7e0..ec01a941b6 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -522,4 +522,29 @@ Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_ ``` +## **UI examples** +### Main publish window +Main window of publisher shows instances and their values, collected by creators. +**Card view** +![Publisher UI - Card view](assets/publisher_card_view.png) +**List view** +![Publisher UI - List view](assets/publisher_list_view.png) + +#### *Instances views* +List of instances always contains `Options` item which is used to show attributes of context plugins. Values from the item are saved and loaded using [host implementation](#required-functions-in-host-implementation) **get_context_data** and **update_context_data**. Instances are grouped by family and can be shown in card view (single selection) or list view (multi selection). + +Instance view has at the bottom 3 buttons. Plus sign opens [create dialog](#create-dialog), bin removes selected instances and stripes swap card and list view. + +#### *Context options* +It is possible to change variant or asset and task context of instances at the top part but all changes there must be confirmed. Confirmation will trigger recalculation of subset names and all new data are stored to instances. + +#### *Create attributes* +Instance attributes display all create attributes of all selected instances. All attributes that have same definition are grouped into one input and is visually indicated if values are not same for selected instances. In most of cases have **< Multiselection >** placeholder. + +#### *Publish attributes* +Publish attributes work the same way as create attributes but the source of attribute definitions are pyblish plugins. Attributes are filtered based on families of selected instances and families defined in pyblish plugin. + +### Create dialog +![Publisher UI - Create dialog](assets/publisher_create_dialog.png) +Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In middle part artist select what will be created and what variant it is. On right side is information about selected creator and it's pre-create attributes. There is also question mark button which extends window and display more detailed information about the creator. From 2e890b5500b82d5ec480ca22a2ae97bd7f7162a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Mar 2022 19:16:22 +0200 Subject: [PATCH 129/180] fix example plugin --- website/docs/dev_publishing.md | 36 ++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index ec01a941b6..710090af40 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -465,21 +465,21 @@ Values of publish attributes from created instance are never removed automatical Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`.
- Toggle me! +Example plugin +

- ``` python - - import pyblish.api - from openpype.pipeline import ( +```python +import pyblish.api +from openpype.pipeline import ( OpenPypePyblishPluginMixin, attribute_definitions, - ) +) - # Example context plugin - class MyExtendedPlugin( +# Example context plugin +class MyExtendedPlugin( pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin - ): +): optional = True active = True @@ -501,16 +501,13 @@ Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_ if not self.optional: return True + # Attribute values are stored by class names + # - for those purposes was implemented 'get_attr_values_from_data' + # to help with accessing it + attribute_values = self.get_attr_values_from_data(context.data) # Get 'process' key - process_value = ( - context.data - .get("publish_attributes", {}) - # Attribute values are stored by class names - .get(self.__class__.__name__, {}) - # Access the key - .get("process") - ) - if process_value or process_value is None: + process_value = attribute_values.get("process") + if process_value is None or process_value: return True return False @@ -519,7 +516,8 @@ Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_ return # Do plugin logic ... - ``` +``` +

## **UI examples** From f80bc13cc309e3d2769da17acc73e64cedaeb134 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Mar 2022 20:26:08 +0200 Subject: [PATCH 130/180] adding limitations for pyright speeding up development --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 479cd731fe..e42fd75db2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,3 +136,19 @@ hash = "de63a8bf7f6c45ff59ecafeba13123f710c2cbc1783ec9e0b938e980d4f5c37f" [openpype.thirdparty.oiio.darwin] url = "https://distribute.openpype.io/thirdparty/oiio-2.2.0-darwin.tgz" hash = "sha256:..." + +[tool.pyright] +include = [ + "igniter", + "openpype", + "repos", + "vendor" +] +exclude = [ + "**/node_modules", + "**/__pycache__" +] +ignore = ["website", "docs", ".git"] + +reportMissingImports = true +reportMissingTypeStubs = false \ No newline at end of file From bc0054cd88ca6494305dde5bc992b370de59592b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 11:05:15 +0200 Subject: [PATCH 131/180] nuke | general: removing redundant review representation --- .../deadline/plugins/publish/submit_publish_job.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fad4d14ea0..6730c6a7dd 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -509,8 +509,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): most cases, but if not - we create representation from each of them. Arguments: - instance (pyblish.plugin.Instance): instance for which we are - setting representations + instance (dict): instance data for which we are + setting representations exp_files (list): list of expected files Returns: @@ -528,6 +528,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # preview video rendering for app in self.aov_filter.keys(): if os.environ.get("AVALON_APP", "") == app: + # no need to add review if baking in nuke present + if ( + app == "nuke" + and instance.get("bakingNukeScripts") + ): + break + + # iteratre all aov filters for aov in self.aov_filter[app]: if re.match( aov, From 35e6b8e42d14188650b5fcf58a5891498514721c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 11:07:22 +0200 Subject: [PATCH 132/180] ftrack: improving asset name if multiple reviewable representation --- .../publish/integrate_ftrack_instances.py | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index cff7cd32cb..f157f0db22 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -2,7 +2,7 @@ import os import json import copy import pyblish.api - +from pprint import pformat class IntegrateFtrackInstance(pyblish.api.InstancePlugin): """Collect ftrack component data (not integrate yet). @@ -168,7 +168,31 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Change asset name of each new component for review is_first_review_repre = True not_first_components = [] + extended_asset_name = False for repre in review_representations: + # Create copy of base comp item and append it + review_item = copy.deepcopy(base_component_item) + + # condition for multiple reviewable representations + # expand name to better label componenst + if is_first_review_repre and len(review_representations) > 1: + asset_name = review_item["asset_data"]["name"] + # define new extended name + extended_asset_name = "_".join( + (asset_name, repre["name"]) + ) + review_item["asset_data"]["name"] = extended_asset_name + # and rename all already created components + for _ci in component_list: + _ci["asset_data"]["name"] = extended_asset_name + + # and rename all already created src components + for _sci in src_components_to_add: + _sci["asset_data"]["name"] = extended_asset_name + + first_thumbnail_component[ + "asset_data"]["name"] = extended_asset_name + frame_start = repre.get("frameStartFtrack") frame_end = repre.get("frameEndFtrack") if frame_start is None or frame_end is None: @@ -184,8 +208,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if fps is None: fps = instance_fps - # Create copy of base comp item and append it - review_item = copy.deepcopy(base_component_item) # Change location review_item["component_path"] = repre["published_path"] # Change component data @@ -200,8 +222,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): }) } } - # Create copy of item before setting location or changing asset - src_components_to_add.append(copy.deepcopy(review_item)) + + # rename asset name only if multiple reviewable repre if is_first_review_repre: is_first_review_repre = False else: @@ -212,6 +234,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ) not_first_components.append(review_item) + # Create copy of item before setting location + src_components_to_add.append(copy.deepcopy(review_item)) + # Set location review_item["component_location"] = ftrack_server_location # Add item to component list @@ -249,6 +274,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): continue # Create copy of base comp item and append it other_item = copy.deepcopy(base_component_item) + + # add extended name if any + if extended_asset_name: + other_item["asset_data"]["name"] = extended_asset_name + other_item["component_data"] = { "name": repre["name"] } From b69032e9f57c8efcb1b4e88ed26fbbf57c1a59bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 11:19:15 +0200 Subject: [PATCH 133/180] hound catch --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index f157f0db22..c0d188c6ab 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -2,7 +2,7 @@ import os import json import copy import pyblish.api -from pprint import pformat + class IntegrateFtrackInstance(pyblish.api.InstancePlugin): """Collect ftrack component data (not integrate yet). From 1994a11fe1a259707d58e369d82cdf77bb865e7e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 11:48:36 +0200 Subject: [PATCH 134/180] modified collect instance --- .../plugins/publish/collect_instances.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 9cbfb61550..0008248405 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -20,21 +20,35 @@ class CollectInstances(pyblish.api.ContextPlugin): json.dumps(workfile_instances, indent=4) )) + filtered_instance_data = [] + # Check if there is any created instance + any_created_instance = False # Backwards compatibility for workfiles that already have review # instance in metadata. review_instance_exist = False for instance_data in workfile_instances: - if instance_data["family"] == "review": + family = instance_data["family"] + if family == "review": review_instance_exist = True - break + + elif family in ("renderPass", "renderLayer"): + any_created_instance = True + + else: + self.log.info("Unknown family \"{}\". Skipping {}".format( + family, json.dumps(instance_data, indent=4) + )) + continue + + filtered_instance_data.append(instance_data) # Fake review instance if review was not found in metadata families if not review_instance_exist: - workfile_instances.append( + filtered_instance_data.append( self._create_review_instance_data(context) ) - for instance_data in workfile_instances: + for instance_data in filtered_instance_data: instance_data["fps"] = context.data["sceneFps"] # Store workfile instance data to instance data @@ -42,8 +56,11 @@ class CollectInstances(pyblish.api.ContextPlugin): # Global instance data modifications # Fill families family = instance_data["family"] + families = [family] + if family != "review": + families.append("review") # Add `review` family for thumbnail integration - instance_data["families"] = [family, "review"] + instance_data["families"] = families # Instance name subset_name = instance_data["subset"] @@ -78,7 +95,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Project name from workfile context project_name = context.data["workfile_context"]["project"] # Host name from environment variable - host_name = os.environ["AVALON_APP"] + host_name = context.data["hostName"] # Use empty variant value variant = "" task_name = io.Session["AVALON_TASK"] @@ -106,12 +123,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance = self.create_render_pass_instance( context, instance_data ) - else: - raise AssertionError( - "Instance with unknown family \"{}\": {}".format( - family, instance_data - ) - ) if instance is None: continue From a29a7a67f23b7a132b099b7c1487084403ad7b1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 11:59:27 +0200 Subject: [PATCH 135/180] added collector which collects renderScene family --- .../plugins/publish/collect_scene_render.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py new file mode 100644 index 0000000000..2dcdab5c69 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -0,0 +1,104 @@ +import json +import copy +import pyblish.api +from avalon import io + +from openpype.lib import get_subset_name_with_asset_doc + + +class CollectRenderScene(pyblish.api.ContextPlugin): + """Collect instance which renders whole scene in PNG. + + Creates instance with family 'renderScene' which will have all layers + to render which will be composite into one result. The instance is not + collected from scene. + + Scene will be rendered with all visible layers similar way like review is. + + Instance is disabled if there are any created instances of 'renderLayer' + or 'renderPass'. That is because it is expected that this instance is + used as lazy publish of TVPaint file. + + Subset name is created similar way like 'renderLayer' family. It can use + `renderPass` and `renderLayer` keys which can be set using settings and + `variant` is filled using `renderPass` value. + """ + label = "Collect Render Scene" + order = pyblish.api.CollectorOrder - 0.4 + hosts = ["tvpaint"] + + # Settings attributes + enabled = False + # Value of 'renderLayer' for subset name template + render_layer_name = "Scene" + # Value of 'renderPass' for subset name template + render_pass_name = "Beauty" + + def process(self, context): + # Check if there are created instances of renderPass and renderLayer + # - that will define if renderScene instance is enabled after + # collection + any_created_instance = False + for instance in context: + family = instance.data["family"] + if family in ("renderPass", "renderLayer"): + any_created_instance = True + break + + # Global instance data modifications + # Fill families + family = "renderScene" + # Add `review` family for thumbnail integration + families = [family, "review"] + + # Collect asset doc to get asset id + # - not sure if it's good idea to require asset id in + # get_subset_name? + asset_name = context.data["workfile_context"]["asset"] + asset_doc = io.find_one({ + "type": "asset", + "name": asset_name + }) + + # Project name from workfile context + project_name = context.data["workfile_context"]["project"] + # Host name from environment variable + host_name = context.data["hostName"] + # Variant is using render pass name + variant = self.render_pass_name + dynamic_data = { + "renderLayer": self.render_layer_name, + "renderPass": self.render_pass_name, + } + task_name = io.Session["AVALON_TASK"] + subset_name = get_subset_name_with_asset_doc( + family, + variant, + task_name, + asset_doc, + project_name, + host_name, + dynamic_data=dynamic_data + ) + + instance_data = { + "family": family, + "families": families, + "fps": context.data["sceneFps"], + "name": subset_name, + "label": "{} [{}-{}]".format( + subset_name, + context.data["sceneMarkIn"] + 1, + context.data["sceneMarkOut"] + 1 + ), + "active": not any_created_instance, + "publish": not any_created_instance, + "representations": [], + "layers": copy.deepcopy(context.data["layersData"]) + } + + instance = context.create_instance(**instance_data) + + self.log.debug("Created instance: {}\n{}".format( + instance, json.dumps(instance.data, indent=4) + )) From 288fa288c1dcca485febdf588becac109e76daa5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 12:01:53 +0200 Subject: [PATCH 136/180] added renderScene into other tvpaint plugins related to the renderScene logic --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 6 +++--- .../tvpaint/plugins/publish/validate_layers_visibility.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 139dabadee..73daf60567 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -18,7 +18,7 @@ from openpype.hosts.tvpaint.lib import ( class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] - families = ["review", "renderPass", "renderLayer"] + families = ["review", "renderPass", "renderLayer", "renderScene"] # Modifiable with settings review_bg = [255, 255, 255, 255] @@ -159,7 +159,7 @@ class ExtractSequence(pyblish.api.Extractor): # Fill tags and new families tags = [] - if family_lowered in ("review", "renderlayer"): + if family_lowered in ("review", "renderlayer", "renderScene"): tags.append("review") # Sequence of one frame @@ -185,7 +185,7 @@ class ExtractSequence(pyblish.api.Extractor): instance.data["representations"].append(new_repre) - if family_lowered in ("renderpass", "renderlayer"): + if family_lowered in ("renderpass", "renderlayer", "renderscene"): # Change family to render instance.data["family"] = "render" diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py index 7ea0587b8f..d3a04cc69f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -8,7 +8,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): label = "Validate Layers Visibility" order = pyblish.api.ValidatorOrder - families = ["review", "renderPass", "renderLayer"] + families = ["review", "renderPass", "renderLayer", "renderScene"] def process(self, instance): layer_names = set() From 2951fe77469716bf7e105ccf408f8bcba3f41db8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 12:26:39 +0200 Subject: [PATCH 137/180] styling tweaks, mostly adding of articles --- website/docs/dev_publishing.md | 104 ++++++++++++++++----------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 710090af40..8ee3b7e85f 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -5,19 +5,19 @@ sidebar_label: Publishing toc_max_heading_level: 4 --- -Publishing workflow consist of 2 parts: +Publishing workflow consists of 2 parts: - Creating - Mark what will be published and how. -- Publishing - Use data from Creating to go through pyblish process. +- Publishing - Use data from Creating to go through the pyblish process. -OpenPype is using [pyblish](https://pyblish.com/) for publishing process. OpenPype a little bit extend and modify few functions mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during Creating part instead of in publishing part and has limited plugin actions only for failed validation plugins. +OpenPype is using [pyblish](https://pyblish.com/) for the publishing process. OpenPype extends and modifies its few functions a bit, mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during Creating part instead of in the publishing part and has limited plugin actions only for failed validation plugins. ## **Creating** -Concept of Creating does not have to "create" anything but prepare and store metadata about an "instance" (becomes a subset after publish process). Created instance always has `family` which defines what kind of data will be published, best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. In most of hosts are metadata stored to workfile (Maya scene, Nuke script, etc.) to an item or a node the same way so consistency of host implementation is kept, but some features may require different approach that is the reason why it is creator plugin responsibility. Storing the metadata to workfile gives ability to keep values so artist does not have to do create and set what should be published and how over and over. +Concept of Creating does not have to "create" anything yet, but prepare and store metadata about an "instance" (becomes a subset after the publish process). Created instance always has `family` which defines what kind of data will be published, the best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. Most hosts are storing metadata into a workfile (Maya scene, Nuke script, etc.) to an item or a node the same way as regular Pyblish instances, so consistency of host implementation is kept, but some features may require a different approach that is the reason why it is creator plugin responsibility. Storing the metadata to the workfile persists values, so the artist does not have to create and set what should be published and how over and over. ### Created instance -Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is dictionary like object with few immutable keys (marked with start `*` in table). The immutable keys are set by creator plugin or create context on initialization and thei values can't change. Instance can have more arbitrary data, for example ids of nodes in scene but keep in mind that some keys are reserved. +Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is a dictionary-like object with few immutable keys (marked with start `*` in table). The immutable keys are set by the creator plugin or create context on initialization and their values can't change. Instance can have more arbitrary data, for example ids of nodes in scene but keep in mind that some keys are reserved. | Key | Type | Description | |---|---|---| @@ -25,34 +25,34 @@ Objected representation of created instance metadata defined by class **CreatedI | *instance_id | str | Unique ID of instance. Set automatically on instance creation using `str(uuid.uuid4())` | | *family | str | Instance's family representing type defined by creator plugin. | | *creator_identifier | str | Identifier of creator that collected/created the instance. | -| *creator_attributes | dict | Dictionary of attributes that are defined by creator plugin (`get_instance_attr_defs`). | +| *creator_attributes | dict | Dictionary of attributes that are defined by the creator plugin (`get_instance_attr_defs`). | | *publish_attributes | dict | Dictionary of attributes that are defined by publish plugins. | -| variant | str | Variant is entered by artist on creation and may affect **subset**. | -| subset | str | Name of instance. This name will be used as subset name during publishing. Can be changed on context change or variant change. | -| active | bool | Is instance active and will be published or not. | +| variant | str | Variant is entered by the artist on creation and may affect **subset**. | +| subset | str | Name of instance. This name will be used as a subset name during publishing. Can be changed on context change or variant change. | +| active | bool | Is the instance active and will be published or not. | | asset | str | Name of asset in which context was created. | | task | str | Name of task in which context was created. Can be set to `None`. | :::note -Task should not be required until subset name template expect it. +Task should not be required until the subset name template expects it. ::: -object of **CreatedInstance** has method **data_to_store** which returns dictionary that can be parsed to json string. This method will return all data related to instance so can be re-created using `CreatedInstance.from_existing(data)`. +object of **CreatedInstance** has method **data_to_store** which returns a dictionary that can be parsed to a json string. This method will return all data related to the instance so it can be re-created using `CreatedInstance.from_existing(data)`. #### *Create context* {#category-doc-link} Controller and wrapper around Creating is `CreateContext` which cares about loading of plugins needed for Creating. And validates required functions in host implementation. -Context discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger Creating itself. Also keeps in mind instance objects by their ids. +Context discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger Creating itself. Also it keeps in mind instance objects by their ids. -Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instance but these methods are not meant to be called directly out of creator. The reason is that is creator's responsibility to remove metadata or decide if should remove the instance. +Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance. #### Required functions in host implementation -Host implementation **must** have implemented **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now are there stored data about enabled/disabled optional publish plugins. When data are not stored and loaded properly reset of publishing will cause that they will be set to default value. Similar to instance data can be context data also parsed to json string. +Host implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data. -There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return string showed in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. +There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/{asset name}/{task name}"`. -Another optional function is **get_current_context**. This function is handy in hosts where is possible to open multiple workfiles in one process so using global context variables is not relevant because artist can switch between opened workfiles without being acknowledged. When function is not implemented or won't return right keys the global context is used. +Another optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used. ```json # Expected keys in output { @@ -75,7 +75,7 @@ class WorkfileCreator(Creator): family = "workfile" ``` -- **`collect_instances`** (method) - Collect already existing instances from workfile and add them to create context. This method is called on initialization or reset of **CreateContext**. Each creator is responsible to find it's instances metadata, convert them to **CreatedInstance** object and add the to create context (`self._add_instance_to_context(instnace_obj)`). +- **`collect_instances`** (method) - Collect already existing instances from the workfile and add them to create context. This method is called on initialization or reset of **CreateContext**. Each creator is responsible to find its instance metadata, convert them to **CreatedInstance** object and add them to create context (`self._add_instance_to_context(instnace_obj)`). ```python def collect_instances(self): # Using 'pipeline.list_instances' is just example how to get existing instances from scene @@ -92,7 +92,7 @@ def collect_instances(self): self._add_instance_to_context(instance) ``` -- **`create`** (method) - Create new object of **CreatedInstance** store it's metadata to workfile and add the instance into create context. Failed Creating should raise **CreatorError** if happens error that can artist fix or give him some useful information. Trigger and implementation differs for **Creator** and **AutoCreator**. +- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator** and **AutoCreator**. - **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**. ```python @@ -145,8 +145,8 @@ When host implementation use universal way how to store and load instances you s **Optional implementations** -- **`enabled`** (attr) - Boolean if creator plugin is enabled and used. -- **`identifier`** (class attr) - Consistent unique string identifier of the creator plugin. Is used to identify source plugin of existing instances. There can't be 2 creator plugins with same identifier. Default implementation returns `family` attribute. +- **`enabled`** (attr) - Boolean if the creator plugin is enabled and used. +- **`identifier`** (class attr) - Consistent unique string identifier of the creator plugin. Is used to identify source plugin of existing instances. There can't be 2 creator plugins with the same identifier. Default implementation returns `family` attribute. ```python class RenderLayerCreator(Creator): family = "render" @@ -158,13 +158,13 @@ class RenderPassCreator(Creator): identifier = "render_pass" ``` -- **`label`** (attr) - String label of creator plugin which will showed in UI, `identifier` is used when not set. It should be possible to use html tags. +- **`label`** (attr) - String label of creator plugin which will show up in UI, `identifier` is used when not set. It should be possible to use html tags. ```python class RenderLayerCreator(Creator): label = "Render Layer" ``` -- **`get_icon`** (attr) - Icon of creator and it's instances. Value can be a path to image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for used version in pyproject.toml). Default implementation return **icon** attribute. +- **`get_icon`** (attr) - Icon of creator and its instances. Value can be a path to an image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for the used version in pyproject.toml). Default implementation return **icon** attribute. - **`icon`** (method) - Attribute for default implementation of **get_icon**. ```python class RenderLayerCreator(Creator): @@ -172,7 +172,7 @@ class RenderLayerCreator(Creator): icon = "fa5.building" ``` -- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic type of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementations returns **instance_attr_defs**. +- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**. - **`instance_attr_defs`** (attr) - Attribute for default implementation of **get_instance_attr_defs**. ```python @@ -194,16 +194,16 @@ class RenderLayerCreator(Creator): ] ``` -- **`get_subset_name`** (method) - Calculate subset name based on passed data. Data can be extended using `get_dynamic_data` method. Default implementation is using `get_subset_name` from `openpype.lib` which is recommended. +- **`get_subset_name`** (method) - Calculate subset name based on passed data. Data can be extended using the `get_dynamic_data` method. Default implementation is using `get_subset_name` from `openpype.lib` which is recommended. -- **`get_dynamic_data`** (method) - Can be used to extend data for subset template which may be required in some cases. +- **`get_dynamic_data`** (method) - Can be used to extend data for subset templates which may be required in some cases. #### *AutoCreator* -Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting of all creators. +Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting all creators. :::important -**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would lead to create new instance immediately or on refresh. +**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would lead to creating new instance immediately or on refresh. ::: ```python @@ -273,10 +273,10 @@ def create(self): ``` #### *Creator* -Implementation of creator plugin that is triggered manually by artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments. +Implementation of creator plugin that is triggered manually by the artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments. **Optional implementations** -- **`create_allow_context_change`** (class attr) - Allow to set context in UI before Creating. Some creator may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default. +- **`create_allow_context_change`** (class attr) - Allow to set context in UI before Creating. Some creators may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default. ```python class BulkRenderCreator(Creator): create_allow_context_change = False @@ -287,7 +287,7 @@ class BulkRenderCreator(Creator): - **`get_default_variant`** (method) - Returns default variant that is prefilled in UI (value does not have to be in default variants). By default returns **default_variant** attribute. If returns `None` then UI logic will take first item from **get_default_variants** if there is any otherwise **"Main"** is used. - **`default_variant`** (attr) - Attribute for default implementation of **get_default_variant**. -- **`get_description`** (method) - Returns short string description of creator. Returns **description** attribute by default. +- **`get_description`** (method) - Returns a short string description of the creator. Returns **description** attribute by default. - **`description`** (attr) - Attribute for default implementation of **get_description**. - **`get_detailed_description`** (method) - Returns detailed string description of creator. Can contain markdown. Returns **detailed_description** attribute by default. @@ -397,34 +397,34 @@ class CreateRender(Creator): OpenPype define few specific exceptions that should be used in publish plugins. #### *Validation exception* -Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. +Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that errors in the plugin can be fixed by the artist himself (with or without action on plugin). Any other errors will stop publishing immediately. The exception `PublishValidationError` raised after validation order has the same effect as any other exception. Exception `PublishValidationError` expects 4 arguments: - **message** Which is not used in UI but for headless publishing. - **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin. -- **description** Detailed description of happened issue where markdown and html can be used. -- **detail** Is optional to give even more detailed information for advanced users. At this moment is detail showed under description but it is in plan to have detail in collapsible widget. +- **description** Detailed description of the issue where markdown and html can be used. +- **detail** Is optional to give even more detailed information for advanced users. At this moment the detail is shown directly under description but it is in plan to have detail in a collapsible widget. Extended version is `PublishXmlValidationError` which uses xml files with stored descriptions. This helps to avoid having huge markdown texts inside code. The exception has 4 arguments: -- **plugin** The plugin object which raises the exception to find it's related xml file. +- **plugin** The plugin object which raises the exception to find its related xml file. - **message** Exception message for publishing without UI or different pyblish UI. -- **key** Optional argument says which error from xml is used as validation plugin may raise error with different messages based on the current errors. Default is **"main"**. +- **key** Optional argument says which error from xml is used as a validation plugin may raise error with different messages based on the current errors. Default is **"main"**. - **formatting_data** Optional dictionary to format data in the error. This is used to fill detailed description with data from the publishing so artist can get more precise information. **Where and how to create xml file** -Xml files for `PublishXmlValidationError` must be located in **./help** subfolder next to plugin and the filename must match the filename of plugin. +Xml files for `PublishXmlValidationError` must be located in **./help** subfolder next to the plugin and the filename must match the filename of the plugin. ``` # File location related to plugin file └ publish - ├ help - │ ├ validate_scene.xml - │ └ ... - ├ validate_scene.py - └ ... + ├ help + │ ├ validate_scene.xml + │ └ ... + ├ validate_scene.py + └ ... ``` -Xml file content has **<root>** node which may contain any amount of **<error>** nodes, but each of them must have **id** attribute with unique value. That is then used for **key**. Each error must have **<title>** and **<description>** and **<detail>**. Text content may contain python formatting keys that can be filled when exception is raised. +Xml file content has **<root>** node which may contain any amount of **<error>** nodes, but each of them must have **id** attribute with unique value. That is then used for **key**. Each error must have **<title>** and **<description>** and **<detail>**. Text content may contain python formatting keys that can be filled when an exception is raised. ```xml @@ -436,7 +436,7 @@ Context of the given subset doesn't match your current scene. ### How to repair? -Yout can fix this with "Repair" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata. +You can fix this with the "Repair" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata. After that restart publishing with Reload button. @@ -451,15 +451,15 @@ or the scene file was copy pasted from different context. ``` #### *Known errors* -When there is a known error that can't be fixed by user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raise. The only difference is that it's message is shown in UI to artist otherwise a neutral message without context is shown. +When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown. ### Plugin extension -Publish plugins can be extended by additional logic when inherits from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of most important usages is to be able turn on/off optional plugins. +Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins. -Attributes are defined by return value of `get_attribute_defs` method. Attribute definitions are for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be re-implemented `convert_attribute_values`. Default implementation just converts the values to right types. +Attributes are defined by the return value of `get_attribute_defs` method. Attribute definitions are for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be re-implemented `convert_attribute_values`. Default implementation just converts the values to right types. -:::important -Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure. +:::Important +Values of publish attributes from created instance are never removed automatically so implementing this method is the best way to remove legacy data or convert them to new data structure. ::: Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. @@ -530,7 +530,7 @@ Main window of publisher shows instances and their values, collected by creators ![Publisher UI - List view](assets/publisher_list_view.png) #### *Instances views* -List of instances always contains `Options` item which is used to show attributes of context plugins. Values from the item are saved and loaded using [host implementation](#required-functions-in-host-implementation) **get_context_data** and **update_context_data**. Instances are grouped by family and can be shown in card view (single selection) or list view (multi selection). +List of instances always contains an `Options` item which is used to show attributes of context plugins. Values from the item are saved and loaded using [host implementation](#required-functions-in-host-implementation) **get_context_data** and **update_context_data**. Instances are grouped by family and can be shown in card view (single selection) or list view (multi selection). Instance view has at the bottom 3 buttons. Plus sign opens [create dialog](#create-dialog), bin removes selected instances and stripes swap card and list view. @@ -538,11 +538,11 @@ Instance view has at the bottom 3 buttons. Plus sign opens [create dialog](#crea It is possible to change variant or asset and task context of instances at the top part but all changes there must be confirmed. Confirmation will trigger recalculation of subset names and all new data are stored to instances. #### *Create attributes* -Instance attributes display all create attributes of all selected instances. All attributes that have same definition are grouped into one input and is visually indicated if values are not same for selected instances. In most of cases have **< Multiselection >** placeholder. +Instance attributes display all created attributes of all selected instances. All attributes that have the same definition are grouped into one input and are visually indicated if values are not the same for selected instances. In most cases have **< Multiselection >** placeholder. #### *Publish attributes* -Publish attributes work the same way as create attributes but the source of attribute definitions are pyblish plugins. Attributes are filtered based on families of selected instances and families defined in pyblish plugin. +Publish attributes work the same way as create attributes but the source of attribute definitions are pyblish plugins. Attributes are filtered based on families of selected instances and families defined in the pyblish plugin. ### Create dialog ![Publisher UI - Create dialog](assets/publisher_create_dialog.png) -Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In middle part artist select what will be created and what variant it is. On right side is information about selected creator and it's pre-create attributes. There is also question mark button which extends window and display more detailed information about the creator. +Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. \ No newline at end of file From 61d5f32fe868f118aa84722b3ccb07594caff580 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 12:56:18 +0200 Subject: [PATCH 138/180] ftrack: improve conditional rename --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index c0d188c6ab..b1a7da58f9 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -190,8 +190,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): for _sci in src_components_to_add: _sci["asset_data"]["name"] = extended_asset_name - first_thumbnail_component[ - "asset_data"]["name"] = extended_asset_name + # rename also first thumbnail component if any + if first_thumbnail_component is not None: + first_thumbnail_component[ + "asset_data"]["name"] = extended_asset_name frame_start = repre.get("frameStartFtrack") frame_end = repre.get("frameEndFtrack") From 77b7b4b1bc3e6ea467d5a0c38eb3fd96523d69ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 13:16:02 +0200 Subject: [PATCH 139/180] Fix - remove doubled dot in workfile created from template --- openpype/lib/avalon_context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index cc6bcba58e..0348d88be2 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1965,6 +1965,7 @@ def get_last_workfile( data.pop("comment", None) if not data.get("ext"): data["ext"] = extensions[0] + data["ext"] = data["ext"].replace('.', '') filename = StringTemplate.format_strict_template(file_template, data) if full_path: From 7bfc66efed073f47fb1c4e13692020074b155ced Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 13:42:12 +0200 Subject: [PATCH 140/180] Added mention of adding My Drive as a root --- website/docs/module_site_sync.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/module_site_sync.md b/website/docs/module_site_sync.md index 78f482352e..2e9cf01102 100644 --- a/website/docs/module_site_sync.md +++ b/website/docs/module_site_sync.md @@ -123,6 +123,10 @@ To get working connection to Google Drive there are some necessary steps: - add new site back in OpenPype Settings, name as you want, provider needs to be 'gdrive' - distribute credentials file via shared mounted disk location +:::note +If you are using regular personal GDrive for testing don't forget adding `/My Drive` as the prefix in root configuration. Business accounts and share drives don't need this. +::: + ### SFTP SFTP provider is used to connect to SFTP server. Currently authentication with `user:password` or `user:ssh key` is implemented. From 82d2a4be19c011378f898020a61b8668c888d109 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 14:19:42 +0200 Subject: [PATCH 141/180] added settings and pass is now defined only by plugin --- .../plugins/publish/collect_scene_render.py | 15 ++++++------ .../defaults/project_settings/tvpaint.json | 4 ++++ .../schema_project_tvpaint.json | 24 +++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index 2dcdab5c69..dc9c63f3bd 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -27,12 +27,13 @@ class CollectRenderScene(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.4 hosts = ["tvpaint"] + # Value of 'render_pass' in subset name template + render_pass = "beauty" + # Settings attributes enabled = False - # Value of 'renderLayer' for subset name template - render_layer_name = "Scene" - # Value of 'renderPass' for subset name template - render_pass_name = "Beauty" + # Value of 'render_layer' and 'variant' in subset name template + render_layer = "Main" def process(self, context): # Check if there are created instances of renderPass and renderLayer @@ -65,10 +66,10 @@ class CollectRenderScene(pyblish.api.ContextPlugin): # Host name from environment variable host_name = context.data["hostName"] # Variant is using render pass name - variant = self.render_pass_name + variant = self.render_layer dynamic_data = { - "renderLayer": self.render_layer_name, - "renderPass": self.render_pass_name, + "render_layer": self.render_layer, + "render_pass": self.render_pass } task_name = io.Session["AVALON_TASK"] subset_name = get_subset_name_with_asset_doc( diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 46beeb85b9..88b5a598cd 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,6 +1,10 @@ { "stop_timer_on_application_exit": false, "publish": { + "CollectRenderScene": { + "enabled": false, + "render_layer": "Main" + }, "ExtractSequence": { "review_bg": [ 255, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 97462a8b62..e1166dc2bb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -16,6 +16,30 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectRenderScene", + "label": "Collect Render Scene", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "It is possible to fill 'render_layer' or 'variant' in subset name template with custom value. Value of 'render_pass' is \"Beauty\"." + }, + { + "type": "text", + "key": "render_layer", + "label": "Render Layer" + } + ] + }, { "type": "dict", "collapsible": true, From 53c95cd1ab48fc8ff30fb53fdd4e3da14d64f4a5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 14:22:15 +0200 Subject: [PATCH 142/180] nuke: adding comments and fixing condition --- .../hosts/nuke/plugins/publish/extract_review_data.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data.py b/openpype/hosts/nuke/plugins/publish/extract_review_data.py index d973e6accd..38a8140cff 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data.py @@ -21,9 +21,15 @@ class ExtractReviewData(openpype.api.Extractor): representations = instance.data.get("representations", []) - if "render.farm" in instance.data["families"]: + # review can be removed since `ProcessSubmittedJobOnFarm` will create + # reviable representation if needed + if ( + "render.farm" in instance.data["families"] + and "review" in instance.data["families"] + ): instance.data["families"].remove("review") + # iterate representations and add `review` tag for repre in representations: if ext != repre["ext"]: continue From 7ce9095115653f0b0ef9e0f5faf26e4d9a24c2fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 14:27:23 +0200 Subject: [PATCH 143/180] change order --- openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index dc9c63f3bd..38dc431778 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -24,7 +24,7 @@ class CollectRenderScene(pyblish.api.ContextPlugin): `variant` is filled using `renderPass` value. """ label = "Collect Render Scene" - order = pyblish.api.CollectorOrder - 0.4 + order = pyblish.api.CollectorOrder - 0.39 hosts = ["tvpaint"] # Value of 'render_pass' in subset name template From 1bde9c0fa96c3b20101316ceedb0c84b07673882 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 14:30:45 +0200 Subject: [PATCH 144/180] remove unused variable --- .../hosts/tvpaint/plugins/publish/collect_instances.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 0008248405..5e8d13592c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -21,8 +21,6 @@ class CollectInstances(pyblish.api.ContextPlugin): )) filtered_instance_data = [] - # Check if there is any created instance - any_created_instance = False # Backwards compatibility for workfiles that already have review # instance in metadata. review_instance_exist = False @@ -31,10 +29,7 @@ class CollectInstances(pyblish.api.ContextPlugin): if family == "review": review_instance_exist = True - elif family in ("renderPass", "renderLayer"): - any_created_instance = True - - else: + elif family not in ("renderPass", "renderLayer"): self.log.info("Unknown family \"{}\". Skipping {}".format( family, json.dumps(instance_data, indent=4) )) From d63a0aad71cbc40aac8a8baa46b3dc183eeac977 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 14:42:39 +0200 Subject: [PATCH 145/180] Nuke: adding concurrent tasks attribute to job submission also adding to settings --- .../plugins/publish/submit_nuke_deadline.py | 2 ++ .../defaults/project_settings/deadline.json | 1 + .../projects_schema/schema_project_deadline.json | 14 ++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index d6bd11620d..055d3c8a2c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -27,6 +27,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): # presets priority = 50 chunk_size = 1 + concurent_task = 1 primary_pool = "" secondary_pool = "" group = "" @@ -177,6 +178,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "Priority": priority, "ChunkSize": chunk_size, + "ConcurrentTasks": self.concurent_task, "Department": self.department, "Pool": self.primary_pool, diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 5bb0a4022e..cfbb92e590 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -62,6 +62,7 @@ "use_published": true, "priority": 50, "chunk_size": 10, + "concurent_task": 1, "primary_pool": "", "secondary_pool": "", "group": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index e6097a2b14..700c3863fb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -192,6 +192,9 @@ "key": "use_published", "label": "Use Published scene" }, + { + "type": "splitter" + }, { "type": "number", "key": "priority", @@ -202,6 +205,14 @@ "key": "chunk_size", "label": "Chunk Size" }, + { + "type": "number", + "key": "concurent_task", + "label": "Number of concurent tasks" + }, + { + "type": "splitter" + }, { "type": "text", "key": "primary_pool", @@ -217,6 +228,9 @@ "key": "group", "label": "Group" }, + { + "type": "splitter" + }, { "type": "text", "key": "department", From 83ad95bed5f180cf90c551b389db970bb0cc92b0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 15:27:35 +0200 Subject: [PATCH 146/180] OP-2895 - added flatten_subset_template to Settings for PS Used as a subset name for ephemeral instance created if no image instance present in a workfile. --- .../defaults/project_settings/photoshop.json | 5 ++++- .../schema_project_photoshop.json | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 822a94a8eb..d9b7a8083f 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -12,6 +12,9 @@ "flatten_subset_template": "", "color_code_mapping": [] }, + "CollectInstances": { + "flatten_subset_template": "" + }, "ValidateContainers": { "enabled": true, "optional": true, @@ -44,4 +47,4 @@ "create_first_version": false, "custom_templates": [] } -} +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index b499ccc4be..f6e0e51f49 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -108,6 +108,23 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectInstances", + "label": "Collect Instances", + "children": [ + { + "type": "label", + "label": "Name for flatten image created if no image instance present" + }, + { + "type": "text", + "key": "flatten_subset_template", + "label": "Subset template for flatten image" + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 2e35be01507e9af8abec4fa538d2ceb6e8b17ad2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 15:28:32 +0200 Subject: [PATCH 147/180] OP-2895 - added functionality to create flatten image if no instances present --- .../plugins/publish/collect_instances.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index c3e27e9646..f1446f045f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -1,6 +1,9 @@ +from avalon import api import pyblish.api +from openpype.settings import get_project_settings from openpype.hosts.photoshop import api as photoshop +from openpype.lib import prepare_template_data class CollectInstances(pyblish.api.ContextPlugin): @@ -19,13 +22,16 @@ class CollectInstances(pyblish.api.ContextPlugin): families_mapping = { "image": [] } + flatten_subset_template = "" def process(self, context): stub = photoshop.stub() layers = stub.get_layers() layers_meta = stub.get_layers_metadata() instance_names = [] + all_layer_ids = [] for layer in layers: + all_layer_ids.append(layer.id) layer_data = stub.read(layer, layers_meta) # Skip layers without metadata. @@ -59,3 +65,33 @@ class CollectInstances(pyblish.api.ContextPlugin): if len(instance_names) != len(set(instance_names)): self.log.warning("Duplicate instances found. " + "Remove unwanted via SubsetManager") + + if len(instance_names) == 0 and self.flatten_subset_template: + project_name = context.data["projectEntity"]["name"] + variants = get_project_settings(project_name).get( + "photoshop", {}).get( + "create", {}).get( + "CreateImage", {}).get( + "defaults", ['']) + family = "image" + task_name = api.Session["AVALON_TASK"] + asset_name = context.data["assetEntity"]["name"] + + fill_pairs = { + "variant": variants[0], + "family": family, + "task": task_name + } + + subset = self.flatten_subset_template.format( + **prepare_template_data(fill_pairs)) + + instance = context.create_instance(subset) + instance.data["family"] = family + instance.data["asset"] = asset_name + instance.data["subset"] = subset + instance.data["ids"] = all_layer_ids + instance.data["families"] = self.families_mapping[family] + instance.data["publish"] = True + + self.log.info("flatten instance: {} ".format(instance.data)) From ee0bb56459f170ff0644b64c8b07fa982bcef914 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 15:30:56 +0200 Subject: [PATCH 148/180] OP-2895 - updates for flatten image New ephemeral instance doesn't have any layer, previous implementation depended on it. --- openpype/hosts/photoshop/plugins/publish/extract_image.py | 4 +++- openpype/hosts/photoshop/plugins/publish/extract_review.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index 04ce77ee34..b07d0740c1 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -26,8 +26,10 @@ class ExtractImage(openpype.api.Extractor): with photoshop.maintained_selection(): self.log.info("Extracting %s" % str(list(instance))) with photoshop.maintained_visibility(): + ids = set() layer = instance.data.get("layer") - ids = set([layer.id]) + if layer: + ids.add(layer.id) add_ids = instance.data.pop("ids", None) if add_ids: ids.update(set(add_ids)) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index b8f4470c7b..d076610ead 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -155,6 +155,9 @@ class ExtractReview(openpype.api.Extractor): for image_instance in instance.context: if image_instance.data["family"] != "image": continue + if not image_instance.data.get("layer"): + # dummy instance for flatten image + continue layers.append(image_instance.data.get("layer")) return sorted(layers) From f0189f4703bdbee8684ef6d19b96fda2249216a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 15:42:14 +0200 Subject: [PATCH 149/180] OP-2895 - added documentation --- .../hosts/photoshop/plugins/publish/collect_instances.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index f1446f045f..6198ed0156 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -12,6 +12,10 @@ class CollectInstances(pyblish.api.ContextPlugin): This collector takes into account assets that are associated with an LayerSet and marked with a unique identifier; + If no image instances are explicitly created, it looks if there is value + in `flatten_subset_template` (configurable in Settings), in that case it + produces flatten image with all visible layers. + Identifier: id (str): "pyblish.avalon.instance" """ @@ -22,6 +26,7 @@ class CollectInstances(pyblish.api.ContextPlugin): families_mapping = { "image": [] } + # configurable in Settings flatten_subset_template = "" def process(self, context): From eb400c2b9eae5fd7e51d15c61c96c2506884226b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 15:46:24 +0200 Subject: [PATCH 150/180] OP-2895 - added documentation --- .../schemas/projects_schema/schema_project_photoshop.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index f6e0e51f49..badf94229b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -42,7 +42,7 @@ "children": [ { "type": "label", - "label": "Set color for publishable layers, set its resulting family and template for subset name. Can create flatten image from published instances" + "label": "Set color for publishable layers, set its resulting family and template for subset name. \nCan create flatten image from published instances.(Applicable only for remote publishing!)" }, { "type": "boolean", From f3f5007af5b3fdc4512adc2fdba85b9a4030e5a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 15:47:18 +0200 Subject: [PATCH 151/180] added asset and task on instance data --- .../tvpaint/plugins/publish/collect_scene_render.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index 38dc431778..df3f715853 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -55,7 +55,8 @@ class CollectRenderScene(pyblish.api.ContextPlugin): # Collect asset doc to get asset id # - not sure if it's good idea to require asset id in # get_subset_name? - asset_name = context.data["workfile_context"]["asset"] + workfile_context = context.data["workfile_context"] + asset_name = workfile_context["asset"] asset_doc = io.find_one({ "type": "asset", "name": asset_name @@ -71,7 +72,8 @@ class CollectRenderScene(pyblish.api.ContextPlugin): "render_layer": self.render_layer, "render_pass": self.render_pass } - task_name = io.Session["AVALON_TASK"] + + task_name = workfile_context["task"] subset_name = get_subset_name_with_asset_doc( family, variant, @@ -95,7 +97,9 @@ class CollectRenderScene(pyblish.api.ContextPlugin): "active": not any_created_instance, "publish": not any_created_instance, "representations": [], - "layers": copy.deepcopy(context.data["layersData"]) + "layers": copy.deepcopy(context.data["layersData"]), + "asset": asset_name, + "task": task_name } instance = context.create_instance(**instance_data) From 3dbd2ef7493a620e2de584cff640f3ad0b45ce4f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 15:52:04 +0200 Subject: [PATCH 152/180] OP-2895 - added documentation for simplified workflow --- website/docs/artist_hosts_photoshop.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/artist_hosts_photoshop.md b/website/docs/artist_hosts_photoshop.md index b2b5fd58da..a140170c49 100644 --- a/website/docs/artist_hosts_photoshop.md +++ b/website/docs/artist_hosts_photoshop.md @@ -49,6 +49,12 @@ With the `Creator` you have a variety of options to create: - Uncheck `Use selection`. - This will create a single group named after the `Subset` in the `Creator`. +#### Simplified publish + +There is a simplified workflow for simple use case where only single image should be created containing all visible layers. +No image instances must be present in a workfile and `project_settings/photoshop/publish/CollectInstances/flatten_subset_template` must be filled in Settings. +Then artists just need to hit 'Publish' button in menu. + ### Publish When you are ready to share some work, you will need to publish. This is done by opening the `Pyblish` through the extensions `Publish` button. From 486ccff38f6837465698b107c6682ec4e586415b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 15:52:56 +0200 Subject: [PATCH 153/180] added missing subset key --- openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index df3f715853..9d5c4dbb62 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -88,6 +88,7 @@ class CollectRenderScene(pyblish.api.ContextPlugin): "family": family, "families": families, "fps": context.data["sceneFps"], + "subset": subset_name, "name": subset_name, "label": "{} [{}-{}]".format( subset_name, From ee827e8dd2e3ec5209969f33392c0dea5c952195 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 16:02:19 +0200 Subject: [PATCH 154/180] fix case family --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 73daf60567..d4fd1dff4b 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -159,7 +159,7 @@ class ExtractSequence(pyblish.api.Extractor): # Fill tags and new families tags = [] - if family_lowered in ("review", "renderlayer", "renderScene"): + if family_lowered in ("review", "renderlayer", "renderscene"): tags.append("review") # Sequence of one frame From 81bbdc972f31e4dc5d7c25f6e3a4a136b1ef220d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 16:05:24 +0200 Subject: [PATCH 155/180] fix family used to get subset name --- openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py index 9d5c4dbb62..0af9a9a400 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py @@ -75,7 +75,7 @@ class CollectRenderScene(pyblish.api.ContextPlugin): task_name = workfile_context["task"] subset_name = get_subset_name_with_asset_doc( - family, + "render", variant, task_name, asset_doc, From 8c1fbf88d5e431156254583380ff24ec251832e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 16:20:15 +0200 Subject: [PATCH 156/180] added more info into label --- .../schemas/projects_schema/schema_project_tvpaint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index e1166dc2bb..20fe5b0855 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -31,7 +31,7 @@ }, { "type": "label", - "label": "It is possible to fill 'render_layer' or 'variant' in subset name template with custom value. Value of 'render_pass' is \"Beauty\"." + "label": "It is possible to fill 'render_layer' or 'variant' in subset name template with custom value.
- value of 'render_pass' is always \"beauty\"." }, { "type": "text", From 5014dfddf9b2a2c657d557871ae1e137b9415726 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 16:40:14 +0200 Subject: [PATCH 157/180] nuke: adding node knob for concurrent tasks - and fixing misspelling --- openpype/hosts/nuke/api/lib.py | 8 ++++++-- .../deadline/plugins/publish/submit_nuke_deadline.py | 10 ++++++++-- .../settings/defaults/project_settings/deadline.json | 2 +- .../projects_schema/schema_project_deadline.json | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index c22488f728..9601244d1d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1048,12 +1048,16 @@ def add_review_knob(node): def add_deadline_tab(node): node.addKnob(nuke.Tab_Knob("Deadline")) + knob = nuke.Int_Knob("deadlinePriority", "Priority") + knob.setValue(50) + node.addKnob(knob) + knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size") knob.setValue(0) node.addKnob(knob) - knob = nuke.Int_Knob("deadlinePriority", "Priority") - knob.setValue(50) + knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks") + knob.setValue(0) node.addKnob(knob) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 055d3c8a2c..442fcc1ddf 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -27,7 +27,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): # presets priority = 50 chunk_size = 1 - concurent_task = 1 + concurrent_tasks = 1 primary_pool = "" secondary_pool = "" group = "" @@ -154,6 +154,11 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): if chunk_size == 0 and self.chunk_size: chunk_size = self.chunk_size + # define chunk and priority + concurrent_tasks = instance.data.get("deadlineConcurrentTasks") + if concurrent_tasks == 0 and self.concurrent_tasks: + concurrent_tasks = self.concurrent_tasks + priority = instance.data.get("deadlinePriority") if not priority: priority = self.priority @@ -178,7 +183,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "Priority": priority, "ChunkSize": chunk_size, - "ConcurrentTasks": self.concurent_task, + "ConcurrentTasks": concurrent_tasks, + "Department": self.department, "Pool": self.primary_pool, diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index cfbb92e590..efaaa07be6 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -62,7 +62,7 @@ "use_published": true, "priority": 50, "chunk_size": 10, - "concurent_task": 1, + "concurrent_tasks": 1, "primary_pool": "", "secondary_pool": "", "group": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 700c3863fb..ea1173313b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -207,8 +207,8 @@ }, { "type": "number", - "key": "concurent_task", - "label": "Number of concurent tasks" + "key": "concurrent_tasks", + "label": "Number of concurrent tasks" }, { "type": "splitter" From 6916cc73d3ca4a8edf9aa59a8e3e1066f0cfc4ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 16:42:16 +0200 Subject: [PATCH 158/180] Nuke: fixing unicode type detection in effect loaders --- openpype/hosts/nuke/plugins/load/load_effects.py | 3 ++- openpype/hosts/nuke/plugins/load/load_effects_ip.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index 1ed32996e1..56c5acbb0a 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -1,6 +1,7 @@ import json from collections import OrderedDict import nuke +import six from avalon import io @@ -333,7 +334,7 @@ class LoadEffects(load.LoaderPlugin): for key, value in input.items()} elif isinstance(input, list): return [self.byteify(element) for element in input] - elif isinstance(input, str): + elif isinstance(input, six.text_type): return str(input) else: return input diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index 383776111f..0bc5f5a514 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -1,6 +1,6 @@ import json from collections import OrderedDict - +import six import nuke from avalon import io @@ -353,7 +353,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): for key, value in input.items()} elif isinstance(input, list): return [self.byteify(element) for element in input] - elif isinstance(input, str): + elif isinstance(input, six.text_type): return str(input) else: return input From 4fbf9c305087cd3b8e2795ed65ba30347354342d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 17:08:22 +0200 Subject: [PATCH 159/180] nuke: gizmo loader was also having unicode type tests --- openpype/hosts/nuke/plugins/load/load_gizmo_ip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py index df52a22364..46134afcf0 100644 --- a/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -1,5 +1,5 @@ import nuke - +import six from avalon import io from openpype.pipeline import ( @@ -243,8 +243,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin): for key, value in input.items()} elif isinstance(input, list): return [self.byteify(element) for element in input] - elif isinstance(input, unicode): - return input.encode('utf-8') + elif isinstance(input, six.text_type): + return str(input) else: return input From ea3cae8bc413474673a8242b47985b91dfd1eee6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 17:36:18 +0200 Subject: [PATCH 160/180] remove path existence checks in 'add_implementation_envs' --- openpype/hosts/blender/__init__.py | 4 ++-- openpype/hosts/hiero/__init__.py | 2 +- openpype/hosts/houdini/__init__.py | 4 ++-- openpype/hosts/maya/__init__.py | 2 +- openpype/hosts/nuke/__init__.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index 3081d3c9ba..0f27882c7e 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -29,12 +29,12 @@ def add_implementation_envs(env, _app): env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or "" ) for path in openpype_blender_user_scripts.split(os.pathsep): - if path and os.path.exists(path): + if path: previous_user_scripts.add(os.path.normpath(path)) blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or "" for path in blender_user_scripts.split(os.pathsep): - if path and os.path.exists(path): + if path: previous_user_scripts.add(os.path.normpath(path)) # Remove implementation path from user script paths as is set to diff --git a/openpype/hosts/hiero/__init__.py b/openpype/hosts/hiero/__init__.py index 2d674b3fa7..d2ac82391b 100644 --- a/openpype/hosts/hiero/__init__.py +++ b/openpype/hosts/hiero/__init__.py @@ -10,7 +10,7 @@ def add_implementation_envs(env, _app): ] old_hiero_path = env.get("HIERO_PLUGIN_PATH") or "" for path in old_hiero_path.split(os.pathsep): - if not path or not os.path.exists(path): + if not path: continue norm_path = os.path.normpath(path) diff --git a/openpype/hosts/houdini/__init__.py b/openpype/hosts/houdini/__init__.py index 8c12d13c81..a3ee38db8d 100644 --- a/openpype/hosts/houdini/__init__.py +++ b/openpype/hosts/houdini/__init__.py @@ -15,7 +15,7 @@ def add_implementation_envs(env, _app): old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or "" for path in old_houdini_path.split(os.pathsep): - if not path or not os.path.exists(path): + if not path: continue norm_path = os.path.normpath(path) @@ -23,7 +23,7 @@ def add_implementation_envs(env, _app): new_houdini_path.append(norm_path) for path in old_houdini_menu_path.split(os.pathsep): - if not path or not os.path.exists(path): + if not path: continue norm_path = os.path.normpath(path) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index b7d26a7818..c1c82c62e5 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -9,7 +9,7 @@ def add_implementation_envs(env, _app): ] old_python_path = env.get("PYTHONPATH") or "" for path in old_python_path.split(os.pathsep): - if not path or not os.path.exists(path): + if not path: continue norm_path = os.path.normpath(path) diff --git a/openpype/hosts/nuke/__init__.py b/openpype/hosts/nuke/__init__.py index 60b37ce1dd..134a6621c4 100644 --- a/openpype/hosts/nuke/__init__.py +++ b/openpype/hosts/nuke/__init__.py @@ -10,7 +10,7 @@ def add_implementation_envs(env, _app): ] old_nuke_path = env.get("NUKE_PATH") or "" for path in old_nuke_path.split(os.pathsep): - if not path or not os.path.exists(path): + if not path: continue norm_path = os.path.normpath(path) From fd420ff0dad0fe9c6e658613f587dfea65132340 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 17:40:54 +0200 Subject: [PATCH 161/180] nuke: adding concurrent task knob input to instance data --- .../nuke/plugins/publish/precollect_writes.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 85e98db7ed..4826b2788f 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -128,13 +128,17 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): } group_node = [x for x in instance if x.Class() == "Group"][0] - deadlineChunkSize = 1 + dl_chunk_size = 1 if "deadlineChunkSize" in group_node.knobs(): - deadlineChunkSize = group_node["deadlineChunkSize"].value() + dl_chunk_size = group_node["deadlineChunkSize"].value() - deadlinePriority = 50 + dl_priority = 50 if "deadlinePriority" in group_node.knobs(): - deadlinePriority = group_node["deadlinePriority"].value() + dl_priority = group_node["deadlinePriority"].value() + + dl_concurrent_tasks = 0 + if "deadlineConcurrentTasks" in group_node.knobs(): + dl_concurrent_tasks = group_node["deadlineConcurrentTasks"].value() instance.data.update({ "versionData": version_data, @@ -144,8 +148,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "label": label, "outputType": output_type, "colorspace": colorspace, - "deadlineChunkSize": deadlineChunkSize, - "deadlinePriority": deadlinePriority + "deadlineChunkSize": dl_chunk_size, + "deadlinePriority": dl_priority, + "deadlineConcurrentTasks": dl_concurrent_tasks }) if self.is_prerender(_families_test): From 23b31bb23bdc1188e25324deef0fd746f8b984b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 1 Apr 2022 18:00:50 +0200 Subject: [PATCH 162/180] ftrack: make nicer name only if set in settings --- .../plugins/publish/integrate_ftrack_instances.py | 12 ++++++++++-- .../settings/defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 6 ++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index b1a7da58f9..5c0b414d86 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -35,6 +35,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "image": "img", "reference": "reference" } + nicer_asset_name = False def process(self, instance): self.log.debug("instance {}".format(instance)) @@ -175,7 +176,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # condition for multiple reviewable representations # expand name to better label componenst - if is_first_review_repre and len(review_representations) > 1: + if ( + self.nicer_asset_name is not False + and is_first_review_repre + and len(review_representations) > 1 + ): asset_name = review_item["asset_data"]["name"] # define new extended name extended_asset_name = "_".join( @@ -278,7 +283,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): other_item = copy.deepcopy(base_component_item) # add extended name if any - if extended_asset_name: + if ( + self.nicer_asset_name is not False + and extended_asset_name is not False + ): other_item["asset_data"]["name"] = extended_asset_name other_item["component_data"] = { diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 89bb41a164..e97258b750 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -395,7 +395,8 @@ "vrayproxy": "cache", "redshiftproxy": "cache", "usd": "usd" - } + }, + "nicer_asset_name": false } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index cb59e9d67e..702b1812a2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -784,6 +784,12 @@ "object_type": { "type": "text" } + }, + { + "type": "boolean", + "key": "nicer_asset_name", + "label": "Nicer Asset name if multiple reviewable", + "default": false } ] } From 85e2601022e0f5bf4596293c30f0bc653992013a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 1 Apr 2022 18:36:29 +0200 Subject: [PATCH 163/180] =?UTF-8?q?fix=20=F0=9F=90=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../maya/plugins/publish/collect_unreal_skeletalmesh.py | 2 -- .../maya/plugins/publish/extract_unreal_skeletalmesh.py | 6 ------ .../maya/plugins/publish/extract_unreal_staticmesh.py | 3 +-- .../plugins/publish/validate_unreal_staticmesh_naming.py | 9 +-------- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py index 2b176e3a6d..79693bb35e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_skeletalmesh.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api -from avalon.api import Session -from openpype.api import get_project_settings class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin): diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 98dbf117dc..4dcad47e8c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -7,10 +7,6 @@ from maya import cmds # noqa import pyblish.api import openpype.api -from openpype.hosts.maya.api.lib import ( - parent_nodes, - maintained_selection -) from openpype.hosts.maya.api import fbx @@ -42,8 +38,6 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): geo = instance.data.get("geometry") joints = instance.data.get("joints") - joints_parent = cmds.listRelatives(joints, p=True) - to_extract = geo + joints # The export requires forward slashes because we need diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 92fa1b5933..69d51f9ff1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -8,8 +8,7 @@ import pyblish.api import openpype.api from openpype.hosts.maya.api.lib import ( parent_nodes, - maintained_selection, - delete_after + maintained_selection ) from openpype.hosts.maya.api import fbx diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index c0eeb82688..43f6c85827 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +"""Validator for correct naming of Static Meshes.""" from maya import cmds # noqa import pyblish.api import openpype.api @@ -71,13 +71,6 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin): ["CreateUnrealStaticMesh"] ["collision_prefixes"] ) - static_mesh_prefix = ( - project_settings - ["maya"] - ["create"] - ["CreateUnrealStaticMesh"] - ["static_mesh_prefix"] - ) if cls.validate_mesh: # compile regex for testing names From aeb0e4d060ebbb33cc672dc8fbe78077706dfa8e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 2 Apr 2022 03:39:54 +0000 Subject: [PATCH 164/180] [Automated] Bump version --- CHANGELOG.md | 64 +++++++++++++++++++-------------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f767bc71d5..d6bbef702a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,50 @@ # Changelog -## [3.9.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.2-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...HEAD) ### 📖 Documentation +- Documentation: Added mention of adding My Drive as a root [\#2999](https://github.com/pypeclub/OpenPype/pull/2999) - Docs: Added MongoDB requirements [\#2951](https://github.com/pypeclub/OpenPype/pull/2951) +- Documentation: New publisher develop docs [\#2896](https://github.com/pypeclub/OpenPype/pull/2896) **🆕 New features** -- Multiverse: First PR [\#2908](https://github.com/pypeclub/OpenPype/pull/2908) +- nuke: bypass baking [\#2992](https://github.com/pypeclub/OpenPype/pull/2992) +- Multiverse: Initial Support [\#2908](https://github.com/pypeclub/OpenPype/pull/2908) **🚀 Enhancements** +- Photoshop: create image without instance [\#3001](https://github.com/pypeclub/OpenPype/pull/3001) +- TVPaint: Render scene family [\#3000](https://github.com/pypeclub/OpenPype/pull/3000) +- Nuke: ReviewDataMov Read RAW attribute [\#2985](https://github.com/pypeclub/OpenPype/pull/2985) +- General: `METADATA\_KEYS` constant as `frozenset` for optimal immutable lookup [\#2980](https://github.com/pypeclub/OpenPype/pull/2980) +- General: Tools with host filters [\#2975](https://github.com/pypeclub/OpenPype/pull/2975) +- Hero versions: Use custom templates [\#2967](https://github.com/pypeclub/OpenPype/pull/2967) - Slack: Added configurable maximum file size of review upload to Slack [\#2945](https://github.com/pypeclub/OpenPype/pull/2945) - NewPublisher: Prepared implementation of optional pyblish plugin [\#2943](https://github.com/pypeclub/OpenPype/pull/2943) +- TVPaint: Extractor to convert PNG into EXR [\#2942](https://github.com/pypeclub/OpenPype/pull/2942) - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) - General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) -- CI: change the version bump logic [\#2919](https://github.com/pypeclub/OpenPype/pull/2919) -- Deadline: Add headless argument [\#2916](https://github.com/pypeclub/OpenPype/pull/2916) - Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) -- Ftrack: Fill workfile in custom attribute [\#2906](https://github.com/pypeclub/OpenPype/pull/2906) - Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) -- Settings UI: Add simple tooltips for settings entities [\#2901](https://github.com/pypeclub/OpenPype/pull/2901) **🐛 Bug fixes** +- Hosts: Remove path existence checks in 'add\_implementation\_envs' [\#3004](https://github.com/pypeclub/OpenPype/pull/3004) +- Fix - remove doubled dot in workfile created from template [\#2998](https://github.com/pypeclub/OpenPype/pull/2998) +- PS: fix renaming subset incorrectly in PS [\#2991](https://github.com/pypeclub/OpenPype/pull/2991) +- Fix: Disable setuptools auto discovery [\#2990](https://github.com/pypeclub/OpenPype/pull/2990) +- AEL: fix opening existing workfile if no scene opened [\#2989](https://github.com/pypeclub/OpenPype/pull/2989) +- Maya: Don't do hardlinks on windows for look publishing [\#2986](https://github.com/pypeclub/OpenPype/pull/2986) +- Settings UI: Fix version completer on linux [\#2981](https://github.com/pypeclub/OpenPype/pull/2981) +- Photoshop: Fix creation of subset names in PS review and workfile [\#2969](https://github.com/pypeclub/OpenPype/pull/2969) - Slack: Added default for review\_upload\_limit for Slack [\#2965](https://github.com/pypeclub/OpenPype/pull/2965) +- General: OIIO conversion for ffmeg can handle sequences [\#2958](https://github.com/pypeclub/OpenPype/pull/2958) - Settings: Conditional dictionary avoid invalid logs [\#2956](https://github.com/pypeclub/OpenPype/pull/2956) +- General: Smaller fixes and typos [\#2950](https://github.com/pypeclub/OpenPype/pull/2950) - LogViewer: Don't refresh on initialization [\#2949](https://github.com/pypeclub/OpenPype/pull/2949) - nuke: python3 compatibility issue with `iteritems` [\#2948](https://github.com/pypeclub/OpenPype/pull/2948) - General: anatomy data with correct task short key [\#2947](https://github.com/pypeclub/OpenPype/pull/2947) @@ -39,20 +55,21 @@ - Settings UI: Collapsed of collapsible wrapper works as expected [\#2934](https://github.com/pypeclub/OpenPype/pull/2934) - Maya: Do not pass `set` to maya commands \(fixes support for older maya versions\) [\#2932](https://github.com/pypeclub/OpenPype/pull/2932) - General: Don't print log record on OSError [\#2926](https://github.com/pypeclub/OpenPype/pull/2926) -- Hiero: Fix import of 'register\_event\_callback' [\#2924](https://github.com/pypeclub/OpenPype/pull/2924) -- Ftrack: Missing Ftrack id after editorial publish [\#2905](https://github.com/pypeclub/OpenPype/pull/2905) -- AfterEffects: Fix rendering for single frame in DL [\#2875](https://github.com/pypeclub/OpenPype/pull/2875) +- Flame: centos related debugging [\#2922](https://github.com/pypeclub/OpenPype/pull/2922) **🔀 Refactored code** +- General: Move plugins register and discover [\#2935](https://github.com/pypeclub/OpenPype/pull/2935) - General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931) - General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927) - General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918) -- General: Move formatting and workfile functions [\#2914](https://github.com/pypeclub/OpenPype/pull/2914) - General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912) **Merged pull requests:** +- Bump paramiko from 2.9.2 to 2.10.1 [\#2973](https://github.com/pypeclub/OpenPype/pull/2973) +- Bump minimist from 1.2.5 to 1.2.6 in /website [\#2954](https://github.com/pypeclub/OpenPype/pull/2954) +- Bump node-forge from 1.2.1 to 1.3.0 in /website [\#2953](https://github.com/pypeclub/OpenPype/pull/2953) - Maya - added transparency into review creator [\#2952](https://github.com/pypeclub/OpenPype/pull/2952) ## [3.9.1](https://github.com/pypeclub/OpenPype/tree/3.9.1) (2022-03-18) @@ -77,7 +94,6 @@ - General: Remove forgotten use of avalon Creator [\#2885](https://github.com/pypeclub/OpenPype/pull/2885) - General: Avoid circular import [\#2884](https://github.com/pypeclub/OpenPype/pull/2884) - Fixes for attaching loaded containers \(\#2837\) [\#2874](https://github.com/pypeclub/OpenPype/pull/2874) -- Maya: Deformer node ids validation plugin [\#2826](https://github.com/pypeclub/OpenPype/pull/2826) **🔀 Refactored code** @@ -88,10 +104,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.0-nightly.9...3.9.0) -**Deprecated:** - -- AssetCreator: Remove the tool [\#2845](https://github.com/pypeclub/OpenPype/pull/2845) - ### 📖 Documentation - Documentation: Change Photoshop & AfterEffects plugin path [\#2878](https://github.com/pypeclub/OpenPype/pull/2878) @@ -102,10 +114,6 @@ - NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867) - NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863) - TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859) -- New: Validation exceptions [\#2841](https://github.com/pypeclub/OpenPype/pull/2841) -- Maya: add loaded containers to published instance [\#2837](https://github.com/pypeclub/OpenPype/pull/2837) -- Ftrack: Can sync fps as string [\#2836](https://github.com/pypeclub/OpenPype/pull/2836) -- General: Custom function for find executable [\#2822](https://github.com/pypeclub/OpenPype/pull/2822) **🐛 Bug fixes** @@ -119,29 +127,11 @@ - General: Fix getattr clalback on dynamic modules [\#2855](https://github.com/pypeclub/OpenPype/pull/2855) - Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853) - WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852) -- WebPublisher: Fix wrong number of frames for video file [\#2851](https://github.com/pypeclub/OpenPype/pull/2851) -- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847) -- Nuke: fix multiple baking profile farm publishing [\#2842](https://github.com/pypeclub/OpenPype/pull/2842) -- Blender: Fixed parameters for FBX export of the camera [\#2840](https://github.com/pypeclub/OpenPype/pull/2840) -- Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832) -- Deadline: Remove recreated event [\#2828](https://github.com/pypeclub/OpenPype/pull/2828) -- Deadline: Added missing events folder [\#2827](https://github.com/pypeclub/OpenPype/pull/2827) -- Settings: Missing document with OP versions may break start of OpenPype [\#2825](https://github.com/pypeclub/OpenPype/pull/2825) -- Deadline: more detailed temp file name for environment json [\#2824](https://github.com/pypeclub/OpenPype/pull/2824) -- General: Host name was formed from obsolete code [\#2821](https://github.com/pypeclub/OpenPype/pull/2821) -- Settings UI: Fix "Apply from" action [\#2820](https://github.com/pypeclub/OpenPype/pull/2820) -- Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819) -- Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818) **🔀 Refactored code** - Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876) - General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854) -- General: Add vendors from avalon [\#2848](https://github.com/pypeclub/OpenPype/pull/2848) -- General: Basic event system [\#2846](https://github.com/pypeclub/OpenPype/pull/2846) -- General: Move change context functions [\#2839](https://github.com/pypeclub/OpenPype/pull/2839) -- Tools: Don't use avalon tools code [\#2829](https://github.com/pypeclub/OpenPype/pull/2829) -- Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823) ## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07) diff --git a/openpype/version.py b/openpype/version.py index 6d55672aca..c7ee5f0415 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.2-nightly.3" +__version__ = "3.9.2-nightly.4" diff --git a/pyproject.toml b/pyproject.toml index 479cd731fe..01bbaf48ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.2-nightly.3" # OpenPype +version = "3.9.2-nightly.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 8253ee7d3a7f08531b215acf243196b5fb497a03 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 4 Apr 2022 08:19:23 +0000 Subject: [PATCH 165/180] [Automated] Release --- CHANGELOG.md | 5 ++--- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6bbef702a..88623cee3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.9.2-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2) ### 📖 Documentation @@ -126,7 +126,6 @@ - New Publisher: Error dialog got right styles [\#2857](https://github.com/pypeclub/OpenPype/pull/2857) - General: Fix getattr clalback on dynamic modules [\#2855](https://github.com/pypeclub/OpenPype/pull/2855) - Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853) -- WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852) **🔀 Refactored code** diff --git a/openpype/version.py b/openpype/version.py index c7ee5f0415..3d759096c8 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.2-nightly.4" +__version__ = "3.9.2" diff --git a/pyproject.toml b/pyproject.toml index 01bbaf48ae..520234325c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.2-nightly.4" # OpenPype +version = "3.9.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From f186b99e22ab82d58e89fee7308356e6479042b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 11:16:59 +0200 Subject: [PATCH 166/180] nuke: add comment to code --- openpype/hosts/nuke/api/lib.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 9601244d1d..b1717ea7ff 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1057,12 +1057,19 @@ def add_deadline_tab(node): node.addKnob(knob) knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks") + # zero as default will trigger value from Setting during collection + # look to precollect_write.py knob.setValue(0) node.addKnob(knob) def get_deadline_knob_names(): - return ["Deadline", "deadlineChunkSize", "deadlinePriority"] + return [ + "Deadline", + "deadlineChunkSize", + "deadlinePriority", + "deadlineConcurrentTasks" + ] def create_backdrop(label="", color=None, layer=0, From 71384d8cd6b049f5341167b583a3c830d7104c11 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 11:32:39 +0200 Subject: [PATCH 167/180] deadline: adding strict method for getting --- .../deadline/plugins/publish/submit_nuke_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 442fcc1ddf..9b5800c33f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -150,16 +150,16 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): pass # define chunk and priority - chunk_size = instance.data.get("deadlineChunkSize") + chunk_size = instance.data["deadlineChunkSize"] if chunk_size == 0 and self.chunk_size: chunk_size = self.chunk_size # define chunk and priority - concurrent_tasks = instance.data.get("deadlineConcurrentTasks") + concurrent_tasks = instance.data["deadlineConcurrentTasks"] if concurrent_tasks == 0 and self.concurrent_tasks: concurrent_tasks = self.concurrent_tasks - priority = instance.data.get("deadlinePriority") + priority = instance.data["deadlinePriority"] if not priority: priority = self.priority From 97dca92bbe930b2063119dceba2c306a0ea69e6c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 11:56:42 +0200 Subject: [PATCH 168/180] global | nuke: generalizing attribute detection --- .../hosts/nuke/plugins/publish/extract_review_data_mov.py | 1 + .../modules/deadline/plugins/publish/submit_publish_job.py | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 31a8ff18ee..22b371d8e9 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -123,6 +123,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): if generated_repres: # assign to representations instance.data["representations"] += generated_repres + instance.data["hasReviewableRepresentations"] = True else: instance.data["families"].remove("review") self.log.info(( diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 6730c6a7dd..5755619292 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -528,11 +528,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # preview video rendering for app in self.aov_filter.keys(): if os.environ.get("AVALON_APP", "") == app: - # no need to add review if baking in nuke present - if ( - app == "nuke" - and instance.get("bakingNukeScripts") - ): + # no need to add review if `hasReviewableRepresentations` + if instance.get("hasReviewableRepresentations"): break # iteratre all aov filters From c9edd81e17cf5605890a237fff9cc6d2f12addc9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 12:04:56 +0200 Subject: [PATCH 169/180] ftrack: renameing attribute to `keep_first_subset_name_for_review` --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 6 +++--- openpype/settings/defaults/project_settings/ftrack.json | 2 +- .../schemas/projects_schema/schema_project_ftrack.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 5c0b414d86..f79bdb31d7 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -35,7 +35,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "image": "img", "reference": "reference" } - nicer_asset_name = False + keep_first_subset_name_for_review = True def process(self, instance): self.log.debug("instance {}".format(instance)) @@ -177,7 +177,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # condition for multiple reviewable representations # expand name to better label componenst if ( - self.nicer_asset_name is not False + not self.keep_first_subset_name_for_review and is_first_review_repre and len(review_representations) > 1 ): @@ -284,7 +284,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # add extended name if any if ( - self.nicer_asset_name is not False + not self.keep_first_subset_name_for_review and extended_asset_name is not False ): other_item["asset_data"]["name"] = extended_asset_name diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index e97258b750..ca1cfe1e12 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -396,7 +396,7 @@ "redshiftproxy": "cache", "usd": "usd" }, - "nicer_asset_name": false + "keep_first_subset_name_for_review": true } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 702b1812a2..fb384882c6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -787,9 +787,9 @@ }, { "type": "boolean", - "key": "nicer_asset_name", - "label": "Nicer Asset name if multiple reviewable", - "default": false + "key": "keep_first_subset_name_for_review", + "label": "Make subset name as first asset name", + "default": true } ] } From c32b0ec7703e2821204667afb354a18186e36c13 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Apr 2022 12:21:17 +0200 Subject: [PATCH 170/180] ftrack: simplification of logic --- .../publish/integrate_ftrack_instances.py | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index f79bdb31d7..c11d5b9c68 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -169,7 +169,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Change asset name of each new component for review is_first_review_repre = True not_first_components = [] - extended_asset_name = False + extended_asset_name = "" + multiple_reviewable = len(review_representations) > 1 for repre in review_representations: # Create copy of base comp item and append it review_item = copy.deepcopy(base_component_item) @@ -178,8 +179,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # expand name to better label componenst if ( not self.keep_first_subset_name_for_review - and is_first_review_repre - and len(review_representations) > 1 + and multiple_reviewable ): asset_name = review_item["asset_data"]["name"] # define new extended name @@ -187,18 +187,21 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): (asset_name, repre["name"]) ) review_item["asset_data"]["name"] = extended_asset_name - # and rename all already created components - for _ci in component_list: - _ci["asset_data"]["name"] = extended_asset_name - # and rename all already created src components - for _sci in src_components_to_add: - _sci["asset_data"]["name"] = extended_asset_name + # rename asset name only if multiple reviewable repre + if is_first_review_repre: + # and rename all already created components + for _ci in component_list: + _ci["asset_data"]["name"] = extended_asset_name - # rename also first thumbnail component if any - if first_thumbnail_component is not None: - first_thumbnail_component[ - "asset_data"]["name"] = extended_asset_name + # and rename all already created src components + for _sci in src_components_to_add: + _sci["asset_data"]["name"] = extended_asset_name + + # rename also first thumbnail component if any + if first_thumbnail_component is not None: + first_thumbnail_component[ + "asset_data"]["name"] = extended_asset_name frame_start = repre.get("frameStartFtrack") frame_end = repre.get("frameEndFtrack") @@ -230,15 +233,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): } } - # rename asset name only if multiple reviewable repre if is_first_review_repre: is_first_review_repre = False else: - # Add representation name to asset name of "not first" review - asset_name = review_item["asset_data"]["name"] - review_item["asset_data"]["name"] = "_".join( - (asset_name, repre["name"]) - ) not_first_components.append(review_item) # Create copy of item before setting location @@ -283,10 +280,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): other_item = copy.deepcopy(base_component_item) # add extended name if any - if ( - not self.keep_first_subset_name_for_review - and extended_asset_name is not False - ): + if extended_asset_name: other_item["asset_data"]["name"] = extended_asset_name other_item["component_data"] = { From 582e35a4829f6494c9d145cbf027e4f8446f6ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 4 Apr 2022 16:11:04 +0200 Subject: [PATCH 171/180] Update openpype/hosts/nuke/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/nuke/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index b1717ea7ff..e05c6aecbd 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1057,8 +1057,8 @@ def add_deadline_tab(node): node.addKnob(knob) knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks") - # zero as default will trigger value from Setting during collection - # look to precollect_write.py + # zero as default will get value from Settings during collection + # instead of being an explicit user override, see precollect_write.py knob.setValue(0) node.addKnob(knob) From ee7ae9edfb9657320e1b2e0610ae9df07f0182bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 10:27:47 +0200 Subject: [PATCH 172/180] fix registration of creator paths --- openpype/hosts/traypublisher/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/traypublisher/api/pipeline.py b/openpype/hosts/traypublisher/api/pipeline.py index a39e5641ae..24175883d9 100644 --- a/openpype/hosts/traypublisher/api/pipeline.py +++ b/openpype/hosts/traypublisher/api/pipeline.py @@ -7,7 +7,7 @@ from avalon import io import avalon.api import pyblish.api -from openpype.pipeline import BaseCreator +from openpype.pipeline import register_creator_plugin_path ROOT_DIR = os.path.dirname(os.path.dirname( os.path.abspath(__file__) @@ -169,7 +169,7 @@ def install(): pyblish.api.register_host("traypublisher") pyblish.api.register_plugin_path(PUBLISH_PATH) - avalon.api.register_plugin_path(BaseCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) def set_project_name(project_name): From 38652d2c6a620634d5b5ec3ebc58169de94beed3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 10:28:03 +0200 Subject: [PATCH 173/180] added some titles to validation errors --- .../plugins/publish/validate_workfile.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py index e8eeb46065..7501051669 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py @@ -14,11 +14,22 @@ class ValidateWorkfilePath(pyblish.api.InstancePlugin): def process(self, instance): filepath = instance.data["sourceFilepath"] if not filepath: - raise PublishValidationError(( - "Filepath of 'workfile' instance \"{}\" is not set" - ).format(instance.data["name"])) + raise PublishValidationError( + ( + "Filepath of 'workfile' instance \"{}\" is not set" + ).format(instance.data["name"]), + "File not filled", + "## Missing file\nYou are supposed to fill the path." + ) if not os.path.exists(filepath): - raise PublishValidationError(( - "Filepath of 'workfile' instance \"{}\" does not exist: {}" - ).format(instance.data["name"], filepath)) + raise PublishValidationError( + ( + "Filepath of 'workfile' instance \"{}\" does not exist: {}" + ).format(instance.data["name"], filepath), + "File not found", + ( + "## File was not found\nFile \"{}\" was not found." + " Check if the path is still available." + ).format(filepath) + ) From dded71a5d41d90c55bace8e06732a8c8f558a667 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 10:29:28 +0200 Subject: [PATCH 174/180] moved "published" checkbox after filter --- openpype/tools/workfiles/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index edfcb17722..b4ff830459 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -113,8 +113,8 @@ class FilesWidget(QtWidgets.QWidget): filter_layout = QtWidgets.QHBoxLayout(filter_widget) filter_layout.setContentsMargins(0, 0, 0, 0) - filter_layout.addWidget(published_checkbox, 0) filter_layout.addWidget(filter_input, 1) + filter_layout.addWidget(published_checkbox, 0) # Create the Files models extensions = set(self.host.file_extensions()) From 1bb36035f94487abaff9abd9b4efd3423c4c9e83 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Apr 2022 10:29:51 +0200 Subject: [PATCH 175/180] swapped cancel and copy & open buttons --- openpype/tools/workfiles/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index b4ff830459..56af7752da 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -210,8 +210,8 @@ class FilesWidget(QtWidgets.QWidget): publish_btns_layout.setContentsMargins(0, 0, 0, 0) publish_btns_layout.addWidget(btn_save_as_published, 1) publish_btns_layout.addWidget(btn_change_context, 1) - publish_btns_layout.addWidget(btn_cancel_published, 1) publish_btns_layout.addWidget(btn_select_context_published, 1) + publish_btns_layout.addWidget(btn_cancel_published, 1) btns_layout = QtWidgets.QHBoxLayout(btns_widget) btns_layout.setContentsMargins(0, 0, 0, 0) From 0437d0a1304347d9bf803083900a78f8e8a8a1fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 13:54:26 +0200 Subject: [PATCH 176/180] general: adding `hasReviewableRepresentations` to skeleton data --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 5755619292..a8f4fec563 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -729,7 +729,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "resolutionWidth": data.get("resolutionWidth", 1920), "resolutionHeight": data.get("resolutionHeight", 1080), "multipartExr": data.get("multipartExr", False), - "jobBatchName": data.get("jobBatchName", "") + "jobBatchName": data.get("jobBatchName", ""), + "hasReviewableRepresentations": data.get( + "hasReviewableRepresentations") } if "prerender" in instance.data["families"]: From 6893fa22e90e5d36d6a0bcd5726636534a903bec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 5 Apr 2022 14:01:35 +0200 Subject: [PATCH 177/180] ftrack: improving code logic it was not correctly distributing components --- .../publish/integrate_ftrack_instances.py | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index c11d5b9c68..b54db918a6 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -175,33 +175,40 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # Create copy of base comp item and append it review_item = copy.deepcopy(base_component_item) - # condition for multiple reviewable representations - # expand name to better label componenst + # get asset name and define extended name variant + asset_name = review_item["asset_data"]["name"] + extended_asset_name = "_".join( + (asset_name, repre["name"]) + ) + + # reset extended if no need for extended asset name if ( - not self.keep_first_subset_name_for_review - and multiple_reviewable + self.keep_first_subset_name_for_review + and is_first_review_repre ): - asset_name = review_item["asset_data"]["name"] - # define new extended name - extended_asset_name = "_".join( - (asset_name, repre["name"]) - ) - review_item["asset_data"]["name"] = extended_asset_name + extended_asset_name = "" + else: + # only rename if multiple reviewable + if multiple_reviewable: + review_item["asset_data"]["name"] = extended_asset_name + else: + extended_asset_name = "" - # rename asset name only if multiple reviewable repre - if is_first_review_repre: - # and rename all already created components - for _ci in component_list: - _ci["asset_data"]["name"] = extended_asset_name + # rename all already created components + # only if first repre and extended name available + if is_first_review_repre and extended_asset_name: + # and rename all already created components + for _ci in component_list: + _ci["asset_data"]["name"] = extended_asset_name - # and rename all already created src components - for _sci in src_components_to_add: - _sci["asset_data"]["name"] = extended_asset_name + # and rename all already created src components + for _sci in src_components_to_add: + _sci["asset_data"]["name"] = extended_asset_name - # rename also first thumbnail component if any - if first_thumbnail_component is not None: - first_thumbnail_component[ - "asset_data"]["name"] = extended_asset_name + # rename also first thumbnail component if any + if first_thumbnail_component is not None: + first_thumbnail_component[ + "asset_data"]["name"] = extended_asset_name frame_start = repre.get("frameStartFtrack") frame_end = repre.get("frameEndFtrack") @@ -236,6 +243,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if is_first_review_repre: is_first_review_repre = False else: + # later detection for thumbnail duplication not_first_components.append(review_item) # Create copy of item before setting location @@ -280,7 +288,10 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): other_item = copy.deepcopy(base_component_item) # add extended name if any - if extended_asset_name: + if ( + not self.keep_first_subset_name_for_review + and extended_asset_name + ): other_item["asset_data"]["name"] = extended_asset_name other_item["component_data"] = { From 557aafdae326e5799f9258f02e5f27ef5ecb5f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 5 Apr 2022 14:36:11 +0200 Subject: [PATCH 178/180] fixed skeletal root --- .../publish/extract_unreal_skeletalmesh.py | 16 +++++----------- openpype/plugins/publish/integrate_new.py | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py index 4dcad47e8c..7ef7f2f181 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh.py @@ -55,25 +55,19 @@ class ExtractUnrealSkeletalMesh(openpype.api.Extractor): # variant we extract we need to rename top node of the rig correctly. # It is finally done in context manager so it won't affect current # scene. - parent = "{}{}".format( - instance.data["asset"], - instance.data.get("variant", "") - ) - joints_parents = cmds.ls(joints, long=True) - geo_parents = cmds.ls(geo, long=True) + # we rely on hierarchy under one root. + original_parent = to_extract[0].split("|")[1] - parent_node = { - parent.split("|")[1] for parent in (joints_parents + geo_parents) - }.pop() + parent_node = instance.data.get("asset") renamed_to_extract = [] for node in to_extract: node_path = node.split("|") - node_path[1] = parent + node_path[1] = parent_node renamed_to_extract.append("|".join(node_path)) - with renamed(parent_node, parent): + with renamed(original_parent, parent_node): self.log.info("Extracting: {}".format(renamed_to_extract, path)) fbx_exporter.export(renamed_to_extract, path) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index afa4e0a9cf..acdb05dd93 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -107,7 +107,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hda", "usd", "staticMesh", - "skeletalMesh" + "skeletalMesh", "usdComposition", "usdOverride" ] From 64036c77d47fe84bd7b16a9c467dec73161d0cda Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 6 Apr 2022 03:40:50 +0000 Subject: [PATCH 179/180] [Automated] Bump version --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++++++----------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88623cee3d..9a53311d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,37 @@ # Changelog +## [3.9.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.2...HEAD) + +### 📖 Documentation + +- Website Docs: Manager Ftrack fix broken links [\#2979](https://github.com/pypeclub/OpenPype/pull/2979) + +**🆕 New features** + +- Publishing textures for Unreal [\#2988](https://github.com/pypeclub/OpenPype/pull/2988) +- Maya to Unreal \> Static and Skeletal Meshes [\#2978](https://github.com/pypeclub/OpenPype/pull/2978) + +**🚀 Enhancements** + +- Nuke: add concurrency attr to deadline job [\#3005](https://github.com/pypeclub/OpenPype/pull/3005) +- Workfiles tool: Save as published workfiles [\#2937](https://github.com/pypeclub/OpenPype/pull/2937) + +**🐛 Bug fixes** + +- Ftrack: multiple reviewable componets [\#3012](https://github.com/pypeclub/OpenPype/pull/3012) +- Tray publisher: Fixes after code movement [\#3010](https://github.com/pypeclub/OpenPype/pull/3010) +- Nuke: fixing unicode type detection in effect loaders [\#3002](https://github.com/pypeclub/OpenPype/pull/3002) +- Nuke: removing redundant Ftrack asset when farm publishing [\#2996](https://github.com/pypeclub/OpenPype/pull/2996) + +**Merged pull requests:** + +- General: adding limitations for pyright [\#2994](https://github.com/pypeclub/OpenPype/pull/2994) + ## [3.9.2](https://github.com/pypeclub/OpenPype/tree/3.9.2) (2022-04-04) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.1...3.9.2) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.9.2-nightly.4...3.9.2) ### 📖 Documentation @@ -29,7 +58,7 @@ - Workfiles: Open published workfiles [\#2925](https://github.com/pypeclub/OpenPype/pull/2925) - General: Default modules loaded dynamically [\#2923](https://github.com/pypeclub/OpenPype/pull/2923) - Nuke: Add no-audio Tag [\#2911](https://github.com/pypeclub/OpenPype/pull/2911) -- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) +- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) **🐛 Bug fixes** @@ -63,6 +92,7 @@ - General: Move Attribute Definitions from pipeline [\#2931](https://github.com/pypeclub/OpenPype/pull/2931) - General: Removed silo references and terminal splash [\#2927](https://github.com/pypeclub/OpenPype/pull/2927) - General: Move pipeline constants to OpenPype [\#2918](https://github.com/pypeclub/OpenPype/pull/2918) +- General: Move formatting and workfile functions [\#2914](https://github.com/pypeclub/OpenPype/pull/2914) - General: Move remaining plugins from avalon [\#2912](https://github.com/pypeclub/OpenPype/pull/2912) **Merged pull requests:** @@ -79,8 +109,8 @@ **🚀 Enhancements** - General: Change how OPENPYPE\_DEBUG value is handled [\#2907](https://github.com/pypeclub/OpenPype/pull/2907) +- Nuke: improving readability [\#2903](https://github.com/pypeclub/OpenPype/pull/2903) - nuke: imageio adding ocio config version 1.2 [\#2897](https://github.com/pypeclub/OpenPype/pull/2897) -- Flame: support for comment with xml attribute overrides [\#2892](https://github.com/pypeclub/OpenPype/pull/2892) - Nuke: ExtractReviewSlate can handle more codes and profiles [\#2879](https://github.com/pypeclub/OpenPype/pull/2879) - Flame: sequence used for reference video [\#2869](https://github.com/pypeclub/OpenPype/pull/2869) @@ -112,8 +142,6 @@ - General: Subset name filtering in ExtractReview outpus [\#2872](https://github.com/pypeclub/OpenPype/pull/2872) - NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867) -- NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863) -- TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859) **🐛 Bug fixes** @@ -121,16 +149,10 @@ - Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868) - Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866) - General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864) -- General: ffmpeg was crashing on slate merge [\#2860](https://github.com/pypeclub/OpenPype/pull/2860) -- WebPublisher: Video file was published with one too many frame [\#2858](https://github.com/pypeclub/OpenPype/pull/2858) -- New Publisher: Error dialog got right styles [\#2857](https://github.com/pypeclub/OpenPype/pull/2857) -- General: Fix getattr clalback on dynamic modules [\#2855](https://github.com/pypeclub/OpenPype/pull/2855) -- Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853) **🔀 Refactored code** - Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876) -- General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854) ## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07) diff --git a/openpype/version.py b/openpype/version.py index 3d759096c8..c314151e9b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.2" +__version__ = "3.9.3-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 1084382d9a..dd1a666dea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.2" # OpenPype +version = "3.9.3-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 60df02602b3a27e6b8755feb0762e3639375ae2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:07:51 +0200 Subject: [PATCH 180/180] removed usage of config callback in maya --- openpype/hosts/maya/api/pipeline.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index a8834d1ea3..f6f3472eef 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -9,8 +9,6 @@ import maya.api.OpenMaya as om import pyblish.api import avalon.api -from avalon.lib import find_submodule - import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -20,7 +18,6 @@ from openpype.lib import ( ) from openpype.lib.path_tools import HostDirmap from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, @@ -270,21 +267,8 @@ def ls(): """ container_names = _ls() - - has_metadata_collector = False - config_host = find_submodule(avalon.api.registered_config(), "maya") - if hasattr(config_host, "collect_container_metadata"): - has_metadata_collector = True - for container in sorted(container_names): - data = parse_container(container) - - # Collect custom data if attribute is present - if has_metadata_collector: - metadata = config_host.collect_container_metadata(container) - data.update(metadata) - - yield data + yield parse_container(container) def containerise(name,