From 6af973fc8fc0c1ad5a5adf0996ef64c413629fce Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Dec 2022 08:59:14 +0000 Subject: [PATCH 001/158] Adjust xgen creation --- openpype/hosts/maya/plugins/create/create_xgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_xgen.py b/openpype/hosts/maya/plugins/create/create_xgen.py index 8672c06a1e..70e23cf47b 100644 --- a/openpype/hosts/maya/plugins/create/create_xgen.py +++ b/openpype/hosts/maya/plugins/create/create_xgen.py @@ -2,9 +2,9 @@ from openpype.hosts.maya.api import plugin class CreateXgen(plugin.Creator): - """Xgen interactive export""" + """Xgen""" name = "xgen" - label = "Xgen Interactive" + label = "Xgen" family = "xgen" icon = "pagelines" From 91d935c948c18ddf85dbfa54db924907b0067fcb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Dec 2022 08:59:26 +0000 Subject: [PATCH 002/158] Validate xgen --- .../maya/plugins/publish/validate_xgen.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_xgen.py diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py new file mode 100644 index 0000000000..7ccd02bb07 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -0,0 +1,28 @@ +import maya.cmds as cmds + +import pyblish.api + + +class ValidateXgen(pyblish.api.InstancePlugin): + """Ensure Xgen objectset only contains collections.""" + + label = "Xgen" + order = pyblish.api.ValidatorOrder + host = ["maya"] + families = ["xgen"] + + def process(self, instance): + nodes = ( + cmds.ls(instance, type="xgmPalette", long=True) + + cmds.ls(instance, type="transform", long=True) + + cmds.ls(instance, type="xgmDescription", long=True) + + cmds.ls(instance, type="xgmSubdPatch", long=True) + ) + remainder_nodes = [] + for node in instance: + if node in nodes: + continue + remainder_nodes.append(node) + + msg = "Invalid nodes in the objectset:\n{}".format(remainder_nodes) + assert not remainder_nodes, msg From 94f0f773f865857da894e824630fdacec8666b4c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 18 Dec 2022 21:08:25 +0000 Subject: [PATCH 003/158] Exclude xgen from default Maya loading. --- openpype/hosts/maya/plugins/load/actions.py | 15 ++++++++++++++- .../hosts/maya/plugins/load/load_reference.py | 1 - 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 9cc9180d6e..ced0f7ccbb 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -93,7 +93,20 @@ class ImportMayaLoader(load.LoaderPlugin): """ representations = ["ma", "mb", "obj"] - families = ["*"] + families = [ + "model", + "pointcache", + "proxyAbc", + "animation", + "mayaAscii", + "mayaScene", + "setdress", + "layout", + "camera", + "rig", + "camerarig", + "staticMesh" + ] label = "Import" order = 10 diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c6b07b036d..31a36a587c 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -25,7 +25,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "camera", "rig", "camerarig", - "xgen", "staticMesh"] representations = ["ma", "abc", "fbx", "mb"] From bf2d4ee323eb3df631cdbb93fabeb58242da0886 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 18 Dec 2022 21:09:55 +0000 Subject: [PATCH 004/158] Initial working publish of xgen --- .../maya/plugins/publish/collect_xgen.py | 39 +++++ .../plugins/publish/extract_maya_scene_raw.py | 3 +- .../maya/plugins/publish/extract_xgen.py | 138 ++++++++++++++++++ .../maya/plugins/publish/validate_xgen.py | 11 +- .../plugins/publish/collect_resources_path.py | 3 +- 5 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_xgen.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_xgen.py diff --git a/openpype/hosts/maya/plugins/publish/collect_xgen.py b/openpype/hosts/maya/plugins/publish/collect_xgen.py new file mode 100644 index 0000000000..fa7ec3776d --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_xgen.py @@ -0,0 +1,39 @@ +from maya import cmds + +import pyblish.api +from openpype.hosts.maya.api.lib import get_attribute_input + + +class CollectXgen(pyblish.api.InstancePlugin): + """Collect Xgen""" + + order = pyblish.api.CollectorOrder + 0.499999 + label = "Collect Xgen" + families = ["xgen"] + + def process(self, instance): + data = { + "xgenPalettes": cmds.ls(instance, type="xgmPalette", long=True), + "xgmDescriptions": cmds.ls( + instance, type="xgmDescription", long=True + ), + "xgmSubdPatches": cmds.ls(instance, type="xgmSubdPatch", long=True) + } + data["xgenNodes"] = ( + data["xgenPalettes"] + + data["xgmDescriptions"] + + data["xgmSubdPatches"] + ) + + if data["xgenPalettes"]: + data["xgenPalette"] = data["xgenPalettes"][0] + + data["xgenConnections"] = {} + for node in data["xgmSubdPatches"]: + data["xgenConnections"][node] = {} + for attr in ["transform", "geometry"]: + input = get_attribute_input("{}.{}".format(node, attr)) + data["xgenConnections"][node][attr] = input + + self.log.info(data) + instance.data.update(data) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 3769ec3605..c2411ca651 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -20,8 +20,7 @@ class ExtractMayaSceneRaw(publish.Extractor): "mayaScene", "setdress", "layout", - "camerarig", - "xgen"] + "camerarig"] scene_type = "ma" def process(self, instance): diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py new file mode 100644 index 0000000000..69d1d3db40 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -0,0 +1,138 @@ +import os +import copy + +from maya import cmds +import pymel.core as pc +import xgenm + +from openpype.pipeline import publish +from openpype.hosts.maya.api.lib import maintained_selection, attribute_values +from openpype.lib import StringTemplate + + +class ExtractXgenCache(publish.Extractor): + """Extract Xgen""" + + label = "Extract Xgen" + hosts = ["maya"] + families = ["xgen"] + scene_type = "ma" + + def process(self, instance): + if "representations" not in instance.data: + instance.data["representations"] = [] + + staging_dir = self.staging_dir(instance) + maya_filename = "{}.{}".format(instance.data["name"], self.scene_type) + maya_filepath = os.path.join(staging_dir, maya_filename) + + # Get published xgen file name. + template_data = copy.deepcopy(instance.data["anatomyData"]) + template_data.update({"ext": "xgen"}) + templates = instance.context.data["anatomy"].templates["publish"] + xgen_filename = StringTemplate(templates["file"]).format(template_data) + name = instance.data["xgenPalette"].replace(":", "__").replace("|", "") + value = xgen_filename.replace(".xgen", "__" + name + ".xgen") + attribute_data = { + "{}.xgFileName".format(instance.data["xgenPalette"]): xgen_filename + } + + # Export xgen palette files. + xgen_path = os.path.join(staging_dir, value).replace("\\", "/") + xgenm.exportPalette( + instance.data["xgenPalette"].replace("|", ""), xgen_path + ) + self.log.info("Extracted to {}".format(xgen_path)) + + representation = { + "name": name, + "ext": "xgen", + "files": value, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + # Collect nodes to export. + duplicate_nodes = [] + for node, connections in instance.data["xgenConnections"].items(): + transform_name = connections["transform"].split(".")[0] + + # Duplicate_transform subd patch geometry. + duplicate_transform = pc.duplicate(transform_name)[0] + pc.parent(duplicate_transform, world=True) + duplicate_transform.name(stripNamespace=True) + duplicate_shape = pc.listRelatives( + duplicate_transform, shapes=True + )[0] + + pc.connectAttr( + "{}.matrix".format(duplicate_transform), + "{}.transform".format(node), + force=True + ) + pc.connectAttr( + "{}.worldMesh".format(duplicate_shape), + "{}.geometry".format(node), + force=True + ) + + duplicate_nodes.append(duplicate_transform) + + # Import xgen onto the duplicate. + with maintained_selection(): + cmds.select(duplicate_nodes) + collection = xgenm.importPalette(xgen_path, []) + + # Export Maya file. + type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" + with attribute_values(attribute_data): + with maintained_selection(): + cmds.select(duplicate_nodes + [collection]) + cmds.file( + maya_filepath, + force=True, + type=type, + exportSelected=True, + preserveReferences=True, + constructionHistory=True, + shader=True, + constraints=True, + expressions=True + ) + + self.log.info("Extracted to {}".format(maya_filepath)) + + representation = { + "name": self.scene_type, + "ext": self.scene_type, + "files": maya_filename, + "stagingDir": staging_dir, + "data": {"xgenName": collection} + } + instance.data["representations"].append(representation) + + # Revert to original xgen connections. + for node, connections in instance.data["xgenConnections"].items(): + for attr, src in connections.items(): + cmds.connectAttr( + src, "{}.{}".format(node, attr), force=True + ) + + cmds.delete(duplicate_nodes + [collection]) + + # Setup transfers. + #needs to reduce resources to only what is used for the collections in + #the objectset + xgen_dir = os.path.join( + os.path.dirname(instance.context.data["currentFile"]), "xgen" + ) + transfers = [] + for root, dirs, files in os.walk(xgen_dir): + for file in files: + source = os.path.join(root, file) + destination = source.replace( + xgen_dir, instance.data["resourcesDir"] + ) + transfers.append((source, destination)) + + instance.data["transfers"] = transfers diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 7ccd02bb07..fc93f62846 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -4,19 +4,18 @@ import pyblish.api class ValidateXgen(pyblish.api.InstancePlugin): - """Ensure Xgen objectset only contains collections.""" + """Validate Xgen data.""" - label = "Xgen" + label = "Validate Xgen" order = pyblish.api.ValidatorOrder host = ["maya"] families = ["xgen"] def process(self, instance): + # Validate only xgen collections are in objectset. nodes = ( - cmds.ls(instance, type="xgmPalette", long=True) + - cmds.ls(instance, type="transform", long=True) + - cmds.ls(instance, type="xgmDescription", long=True) + - cmds.ls(instance, type="xgmSubdPatch", long=True) + instance.data["xgenNodes"] + + cmds.ls(instance, type="transform", long=True) ) remainder_nodes = [] for node in instance: diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index a2d5b95ab2..52aba0eca3 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -57,7 +57,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "background", "effect", "staticMesh", - "skeletalMesh" + "skeletalMesh", + "xgen" ] def process(self, instance): From a1890ee04d9b06031a9c136a276ffba70b2f67b3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 18 Dec 2022 21:10:07 +0000 Subject: [PATCH 005/158] Initial working loading of xgen --- openpype/hosts/maya/plugins/load/load_xgen.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 openpype/hosts/maya/plugins/load/load_xgen.py diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py new file mode 100644 index 0000000000..3b7451f95c --- /dev/null +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -0,0 +1,90 @@ +import os +import shutil + +import maya.cmds as cmds +import pymel.core as pm + +import openpype.hosts.maya.api.plugin +from openpype.hosts.maya.api.lib import maintained_selection +from openpype.hosts.maya.api import current_file + + +class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): + """Load Xgen as reference""" + + families = ["xgen"] + representations = ["ma", "mb"] + + label = "Reference Xgen" + icon = "code-fork" + color = "orange" + + def process_reference(self, context, name, namespace, options): + maya_filepath = self.prepare_root_value( + self.fname, context["project"]["name"] + ) + project_path = os.path.dirname(current_file()) + + # Setup xgen palette file. + # Copy the xgen palette file from published version. + _, maya_extension = os.path.splitext(maya_filepath) + source = maya_filepath.replace(maya_extension, ".xgen") + destination = os.path.join( + project_path, + "{basename}__{namespace}__{name}.xgen".format( + basename=os.path.splitext(os.path.basename(current_file()))[0], + namespace=namespace, + name=context["representation"]["data"]["xgenName"] + ) + ) + shutil.copy(source, destination) + + # Modify xgDataPath and xgProjectPath to have current workspace first + # and published version directory second. This ensure that any newly + # created xgen files are created in the current workspace. + resources_path = os.path.join(os.path.dirname(source), "resources") + lines = [] + with open(destination, "r") as f: + for line in [line.rstrip() for line in f]: + if line.startswith("\txgDataPath"): + data_path = line.split("\t")[-1] + line = "\txgDataPath\t\t{}{}{}".format( + data_path, + os.pathsep, + data_path.replace( + "${PROJECT}xgen", resources_path.replace("\\", "/") + ) + ) + + if line.startswith("\txgProjectPath"): + line = "\txgProjectPath\t\t{}/".format( + project_path.replace("\\", "/") + ) + + lines.append(line) + + with open(destination, "w") as f: + f.write("\n".join(lines)) + + # Reference xgen. Xgen does not like being referenced in under a group. + new_nodes = [] + + with maintained_selection(): + nodes = cmds.file( + maya_filepath, + namespace=namespace, + sharedReferenceFile=False, + reference=True, + returnNewNodes=True + ) + + shapes = cmds.ls(nodes, shapes=True, long=True) + + new_nodes = (list(set(nodes) - set(shapes))) + + self[:] = new_nodes + + return new_nodes + + def update(self, container, representation): + pass From 2a7d05e090b6b3a5d30f52f7006f57f9137603bd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 20 Dec 2022 07:25:00 +0000 Subject: [PATCH 006/158] Working destructive updating --- openpype/hosts/maya/plugins/load/load_xgen.py | 63 ++++++++++++++++--- .../maya/plugins/publish/extract_xgen.py | 13 ++-- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 3b7451f95c..2d58550bc7 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -2,11 +2,15 @@ import os import shutil import maya.cmds as cmds -import pymel.core as pm +import xgenm import openpype.hosts.maya.api.plugin -from openpype.hosts.maya.api.lib import maintained_selection +from openpype.hosts.maya.api.lib import ( + maintained_selection, get_container_members +) from openpype.hosts.maya.api import current_file +from openpype.hosts.maya.api.plugin import get_reference_node +from openpype.pipeline import get_representation_path class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): @@ -19,13 +23,10 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def process_reference(self, context, name, namespace, options): - maya_filepath = self.prepare_root_value( - self.fname, context["project"]["name"] - ) + def setup_xgen_palette_file(self, maya_filepath, namespace, name): + # Setup xgen palette file. project_path = os.path.dirname(current_file()) - # Setup xgen palette file. # Copy the xgen palette file from published version. _, maya_extension = os.path.splitext(maya_filepath) source = maya_filepath.replace(maya_extension, ".xgen") @@ -34,9 +35,10 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "{basename}__{namespace}__{name}.xgen".format( basename=os.path.splitext(os.path.basename(current_file()))[0], namespace=namespace, - name=context["representation"]["data"]["xgenName"] + name=name ) - ) + ).replace("\\", "/") + self.log.info("Copying {} to {}".format(source, destination)) shutil.copy(source, destination) # Modify xgDataPath and xgProjectPath to have current workspace first @@ -66,6 +68,18 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with open(destination, "w") as f: f.write("\n".join(lines)) + return destination + + def process_reference(self, context, name, namespace, options): + maya_filepath = self.prepare_root_value( + self.fname, context["project"]["name"] + ) + + name = context["representation"]["data"]["xgenName"] + xgen_file = self.setup_xgen_palette_file( + maya_filepath, namespace, name + ) + # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] @@ -78,6 +92,13 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): returnNewNodes=True ) + xgen_palette = cmds.ls(nodes, type="xgmPalette", long=True)[0] + cmds.setAttr( + "{}.xgFileName".format(xgen_palette), + os.path.basename(xgen_file), + type="string" + ) + shapes = cmds.ls(nodes, shapes=True, long=True) new_nodes = (list(set(nodes) - set(shapes))) @@ -87,4 +108,26 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): return new_nodes def update(self, container, representation): - pass + super().update(container, representation) + + # Get reference node from container members + node = container["objectName"] + members = get_container_members(node) + reference_node = get_reference_node(members, self.log) + namespace = cmds.referenceQuery(reference_node, namespace=True)[1:] + + xgen_file = self.setup_xgen_palette_file( + get_representation_path(representation), + namespace, + representation["data"]["xgenName"] + ) + + xgen_palette = cmds.ls(members, type="xgmPalette", long=True)[0] + cmds.setAttr( + "{}.xgFileName".format(xgen_palette), + os.path.basename(xgen_file), + type="string" + ) + + # Reload reference to update the xgen. + cmds.file(loadReference=reference_node, force=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 69d1d3db40..2daa6e238d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -32,13 +32,10 @@ class ExtractXgenCache(publish.Extractor): templates = instance.context.data["anatomy"].templates["publish"] xgen_filename = StringTemplate(templates["file"]).format(template_data) name = instance.data["xgenPalette"].replace(":", "__").replace("|", "") - value = xgen_filename.replace(".xgen", "__" + name + ".xgen") - attribute_data = { - "{}.xgFileName".format(instance.data["xgenPalette"]): xgen_filename - } + xgen_filename = xgen_filename.replace(".xgen", "__" + name + ".xgen") # Export xgen palette files. - xgen_path = os.path.join(staging_dir, value).replace("\\", "/") + xgen_path = os.path.join(staging_dir, xgen_filename).replace("\\", "/") xgenm.exportPalette( instance.data["xgenPalette"].replace("|", ""), xgen_path ) @@ -47,7 +44,7 @@ class ExtractXgenCache(publish.Extractor): representation = { "name": name, "ext": "xgen", - "files": value, + "files": xgen_filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) @@ -83,6 +80,10 @@ class ExtractXgenCache(publish.Extractor): cmds.select(duplicate_nodes) collection = xgenm.importPalette(xgen_path, []) + attribute_data = { + "{}.xgFileName".format(collection): xgen_filename + } + # Export Maya file. type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" with attribute_values(attribute_data): From 89ea46189e3f8fe6df6006804ec84227003641ad Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 20 Dec 2022 11:46:16 +0000 Subject: [PATCH 007/158] Initial non-destructive updating --- openpype/hosts/maya/plugins/load/load_xgen.py | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 2d58550bc7..99673440fd 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -79,6 +79,22 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): xgen_file = self.setup_xgen_palette_file( maya_filepath, namespace, name ) + xgd_file = xgen_file.replace(".xgen", ".xgd") + + # Create a placeholder xgen delta file. + #change author and date + xgd_template = """ +# XGen Delta File +# +# Version: C:/Program Files/Autodesk/Maya2022/plug-ins/xgen/ +# Author: tokejepsen +# Date: Tue Dec 20 09:03:29 2022 + +FileVersion 18 + +""" + with open(xgd_file, "w") as f: + f.write(xgd_template) # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] @@ -94,10 +110,16 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): xgen_palette = cmds.ls(nodes, type="xgmPalette", long=True)[0] cmds.setAttr( - "{}.xgFileName".format(xgen_palette), + "{}.xgBaseFile".format(xgen_palette), os.path.basename(xgen_file), type="string" ) + cmds.setAttr( + "{}.xgFileName".format(xgen_palette), + os.path.basename(xgd_file), + type="string" + ) + cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) shapes = cmds.ls(nodes, shapes=True, long=True) @@ -108,11 +130,9 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): return new_nodes def update(self, container, representation): - super().update(container, representation) - # Get reference node from container members - node = container["objectName"] - members = get_container_members(node) + container_node = container["objectName"] + members = get_container_members(container_node) reference_node = get_reference_node(members, self.log) namespace = cmds.referenceQuery(reference_node, namespace=True)[1:] @@ -121,13 +141,35 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): namespace, representation["data"]["xgenName"] ) + xgd_file = xgen_file.replace(".xgen", ".xgd") xgen_palette = cmds.ls(members, type="xgmPalette", long=True)[0] + cmds.setAttr( "{}.xgFileName".format(xgen_palette), os.path.basename(xgen_file), type="string" ) + cmds.setAttr( + "{}.xgBaseFile".format(xgen_palette), + "", + type="string" + ) + cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), False) - # Reload reference to update the xgen. - cmds.file(loadReference=reference_node, force=True) + super().update(container, representation) + + # Apply xgen delta. + xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) + + cmds.setAttr( + "{}.xgBaseFile".format(xgen_palette), + os.path.basename(xgen_file), + type="string" + ) + cmds.setAttr( + "{}.xgFileName".format(xgen_palette), + os.path.basename(xgd_file), + type="string" + ) + cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) From c58c0d98784d3579b72bfa721d0dcb35442bf296 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 20 Dec 2022 11:52:39 +0000 Subject: [PATCH 008/158] Refactor --- openpype/hosts/maya/plugins/load/load_xgen.py | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 99673440fd..61204522e6 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -6,7 +6,7 @@ import xgenm import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( - maintained_selection, get_container_members + maintained_selection, get_container_members, attribute_values ) from openpype.hosts.maya.api import current_file from openpype.hosts.maya.api.plugin import get_reference_node @@ -145,31 +145,14 @@ FileVersion 18 xgen_palette = cmds.ls(members, type="xgmPalette", long=True)[0] - cmds.setAttr( - "{}.xgFileName".format(xgen_palette), - os.path.basename(xgen_file), - type="string" - ) - cmds.setAttr( - "{}.xgBaseFile".format(xgen_palette), - "", - type="string" - ) - cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), False) + attribute_data = { + "{}.xgFileName".format(xgen_palette): os.path.basename(xgen_file), + "{}.xgBaseFile".format(xgen_palette): "", + "{}.xgExportAsDelta".format(xgen_palette): False + } - super().update(container, representation) + with attribute_values(attribute_data): + super().update(container, representation) - # Apply xgen delta. - xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) - - cmds.setAttr( - "{}.xgBaseFile".format(xgen_palette), - os.path.basename(xgen_file), - type="string" - ) - cmds.setAttr( - "{}.xgFileName".format(xgen_palette), - os.path.basename(xgd_file), - type="string" - ) - cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) + # Apply xgen delta. + xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) From 55e3ae2729248c5d9db380fa2f52c20624f3c892 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 09:23:57 +0000 Subject: [PATCH 009/158] Fix xgd missing file --- openpype/hosts/maya/plugins/load/load_xgen.py | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 61204522e6..f6b487e7e8 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -81,8 +81,12 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): ) xgd_file = xgen_file.replace(".xgen", ".xgd") - # Create a placeholder xgen delta file. - #change author and date + # Create a placeholder xgen delta file. This create an expression + # attribute of float. If we did not add any changes to the delta file, + # then Xgen deletes the xgd file on save to clean up (?). This gives + # errors when launching the workfile again due to trying to find the + # xgd file. + #change author, date and version path xgd_template = """ # XGen Delta File # @@ -92,6 +96,8 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): FileVersion 18 +Palette custom_float_ignore 0 + """ with open(xgd_file, "w") as f: f.write(xgd_template) @@ -130,7 +136,19 @@ FileVersion 18 return new_nodes def update(self, container, representation): - # Get reference node from container members + """Workflow for updating Xgen. + + - Copy and overwrite the workspace .xgen file. + - Set collection attributes to not include delta files. + - Update xgen maya file reference. + - Apply the delta file changes. + - Reset collection attributes to include delta files. + + We have to do this workflow because when using referencing of the xgen + collection, Maya implicitly imports the Xgen data from the xgen file so + we dont have any control over added the delta file changes. + """ + container_node = container["objectName"] members = get_container_members(container_node) reference_node = get_reference_node(members, self.log) @@ -150,9 +168,7 @@ FileVersion 18 "{}.xgBaseFile".format(xgen_palette): "", "{}.xgExportAsDelta".format(xgen_palette): False } - with attribute_values(attribute_data): super().update(container, representation) - # Apply xgen delta. xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) From 87e72491898df9a29c0b44bc044c9f6e08de40fc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 11:03:50 +0000 Subject: [PATCH 010/158] Avoid making placeholder xgd file --- openpype/hosts/maya/plugins/load/load_xgen.py | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index f6b487e7e8..23a22127e9 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -81,27 +81,6 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): ) xgd_file = xgen_file.replace(".xgen", ".xgd") - # Create a placeholder xgen delta file. This create an expression - # attribute of float. If we did not add any changes to the delta file, - # then Xgen deletes the xgd file on save to clean up (?). This gives - # errors when launching the workfile again due to trying to find the - # xgd file. - #change author, date and version path - xgd_template = """ -# XGen Delta File -# -# Version: C:/Program Files/Autodesk/Maya2022/plug-ins/xgen/ -# Author: tokejepsen -# Date: Tue Dec 20 09:03:29 2022 - -FileVersion 18 - -Palette custom_float_ignore 0 - -""" - with open(xgd_file, "w") as f: - f.write(xgd_template) - # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] @@ -127,6 +106,14 @@ Palette custom_float_ignore 0 ) cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) + # This create an expression attribute of float. If we did not add + # any changes to collection, then Xgen does not create an xgd file + # on save. This gives errors when launching the workfile again due + # to trying to find the xgd file. + xgenm.addCustomAttr( + "custom_float_ignore", xgen_palette.replace("|", "") + ) + shapes = cmds.ls(nodes, shapes=True, long=True) new_nodes = (list(set(nodes) - set(shapes))) From 2c9613b55e67e2854e149849dc3a057fc6583591 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 11:38:22 +0000 Subject: [PATCH 011/158] Validate against deactive modifiers --- .../maya/plugins/publish/validate_xgen.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index fc93f62846..9393520d6d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -1,4 +1,7 @@ +import json + import maya.cmds as cmds +import xgenm import pyblish.api @@ -25,3 +28,27 @@ class ValidateXgen(pyblish.api.InstancePlugin): msg = "Invalid nodes in the objectset:\n{}".format(remainder_nodes) assert not remainder_nodes, msg + + # Cant have deactive modifiers in collection cause Xgen will try and + # look for them when loading. + palette = instance.data["xgenPalette"].replace("|", "") + deactive_modifiers = {} + for description in instance.data["xgmDescriptions"]: + description = description.split("|")[-2] + modifier_names = xgenm.fxModules(palette, description) + for name in modifier_names: + attr = xgenm.getAttr("active", palette, description, name) + # Attribute value are lowercase strings of false/true. + if attr == "false": + try: + deactive_modifiers[description].append(name) + except KeyError: + deactive_modifiers[description] = [name] + + msg = ( + "There are deactive modifiers on the collection. " + "Please delete these:\n{}".format( + json.dumps(deactive_modifiers, indent=4, sort_keys=True) + ) + ) + assert not deactive_modifiers, msg From 0fc1e357a35c32bf505f4f1661eb631ffe340585 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 12:19:23 +0000 Subject: [PATCH 012/158] Validate against multiple collections per instance. --- openpype/hosts/maya/plugins/publish/validate_xgen.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 9393520d6d..1ef85c4cc6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -29,6 +29,13 @@ class ValidateXgen(pyblish.api.InstancePlugin): msg = "Invalid nodes in the objectset:\n{}".format(remainder_nodes) assert not remainder_nodes, msg + # Only one collection per instance. + palette_amount = len(instance.data["xgenPalettes"]) + msg = "Only one collection per instance allow. Found {}:\n{}".format( + palette_amount, instance.data["xgenPalettes"] + ) + assert palette_amount == 1, msg + # Cant have deactive modifiers in collection cause Xgen will try and # look for them when loading. palette = instance.data["xgenPalette"].replace("|", "") From 1bb6d77e4eab876b354f14637f70ad3a2872c777 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 12:37:24 +0000 Subject: [PATCH 013/158] Hound --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 2daa6e238d..4979f583b4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -122,13 +122,11 @@ class ExtractXgenCache(publish.Extractor): cmds.delete(duplicate_nodes + [collection]) # Setup transfers. - #needs to reduce resources to only what is used for the collections in - #the objectset xgen_dir = os.path.join( os.path.dirname(instance.context.data["currentFile"]), "xgen" ) transfers = [] - for root, dirs, files in os.walk(xgen_dir): + for root, _, files in os.walk(xgen_dir): for file in files: source = os.path.join(root, file) destination = source.replace( From 2875bb81fbe98d9504f582b05ae75033360ba140 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 21 Dec 2022 12:51:49 +0000 Subject: [PATCH 014/158] Update openpype/hosts/maya/plugins/publish/extract_xgen.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_xgen.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 4979f583b4..003b6aaa85 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -55,24 +55,26 @@ class ExtractXgenCache(publish.Extractor): transform_name = connections["transform"].split(".")[0] # Duplicate_transform subd patch geometry. - duplicate_transform = pc.duplicate(transform_name)[0] - pc.parent(duplicate_transform, world=True) - duplicate_transform.name(stripNamespace=True) - duplicate_shape = pc.listRelatives( - duplicate_transform, shapes=True + duplicate_transform = cmds.duplicate(transform_name)[0] + duplicate_shape = cmds.listRelatives( + duplicate_transform, + shapes=True, + fullPath=True )[0] - pc.connectAttr( + cmds.connectAttr( "{}.matrix".format(duplicate_transform), "{}.transform".format(node), force=True ) - pc.connectAttr( + cmds.connectAttr( "{}.worldMesh".format(duplicate_shape), "{}.geometry".format(node), force=True ) + duplicate_transform = cmds.parent(duplicate_transform, world=True)[0] + duplicate_nodes.append(duplicate_transform) # Import xgen onto the duplicate. From b1bef86858d92239868cfcaffa8e55d5319cfecc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 12:56:23 +0000 Subject: [PATCH 015/158] @BigRoy suggestion --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 003b6aaa85..d8045a90e7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -131,9 +131,7 @@ class ExtractXgenCache(publish.Extractor): for root, _, files in os.walk(xgen_dir): for file in files: source = os.path.join(root, file) - destination = source.replace( - xgen_dir, instance.data["resourcesDir"] - ) + destination = os.path.join(instance.data["resourcesDir"], file) transfers.append((source, destination)) instance.data["transfers"] = transfers From 9fdd402fec2a6b84da8da64753bd03bb7e101ecd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 12:56:52 +0000 Subject: [PATCH 016/158] Hound --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index d8045a90e7..89b73a2d56 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -2,7 +2,6 @@ import os import copy from maya import cmds -import pymel.core as pc import xgenm from openpype.pipeline import publish @@ -57,8 +56,8 @@ class ExtractXgenCache(publish.Extractor): # Duplicate_transform subd patch geometry. duplicate_transform = cmds.duplicate(transform_name)[0] duplicate_shape = cmds.listRelatives( - duplicate_transform, - shapes=True, + duplicate_transform, + shapes=True, fullPath=True )[0] @@ -73,7 +72,9 @@ class ExtractXgenCache(publish.Extractor): force=True ) - duplicate_transform = cmds.parent(duplicate_transform, world=True)[0] + duplicate_transform = cmds.parent( + duplicate_transform, world=True + )[0] duplicate_nodes.append(duplicate_transform) From 2609508ced1e5076a91288d290204ac30f146c6d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 21 Dec 2022 13:01:16 +0000 Subject: [PATCH 017/158] deactive > inactive --- .../hosts/maya/plugins/publish/validate_xgen.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 1ef85c4cc6..442af794cd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -36,10 +36,10 @@ class ValidateXgen(pyblish.api.InstancePlugin): ) assert palette_amount == 1, msg - # Cant have deactive modifiers in collection cause Xgen will try and + # Cant have inactive modifiers in collection cause Xgen will try and # look for them when loading. palette = instance.data["xgenPalette"].replace("|", "") - deactive_modifiers = {} + inactive_modifiers = {} for description in instance.data["xgmDescriptions"]: description = description.split("|")[-2] modifier_names = xgenm.fxModules(palette, description) @@ -48,14 +48,14 @@ class ValidateXgen(pyblish.api.InstancePlugin): # Attribute value are lowercase strings of false/true. if attr == "false": try: - deactive_modifiers[description].append(name) + inactive_modifiers[description].append(name) except KeyError: - deactive_modifiers[description] = [name] + inactive_modifiers[description] = [name] msg = ( - "There are deactive modifiers on the collection. " + "There are inactive modifiers on the collection. " "Please delete these:\n{}".format( - json.dumps(deactive_modifiers, indent=4, sort_keys=True) + json.dumps(inactive_modifiers, indent=4, sort_keys=True) ) ) - assert not deactive_modifiers, msg + assert not inactive_modifiers, msg From cbbb3dc8041994912ed6bf5d638ee9f06d36556d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 21 Dec 2022 13:04:01 +0000 Subject: [PATCH 018/158] Update openpype/hosts/maya/plugins/publish/validate_xgen.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/validate_xgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 442af794cd..bc25082aa8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -16,7 +16,7 @@ class ValidateXgen(pyblish.api.InstancePlugin): def process(self, instance): # Validate only xgen collections are in objectset. - nodes = ( + nodes = set( instance.data["xgenNodes"] + cmds.ls(instance, type="transform", long=True) ) From 2a748047ed777067ada7e3b78eca8b6df0f65678 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Jan 2023 12:41:12 +0000 Subject: [PATCH 019/158] Only collect resource files from instance collection --- .../maya/plugins/publish/extract_xgen.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 89b73a2d56..5b4c6b29f2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -124,15 +124,26 @@ class ExtractXgenCache(publish.Extractor): cmds.delete(duplicate_nodes + [collection]) - # Setup transfers. - xgen_dir = os.path.join( - os.path.dirname(instance.context.data["currentFile"]), "xgen" + # Collect all files under palette root as resources. + data_path = xgenm.getAttr( + "xgDataPath", instance.data["xgenPalette"].replace("|", "") + ).split(os.pathsep)[0] + data_path = data_path.replace( + "${PROJECT}", + xgenm.getAttr( + "xgProjectPath", instance.data["xgenPalette"].replace("|", "") + ) ) transfers = [] - for root, _, files in os.walk(xgen_dir): + for root, _, files in os.walk(data_path): for file in files: - source = os.path.join(root, file) - destination = os.path.join(instance.data["resourcesDir"], file) - transfers.append((source, destination)) + source = os.path.join(root, file).replace("\\", "/") + destination = os.path.join( + instance.data["resourcesDir"], + "collections", + os.path.basename(data_path), + source.replace(data_path, "")[1:] + ) + transfers.append((source, destination.replace("\\", "/"))) instance.data["transfers"] = transfers From 370bb96408c88e68273593a8ae7aabcbfff93f99 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Jan 2023 15:03:30 +0000 Subject: [PATCH 020/158] Improve validation error message --- openpype/hosts/maya/plugins/publish/validate_xgen.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index bc25082aa8..5ba9ddad52 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -26,7 +26,10 @@ class ValidateXgen(pyblish.api.InstancePlugin): continue remainder_nodes.append(node) - msg = "Invalid nodes in the objectset:\n{}".format(remainder_nodes) + msg = ( + "Only the collection is used when publishing. Found these invalid" + " nodes in the objectset:\n{}".format(remainder_nodes) + ) assert not remainder_nodes, msg # Only one collection per instance. From 8e233c46c8c1fceea3b0c05f0b31cf20172eb410 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 4 Jan 2023 16:54:25 +0000 Subject: [PATCH 021/158] Update openpype/hosts/maya/plugins/publish/validate_xgen.py Co-authored-by: Roy Nieterau --- .../maya/plugins/publish/validate_xgen.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 5ba9ddad52..b9d67dad25 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -16,21 +16,17 @@ class ValidateXgen(pyblish.api.InstancePlugin): def process(self, instance): # Validate only xgen collections are in objectset. - nodes = set( + valid_nodes = set( instance.data["xgenNodes"] + cmds.ls(instance, type="transform", long=True) ) - remainder_nodes = [] - for node in instance: - if node in nodes: - continue - remainder_nodes.append(node) - - msg = ( - "Only the collection is used when publishing. Found these invalid" - " nodes in the objectset:\n{}".format(remainder_nodes) - ) - assert not remainder_nodes, msg + invalid_nodes = [node for node in instance if node not in valid_nodes] + + if invalid_nodes: + raise KnownPublishError( + "Only the collection is used when publishing. Found these invalid" + " nodes in the objectset:\n{}".format(invalid_nodes) + ) # Only one collection per instance. palette_amount = len(instance.data["xgenPalettes"]) From 8918f8965c7d591aaec1b3da608dd8be77d68ea9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Jan 2023 16:56:32 +0000 Subject: [PATCH 022/158] Fix suggestion --- openpype/hosts/maya/plugins/publish/validate_xgen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index b9d67dad25..b5817d4920 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -4,6 +4,7 @@ import maya.cmds as cmds import xgenm import pyblish.api +from openpype.pipeline.publish import KnownPublishError class ValidateXgen(pyblish.api.InstancePlugin): @@ -21,11 +22,11 @@ class ValidateXgen(pyblish.api.InstancePlugin): cmds.ls(instance, type="transform", long=True) ) invalid_nodes = [node for node in instance if node not in valid_nodes] - + if invalid_nodes: raise KnownPublishError( - "Only the collection is used when publishing. Found these invalid" - " nodes in the objectset:\n{}".format(invalid_nodes) + "Only the collection is used when publishing. Found these " + "invalid nodes in the objectset:\n{}".format(invalid_nodes) ) # Only one collection per instance. From 6575e304b8a8127e4fa0d0989efcacb3d773be46 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 5 Jan 2023 18:20:03 +0000 Subject: [PATCH 023/158] Connect Geometry action --- .../plugins/inventory/connect_geometry.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 openpype/hosts/maya/plugins/inventory/connect_geometry.py diff --git a/openpype/hosts/maya/plugins/inventory/connect_geometry.py b/openpype/hosts/maya/plugins/inventory/connect_geometry.py new file mode 100644 index 0000000000..375ad29ef0 --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/connect_geometry.py @@ -0,0 +1,139 @@ +from maya import cmds + +from openpype.pipeline import InventoryAction, get_representation_context + + +class ConnectGeometry(InventoryAction): + """Connect geometries within containers. + + Source container will connect to the target containers, by searching for + matching geometry IDs (cbid). + Source containers are of family; "animation" and "pointcache". + The connection with be done with a live world space blendshape. + """ + + label = "Connect Geometry" + icon = "link" + color = "white" + + def process(self, containers): + # Validate selection is more than 1. + message = ( + "Only 1 container selected. 2+ containers needed for this action." + ) + if len(containers) == 1: + self.display_warning(message) + return + + # Categorize containers by family. + containers_by_family = {} + for container in containers: + family = get_representation_context( + container["representation"] + )["subset"]["data"]["family"] + try: + containers_by_family[family].append(container) + except KeyError: + containers_by_family[family] = [container] + + # Validate to only 1 source container. + source_containers = containers_by_family.get("animation", []) + source_containers += containers_by_family.get("pointcache", []) + source_container_namespaces = [ + x["namespace"] for x in source_containers + ] + message = ( + "{} animation containers selected:\n\n{}\n\nOnly select 1 of type " + "\"animation\" or \"pointcache\".".format( + len(source_containers), source_container_namespaces + ) + ) + if len(source_containers) != 1: + self.display_warning(message) + return + + source_container = source_containers[0] + + # Collect matching geometry transforms based cbId attribute. + target_containers = [] + for family, containers in containers_by_family.items(): + if family in ["animation", "pointcache"]: + continue + + target_containers.extend(containers) + + source_data = self.get_container_data(source_container["objectName"]) + matches = [] + node_types = [] + for target_container in target_containers: + target_data = self.get_container_data( + target_container["objectName"] + ) + node_types.extend(target_data["node_types"]) + for id, transform in target_data["ids"].items(): + source_match = source_data["ids"].get(id) + if source_match: + matches.append([source_match, transform]) + + # Message user about what is about to happen. + if not matches: + self.display_warning("No matching geometries found.") + return + + message = "Linking geometries:\n\n" + for match in matches: + message += "{} > {}\n".format(match[0], match[1]) + + choice = self.display_warning(message, show_cancel=True) + if choice is False: + return + + # Setup live worldspace blendshape connection. + for match in matches: + source = match[0] + target = match[1] + blendshape = cmds.blendShape(source, target)[0] + cmds.setAttr(blendshape + ".origin", 0) + cmds.setAttr(blendshape + "." + target.split(":")[-1], 1) + + # Update Xgen if in any of the containers. + if "xgmPalette" in node_types: + cmds.xgmPreview() + + def get_container_data(self, container): + data = {"node_types": [], "ids": {}} + ref_node = cmds.sets(container, query=True, nodesOnly=True)[0] + for node in cmds.referenceQuery(ref_node, nodes=True): + node_type = cmds.nodeType(node) + data["node_types"].append(node_type) + if node_type == "mesh": + transform = cmds.listRelatives(node, parent=True)[0] + id = cmds.getAttr(transform + ".cbId") + data["ids"][id] = transform + + return data + + def display_warning(self, message, show_cancel=False): + """Show feedback to user. + + Returns: + bool + """ + + from Qt import QtWidgets + + accept = QtWidgets.QMessageBox.Ok + if show_cancel: + buttons = accept | QtWidgets.QMessageBox.Cancel + else: + buttons = accept + + state = QtWidgets.QMessageBox.warning( + None, + "", + message, + buttons=buttons, + defaultButton=accept + ) + + return state == accept From 2facf4d49eb3c18f3bfe39a3d50bb2bac51abca7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 6 Jan 2023 10:19:21 +0000 Subject: [PATCH 024/158] Open workfile post initialization --- openpype/hooks/pre_add_last_workfile_arg.py | 12 ++++++++++ openpype/hosts/maya/startup/userSetup.py | 22 +++++++++++++++---- .../defaults/project_settings/maya.json | 1 + .../projects_schema/schema_project_maya.json | 5 +++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 3609620917..ffb116d2e4 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -1,5 +1,7 @@ import os + from openpype.lib import PreLaunchHook +from openpype.settings import get_project_settings class AddLastWorkfileToLaunchArgs(PreLaunchHook): @@ -40,5 +42,15 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): self.log.info("Current context does not have any workfile yet.") return + # Determine whether to open workfile post initialization. + if self.data["app"].host_name == "maya": + project_name = self.data["project_name"] + settings = get_project_settings(project_name) + key = "open_workfile_post_initialization" + if settings["maya"][key]: + self.log.debug("Opening workfile post initialization.") + self.data["env"]["OPENPYPE_" + key.upper()] = "1" + return + # Add path to workfile to arguments self.launch_context.launch_args.append(last_workfile) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 40cd51f2d8..b64ed93000 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,16 +1,27 @@ import os + from openpype.settings import get_project_settings from openpype.pipeline import install_host from openpype.hosts.maya.api import MayaHost + from maya import cmds host = MayaHost() install_host(host) -print("starting OpenPype usersetup") +print("Starting OpenPype usersetup...") -# build a shelf +# Open Workfile Post Initialization. +key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" +if bool(int(os.environ.get(key, "0"))): + cmds.evalDeferred( + "cmds.file(os.environ[\"AVALON_LAST_WORKFILE\"], open=True," + " force=True)", + lowestPriority=True + ) + +# Build a shelf. settings = get_project_settings(os.environ['AVALON_PROJECT']) shelf_preset = settings['maya'].get('project_shelf') @@ -26,7 +37,10 @@ if shelf_preset: print(import_string) exec(import_string) - cmds.evalDeferred("mlib.shelf(name=shelf_preset['name'], iconPath=icon_path, preset=shelf_preset)") + cmds.evalDeferred( + "mlib.shelf(name=shelf_preset['name'], iconPath=icon_path," + " preset=shelf_preset)" + ) -print("finished OpenPype usersetup") +print("Finished OpenPype usersetup.") diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5f40c2a10c..dbc3282ce8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,4 +1,5 @@ { + "open_workfile_post_initialization": false, "imageio": { "colorManagementPreference_v2": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index b2d79797a3..b668d74afd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -5,6 +5,11 @@ "label": "Maya", "is_file": true, "children": [ + { + "type": "boolean", + "key": "open_workfile_post_initialization", + "label": "Open Workfile Post Initialization" + }, { "key": "imageio", "type": "dict", From c0da2c8afc711657b0b46eeba34b1742b9b5afa8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 7 Jan 2023 09:33:38 +0000 Subject: [PATCH 025/158] Simplify validation --- .../maya/plugins/publish/validate_xgen.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index b5817d4920..19cf612848 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -16,29 +16,27 @@ class ValidateXgen(pyblish.api.InstancePlugin): families = ["xgen"] def process(self, instance): - # Validate only xgen collections are in objectset. - valid_nodes = set( - instance.data["xgenNodes"] + - cmds.ls(instance, type="transform", long=True) - ) - invalid_nodes = [node for node in instance if node not in valid_nodes] + set_members = instance.data.get("setMembers") - if invalid_nodes: + # Only 1 collection/node per instance. + if len(set_members) != 1: raise KnownPublishError( - "Only the collection is used when publishing. Found these " - "invalid nodes in the objectset:\n{}".format(invalid_nodes) + "Only one collection per instance is allowed." + " Found:\n{}".format(set_members) ) - # Only one collection per instance. - palette_amount = len(instance.data["xgenPalettes"]) - msg = "Only one collection per instance allow. Found {}:\n{}".format( - palette_amount, instance.data["xgenPalettes"] - ) - assert palette_amount == 1, msg + # Only xgen palette node is allowed. + node_type = cmds.nodeType(set_members[0]) + if node_type != "xgmPalette": + raise KnownPublishError( + "Only node of type \"xgmPalette\" are allowed. Referred to as" + " \"collection\" in the Maya UI." + " Node type found: {}".format(node_type) + ) # Cant have inactive modifiers in collection cause Xgen will try and # look for them when loading. - palette = instance.data["xgenPalette"].replace("|", "") + palette = instance.data["xgmPalette"].replace("|", "") inactive_modifiers = {} for description in instance.data["xgmDescriptions"]: description = description.split("|")[-2] From 239995f715ea3df72af6cc9416152d8b043a3938 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 7 Jan 2023 09:34:20 +0000 Subject: [PATCH 026/158] Account for references and children of patches --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 5b4c6b29f2..99efcb34ce 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -61,6 +61,12 @@ class ExtractXgenCache(publish.Extractor): fullPath=True )[0] + # Discard the children. + shapes = cmds.listRelatives(duplicate_transform, shapes=True) + children = cmds.listRelatives(duplicate_transform, children=True) + cmds.delete(set(children) - set(shapes)) + + # Connect attributes. cmds.connectAttr( "{}.matrix".format(duplicate_transform), "{}.transform".format(node), @@ -97,7 +103,7 @@ class ExtractXgenCache(publish.Extractor): force=True, type=type, exportSelected=True, - preserveReferences=True, + preserveReferences=False, constructionHistory=True, shader=True, constraints=True, From efb7d42a7fa9d267b1f7e284bc1d659f3cd41583 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 7 Jan 2023 09:34:34 +0000 Subject: [PATCH 027/158] Code cosmetics --- openpype/hosts/maya/plugins/publish/collect_xgen.py | 8 ++++---- openpype/hosts/maya/plugins/publish/extract_xgen.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_xgen.py b/openpype/hosts/maya/plugins/publish/collect_xgen.py index fa7ec3776d..5a48b1d221 100644 --- a/openpype/hosts/maya/plugins/publish/collect_xgen.py +++ b/openpype/hosts/maya/plugins/publish/collect_xgen.py @@ -13,20 +13,20 @@ class CollectXgen(pyblish.api.InstancePlugin): def process(self, instance): data = { - "xgenPalettes": cmds.ls(instance, type="xgmPalette", long=True), + "xgmPalettes": cmds.ls(instance, type="xgmPalette", long=True), "xgmDescriptions": cmds.ls( instance, type="xgmDescription", long=True ), "xgmSubdPatches": cmds.ls(instance, type="xgmSubdPatch", long=True) } data["xgenNodes"] = ( - data["xgenPalettes"] + + data["xgmPalettes"] + data["xgmDescriptions"] + data["xgmSubdPatches"] ) - if data["xgenPalettes"]: - data["xgenPalette"] = data["xgenPalettes"][0] + if data["xgmPalettes"]: + data["xgmPalette"] = data["xgmPalettes"][0] data["xgenConnections"] = {} for node in data["xgmSubdPatches"]: diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 99efcb34ce..22260df2c7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -30,13 +30,13 @@ class ExtractXgenCache(publish.Extractor): template_data.update({"ext": "xgen"}) templates = instance.context.data["anatomy"].templates["publish"] xgen_filename = StringTemplate(templates["file"]).format(template_data) - name = instance.data["xgenPalette"].replace(":", "__").replace("|", "") + name = instance.data["xgmPalette"].replace(":", "__").replace("|", "") xgen_filename = xgen_filename.replace(".xgen", "__" + name + ".xgen") # Export xgen palette files. xgen_path = os.path.join(staging_dir, xgen_filename).replace("\\", "/") xgenm.exportPalette( - instance.data["xgenPalette"].replace("|", ""), xgen_path + instance.data["xgmPalette"].replace("|", ""), xgen_path ) self.log.info("Extracted to {}".format(xgen_path)) @@ -132,12 +132,12 @@ class ExtractXgenCache(publish.Extractor): # Collect all files under palette root as resources. data_path = xgenm.getAttr( - "xgDataPath", instance.data["xgenPalette"].replace("|", "") + "xgDataPath", instance.data["xgmPalette"].replace("|", "") ).split(os.pathsep)[0] data_path = data_path.replace( "${PROJECT}", xgenm.getAttr( - "xgProjectPath", instance.data["xgenPalette"].replace("|", "") + "xgProjectPath", instance.data["xgmPalette"].replace("|", "") ) ) transfers = [] From 9ed90e67f8a2fbdc62d3c2a15cc89ed79111301d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 8 Jan 2023 18:03:34 +0000 Subject: [PATCH 028/158] Code cosmetics --- openpype/hosts/maya/plugins/inventory/connect_geometry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/connect_geometry.py b/openpype/hosts/maya/plugins/inventory/connect_geometry.py index 375ad29ef0..9fe4f9e195 100644 --- a/openpype/hosts/maya/plugins/inventory/connect_geometry.py +++ b/openpype/hosts/maya/plugins/inventory/connect_geometry.py @@ -52,7 +52,7 @@ class ConnectGeometry(InventoryAction): self.display_warning(message) return - source_container = source_containers[0] + source_object = source_containers[0]["objectName"] # Collect matching geometry transforms based cbId attribute. target_containers = [] @@ -62,7 +62,7 @@ class ConnectGeometry(InventoryAction): target_containers.extend(containers) - source_data = self.get_container_data(source_container["objectName"]) + source_data = self.get_container_data(source_object) matches = [] node_types = [] for target_container in target_containers: @@ -80,7 +80,7 @@ class ConnectGeometry(InventoryAction): self.display_warning("No matching geometries found.") return - message = "Linking geometries:\n\n" + message = "Connecting geometries:\n\n" for match in matches: message += "{} > {}\n".format(match[0], match[1]) From 0382ef29f5e9b02186d2c207063a3a15b66b3397 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 8 Jan 2023 18:04:01 +0000 Subject: [PATCH 029/158] Connect and update xgen animation. --- openpype/hosts/maya/api/plugin.py | 31 ++++ .../maya/plugins/inventory/connect_xgen.py | 168 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 openpype/hosts/maya/plugins/inventory/connect_xgen.py diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 82df85a8be..f11757b2ab 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -300,6 +300,37 @@ class ReferenceLoader(Loader): str(representation["_id"]), type="string") + # Update any xgen containers. + compound_name = "xgenContainers" + object = "SplinePrimitive" + if cmds.objExists("{}.{}".format(node, compound_name)): + import xgenm + container_amount = cmds.getAttr( + "{}.{}".format(node, compound_name), size=True + ) + # loop through all compound children + for i in range(container_amount): + attr = "{}.{}[{}].container".format(node, compound_name, i) + objectset = cmds.listConnections(attr)[0] + reference_node = cmds.sets(objectset, query=True)[0] + palettes = cmds.ls( + cmds.referenceQuery(reference_node, nodes=True), + type="xgmPalette" + ) + for palette in palettes: + for description in xgenm.descriptions(palette): + xgenm.setAttr( + "cacheFileName", + path.replace("\\", "/"), + palette, + description, + object + ) + + # Refresh UI and viewport. + de = xgenm.xgGlobal.DescriptionEditor + de.refresh("Full") + def remove(self, container): """Remove an existing `container` from Maya scene diff --git a/openpype/hosts/maya/plugins/inventory/connect_xgen.py b/openpype/hosts/maya/plugins/inventory/connect_xgen.py new file mode 100644 index 0000000000..933a1b4025 --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/connect_xgen.py @@ -0,0 +1,168 @@ +from maya import cmds +import xgenm + +from openpype.pipeline import ( + InventoryAction, get_representation_context, get_representation_path +) + + +class ConnectXgen(InventoryAction): + """Connect Xgen with an animation or pointcache. + """ + + label = "Connect Xgen" + icon = "link" + color = "white" + + def process(self, containers): + # Validate selection is more than 1. + message = ( + "Only 1 container selected. 2+ containers needed for this action." + ) + if len(containers) == 1: + self.display_warning(message) + return + + # Categorize containers by family. + containers_by_family = {} + for container in containers: + family = get_representation_context( + container["representation"] + )["subset"]["data"]["family"] + try: + containers_by_family[family].append(container) + except KeyError: + containers_by_family[family] = [container] + + # Validate to only 1 source container. + source_containers = containers_by_family.get("animation", []) + source_containers += containers_by_family.get("pointcache", []) + source_container_namespaces = [ + x["namespace"] for x in source_containers + ] + message = ( + "{} animation containers selected:\n\n{}\n\nOnly select 1 of type " + "\"animation\" or \"pointcache\".".format( + len(source_containers), source_container_namespaces + ) + ) + if len(source_containers) != 1: + self.display_warning(message) + return + + source_container = source_containers[0] + source_object = source_container["objectName"] + + # Validate source representation is an alembic. + source_path = get_representation_path( + get_representation_context( + source_container["representation"] + )["representation"] + ).replace("\\", "/") + message = "Animation container \"{}\" is not an alembic:\n{}".format( + source_container["namespace"], source_path + ) + if not source_path.endswith(".abc"): + self.display_warning(message) + return + + # Target containers. + target_containers = [] + for family, containers in containers_by_family.items(): + if family in ["animation", "pointcache"]: + continue + + target_containers.extend(containers) + + # Inform user of connections from source representation to target + # descriptions. + descriptions_data = [] + connections_msg = "" + for target_container in target_containers: + reference_node = cmds.sets( + target_container["objectName"], query=True + )[0] + palettes = cmds.ls( + cmds.referenceQuery(reference_node, nodes=True), + type="xgmPalette" + ) + for palette in palettes: + for description in xgenm.descriptions(palette): + descriptions_data.append([palette, description]) + connections_msg += "\n{}/{}".format(palette, description) + + message = "Connecting \"{}\" to:\n".format( + source_container["namespace"] + ) + message += connections_msg + choice = self.display_warning(message, show_cancel=True) + if choice is False: + return + + # Recreate "xgenContainers" attribute to reset. + compound_name = "xgenContainers" + attr = "{}.{}".format(source_object, compound_name) + if cmds.objExists(attr): + cmds.deleteAttr(attr) + + cmds.addAttr( + source_object, + longName=compound_name, + attributeType="compound", + numberOfChildren=1, + multi=True + ) + + # Connect target containers. + for target_container in target_containers: + cmds.addAttr( + source_object, + longName="container", + attributeType="message", + parent=compound_name + ) + index = target_containers.index(target_container) + cmds.connectAttr( + target_container["objectName"] + ".message", + source_object + ".{}[{}].container".format( + compound_name, index + ) + ) + + # Setup cache on Xgen + object = "SplinePrimitive" + for palette, description in descriptions_data: + xgenm.setAttr("useCache", "true", palette, description, object) + xgenm.setAttr("liveMode", "false", palette, description, object) + xgenm.setAttr( + "cacheFileName", source_path, palette, description, object + ) + + # Refresh UI and viewport. + de = xgenm.xgGlobal.DescriptionEditor + de.refresh("Full") + + def display_warning(self, message, show_cancel=False): + """Show feedback to user. + + Returns: + bool + """ + + from Qt import QtWidgets + + accept = QtWidgets.QMessageBox.Ok + if show_cancel: + buttons = accept | QtWidgets.QMessageBox.Cancel + else: + buttons = accept + + state = QtWidgets.QMessageBox.warning( + None, + "", + message, + buttons=buttons, + defaultButton=accept + ) + + return state == accept From 0dd54b0a5c767a02bcb998e6740b235695f996c6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Jan 2023 15:40:50 +0000 Subject: [PATCH 030/158] Validate workfile has been saved. --- openpype/hosts/maya/plugins/load/load_xgen.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 23a22127e9..565d1306b4 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -4,6 +4,8 @@ import shutil import maya.cmds as cmds import xgenm +from Qt import QtWidgets + import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, attribute_values @@ -71,6 +73,15 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): return destination def process_reference(self, context, name, namespace, options): + # Validate workfile has a path. + if current_file() is None: + QtWidgets.QMessageBox.warning( + None, + "", + "Current workfile has not been saved." + ) + return + maya_filepath = self.prepare_root_value( self.fname, context["project"]["name"] ) From 3021fea4762ec84e53091f1cb954883be6a53634 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 11 Jan 2023 10:04:17 +0000 Subject: [PATCH 031/158] Increment xgen files with workfile. --- openpype/hosts/maya/startup/userSetup.py | 42 +++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b64ed93000..5cd27500dc 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,8 +1,10 @@ import os +import shutil from openpype.settings import get_project_settings from openpype.pipeline import install_host -from openpype.hosts.maya.api import MayaHost +from openpype.hosts.maya.api import MayaHost, current_file +from openpype.lib import register_event_callback from maya import cmds @@ -21,6 +23,44 @@ if bool(int(os.environ.get(key, "0"))): lowestPriority=True ) + +# Setup Xgen save callback. +def xgen_on_save(): + """Increments the xgen side car files .xgen and .xgd + + Only works when incrementing to the same directory. + """ + + file_path = current_file() + current_dir = os.path.dirname(file_path) + basename = os.path.basename(file_path).split(".")[0] + attrs = ["xgFileName", "xgBaseFile"] + for palette in cmds.ls(type="xgmPalette"): + for attr in attrs: + source = os.path.join( + current_dir, cmds.getAttr(palette + "." + attr) + ) + if not os.path.exists(source): + continue + + destination_basename = "{}__{}{}".format( + basename, + palette.replace(":", "_"), + os.path.splitext(source)[1] + ) + destination = os.path.join(current_dir, destination_basename) + + if source == destination: + continue + + shutil.copy(source, destination) + cmds.setAttr( + palette + "." + attr, destination_basename, type="string" + ) + + +register_event_callback("save", xgen_on_save) + # Build a shelf. settings = get_project_settings(os.environ['AVALON_PROJECT']) shelf_preset = settings['maya'].get('project_shelf') From 4dc155d95fb89ccea3310721afcd534f91e07a10 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 12 Jan 2023 15:51:51 +0000 Subject: [PATCH 032/158] Support for rendering Opening the workfile in its published location will load the xgen correctly. --- .../plugins/publish/extract_workfile_xgen.py | 131 ++++++++++++++++++ .../plugins/publish/reset_xgen_attributes.py | 21 +++ 2 files changed, 152 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py create mode 100644 openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py new file mode 100644 index 0000000000..e12a870eaf --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -0,0 +1,131 @@ +import os +import shutil + +from maya import cmds + +import pyblish.api +from openpype.hosts.maya.api import current_file +from openpype.pipeline import publish + + +class ExtractWorkfileXgen(publish.Extractor): + """Extract Workfile Xgen.""" + + # Offset to run before workfile scene save. + order = pyblish.api.ExtractorOrder - 0.499 + label = "Extract Workfile Xgen" + families = ["workfile"] + hosts = ["maya"] + + def process(self, instance): + # Validate to extract only when we are publishing a renderlayer as + # well. + renderlayer = False + for i in instance.context: + is_renderlayer = ( + "renderlayer" in i.data.get("families", []) or + i.data["family"] == "renderlayer" + ) + if is_renderlayer and i.data["publish"]: + renderlayer = True + break + + if not renderlayer: + self.log.debug( + "No publishable renderlayers found in context. Abort Xgen" + " extraction." + ) + return + + # Collect Xgen and Delta files. + xgen_files = [] + sources = [] + file_path = current_file() + current_dir = os.path.dirname(file_path) + attrs = ["xgFileName", "xgBaseFile"] + for palette in cmds.ls(type="xgmPalette"): + for attr in attrs: + source = os.path.join( + current_dir, cmds.getAttr(palette + "." + attr) + ) + if not os.path.exists(source): + continue + + ext = os.path.splitext(source)[1] + if ext == ".xgen": + xgen_files.append(source) + if ext == ".xgd": + sources.append(source) + + # Copy .xgen file to temporary location and modify. + staging_dir = self.staging_dir(instance) + for source in xgen_files: + destination = os.path.join(staging_dir, os.path.basename(source)) + shutil.copy(source, destination) + + lines = [] + with open(destination, "r") as f: + for line in [line.rstrip() for line in f]: + if line.startswith("\txgProjectPath"): + line = "\txgProjectPath\t\t{}/".format( + instance.data["resourcesDir"].replace("\\", "/") + ) + + lines.append(line) + + with open(destination, "w") as f: + f.write("\n".join(lines)) + + sources.append(destination) + + # Add resource files to workfile instance. + transfers = [] + for source in sources: + basename = os.path.basename(source) + destination = os.path.join(instance.data["resourcesDir"], basename) + transfers.append((source, destination)) + + import xgenm + for palette in cmds.ls(type="xgmPalette"): + relative_data_path = xgenm.getAttr( + "xgDataPath", palette.replace("|", "") + ).split(os.pathsep)[0] + absolute_data_path = relative_data_path.replace( + "${PROJECT}", + xgenm.getAttr("xgProjectPath", palette.replace("|", "")) + ) + + for root, _, files in os.walk(absolute_data_path): + for file in files: + source = os.path.join(root, file).replace("\\", "/") + destination = os.path.join( + instance.data["resourcesDir"], + relative_data_path.replace("${PROJECT}", ""), + source.replace(absolute_data_path, "")[1:] + ) + transfers.append((source, destination.replace("\\", "/"))) + + for source, destination in transfers: + self.log.debug("Transfer: {} > {}".format(source, destination)) + + instance.data["transfers"] = transfers + + # Set palette attributes in preparation for workfile publish. + attrs = ["xgFileName", "xgBaseFile"] + data = {} + for palette in cmds.ls(type="xgmPalette"): + for attr in attrs: + value = cmds.getAttr(palette + "." + attr) + if value: + new_value = "resources/{}".format(value) + node_attr = "{}.{}".format(palette, attr) + self.log.info( + "Setting \"{}\" on \"{}\"".format(new_value, node_attr) + ) + cmds.setAttr(node_attr, new_value, type="string") + try: + data[palette][attr] = value + except KeyError: + data[palette] = {attr: value} + + instance.data["xgenAttributes"] = data diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py new file mode 100644 index 0000000000..9397d479d2 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -0,0 +1,21 @@ +from maya import cmds + +import pyblish.api + + +class ResetXgenAttributes(pyblish.api.InstancePlugin): + """Reset Xgen attributes.""" + + label = "Reset Xgen Attributes." + # Offset to run after global integrator. + order = pyblish.api.IntegratorOrder + 1.0 + families = ["workfile"] + + def process(self, instance): + for palette, data in instance.data.get("xgenAttributes", []).items(): + for attr, value in data.items(): + node_attr = "{}.{}".format(palette, attr) + self.log.info( + "Setting \"{}\" on \"{}\"".format(value, node_attr) + ) + cmds.setAttr(node_attr, value, type="string") From 05a8ef36f7472143bffa36837e3cf56c9593733e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 13 Jan 2023 12:59:25 +0000 Subject: [PATCH 033/158] Xgen documentation --- website/docs/artist_hosts_maya.md | 17 +++++ website/docs/artist_hosts_maya_xgen.md | 94 ++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index fb1af765d2..ced6887d08 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -601,3 +601,20 @@ about customizing review process refer to [admin section](project_settings/setti If you don't move `modelMain` into `reviewMain`, review will be generated but it will be published as separate entity. + + +## Inventory Actions + +### Connect Geometry + +This action will connect geometries between containers. + +#### Usage + +Select 1 container of type `animation` or `pointcache`, then 1+ container of any type. + +#### Details + +The action searches the selected containers for 1 animation container of type `animation` or `pointcache`. This animation container will be connected to the rest of the selected containers. Matching geometries between containers is done by comparing the attribute `cbId`. + +The connection between geometries is done with a live blendshape. diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index 8b0174a29f..629b600458 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -4,26 +4,92 @@ title: Xgen for Maya sidebar_label: Xgen --- -## Working with Xgen in OpenPype +## Setup -OpenPype support publishing and loading of Xgen interactive grooms. You can publish -them as mayaAscii files with scalps that can be loaded into another maya scene, or as -alembic caches. +### Settings -### Publishing Xgen Grooms +Go to project settings > Maya > enable "Open Workfile Post Initialization"; -To prepare xgen for publishing just select all the descriptions that should be published together and the create Xgen Subset in the scene using - **OpenPype menu** โ†’ **Create**... and select **Xgen Interactive**. Leave Use selection checked. +`project_settings/maya/open_workfile_post_initialization` -For actual publishing of your groom to go **OpenPype โ†’ Publish** and then press โ–ถ to publish. This will export `.ma` file containing your grooms with any geometries they are attached to and also a baked cache in `.abc` format +This is due to two errors occurring when opening workfile contaiing referenced xgen nodes on launch of Maya; +- ``Critical``: Duplicate collection errors on launching workfile. This is because Maya first imports Xgen when referencing in external Maya files, then imports Xgen again when the reference edits are applied. +``` +Importing XGen Collections... +# Error: XGen: Failed to find description ball_xgenMain_01_:parent in collection ball_xgenMain_01_:collection. Abort applying delta: P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_sh040_Lighting_v001__ball_xgenMain_01___collection.xgen # +# Error: XGen: Tried to import a duplicate collection, ball_xgenMain_02_:collection, from file P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_sh040_Lighting_v001__ball_xgenMain_02___collection.xgen. Aborting import. # +``` +- ``Non-critical``: Errors on opening workfile and failed opening of published xgen. This is because Maya imports Xgen when referencing in external Maya files but the reference edits that ensure the location of the Xgen files are correct, has not been applied yet. +``` +Importing XGen Collections... +# Error: XGen: Failed to open file: P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_ball_xgenMain_v035__ball_rigMain_01___collection.xgen # +# Error: XGen: Failed to import collection from file P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_ball_xgenMain_v035__ball_rigMain_01___collection.xgen # +``` -:::tip adding more descriptions -You can add multiple xgen description into the subset you are about to publish, simply by -adding them to the maya set that was created for you. Please make sure that only xgen description nodes are present inside of the set and not the scalp geometry. -::: +### Workfile Incremental Save -### Loading Xgen +When you increment the Maya workfile to a new version, all `.xgen` and `.xgd` files referenced by the Xgen collection in the workspace is incremented as well. -You can use published xgens by loading them using OpenPype Publisher. You can choose to reference or import xgen. We don't have any automatic mesh linking at the moment and it is expected, that groom is published with a scalp, that can then be manually attached to your animated mesh for example. +## Create -The alembic representation can be loaded too and it contains the groom converted to curves. Keep in mind that the density of the alembic directly depends on your viewport xgen density at the point of export. +Create an Xgen instance to publish. This needs to contain 1 Xgen collection only, but validation will check for this: + +`OpenPype > Create... > Xgen` + +You can create multiple Xgen instances if you have multiple collections to publish. + +### Publish + +The publishing process will grab geometry used for Xgen along with any external files used in the collection's descriptions. This creates an isolated Maya file with just the Xgen collection's dependencies, so you can use any nested geometry when creating the Xgen description. An Xgen version will consist of: + +- Maya file (`.ma`) - this contains the geometry and the connections to the Xgen collection and descriptions. +- Xgen file (`.xgen`) - this contains the Xgen collection and description. +- Resource files (`.ptx`, `.xuv`) - this contains Xgen side car files used in the collection and descriptions. + +## Load + +Open the Loader tool, `OpenPype > Loader...`, and navigate to the published Xgen version. On right-click you'll get the option `Reference Xgen (ma)` +When loading an Xgen version the following happens: + +- References in the Maya file. +- Copies the Xgen file (`.xgen`) to the current workspace. +- Modifies the Xgen file copy to load the current workspace first then the published Xgen collection. +- Makes a custom attribute on the Xgen collection, `float_ignore`, which can be seen under the `Expressions` tab of the `Xgen` UI. This is done to initialize the Xgen delta file workflow. +- Setup an Xgen delta file (`.xgd`) to store any workspace changes of the published Xgen version. + +When the loading is done, Xgen collection will be in the Xgen delta file workflow which means any changes done in the Maya workfile will be stored in the current workspace. The published Xgen collection will remain intact, even if the user assigns maps to any attributes or otherwise modifies any attribute. + +### Updating + +When there are changes to the Xgen version, the user will be notified when opening the workfile or publishing. Since the Xgen is referenced, it follows the standard Maya referencing system and overrides. + +For example publishing `xgenMain` version 1 with the attribute `renderer` set to `None`, then version 2 has `renderer` set to `Arnold Renderer`. When updating from version 1 to 2, the `renderer` attribute will be updated to `Arnold Renderer` unless there is an override. + +### Connect Patches + +When loading in an Xgen version, it does not have any connections to anything in the workfile, so its static in the position it was published in. Use the [Connect Geometry](artist_hosts_maya#connect-geometry) action to connect Xgen to any matching loaded animated geometry. + +### Connect Guides + +Along with patches you can also connect the Xgen guides to an Alembic cache. + +#### Usage + +Select 1 animation container, of family `animation` or `pointcache`, then the Xgen containers to connect to. Right-click > `Actions` > `Connect Xgen`. + +***Note: Only alembic (`.abc`) representations are allowed.*** + +#### Details + +Connecting the guide will make Xgen use the Alembic directly, setting the attributes under `Guide Animation`, so the Alembic needs to contain the same amount of curves as guides in the Xgen. + +The animation container gets connected with the Xgen container, so if the animation container is updated so will the Xgen container's attribute. + +## Rendering + +To render with Xgen, follow the [Rendering With OpenPype](artist_hosts_maya#rendering-with-openpype) guide. + +### Details + +When submitting a workfile with Xgen, all Xgen related files will be collected and published as the workfiles resources. This means the published workfile is no longer referencing the workspace Xgen files. From 27fc690209493147ef965ce0c364465fe5acf223 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 16 Jan 2023 10:04:55 +0000 Subject: [PATCH 034/158] Update website/docs/artist_hosts_maya_xgen.md Co-authored-by: Roy Nieterau --- website/docs/artist_hosts_maya_xgen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index 629b600458..cde56888d4 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -12,7 +12,7 @@ Go to project settings > Maya > enable "Open Workfile Post Initialization"; `project_settings/maya/open_workfile_post_initialization` -This is due to two errors occurring when opening workfile contaiing referenced xgen nodes on launch of Maya; +This is due to two errors occurring when opening workfile containing referenced xgen nodes on launch of Maya, specifically: - ``Critical``: Duplicate collection errors on launching workfile. This is because Maya first imports Xgen when referencing in external Maya files, then imports Xgen again when the reference edits are applied. ``` From 1b1cd149065a561741b5783823aceb88a82e5f62 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 16 Jan 2023 10:12:32 +0000 Subject: [PATCH 035/158] Update website/docs/artist_hosts_maya_xgen.md Co-authored-by: Roy Nieterau --- website/docs/artist_hosts_maya_xgen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index cde56888d4..e0c008929c 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -33,7 +33,7 @@ When you increment the Maya workfile to a new version, all `.xgen` and `.xgd` fi ## Create -Create an Xgen instance to publish. This needs to contain 1 Xgen collection only, but validation will check for this: +Create an Xgen instance to publish. This needs to contain only **one Xgen collection**. `OpenPype > Create... > Xgen` From 86012906f753e48dd1015516c0d795ca3ccd556f Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 16 Jan 2023 10:13:30 +0000 Subject: [PATCH 036/158] Update website/docs/artist_hosts_maya_xgen.md Co-authored-by: Roy Nieterau --- website/docs/artist_hosts_maya_xgen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index e0c008929c..3f47428abe 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -64,7 +64,7 @@ When the loading is done, Xgen collection will be in the Xgen delta file workflo When there are changes to the Xgen version, the user will be notified when opening the workfile or publishing. Since the Xgen is referenced, it follows the standard Maya referencing system and overrides. -For example publishing `xgenMain` version 1 with the attribute `renderer` set to `None`, then version 2 has `renderer` set to `Arnold Renderer`. When updating from version 1 to 2, the `renderer` attribute will be updated to `Arnold Renderer` unless there is an override. +For example publishing `xgenMain` version 1 with the attribute `renderer` set to `None`, then version 2 has `renderer` set to `Arnold Renderer`. When updating from version 1 to 2, the `renderer` attribute will be updated to `Arnold Renderer` unless there is a local override. ### Connect Patches From 9f749edd8a13be373d8d76e15ac8c09729adaa60 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 17 Jan 2023 09:58:23 +0000 Subject: [PATCH 037/158] Fix When there is no xgenAttributes --- openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py index 9397d479d2..0f763613c9 100644 --- a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -12,7 +12,7 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): families = ["workfile"] def process(self, instance): - for palette, data in instance.data.get("xgenAttributes", []).items(): + for palette, data in instance.data.get("xgenAttributes", {}).items(): for attr, value in data.items(): node_attr = "{}.{}".format(palette, attr) self.log.info( From ff6fe13e2a6049e61b829905f2e3bb9f1b0b4cc5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 17 Jan 2023 16:05:58 +0000 Subject: [PATCH 038/158] Code cosmetics --- openpype/hooks/pre_add_last_workfile_arg.py | 6 ++---- openpype/hosts/maya/startup/userSetup.py | 9 +++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index ffb116d2e4..ce59c0b706 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -43,11 +43,9 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): return # Determine whether to open workfile post initialization. - if self.data["app"].host_name == "maya": - project_name = self.data["project_name"] - settings = get_project_settings(project_name) + if self.host_name == "maya": key = "open_workfile_post_initialization" - if settings["maya"][key]: + if self.data["project_settings"]["maya"][key]: self.log.debug("Opening workfile post initialization.") self.data["env"]["OPENPYPE_" + key.upper()] = "1" return diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 5cd27500dc..87025637b6 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,5 +1,6 @@ import os import shutil +from functools import partial from openpype.settings import get_project_settings from openpype.pipeline import install_host @@ -18,8 +19,12 @@ print("Starting OpenPype usersetup...") key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" if bool(int(os.environ.get(key, "0"))): cmds.evalDeferred( - "cmds.file(os.environ[\"AVALON_LAST_WORKFILE\"], open=True," - " force=True)", + partial( + cmds.file, + os.environ["AVALON_LAST_WORKFILE"], + open=True, + force=True + ), lowestPriority=True ) From 40f8bc25f5adcfb2b70ce219f555b2f09a74e079 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 17 Jan 2023 16:06:16 +0000 Subject: [PATCH 039/158] Remove Xgen incremental save feature --- openpype/hosts/maya/startup/userSetup.py | 37 ------------------------ website/docs/artist_hosts_maya_xgen.md | 4 --- 2 files changed, 41 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 87025637b6..a2f6405980 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -29,43 +29,6 @@ if bool(int(os.environ.get(key, "0"))): ) -# Setup Xgen save callback. -def xgen_on_save(): - """Increments the xgen side car files .xgen and .xgd - - Only works when incrementing to the same directory. - """ - - file_path = current_file() - current_dir = os.path.dirname(file_path) - basename = os.path.basename(file_path).split(".")[0] - attrs = ["xgFileName", "xgBaseFile"] - for palette in cmds.ls(type="xgmPalette"): - for attr in attrs: - source = os.path.join( - current_dir, cmds.getAttr(palette + "." + attr) - ) - if not os.path.exists(source): - continue - - destination_basename = "{}__{}{}".format( - basename, - palette.replace(":", "_"), - os.path.splitext(source)[1] - ) - destination = os.path.join(current_dir, destination_basename) - - if source == destination: - continue - - shutil.copy(source, destination) - cmds.setAttr( - palette + "." + attr, destination_basename, type="string" - ) - - -register_event_callback("save", xgen_on_save) - # Build a shelf. settings = get_project_settings(os.environ['AVALON_PROJECT']) shelf_preset = settings['maya'].get('project_shelf') diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index 3f47428abe..fc75959f2b 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -27,10 +27,6 @@ Importing XGen Collections... # Error: XGen: Failed to import collection from file P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_ball_xgenMain_v035__ball_rigMain_01___collection.xgen # ``` -### Workfile Incremental Save - -When you increment the Maya workfile to a new version, all `.xgen` and `.xgd` files referenced by the Xgen collection in the workspace is incremented as well. - ## Create Create an Xgen instance to publish. This needs to contain only **one Xgen collection**. From 59b1caacd2f4e662c44d57c23a8deca2d3fe07fb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 17 Jan 2023 16:06:39 +0000 Subject: [PATCH 040/158] Account for not using the published workfile. --- .../hosts/maya/plugins/publish/extract_workfile_xgen.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index e12a870eaf..13a04615eb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -37,6 +37,13 @@ class ExtractWorkfileXgen(publish.Extractor): ) return + publish_settings = instance.context["deadline"]["publish"] + if not publish_settings["MayaSubmitDeadline"]["use_published"]: + self.log.debug( + "Not using the published workfile. Abort Xgen extraction." + ) + return + # Collect Xgen and Delta files. xgen_files = [] sources = [] From 1c1a41ba5efd0e883d8985832dd7371554597384 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 17 Jan 2023 16:10:39 +0000 Subject: [PATCH 041/158] Hound --- openpype/hooks/pre_add_last_workfile_arg.py | 1 - openpype/hosts/maya/startup/userSetup.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index ce59c0b706..1c8746c559 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -1,7 +1,6 @@ import os from openpype.lib import PreLaunchHook -from openpype.settings import get_project_settings class AddLastWorkfileToLaunchArgs(PreLaunchHook): diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index a2f6405980..bfa5e6e60d 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,11 +1,9 @@ import os -import shutil from functools import partial from openpype.settings import get_project_settings from openpype.pipeline import install_host -from openpype.hosts.maya.api import MayaHost, current_file -from openpype.lib import register_event_callback +from openpype.hosts.maya.api import MayaHost from maya import cmds From 5f709b08929e81f5e7c48a077320fdabee08f946 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 17 Jan 2023 16:22:12 +0000 Subject: [PATCH 042/158] Code cosmetics --- openpype/hosts/maya/api/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index f11757b2ab..b7adf6edfc 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -302,7 +302,6 @@ class ReferenceLoader(Loader): # Update any xgen containers. compound_name = "xgenContainers" - object = "SplinePrimitive" if cmds.objExists("{}.{}".format(node, compound_name)): import xgenm container_amount = cmds.getAttr( @@ -324,7 +323,7 @@ class ReferenceLoader(Loader): path.replace("\\", "/"), palette, description, - object + "SplinePrimitive" ) # Refresh UI and viewport. From cdc0a80846f465d0f12ee6bfa83d28313676d327 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 18 Jan 2023 11:21:43 +0000 Subject: [PATCH 043/158] Fix When there is no Deadline setting avaliable. --- .../maya/plugins/publish/extract_workfile_xgen.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index 13a04615eb..d37a03d1f6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -37,12 +37,14 @@ class ExtractWorkfileXgen(publish.Extractor): ) return - publish_settings = instance.context["deadline"]["publish"] - if not publish_settings["MayaSubmitDeadline"]["use_published"]: - self.log.debug( - "Not using the published workfile. Abort Xgen extraction." - ) - return + deadline_settings = instance.context.get("deadline") + if deadline_settings: + publish_settings = deadline_settings["publish"] + if not publish_settings["MayaSubmitDeadline"]["use_published"]: + self.log.debug( + "Not using the published workfile. Abort Xgen extraction." + ) + return # Collect Xgen and Delta files. xgen_files = [] From 8c626920ccc0ff81145cab1f994478a2c3ef4a44 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 19 Jan 2023 11:01:47 +0000 Subject: [PATCH 044/158] Fix Updating --- openpype/hosts/maya/plugins/load/load_xgen.py | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 565d1306b4..8249c9092e 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -32,7 +32,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # Copy the xgen palette file from published version. _, maya_extension = os.path.splitext(maya_filepath) source = maya_filepath.replace(maya_extension, ".xgen") - destination = os.path.join( + xgen_file = os.path.join( project_path, "{basename}__{namespace}__{name}.xgen".format( basename=os.path.splitext(os.path.basename(current_file()))[0], @@ -40,15 +40,15 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): name=name ) ).replace("\\", "/") - self.log.info("Copying {} to {}".format(source, destination)) - shutil.copy(source, destination) + self.log.info("Copying {} to {}".format(source, xgen_file)) + shutil.copy(source, xgen_file) # Modify xgDataPath and xgProjectPath to have current workspace first # and published version directory second. This ensure that any newly # created xgen files are created in the current workspace. resources_path = os.path.join(os.path.dirname(source), "resources") lines = [] - with open(destination, "r") as f: + with open(xgen_file, "r") as f: for line in [line.rstrip() for line in f]: if line.startswith("\txgDataPath"): data_path = line.split("\t")[-1] @@ -67,10 +67,12 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): lines.append(line) - with open(destination, "w") as f: + with open(xgen_file, "w") as f: f.write("\n".join(lines)) - return destination + xgd_file = xgen_file.replace(".xgen", ".xgd") + + return xgen_file, xgd_file def process_reference(self, context, name, namespace, options): # Validate workfile has a path. @@ -78,7 +80,8 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): QtWidgets.QMessageBox.warning( None, "", - "Current workfile has not been saved." + "Current workfile has not been saved. Please save the workfile" + " before loading an Xgen." ) return @@ -87,10 +90,9 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): ) name = context["representation"]["data"]["xgenName"] - xgen_file = self.setup_xgen_palette_file( + xgen_file, xgd_file = self.setup_xgen_palette_file( maya_filepath, namespace, name ) - xgd_file = xgen_file.replace(".xgen", ".xgd") # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] @@ -105,17 +107,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): ) xgen_palette = cmds.ls(nodes, type="xgmPalette", long=True)[0] - cmds.setAttr( - "{}.xgBaseFile".format(xgen_palette), - os.path.basename(xgen_file), - type="string" - ) - cmds.setAttr( - "{}.xgFileName".format(xgen_palette), - os.path.basename(xgd_file), - type="string" - ) - cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) + self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) # This create an expression attribute of float. If we did not add # any changes to collection, then Xgen does not create an xgd file @@ -133,10 +125,24 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): return new_nodes + def set_palette_attributes(self, xgen_palette, xgen_file, xgd_file): + cmds.setAttr( + "{}.xgBaseFile".format(xgen_palette), + os.path.basename(xgen_file), + type="string" + ) + cmds.setAttr( + "{}.xgFileName".format(xgen_palette), + os.path.basename(xgd_file), + type="string" + ) + cmds.setAttr("{}.xgExportAsDelta".format(xgen_palette), True) + def update(self, container, representation): """Workflow for updating Xgen. - - Copy and overwrite the workspace .xgen file. + - Copy and potentially overwrite the workspace .xgen file. + - Export changes to delta file. - Set collection attributes to not include delta files. - Update xgen maya file reference. - Apply the delta file changes. @@ -144,22 +150,28 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): We have to do this workflow because when using referencing of the xgen collection, Maya implicitly imports the Xgen data from the xgen file so - we dont have any control over added the delta file changes. + we dont have any control over when adding the delta file changes. + + There is an implicit increment of the xgen and delta files, due to + using the workfile basename. """ container_node = container["objectName"] members = get_container_members(container_node) + xgen_palette = cmds.ls(members, type="xgmPalette", long=True)[0] reference_node = get_reference_node(members, self.log) namespace = cmds.referenceQuery(reference_node, namespace=True)[1:] - xgen_file = self.setup_xgen_palette_file( + xgen_file, xgd_file = self.setup_xgen_palette_file( get_representation_path(representation), namespace, representation["data"]["xgenName"] ) - xgd_file = xgen_file.replace(".xgen", ".xgd") - xgen_palette = cmds.ls(members, type="xgmPalette", long=True)[0] + # Export current changes to apply later. + xgenm.createDelta(xgen_palette.replace("|", ""), xgd_file) + + self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) attribute_data = { "{}.xgFileName".format(xgen_palette): os.path.basename(xgen_file), From a91a980b1becaf112ee0b05d4d5c4498ee13611e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 Jan 2023 19:56:38 +0100 Subject: [PATCH 045/158] laoder plugin has compatibility method on it's own --- openpype/pipeline/load/plugins.py | 35 ++++++++++++++++++++++++++++++- openpype/pipeline/load/utils.py | 18 +--------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index b5e55834db..4197ddb2ae 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -2,7 +2,10 @@ import os import logging from openpype.settings import get_system_settings, get_project_settings -from openpype.pipeline import legacy_io +from openpype.pipeline import ( + schema, + legacy_io, +) from openpype.pipeline.plugin_discover import ( discover, register_plugin, @@ -79,6 +82,36 @@ class LoaderPlugin(list): print(" - setting `{}`: `{}`".format(option, value)) setattr(cls, option, value) + @classmethod + def is_compatible_loader(cls, context): + """Return whether a loader is compatible with a context. + + This checks the version's families and the representation for the given + Loader. + + Returns: + bool + """ + + maj_version, _ = schema.get_schema_version(context["subset"]["schema"]) + if maj_version < 3: + families = context["version"]["data"].get("families", []) + else: + families = context["subset"]["data"]["families"] + + representation = context["representation"] + has_family = ( + "*" in cls.families or any( + family in cls.families for family in families + ) + ) + representations = cls.get_representations() + has_representation = ( + "*" in representations + or representation["name"] in representations + ) + return has_family and has_representation + @classmethod def get_representations(cls): return cls.representations diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index e2b3675115..e30923f922 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -748,25 +748,9 @@ def is_compatible_loader(Loader, context): Returns: bool - """ - maj_version, _ = schema.get_schema_version(context["subset"]["schema"]) - if maj_version < 3: - families = context["version"]["data"].get("families", []) - else: - families = context["subset"]["data"]["families"] - representation = context["representation"] - has_family = ( - "*" in Loader.families or any( - family in Loader.families for family in families - ) - ) - representations = Loader.get_representations() - has_representation = ( - "*" in representations or representation["name"] in representations - ) - return has_family and has_representation + return Loader.is_compatible_loader(context) def loaders_from_repre_context(loaders, repre_context): From bce5363e4250bc6e9a83c7702889a30790bf909a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 23 Jan 2023 10:23:07 +0100 Subject: [PATCH 046/158] reorganized the order of conditions --- openpype/pipeline/load/plugins.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index 4197ddb2ae..9b891a4da3 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -93,24 +93,33 @@ class LoaderPlugin(list): bool """ + plugin_repre_names = cls.get_representations() + plugin_families = cls.families + if not plugin_repre_names or not plugin_families: + return False + + repre_doc = context.get("representation") + if not repre_doc: + return False + + plugin_repre_names = set(plugin_repre_names) + if ( + "*" not in plugin_repre_names + and repre_doc["name"] not in plugin_repre_names + ): + return False + maj_version, _ = schema.get_schema_version(context["subset"]["schema"]) if maj_version < 3: families = context["version"]["data"].get("families", []) else: families = context["subset"]["data"]["families"] - representation = context["representation"] - has_family = ( - "*" in cls.families or any( - family in cls.families for family in families - ) + plugin_families = set(plugin_families) + return ( + "*" in plugin_families + or any(family in plugin_families for family in families) ) - representations = cls.get_representations() - has_representation = ( - "*" in representations - or representation["name"] in representations - ) - return has_family and has_representation @classmethod def get_representations(cls): From e4cd14c2a633f13146920a2c0cb4a70a09154e8e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 23 Jan 2023 17:44:11 +0000 Subject: [PATCH 047/158] Clean up --- .../maya/plugins/publish/extract_xgen.py | 43 ++++++------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 22260df2c7..80b62275cd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -10,7 +10,14 @@ from openpype.lib import StringTemplate class ExtractXgenCache(publish.Extractor): - """Extract Xgen""" + """Extract Xgen + + Workflow: + - Duplicate nodes used for patches. + - Export palette and import onto duplicate nodes. + - Export/Publish duplicate nodes and palette. + - Publish all xgen files as resources. + """ label = "Extract Xgen" hosts = ["maya"] @@ -55,29 +62,12 @@ class ExtractXgenCache(publish.Extractor): # Duplicate_transform subd patch geometry. duplicate_transform = cmds.duplicate(transform_name)[0] - duplicate_shape = cmds.listRelatives( - duplicate_transform, - shapes=True, - fullPath=True - )[0] # Discard the children. shapes = cmds.listRelatives(duplicate_transform, shapes=True) children = cmds.listRelatives(duplicate_transform, children=True) cmds.delete(set(children) - set(shapes)) - # Connect attributes. - cmds.connectAttr( - "{}.matrix".format(duplicate_transform), - "{}.transform".format(node), - force=True - ) - cmds.connectAttr( - "{}.worldMesh".format(duplicate_shape), - "{}.geometry".format(node), - force=True - ) - duplicate_transform = cmds.parent( duplicate_transform, world=True )[0] @@ -87,17 +77,17 @@ class ExtractXgenCache(publish.Extractor): # Import xgen onto the duplicate. with maintained_selection(): cmds.select(duplicate_nodes) - collection = xgenm.importPalette(xgen_path, []) + palette = xgenm.importPalette(xgen_path, []) attribute_data = { - "{}.xgFileName".format(collection): xgen_filename + "{}.xgFileName".format(palette): xgen_filename } # Export Maya file. type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" with attribute_values(attribute_data): with maintained_selection(): - cmds.select(duplicate_nodes + [collection]) + cmds.select(duplicate_nodes + [palette]) cmds.file( maya_filepath, force=True, @@ -117,18 +107,11 @@ class ExtractXgenCache(publish.Extractor): "ext": self.scene_type, "files": maya_filename, "stagingDir": staging_dir, - "data": {"xgenName": collection} + "data": {"xgenName": palette} } instance.data["representations"].append(representation) - # Revert to original xgen connections. - for node, connections in instance.data["xgenConnections"].items(): - for attr, src in connections.items(): - cmds.connectAttr( - src, "{}.{}".format(node, attr), force=True - ) - - cmds.delete(duplicate_nodes + [collection]) + cmds.delete(duplicate_nodes + [palette]) # Collect all files under palette root as resources. data_path = xgenm.getAttr( From 6ef2b4be9a5c458364f480325483ead90febdcf9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 24 Jan 2023 16:09:56 +0000 Subject: [PATCH 048/158] Working import --- openpype/hosts/maya/plugins/load/load_xgen.py | 76 +++++++++++++------ .../maya/plugins/publish/extract_xgen.py | 43 ++++------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 8249c9092e..fec1b389fa 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -1,5 +1,6 @@ import os import shutil +import tempfile import maya.cmds as cmds import xgenm @@ -25,6 +26,18 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" + def write_xgen_file(self, file_path, data): + lines = [] + with open(file_path, "r") as f: + for key, value in data.items(): + for line in [line.rstrip() for line in f]: + if line.startswith("\t" + key): + line = "\t{}\t\t{}".format(key, value) + lines.append(line) + + with open(file_path, "w") as f: + f.write("\n".join(lines)) + def setup_xgen_palette_file(self, maya_filepath, namespace, name): # Setup xgen palette file. project_path = os.path.dirname(current_file()) @@ -47,28 +60,26 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # and published version directory second. This ensure that any newly # created xgen files are created in the current workspace. resources_path = os.path.join(os.path.dirname(source), "resources") - lines = [] + with open(xgen_file, "r") as f: for line in [line.rstrip() for line in f]: if line.startswith("\txgDataPath"): data_path = line.split("\t")[-1] - line = "\txgDataPath\t\t{}{}{}".format( - data_path, - os.pathsep, - data_path.replace( - "${PROJECT}xgen", resources_path.replace("\\", "/") - ) + + data = { + "xgDataPath": ( + "${{PROJECT}}xgen/collections/{}__ns__{}/{}{}".format( + namespace, + name, + os.pathsep, + data_path.replace( + "${PROJECT}xgen", resources_path.replace("\\", "/") ) - - if line.startswith("\txgProjectPath"): - line = "\txgProjectPath\t\t{}/".format( - project_path.replace("\\", "/") - ) - - lines.append(line) - - with open(xgen_file, "w") as f: - f.write("\n".join(lines)) + ) + ), + "xgProjectPath": project_path.replace("\\", "/") + } + self.write_xgen_file(xgen_file, data) xgd_file = xgen_file.replace(".xgen", ".xgd") @@ -89,14 +100,31 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.fname, context["project"]["name"] ) - name = context["representation"]["data"]["xgenName"] xgen_file, xgd_file = self.setup_xgen_palette_file( - maya_filepath, namespace, name + maya_filepath, namespace, "collection" ) + # Making temporary copy of xgen file from published so we can + # modify the paths. + temp_xgen_file = os.path.join(tempfile.gettempdir(), "temp.xgen") + _, maya_extension = os.path.splitext(maya_filepath) + source = maya_filepath.replace(maya_extension, ".xgen") + shutil.copy(source, temp_xgen_file) + + resources_path = os.path.join(os.path.dirname(source), "resources") + with open(xgen_file, "r") as f: + for line in [line.rstrip() for line in f]: + if line.startswith("\txgDataPath"): + data_path = line.split("\t")[-1] + data = { + "xgDataPath": data_path.replace( + "${PROJECT}xgen", resources_path.replace("\\", "/") + ) + } + self.write_xgen_file(temp_xgen_file, data) + # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] - with maintained_selection(): nodes = cmds.file( maya_filepath, @@ -106,7 +134,11 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): returnNewNodes=True ) - xgen_palette = cmds.ls(nodes, type="xgmPalette", long=True)[0] + xgen_palette = xgenm.importPalette( + temp_xgen_file.replace("\\", "/"), [], nameSpace=namespace + ) + os.remove(temp_xgen_file) + self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) # This create an expression attribute of float. If we did not add @@ -119,7 +151,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): shapes = cmds.ls(nodes, shapes=True, long=True) - new_nodes = (list(set(nodes) - set(shapes))) + new_nodes = (list(set(nodes) - set(shapes)) + [xgen_palette]) self[:] = new_nodes diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 80b62275cd..91d4352449 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -5,7 +5,7 @@ from maya import cmds import xgenm from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import maintained_selection, attribute_values +from openpype.hosts.maya.api.lib import maintained_selection from openpype.lib import StringTemplate @@ -74,31 +74,21 @@ class ExtractXgenCache(publish.Extractor): duplicate_nodes.append(duplicate_transform) - # Import xgen onto the duplicate. - with maintained_selection(): - cmds.select(duplicate_nodes) - palette = xgenm.importPalette(xgen_path, []) - - attribute_data = { - "{}.xgFileName".format(palette): xgen_filename - } - # Export Maya file. type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" - with attribute_values(attribute_data): - with maintained_selection(): - cmds.select(duplicate_nodes + [palette]) - cmds.file( - maya_filepath, - force=True, - type=type, - exportSelected=True, - preserveReferences=False, - constructionHistory=True, - shader=True, - constraints=True, - expressions=True - ) + with maintained_selection(): + cmds.select(duplicate_nodes) + cmds.file( + maya_filepath, + force=True, + type=type, + exportSelected=True, + preserveReferences=False, + constructionHistory=True, + shader=True, + constraints=True, + expressions=True + ) self.log.info("Extracted to {}".format(maya_filepath)) @@ -106,12 +96,11 @@ class ExtractXgenCache(publish.Extractor): "name": self.scene_type, "ext": self.scene_type, "files": maya_filename, - "stagingDir": staging_dir, - "data": {"xgenName": palette} + "stagingDir": staging_dir } instance.data["representations"].append(representation) - cmds.delete(duplicate_nodes + [palette]) + cmds.delete(duplicate_nodes) # Collect all files under palette root as resources. data_path = xgenm.getAttr( From 04c1b8905508253456e9bab7008bfe1263d98999 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 Jan 2023 09:33:23 +0100 Subject: [PATCH 049/158] fix typo + remove part which creates second reference per placeholder --- .../hosts/maya/api/workfile_template_builder.py | 2 +- .../pipeline/workfile/workfile_template_builder.py | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index ef043ed0f4..8e0f1d0f01 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -240,7 +240,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): cmds.setAttr(node + ".hiddenInOutliner", True) def load_succeed(self, placeholder, container): - self._parent_in_hierarhchy(placeholder, container) + self._parent_in_hierarchy(placeholder, container) def _parent_in_hierarchy(self, placeholder, container): """Parent loaded container to placeholder's parent. diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 582657c735..d262d6a771 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -521,10 +521,6 @@ class AbstractTemplateBuilder(object): if not level_limit: level_limit = 1000 - placeholder_by_scene_id = { - placeholder.scene_identifier: placeholder - for placeholder in placeholders - } all_processed = len(placeholders) == 0 # Counter is checked at the ned of a loop so the loop happens at least # once. @@ -573,16 +569,6 @@ class AbstractTemplateBuilder(object): break all_processed = True - collected_placeholders = self.get_placeholders() - for placeholder in collected_placeholders: - identifier = placeholder.scene_identifier - if identifier in placeholder_by_scene_id: - continue - - all_processed = False - placeholder_by_scene_id[identifier] = placeholder - placeholders.append(placeholder) - self.refresh() def _get_build_profiles(self): From 6a992409f0adac4cf4fe534e5feee5e6f610b363 Mon Sep 17 00:00:00 2001 From: Libor Batek Date: Wed, 25 Jan 2023 09:51:42 +0100 Subject: [PATCH 050/158] Added more elaborate info on installing/running OP and corrected few typos. --- website/docs/artist_getting_started.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 2f88a9f238..474a39642b 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -18,13 +18,17 @@ If this is not the case, please contact your administrator to consult on how to If you are working from home though, you'll need to install it yourself. You should, however, receive the OpenPype installer files from your studio admin, supervisor or production, because OpenPype versions and executables might not be compatible between studios. -To install OpenPype you just need to unzip it anywhere on the disk +Installing OpenPype is possible by Windows installer or by unzipping it anywhere on the disk from downloaded ZIP archive. -To use it, you have two options +There are two options running OpenPype -**openpype_gui.exe** is the most common for artists. It runs OpenPype GUI in system tray. From there you can run all the available tools. To use any of the features, OpenPype must be running in the tray. +first most common one by using OP icon on the Desktop triggering -**openpype_console.exe** in useful for debugging and error reporting. It opens console window where all the necessary information will appear during user's work. +**openpype_gui.exe** suitable for artists. It runs OpenPype GUI in the OS tray. From there you can run all the available tools. To use any of the features, OpenPype must be running in the tray. + +or alternatively by using + +**openpype_console.exe** located in the OpenPype folder which is suitable for TDs/admins for debugging and error reporting. It opens console window where all the necessary information will appear during user's work. Date: Wed, 25 Jan 2023 09:38:17 +0000 Subject: [PATCH 051/158] Revert "Working import" This reverts commit 6ef2b4be9a5c458364f480325483ead90febdcf9. --- openpype/hosts/maya/plugins/load/load_xgen.py | 76 ++++++------------- .../maya/plugins/publish/extract_xgen.py | 43 +++++++---- 2 files changed, 49 insertions(+), 70 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index fec1b389fa..8249c9092e 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -1,6 +1,5 @@ import os import shutil -import tempfile import maya.cmds as cmds import xgenm @@ -26,18 +25,6 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def write_xgen_file(self, file_path, data): - lines = [] - with open(file_path, "r") as f: - for key, value in data.items(): - for line in [line.rstrip() for line in f]: - if line.startswith("\t" + key): - line = "\t{}\t\t{}".format(key, value) - lines.append(line) - - with open(file_path, "w") as f: - f.write("\n".join(lines)) - def setup_xgen_palette_file(self, maya_filepath, namespace, name): # Setup xgen palette file. project_path = os.path.dirname(current_file()) @@ -60,26 +47,28 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # and published version directory second. This ensure that any newly # created xgen files are created in the current workspace. resources_path = os.path.join(os.path.dirname(source), "resources") - + lines = [] with open(xgen_file, "r") as f: for line in [line.rstrip() for line in f]: if line.startswith("\txgDataPath"): data_path = line.split("\t")[-1] - - data = { - "xgDataPath": ( - "${{PROJECT}}xgen/collections/{}__ns__{}/{}{}".format( - namespace, - name, - os.pathsep, - data_path.replace( - "${PROJECT}xgen", resources_path.replace("\\", "/") + line = "\txgDataPath\t\t{}{}{}".format( + data_path, + os.pathsep, + data_path.replace( + "${PROJECT}xgen", resources_path.replace("\\", "/") + ) ) - ) - ), - "xgProjectPath": project_path.replace("\\", "/") - } - self.write_xgen_file(xgen_file, data) + + if line.startswith("\txgProjectPath"): + line = "\txgProjectPath\t\t{}/".format( + project_path.replace("\\", "/") + ) + + lines.append(line) + + with open(xgen_file, "w") as f: + f.write("\n".join(lines)) xgd_file = xgen_file.replace(".xgen", ".xgd") @@ -100,31 +89,14 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.fname, context["project"]["name"] ) + name = context["representation"]["data"]["xgenName"] xgen_file, xgd_file = self.setup_xgen_palette_file( - maya_filepath, namespace, "collection" + maya_filepath, namespace, name ) - # Making temporary copy of xgen file from published so we can - # modify the paths. - temp_xgen_file = os.path.join(tempfile.gettempdir(), "temp.xgen") - _, maya_extension = os.path.splitext(maya_filepath) - source = maya_filepath.replace(maya_extension, ".xgen") - shutil.copy(source, temp_xgen_file) - - resources_path = os.path.join(os.path.dirname(source), "resources") - with open(xgen_file, "r") as f: - for line in [line.rstrip() for line in f]: - if line.startswith("\txgDataPath"): - data_path = line.split("\t")[-1] - data = { - "xgDataPath": data_path.replace( - "${PROJECT}xgen", resources_path.replace("\\", "/") - ) - } - self.write_xgen_file(temp_xgen_file, data) - # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] + with maintained_selection(): nodes = cmds.file( maya_filepath, @@ -134,11 +106,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): returnNewNodes=True ) - xgen_palette = xgenm.importPalette( - temp_xgen_file.replace("\\", "/"), [], nameSpace=namespace - ) - os.remove(temp_xgen_file) - + xgen_palette = cmds.ls(nodes, type="xgmPalette", long=True)[0] self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) # This create an expression attribute of float. If we did not add @@ -151,7 +119,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): shapes = cmds.ls(nodes, shapes=True, long=True) - new_nodes = (list(set(nodes) - set(shapes)) + [xgen_palette]) + new_nodes = (list(set(nodes) - set(shapes))) self[:] = new_nodes diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 91d4352449..80b62275cd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -5,7 +5,7 @@ from maya import cmds import xgenm from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import maintained_selection +from openpype.hosts.maya.api.lib import maintained_selection, attribute_values from openpype.lib import StringTemplate @@ -74,21 +74,31 @@ class ExtractXgenCache(publish.Extractor): duplicate_nodes.append(duplicate_transform) - # Export Maya file. - type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" + # Import xgen onto the duplicate. with maintained_selection(): cmds.select(duplicate_nodes) - cmds.file( - maya_filepath, - force=True, - type=type, - exportSelected=True, - preserveReferences=False, - constructionHistory=True, - shader=True, - constraints=True, - expressions=True - ) + palette = xgenm.importPalette(xgen_path, []) + + attribute_data = { + "{}.xgFileName".format(palette): xgen_filename + } + + # Export Maya file. + type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" + with attribute_values(attribute_data): + with maintained_selection(): + cmds.select(duplicate_nodes + [palette]) + cmds.file( + maya_filepath, + force=True, + type=type, + exportSelected=True, + preserveReferences=False, + constructionHistory=True, + shader=True, + constraints=True, + expressions=True + ) self.log.info("Extracted to {}".format(maya_filepath)) @@ -96,11 +106,12 @@ class ExtractXgenCache(publish.Extractor): "name": self.scene_type, "ext": self.scene_type, "files": maya_filename, - "stagingDir": staging_dir + "stagingDir": staging_dir, + "data": {"xgenName": palette} } instance.data["representations"].append(representation) - cmds.delete(duplicate_nodes) + cmds.delete(duplicate_nodes + [palette]) # Collect all files under palette root as resources. data_path = xgenm.getAttr( From 036509dc4733595e07b000097b6b841b2c76e40e Mon Sep 17 00:00:00 2001 From: Libor Batek Date: Wed, 25 Jan 2023 14:17:20 +0100 Subject: [PATCH 052/158] additional changes incorporated --- website/docs/artist_getting_started.md | 54 +++++++++----------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 474a39642b..40961dbc77 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -7,6 +7,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; + ## Working in the studio In studio environment you should have OpenPype already installed and deployed, so you can start using it without much setup. Your admin has probably put OpenPype icon on your desktop or even had your computer set up so OpenPype will start automatically. @@ -15,63 +16,46 @@ If this is not the case, please contact your administrator to consult on how to ## Working from home -If you are working from home though, you'll need to install it yourself. You should, however, receive the OpenPype installer files from your studio +If you are working from **home** though, you'll **need to install** it yourself. You should, however, receive the OpenPype installer files from your studio admin, supervisor or production, because OpenPype versions and executables might not be compatible between studios. Installing OpenPype is possible by Windows installer or by unzipping it anywhere on the disk from downloaded ZIP archive. -There are two options running OpenPype +For more detailed info about installation on different OS please visit [Installation section](artist_install.md). + +There are two ways running OpenPype first most common one by using OP icon on the Desktop triggering -**openpype_gui.exe** suitable for artists. It runs OpenPype GUI in the OS tray. From there you can run all the available tools. To use any of the features, OpenPype must be running in the tray. +**openpype_gui.exe** suitable **for artists**. It runs OpenPype GUI in the OS tray. From there you can run all the available tools. To use any of the features, OpenPype must be running in the tray. or alternatively by using -**openpype_console.exe** located in the OpenPype folder which is suitable for TDs/admins for debugging and error reporting. It opens console window where all the necessary information will appear during user's work. - - - - - - -WIP - Windows instructions once installers are finished - - - - -WIP - Linux instructions once installers are finished - - - - -WIP - Mac instructions once installers are finished - - - +**openpype_console.exe** located in the OpenPype folder which is suitable for **TDs/Admin** for debugging and error reporting. This one runs with opened console window where all the necessary info will appear during user's work session. ## First Launch -When you first start OpenPype, you will be asked to give it some basic information. +When you first start OpenPype, you will be asked to fill in some basic informations. + ### MongoDB In most cases that will only be your studio MongoDB Address. +It's a URL that you should have received from your Studio admin and most often will look like this -It is a URL that you should receive from you studio and most often will look like this `mongodb://username:passwword@mongo.mystudiodomain.com:12345` or `mongodb://192.168.100.15:27071`, it really depends on your studio setup. When OpenPype Igniter +`mongodb://username:passwword@mongo.mystudiodomain.com:12345` + + or + + `mongodb://192.168.100.15:27071` + +it really depends on your studio setup. When OpenPype Igniter asks for it, just put it in the corresponding text field and press `install` button. ### OpenPype Version Repository -Sometimes your studio might also ask you to fill in the path to it's version +Sometimes your Studio might also ask you to fill in the path to it's version repository. This is a location where OpenPype will be looking for when checking if it's up to date and where updates are installed from automatically. @@ -80,7 +64,7 @@ This path is usually taken from the database directly, so you shouldn't need it. ## Updates -If you're connected to your studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start, it will go through a quick update installation process, even though you might have just installed it. +If you're connected to your Studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start, it will go through a quick update installation process, even though you might have just installed it. ## Advanced Usage From a6df39c77b26c12ef236bf36a601c032633680ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 01:55:11 +0100 Subject: [PATCH 053/158] fix 'update_context_data' in tray publisher --- openpype/hosts/traypublisher/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/pipeline.py b/openpype/hosts/traypublisher/api/pipeline.py index 0a8ddaa343..3264f52b0f 100644 --- a/openpype/hosts/traypublisher/api/pipeline.py +++ b/openpype/hosts/traypublisher/api/pipeline.py @@ -37,7 +37,7 @@ class TrayPublisherHost(HostBase, IPublishHost): return HostContext.get_context_data() def update_context_data(self, data, changes): - HostContext.save_context_data(data, changes) + HostContext.save_context_data(data) def set_project_name(self, project_name): # TODO Deregister project specific plugins and register new project From fa62e7954c0d061695651052651e55c6d1b59ef2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 01:56:18 +0100 Subject: [PATCH 054/158] removed unused methods --- openpype/pipeline/create/context.py | 78 ----------------------------- 1 file changed, 78 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 9c468ae8fc..0d42c663d9 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -315,16 +315,6 @@ class AttributeValues(object): def changes(self): return self.calculate_changes(self._data, self._origin_data) - def apply_changes(self, changes): - for key, item in changes.items(): - old_value, new_value = item - if new_value is None: - if key in self: - self.pop(key) - - elif self.get(key) != new_value: - self[key] = new_value - class CreatorAttributeValues(AttributeValues): """Creator specific attribute values of an instance. @@ -458,21 +448,6 @@ class PublishAttributes: changes[key] = (value, None) return changes - def apply_changes(self, changes): - for key, item in changes.items(): - if isinstance(item, dict): - self._data[key].apply_changes(item) - continue - - old_value, new_value = item - if new_value is not None: - raise ValueError( - "Unexpected type \"{}\" expected None".format( - str(type(new_value)) - ) - ) - self.pop(key) - def set_publish_plugins(self, attr_plugins): """Set publish plugins attribute definitions.""" @@ -909,59 +884,6 @@ class CreatedInstance: return obj - def remote_changes(self): - """Prepare serializable changes on remote side. - - Returns: - Dict[str, Any]: Prepared changes that can be send to client side. - """ - - return { - "changes": self.changes(), - "asset_is_valid": self._asset_is_valid, - "task_is_valid": self._task_is_valid, - } - - def update_from_remote(self, remote_changes): - """Apply changes from remote side on client side. - - Args: - remote_changes (Dict[str, Any]): Changes created on remote side. - """ - - self._asset_is_valid = remote_changes["asset_is_valid"] - self._task_is_valid = remote_changes["task_is_valid"] - - changes = remote_changes["changes"] - creator_attributes = changes.pop("creator_attributes", None) or {} - publish_attributes = changes.pop("publish_attributes", None) or {} - if changes: - self.apply_changes(changes) - - if creator_attributes: - self.creator_attributes.apply_changes(creator_attributes) - - if publish_attributes: - self.publish_attributes.apply_changes(publish_attributes) - - def apply_changes(self, changes): - """Apply changes created via 'changes'. - - Args: - Dict[str, Tuple[Any, Any]]: Instance changes to apply. Same values - are kept untouched. - """ - - for key, item in changes.items(): - old_value, new_value = item - if new_value is None: - if key in self: - self.pop(key) - else: - current_value = self.get(key) - if current_value != new_value: - self[key] = new_value - class ConvertorItem(object): """Item representing convertor plugin. From 1496b74fc2668e8c075be1a771002b7b7c8c7fb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 01:58:52 +0100 Subject: [PATCH 055/158] added helper object to handle changes --- openpype/pipeline/create/context.py | 93 +++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 0d42c663d9..94230bda09 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -177,6 +177,99 @@ def prepare_failed_creator_operation_info( } +class ChangedItem(object): + def __init__(self, old_value, new_value): + self._old_value = copy.deepcopy(old_value) + self._new_value = copy.deepcopy(new_value) + self._changed = self._old_value != self._new_value + + old_is_dict = isinstance(old_value, dict) + new_is_dict = isinstance(new_value, dict) + children = {} + changed_keys = set() + available_keys = set() + if old_is_dict and new_is_dict: + old_keys = set(old_value.keys()) + new_keys = set(new_value.keys()) + available_keys = old_keys | new_keys + for key in available_keys: + item = ChangedItem( + old_value.get(key), new_value.get(key) + ) + children[key] = item + if item.changed or key not in old_keys or key not in new_keys: + changed_keys.add(key) + + elif old_is_dict: + available_keys = set(old_value.keys()) + changed_keys = set(available_keys) + for key in available_keys: + children[key] = ChangedItem(old_value.get(key), None) + + elif new_is_dict: + available_keys = set(new_value.keys()) + changed_keys = set(available_keys) + for key in available_keys: + children[key] = ChangedItem(None, new_value.get(key)) + + self._changed_keys = changed_keys + self._available_keys = available_keys + self._children = children + self._old_is_dict = old_is_dict + self._new_is_dict = new_is_dict + + def __getitem__(self, key): + return self._children[key] + + def __bool__(self): + return self._changed + + def __iter__(self): + for key in self._changed_keys: + yield key + + def keys(self): + return set(self._changed_keys) + + @property + def changed(self): + return self._changed + + @property + def changes(self): + if not self._old_is_dict and not self._new_is_dict: + return (self.old_value, self.new_value) + + old_value = self.old_value + new_value = self.new_value + output = {} + for key in self.changed_keys: + _old = None + _new = None + if self._old_is_dict: + _old = old_value.get(key) + if self._new_is_dict: + _new = new_value.get(key) + output[key] = (_old, _new) + return output + + @property + def changed_keys(self): + return set(self._changed_keys) + + @property + def available_keys(self): + return set(self._available_keys) + + @property + def old_value(self): + return copy.deepcopy(self._old_value) + + @property + def new_value(self): + return copy.deepcopy(self._new_value) + + class InstanceMember: """Representation of instance member. From e46042c2d646333ee8723cd98aaecbe06881f878 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 01:59:35 +0100 Subject: [PATCH 056/158] use the 'ChangedItem' object to handle changes --- openpype/pipeline/create/context.py | 71 ++++++----------------------- 1 file changed, 15 insertions(+), 56 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 94230bda09..7db4849300 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -384,6 +384,10 @@ class AttributeValues(object): """Pointer to attribute definitions.""" return self._attr_defs + @property + def origin_data(self): + return copy.deepcopy(self._origin_data) + def data_to_store(self): """Create new dictionary with data to store.""" output = {} @@ -395,19 +399,6 @@ class AttributeValues(object): output[key] = attr_def.default return output - @staticmethod - def calculate_changes(new_data, old_data): - """Calculate changes of 2 dictionary objects.""" - changes = {} - for key, new_value in new_data.items(): - old_value = old_data.get(key) - if old_value != new_value: - changes[key] = (old_value, new_value) - return changes - - def changes(self): - return self.calculate_changes(self._data, self._origin_data) - class CreatorAttributeValues(AttributeValues): """Creator specific attribute values of an instance. @@ -525,21 +516,9 @@ class PublishAttributes: output[key] = attr_value.data_to_store() return output - def changes(self): - """Return changes per each key.""" - - changes = {} - for key, attr_val in self._data.items(): - attr_changes = attr_val.changes() - if attr_changes: - if key not in changes: - changes[key] = {} - changes[key].update(attr_val) - - for key, value in self._origin_data.items(): - if key not in self._data: - changes[key] = (value, None) - return changes + @property + def origin_data(self): + return copy.deepcopy(self._origin_data) def set_publish_plugins(self, attr_plugins): """Set publish plugins attribute definitions.""" @@ -746,6 +725,10 @@ class CreatedInstance: return label return self.creator.get_group_label() + @property + def origin_data(self): + return copy.deepcopy(self._orig_data) + @property def creator_identifier(self): return self.creator.identifier @@ -837,29 +820,7 @@ class CreatedInstance: def changes(self): """Calculate and return changes.""" - changes = {} - new_keys = set() - for key, new_value in self._data.items(): - new_keys.add(key) - if key in ("creator_attributes", "publish_attributes"): - continue - - old_value = self._orig_data.get(key) - if old_value != new_value: - changes[key] = (old_value, new_value) - - creator_attr_changes = self.creator_attributes.changes() - if creator_attr_changes: - changes["creator_attributes"] = creator_attr_changes - - publish_attr_changes = self.publish_attributes.changes() - if publish_attr_changes: - changes["publish_attributes"] = publish_attr_changes - - for key, old_value in self._orig_data.items(): - if key not in new_keys: - changes[key] = (old_value, None) - return changes + return ChangedItem(self.origin_data, self.data_to_store()) def mark_as_stored(self): """Should be called when instance data are stored. @@ -1390,11 +1351,9 @@ class CreateContext: def context_data_changes(self): """Changes of attributes.""" - changes = {} - publish_attribute_changes = self._publish_attributes.changes() - if publish_attribute_changes: - changes["publish_attributes"] = publish_attribute_changes - return changes + + old_value = copy.deepcopy(self._original_context_data) + return ChangedItem(old_value, self.context_data_to_store()) def creator_adds_instance(self, instance): """Creator adds new instance to context. From f9f077722c161048f7f3328ce1ab5a2aab12f7e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 02:06:26 +0100 Subject: [PATCH 057/158] adde items method to extend dictionary approach --- openpype/pipeline/create/context.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 7db4849300..b6a6747f65 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -231,6 +231,14 @@ class ChangedItem(object): def keys(self): return set(self._changed_keys) + def items(self): + changes = self.changes + if isinstance(changes, tuple): + yield None, changes + else: + for item in changes.items(): + yield item + @property def changed(self): return self._changed From c88579591a7fb5f9d079b6ee656f5af2937266b7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 02:14:16 +0100 Subject: [PATCH 058/158] keep track of new/old keys --- openpype/pipeline/create/context.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index b6a6747f65..485252b4f8 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -188,6 +188,8 @@ class ChangedItem(object): children = {} changed_keys = set() available_keys = set() + new_keys = set() + old_keys = set() if old_is_dict and new_is_dict: old_keys = set(old_value.keys()) new_keys = set(new_value.keys()) @@ -201,13 +203,15 @@ class ChangedItem(object): changed_keys.add(key) elif old_is_dict: - available_keys = set(old_value.keys()) + old_keys = set(old_value.keys()) + available_keys = set(old_keys) changed_keys = set(available_keys) for key in available_keys: children[key] = ChangedItem(old_value.get(key), None) elif new_is_dict: - available_keys = set(new_value.keys()) + new_keys = set(new_value.keys()) + available_keys = set(new_keys) changed_keys = set(available_keys) for key in available_keys: children[key] = ChangedItem(None, new_value.get(key)) @@ -217,6 +221,8 @@ class ChangedItem(object): self._children = children self._old_is_dict = old_is_dict self._new_is_dict = new_is_dict + self._old_keys = old_keys + self._new_keys = new_keys def __getitem__(self, key): return self._children[key] @@ -225,11 +231,14 @@ class ChangedItem(object): return self._changed def __iter__(self): - for key in self._changed_keys: + for key in self.changed_keys: yield key + def get(self, key, default=None): + return self._children.get(key, default) + def keys(self): - return set(self._changed_keys) + return self.changed_keys def items(self): changes = self.changes @@ -269,6 +278,14 @@ class ChangedItem(object): def available_keys(self): return set(self._available_keys) + @property + def old_keys(self): + return set(self._old_keys) + + @property + def new_keys(self): + return set(self._new_keys) + @property def old_value(self): return copy.deepcopy(self._old_value) From 7c1a39d5e4f17d3abf94268d9da6dcf8d5eeea9c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Jan 2023 02:17:13 +0100 Subject: [PATCH 059/158] added 'is_dict' property --- openpype/pipeline/create/context.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 485252b4f8..e44e5db851 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -241,20 +241,23 @@ class ChangedItem(object): return self.changed_keys def items(self): - changes = self.changes - if isinstance(changes, tuple): - yield None, changes + if self.is_dict: + yield None, self.changes else: - for item in changes.items(): + for item in self.changes.items(): yield item @property def changed(self): return self._changed + @property + def is_dict(self): + return self._old_is_dict or self._new_is_dict + @property def changes(self): - if not self._old_is_dict and not self._new_is_dict: + if not self.is_dict: return (self.old_value, self.new_value) old_value = self.old_value From 44bcfc167a448fd4d50aec91aed1195f76430ccf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:34:53 +0100 Subject: [PATCH 060/158] fix 'items' method --- 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 e44e5db851..95e194fda1 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -241,7 +241,7 @@ class ChangedItem(object): return self.changed_keys def items(self): - if self.is_dict: + if not self.is_dict: yield None, self.changes else: for item in self.changes.items(): From 1f08a2734339ae7c40424df778d7402807beed10 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 10:15:04 +0000 Subject: [PATCH 061/158] Workaround for motion blur --- .../plugins/publish/extract_workfile_xgen.py | 147 +++++++++++++----- .../maya/plugins/publish/extract_xgen.py | 62 ++++---- 2 files changed, 146 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index d37a03d1f6..5847563e5b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -1,11 +1,14 @@ import os import shutil +import copy from maya import cmds import pyblish.api from openpype.hosts.maya.api import current_file +from openpype.hosts.maya.api.lib import extract_alembic from openpype.pipeline import publish +from openpype.lib import StringTemplate class ExtractWorkfileXgen(publish.Extractor): @@ -18,9 +21,22 @@ class ExtractWorkfileXgen(publish.Extractor): hosts = ["maya"] def process(self, instance): + transfers = [] + + # Validate there is any palettes in the scene. + if not cmds.ls(type="xgmPalette"): + self.log.debug( + "No collections found in the scene. Abort Xgen extraction." + ) + return + else: + import xgenm + # Validate to extract only when we are publishing a renderlayer as # well. renderlayer = False + start_frame = None + end_frame = None for i in instance.context: is_renderlayer = ( "renderlayer" in i.data.get("families", []) or @@ -28,6 +44,17 @@ class ExtractWorkfileXgen(publish.Extractor): ) if is_renderlayer and i.data["publish"]: renderlayer = True + + if start_frame is None: + start_frame = i.data["frameStart"] + if end_frame is None: + end_frame = i.data["frameEnd"] + + if i.data["frameStart"] < start_frame: + start_frame = i.data["frameStart"] + if i.data["frameEnd"] > end_frame: + end_frame = i.data["frameEnd"] + break if not renderlayer: @@ -37,6 +64,51 @@ class ExtractWorkfileXgen(publish.Extractor): ) return + # We decrement start frame and increment end frame so motion blur will + # render correctly. + start_frame -= 1 + end_frame += 1 + + # Extract patches alembic. + basename, _ = os.path.splitext(current_file()) + dirname = os.path.dirname(current_file()) + kwargs = {"attrPrefix": ["xgen"], "stripNamespaces": True} + alembic_files = [] + for palette in cmds.ls(type="xgmPalette"): + patch_names = [] + for description in xgenm.descriptions(palette): + for name in xgenm.boundGeometry(palette, description): + patch_names.append(name) + + alembic_file = os.path.join( + dirname, + "{}__{}.abc".format(basename, palette.replace(":", "__ns__")) + ) + extract_alembic( + alembic_file, + root=patch_names, + selection=False, + startFrame=float(start_frame), + endFrame=float(end_frame), + verbose=True, + **kwargs + ) + alembic_files.append(alembic_file) + + template_data = copy.deepcopy(instance.data["anatomyData"]) + published_maya_path = StringTemplate( + instance.context.data["anatomy"].templates["publish"]["file"] + ).format(template_data) + published_basename, _ = os.path.splitext(published_maya_path) + + for source in alembic_files: + destination = os.path.join( + os.path.dirname(instance.data["resourcesDir"]), + os.path.basename(source.replace(basename, published_basename)) + ) + transfers.append((source, destination)) + + # Validate that we are using the published workfile. deadline_settings = instance.context.get("deadline") if deadline_settings: publish_settings = deadline_settings["publish"] @@ -76,8 +148,9 @@ class ExtractWorkfileXgen(publish.Extractor): with open(destination, "r") as f: for line in [line.rstrip() for line in f]: if line.startswith("\txgProjectPath"): + path = os.path.dirname(instance.data["resourcesDir"]) line = "\txgProjectPath\t\t{}/".format( - instance.data["resourcesDir"].replace("\\", "/") + path.replace("\\", "/") ) lines.append(line) @@ -88,31 +161,30 @@ class ExtractWorkfileXgen(publish.Extractor): sources.append(destination) # Add resource files to workfile instance. - transfers = [] for source in sources: basename = os.path.basename(source) - destination = os.path.join(instance.data["resourcesDir"], basename) + destination = os.path.join( + os.path.dirname(instance.data["resourcesDir"]), basename + ) transfers.append((source, destination)) - import xgenm + destination_dir = os.path.join( + instance.data["resourcesDir"], "collections" + ) for palette in cmds.ls(type="xgmPalette"): - relative_data_path = xgenm.getAttr( - "xgDataPath", palette.replace("|", "") - ).split(os.pathsep)[0] - absolute_data_path = relative_data_path.replace( - "${PROJECT}", - xgenm.getAttr("xgProjectPath", palette.replace("|", "")) - ) - - for root, _, files in os.walk(absolute_data_path): - for file in files: - source = os.path.join(root, file).replace("\\", "/") - destination = os.path.join( - instance.data["resourcesDir"], - relative_data_path.replace("${PROJECT}", ""), - source.replace(absolute_data_path, "")[1:] - ) - transfers.append((source, destination.replace("\\", "/"))) + project_path = xgenm.getAttr("xgProjectPath", palette) + data_path = xgenm.getAttr("xgDataPath", palette) + data_path = data_path.replace("${PROJECT}", project_path) + for path in data_path.split(os.pathsep): + for root, _, files in os.walk(path): + for f in files: + source = os.path.join(root, f) + destination = "{}/{}{}".format( + destination_dir, + palette.replace(":", "__ns__"), + source.replace(path, "") + ) + transfers.append((source, destination)) for source, destination in transfers: self.log.debug("Transfer: {} > {}".format(source, destination)) @@ -120,21 +192,26 @@ class ExtractWorkfileXgen(publish.Extractor): instance.data["transfers"] = transfers # Set palette attributes in preparation for workfile publish. - attrs = ["xgFileName", "xgBaseFile"] + attrs = {"xgFileName": None, "xgBaseFile": ""} data = {} for palette in cmds.ls(type="xgmPalette"): - for attr in attrs: - value = cmds.getAttr(palette + "." + attr) - if value: - new_value = "resources/{}".format(value) - node_attr = "{}.{}".format(palette, attr) - self.log.info( - "Setting \"{}\" on \"{}\"".format(new_value, node_attr) - ) - cmds.setAttr(node_attr, new_value, type="string") - try: - data[palette][attr] = value - except KeyError: - data[palette] = {attr: value} + attrs["xgFileName"] = "resources/{}.xgen".format( + palette.replace(":", "__ns__") + ) + for attr, value in attrs.items(): + node_attr = palette + "." + attr + + old_value = cmds.getAttr(node_attr) + try: + data[palette][attr] = old_value + except KeyError: + data[palette] = {attr: old_value} + + cmds.setAttr(node_attr, value, type="string") + self.log.info( + "Setting \"{}\" on \"{}\"".format(value, node_attr) + ) + + cmds.setAttr(palette + "." + "xgExportAsDelta", False) instance.data["xgenAttributes"] = data diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 80b62275cd..fd85cadcac 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -1,5 +1,6 @@ import os import copy +import tempfile from maya import cmds import xgenm @@ -16,6 +17,7 @@ class ExtractXgenCache(publish.Extractor): - Duplicate nodes used for patches. - Export palette and import onto duplicate nodes. - Export/Publish duplicate nodes and palette. + - Export duplicate palette to .xgen file and add to publish. - Publish all xgen files as resources. """ @@ -32,29 +34,6 @@ class ExtractXgenCache(publish.Extractor): maya_filename = "{}.{}".format(instance.data["name"], self.scene_type) maya_filepath = os.path.join(staging_dir, maya_filename) - # Get published xgen file name. - template_data = copy.deepcopy(instance.data["anatomyData"]) - template_data.update({"ext": "xgen"}) - templates = instance.context.data["anatomy"].templates["publish"] - xgen_filename = StringTemplate(templates["file"]).format(template_data) - name = instance.data["xgmPalette"].replace(":", "__").replace("|", "") - xgen_filename = xgen_filename.replace(".xgen", "__" + name + ".xgen") - - # Export xgen palette files. - xgen_path = os.path.join(staging_dir, xgen_filename).replace("\\", "/") - xgenm.exportPalette( - instance.data["xgmPalette"].replace("|", ""), xgen_path - ) - self.log.info("Extracted to {}".format(xgen_path)) - - representation = { - "name": name, - "ext": "xgen", - "files": xgen_filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) - # Collect nodes to export. duplicate_nodes = [] for node, connections in instance.data["xgenConnections"].items(): @@ -74,17 +53,43 @@ class ExtractXgenCache(publish.Extractor): duplicate_nodes.append(duplicate_transform) + # Export temp xgen palette files. + temp_xgen_path = os.path.join( + tempfile.gettempdir(), "temp.xgen" + ).replace("\\", "/") + xgenm.exportPalette( + instance.data["xgmPalette"].replace("|", ""), temp_xgen_path + ) + self.log.info("Extracted to {}".format(temp_xgen_path)) + # Import xgen onto the duplicate. with maintained_selection(): cmds.select(duplicate_nodes) - palette = xgenm.importPalette(xgen_path, []) + palette = xgenm.importPalette(temp_xgen_path, []) - attribute_data = { - "{}.xgFileName".format(palette): xgen_filename + # Get published xgen file name. + template_data = copy.deepcopy(instance.data["anatomyData"]) + template_data.update({"ext": "xgen"}) + templates = instance.context.data["anatomy"].templates["publish"] + xgen_filename = StringTemplate(templates["file"]).format(template_data) + + # Export duplicated palette. + xgen_path = os.path.join(staging_dir, xgen_filename).replace("\\", "/") + xgenm.exportPalette(palette, xgen_path) + + representation = { + "name": "xgen", + "ext": "xgen", + "files": xgen_filename, + "stagingDir": staging_dir, } + instance.data["representations"].append(representation) # Export Maya file. type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" + attribute_data = { + "{}.xgFileName".format(palette): xgen_filename + } with attribute_values(attribute_data): with maintained_selection(): cmds.select(duplicate_nodes + [palette]) @@ -106,12 +111,13 @@ class ExtractXgenCache(publish.Extractor): "name": self.scene_type, "ext": self.scene_type, "files": maya_filename, - "stagingDir": staging_dir, - "data": {"xgenName": palette} + "stagingDir": staging_dir } instance.data["representations"].append(representation) + # Clean up. cmds.delete(duplicate_nodes + [palette]) + os.remove(temp_xgen_path) # Collect all files under palette root as resources. data_path = xgenm.getAttr( From c908bd2abaeb2a44b8beb88a4ed30aa1f2dad56c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 10:15:27 +0000 Subject: [PATCH 062/158] Workaround for motion blur --- openpype/hosts/maya/plugins/load/load_xgen.py | 76 +++++++------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 8249c9092e..0f2e13dd79 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -1,5 +1,4 @@ import os -import shutil import maya.cmds as cmds import xgenm @@ -12,7 +11,6 @@ from openpype.hosts.maya.api.lib import ( ) from openpype.hosts.maya.api import current_file from openpype.hosts.maya.api.plugin import get_reference_node -from openpype.pipeline import get_representation_path class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): @@ -25,55 +23,19 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def setup_xgen_palette_file(self, maya_filepath, namespace, name): - # Setup xgen palette file. - project_path = os.path.dirname(current_file()) - - # Copy the xgen palette file from published version. - _, maya_extension = os.path.splitext(maya_filepath) - source = maya_filepath.replace(maya_extension, ".xgen") - xgen_file = os.path.join( - project_path, - "{basename}__{namespace}__{name}.xgen".format( - basename=os.path.splitext(os.path.basename(current_file()))[0], - namespace=namespace, - name=name - ) - ).replace("\\", "/") - self.log.info("Copying {} to {}".format(source, xgen_file)) - shutil.copy(source, xgen_file) - - # Modify xgDataPath and xgProjectPath to have current workspace first - # and published version directory second. This ensure that any newly - # created xgen files are created in the current workspace. - resources_path = os.path.join(os.path.dirname(source), "resources") + def write_xgen_file(self, data, xgen_file): lines = [] with open(xgen_file, "r") as f: for line in [line.rstrip() for line in f]: - if line.startswith("\txgDataPath"): - data_path = line.split("\t")[-1] - line = "\txgDataPath\t\t{}{}{}".format( - data_path, - os.pathsep, - data_path.replace( - "${PROJECT}xgen", resources_path.replace("\\", "/") - ) - ) - - if line.startswith("\txgProjectPath"): - line = "\txgProjectPath\t\t{}/".format( - project_path.replace("\\", "/") - ) + for key, value in data.items(): + if line.startswith("\t{}".format(key)): + line = "\t{}\t\t{}".format(key, value) lines.append(line) with open(xgen_file, "w") as f: f.write("\n".join(lines)) - xgd_file = xgen_file.replace(".xgen", ".xgd") - - return xgen_file, xgd_file - def process_reference(self, context, name, namespace, options): # Validate workfile has a path. if current_file() is None: @@ -89,11 +51,6 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self.fname, context["project"]["name"] ) - name = context["representation"]["data"]["xgenName"] - xgen_file, xgd_file = self.setup_xgen_palette_file( - maya_filepath, namespace, name - ) - # Reference xgen. Xgen does not like being referenced in under a group. new_nodes = [] @@ -106,9 +63,32 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): returnNewNodes=True ) - xgen_palette = cmds.ls(nodes, type="xgmPalette", long=True)[0] + xgen_palette = cmds.ls( + nodes, type="xgmPalette", long=True + )[0].replace("|", "") + + _, maya_extension = os.path.splitext(current_file()) + xgen_file = current_file().replace( + maya_extension, + "__{}.xgen".format(xgen_palette.replace(":", "__")) + ) + xgd_file = xgen_file.replace(".xgen", ".xgd") self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) + # Change the cache and disk values of xgDataPath and xgProjectPath + # to ensure paths are setup correctly. + project_path = os.path.dirname(current_file()).replace("\\", "/") + xgenm.setAttr("xgProjectPath", project_path, xgen_palette) + data_path = "${{PROJECT}}xgen/collections/{}{}{}".format( + xgen_palette.replace(":", "__ns__"), + os.pathsep, + xgenm.getAttr("xgDataPath", xgen_palette) + ) + xgenm.setAttr("xgDataPath", data_path, xgen_palette) + + data = {"xgProjectPath": project_path, "xgDataPath": data_path} + self.write_xgen_file(data, xgen_file) + # This create an expression attribute of float. If we did not add # any changes to collection, then Xgen does not create an xgd file # on save. This gives errors when launching the workfile again due From a5ca4f93b8cdf4102828d5fd91d563e808081440 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 Jan 2023 15:21:29 +0100 Subject: [PATCH 063/158] cancel recursivity removal --- .../workfile/workfile_template_builder.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index d262d6a771..fd5ac16579 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -521,6 +521,11 @@ class AbstractTemplateBuilder(object): if not level_limit: level_limit = 1000 + placeholder_by_scene_id = { + placeholder.scene_identifier: placeholder + for placeholder in placeholders + } + all_processed = len(placeholders) == 0 # Counter is checked at the ned of a loop so the loop happens at least # once. @@ -561,14 +566,24 @@ class AbstractTemplateBuilder(object): placeholder.set_finished() - # Clear shared data before getting new placeholders - self.clear_shared_populate_data() + # self.clear_shared_populate_data() iter_counter += 1 if iter_counter >= level_limit: break all_processed = True + + collected_placeholders = self.get_placeholders() + for placeholder in collected_placeholders: + identifier = placeholder.scene_identifier + if identifier in placeholder_by_scene_id: + continue + + all_processed = False + placeholder_by_scene_id[identifier] = placeholder + placeholders.append(placeholder) + self.refresh() def _get_build_profiles(self): @@ -988,7 +1003,7 @@ class PlaceholderItem(object): return self._log def __repr__(self): - return "< {} {} >".format(self.__class__.__name__, self.name) + return "< {} {} >".format(self.__class__.__name__, self.data['family']) @property def order(self): From a2176420b7ed584d10b7bff37d729a0924000b39 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 17:20:39 +0000 Subject: [PATCH 064/158] Working extraction --- .../hosts/maya/plugins/publish/extract_xgen.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index fd85cadcac..9549aba76d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -6,7 +6,9 @@ from maya import cmds import xgenm from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import maintained_selection, attribute_values +from openpype.hosts.maya.api.lib import ( + maintained_selection, attribute_values, write_xgen_file +) from openpype.lib import StringTemplate @@ -77,6 +79,18 @@ class ExtractXgenCache(publish.Extractor): xgen_path = os.path.join(staging_dir, xgen_filename).replace("\\", "/") xgenm.exportPalette(palette, xgen_path) + data = { + "xgDataPath": os.path.join( + instance.data["resourcesDir"], + "collections", + palette.replace(":", "__ns__") + ).replace("\\", "/"), + "xgProjectPath": os.path.dirname( + instance.data["resourcesDir"] + ).replace("\\", "/") + } + write_xgen_file(data, xgen_path) + representation = { "name": "xgen", "ext": "xgen", @@ -136,7 +150,7 @@ class ExtractXgenCache(publish.Extractor): destination = os.path.join( instance.data["resourcesDir"], "collections", - os.path.basename(data_path), + palette, source.replace(data_path, "")[1:] ) transfers.append((source, destination.replace("\\", "/"))) From 5a280a32bbec68d6a9284f005f062bf387a2a476 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 17:20:46 +0000 Subject: [PATCH 065/158] Working updating --- openpype/hosts/maya/plugins/load/load_xgen.py | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 0f2e13dd79..35f7c21c58 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -7,10 +7,13 @@ from Qt import QtWidgets import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( - maintained_selection, get_container_members, attribute_values + maintained_selection, + get_container_members, + attribute_values, + write_xgen_file ) from openpype.hosts.maya.api import current_file -from openpype.hosts.maya.api.plugin import get_reference_node +from openpype.pipeline import get_representation_path class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): @@ -23,18 +26,14 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - def write_xgen_file(self, data, xgen_file): - lines = [] - with open(xgen_file, "r") as f: - for line in [line.rstrip() for line in f]: - for key, value in data.items(): - if line.startswith("\t{}".format(key)): - line = "\t{}\t\t{}".format(key, value) - - lines.append(line) - - with open(xgen_file, "w") as f: - f.write("\n".join(lines)) + def get_xgen_xgd_paths(self, palette): + _, maya_extension = os.path.splitext(current_file()) + xgen_file = current_file().replace( + maya_extension, + "__{}.xgen".format(palette.replace("|", "").replace(":", "__")) + ) + xgd_file = xgen_file.replace(".xgen", ".xgd") + return xgen_file, xgd_file def process_reference(self, context, name, namespace, options): # Validate workfile has a path. @@ -67,12 +66,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): nodes, type="xgmPalette", long=True )[0].replace("|", "") - _, maya_extension = os.path.splitext(current_file()) - xgen_file = current_file().replace( - maya_extension, - "__{}.xgen".format(xgen_palette.replace(":", "__")) - ) - xgd_file = xgen_file.replace(".xgen", ".xgd") + xgen_file, xgd_file = self.get_xgen_xgd_paths(xgen_palette) self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) # Change the cache and disk values of xgDataPath and xgProjectPath @@ -87,7 +81,7 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): xgenm.setAttr("xgDataPath", data_path, xgen_palette) data = {"xgProjectPath": project_path, "xgDataPath": data_path} - self.write_xgen_file(data, xgen_file) + write_xgen_file(data, xgen_file) # This create an expression attribute of float. If we did not add # any changes to collection, then Xgen does not create an xgd file @@ -138,21 +132,35 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): container_node = container["objectName"] members = get_container_members(container_node) - xgen_palette = cmds.ls(members, type="xgmPalette", long=True)[0] - reference_node = get_reference_node(members, self.log) - namespace = cmds.referenceQuery(reference_node, namespace=True)[1:] - - xgen_file, xgd_file = self.setup_xgen_palette_file( - get_representation_path(representation), - namespace, - representation["data"]["xgenName"] - ) + xgen_palette = cmds.ls( + members, type="xgmPalette", long=True + )[0].replace("|", "") + xgen_file, xgd_file = self.get_xgen_xgd_paths(xgen_palette) # Export current changes to apply later. xgenm.createDelta(xgen_palette.replace("|", ""), xgd_file) self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) + maya_file = get_representation_path(representation) + _, extension = os.path.splitext(maya_file) + new_xgen_file = maya_file.replace(extension, ".xgen") + data_path = "" + with open(new_xgen_file, "r") as f: + for line in [line.rstrip() for line in f]: + if line.startswith("\txgDataPath"): + data_path = line.split("\t")[-1] + break + + project_path = os.path.dirname(current_file()).replace("\\", "/") + data_path = "${{PROJECT}}xgen/collections/{}{}{}".format( + xgen_palette.replace(":", "__ns__"), + os.pathsep, + data_path + ) + data = {"xgProjectPath": project_path, "xgDataPath": data_path} + write_xgen_file(data, xgen_file) + attribute_data = { "{}.xgFileName".format(xgen_palette): os.path.basename(xgen_file), "{}.xgBaseFile".format(xgen_palette): "", From 22ddb58cca40a320af68688e073cf98bb9e73fbc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 17:20:58 +0000 Subject: [PATCH 066/158] Refactor to lib --- openpype/hosts/maya/api/lib.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index dd5da275e8..65f39270f5 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3446,3 +3446,17 @@ def iter_visible_nodes_in_range(nodes, start, end): def get_attribute_input(attr): connections = cmds.listConnections(attr, plugs=True, destination=False) return connections[0] if connections else None + + +def write_xgen_file(data, filepath): + lines = [] + with open(filepath, "r") as f: + for line in [line.rstrip() for line in f]: + for key, value in data.items(): + if line.startswith("\t{}".format(key)): + line = "\t{}\t\t{}".format(key, value) + + lines.append(line) + + with open(filepath, "w") as f: + f.write("\n".join(lines)) From 2e9f60693b2da0b824ae8ad5283f445673e0c29a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 18:05:52 +0000 Subject: [PATCH 067/158] Fix Edge case of loading xgen while existing xgen in cache. --- openpype/hosts/maya/plugins/load/load_xgen.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 35f7c21c58..d0ed3e05b3 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -87,9 +87,11 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # any changes to collection, then Xgen does not create an xgd file # on save. This gives errors when launching the workfile again due # to trying to find the xgd file. - xgenm.addCustomAttr( - "custom_float_ignore", xgen_palette.replace("|", "") - ) + name = "custom_float_ignore" + if name not in xgenm.customAttrs(xgen_palette): + xgenm.addCustomAttr( + "custom_float_ignore", xgen_palette + ) shapes = cmds.ls(nodes, shapes=True, long=True) From 8ec87cba01a8c0d91c67843d6c575cf647cd18e3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 18:12:53 +0000 Subject: [PATCH 068/158] Fix Resetting Xgen attributes after incremental save. --- .../maya/plugins/publish/reset_xgen_attributes.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py index 0f763613c9..d3408f2c76 100644 --- a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -7,8 +7,8 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): """Reset Xgen attributes.""" label = "Reset Xgen Attributes." - # Offset to run after global integrator. - order = pyblish.api.IntegratorOrder + 1.0 + # Offset to run after workfile increment plugin. + order = pyblish.api.IntegratorOrder + 10.0 families = ["workfile"] def process(self, instance): @@ -19,3 +19,9 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): "Setting \"{}\" on \"{}\"".format(value, node_attr) ) cmds.setAttr(node_attr, value, type="string") + + cmds.setAttr(palette + "." + "xgExportAsDelta", True) + + if instance.data.get("xgenAttributes", {}): + self.log.info("Saving changes.") + cmds.file(save=True) From f1e8803f590e90e0cf38e5601cad97b12a5f97ae Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 30 Jan 2023 18:18:37 +0000 Subject: [PATCH 069/158] Update docs --- website/docs/artist_hosts_maya_xgen.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index fc75959f2b..191826f49c 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -8,7 +8,7 @@ sidebar_label: Xgen ### Settings -Go to project settings > Maya > enable "Open Workfile Post Initialization"; +Go to project settings > `Maya` > enable `Open Workfile Post Initialization`; `project_settings/maya/open_workfile_post_initialization` @@ -27,6 +27,12 @@ Importing XGen Collections... # Error: XGen: Failed to import collection from file P:/PROJECTS/OP01_CG_demo/shots/sh040/work/Lighting/cg_ball_xgenMain_v035__ball_rigMain_01___collection.xgen # ``` +Go to project settings > `Deadline` > `Publish plugins` > `Maya Submit to Deadline` > disable `Use Published scene`; + +`project_settings/deadline/publish/MayaSubmitDeadline/use_published` + +This is due to temporary workaround while fixing rendering with published scenes. + ## Create Create an Xgen instance to publish. This needs to contain only **one Xgen collection**. From a8fcd42b6afedbcc0bb82869d0c703edf09f30e2 Mon Sep 17 00:00:00 2001 From: Libor Batek Date: Tue, 31 Jan 2023 10:59:47 +0100 Subject: [PATCH 070/158] adjusted according comments --- website/docs/artist_getting_started.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 40961dbc77..5ab89e31d8 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -19,19 +19,22 @@ If this is not the case, please contact your administrator to consult on how to If you are working from **home** though, you'll **need to install** it yourself. You should, however, receive the OpenPype installer files from your studio admin, supervisor or production, because OpenPype versions and executables might not be compatible between studios. -Installing OpenPype is possible by Windows installer or by unzipping it anywhere on the disk from downloaded ZIP archive. +Installing OpenPype is possible by Installer or by unzipping downloaded ZIP archive to any drive location. -For more detailed info about installation on different OS please visit [Installation section](artist_install.md). +> For more detailed info about installing OpenPype please visit [Installation section](artist_install.md). -There are two ways running OpenPype +--- -first most common one by using OP icon on the Desktop triggering +You can run OpenPype by desktop "OP" icon (if exists after installing) or by directly executing -**openpype_gui.exe** suitable **for artists**. It runs OpenPype GUI in the OS tray. From there you can run all the available tools. To use any of the features, OpenPype must be running in the tray. +**openpype_gui.exe** located in the OpenPype folder. This executable being suitable **for artists**. -or alternatively by using +or alternatively by + +**openpype_console.exe** which is more suitable for **TDs/Admin** for debugging and error reporting. This one runs with opened console window where all the necessary info will appear during user's work session. + +> By seeing the "OP" icon in the OS tray user can easily tell OpenPype already running. -**openpype_console.exe** located in the OpenPype folder which is suitable for **TDs/Admin** for debugging and error reporting. This one runs with opened console window where all the necessary info will appear during user's work session. ## First Launch From aad5a4f6eb84f6d131dc0c7e9f3f2c66b9772837 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 31 Jan 2023 11:36:07 +0000 Subject: [PATCH 071/158] Update openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py Co-authored-by: Roy Nieterau --- .../plugins/publish/reset_xgen_attributes.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py index d3408f2c76..08f367f2d5 100644 --- a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -12,16 +12,18 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): families = ["workfile"] def process(self, instance): - for palette, data in instance.data.get("xgenAttributes", {}).items(): + xgen_attributes = instance.data.get("xgenAttributes", {}) + if not xgen_attributes : + return + + for palette, data in xgen_attributes.items(): for attr, value in data.items(): node_attr = "{}.{}".format(palette, attr) self.log.info( "Setting \"{}\" on \"{}\"".format(value, node_attr) ) cmds.setAttr(node_attr, value, type="string") - - cmds.setAttr(palette + "." + "xgExportAsDelta", True) - - if instance.data.get("xgenAttributes", {}): - self.log.info("Saving changes.") - cmds.file(save=True) + cmds.setAttr(palette + ".xgExportAsDelta", True) + + self.log.info("Saving changes.") + cmds.file(save=True) From 768b3d8ac9d4ab72a8d776b115232df079802dda Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 31 Jan 2023 11:38:43 +0000 Subject: [PATCH 072/158] Improve docs --- .../maya/plugins/publish/reset_xgen_attributes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py index 08f367f2d5..b90885663c 100644 --- a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -4,7 +4,11 @@ import pyblish.api class ResetXgenAttributes(pyblish.api.InstancePlugin): - """Reset Xgen attributes.""" + """Reset Xgen attributes. + + When the incremental save of the workfile triggers, the Xgen attributes + changes so this plugin will change it back to the values before publishing. + """ label = "Reset Xgen Attributes." # Offset to run after workfile increment plugin. @@ -13,7 +17,7 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): def process(self, instance): xgen_attributes = instance.data.get("xgenAttributes", {}) - if not xgen_attributes : + if not xgen_attributes: return for palette, data in xgen_attributes.items(): @@ -24,6 +28,9 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): ) cmds.setAttr(node_attr, value, type="string") cmds.setAttr(palette + ".xgExportAsDelta", True) - + + # Need to save the scene, cause the attribute changes above does not + # mark the scene as modified so user can exit without commiting the + # changes. self.log.info("Saving changes.") cmds.file(save=True) From 658ad2a0c2f34bdc14b4ac586ca88b08db4f8b57 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 31 Jan 2023 11:40:55 +0000 Subject: [PATCH 073/158] Update openpype/hosts/maya/plugins/load/load_xgen.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/load/load_xgen.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index d0ed3e05b3..81a525fe61 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -149,8 +149,9 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): new_xgen_file = maya_file.replace(extension, ".xgen") data_path = "" with open(new_xgen_file, "r") as f: - for line in [line.rstrip() for line in f]: + for line in f: if line.startswith("\txgDataPath"): + line = line.rstrip() data_path = line.split("\t")[-1] break From 67e438dcb90e9050c1d03a7de52489e678eebdf0 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 31 Jan 2023 11:56:55 +0000 Subject: [PATCH 074/158] Update openpype/hosts/maya/plugins/inventory/connect_geometry.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/inventory/connect_geometry.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/connect_geometry.py b/openpype/hosts/maya/plugins/inventory/connect_geometry.py index 9fe4f9e195..bcfc577104 100644 --- a/openpype/hosts/maya/plugins/inventory/connect_geometry.py +++ b/openpype/hosts/maya/plugins/inventory/connect_geometry.py @@ -89,9 +89,7 @@ class ConnectGeometry(InventoryAction): return # Setup live worldspace blendshape connection. - for match in matches: - source = match[0] - target = match[1] + for source, target in matches: blendshape = cmds.blendShape(source, target)[0] cmds.setAttr(blendshape + ".origin", 0) cmds.setAttr(blendshape + "." + target.split(":")[-1], 1) From ab0e3fab01f150cc963579e921c5f9547b276060 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 31 Jan 2023 17:47:12 +0000 Subject: [PATCH 075/158] BigRoy feedback --- openpype/hosts/maya/api/lib.py | 28 +++- openpype/hosts/maya/api/plugin.py | 30 ---- .../plugins/inventory/connect_geometry.py | 32 +++- openpype/hosts/maya/plugins/load/load_xgen.py | 31 ++++ .../maya/plugins/publish/collect_xgen.py | 32 ++++ .../plugins/publish/extract_workfile_xgen.py | 97 +++++++---- .../maya/plugins/publish/extract_xgen.py | 158 ++++++++---------- .../maya/plugins/publish/validate_xgen.py | 18 +- 8 files changed, 255 insertions(+), 171 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 65f39270f5..e9956762f2 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -5,6 +5,7 @@ import sys import platform import uuid import math +import re import json import logging @@ -3449,14 +3450,31 @@ def get_attribute_input(attr): def write_xgen_file(data, filepath): + """Overwrites data in .xgen files. + + Quite naive approach to mainly overwrite "xgDataPath" and "xgProjectPath". + + Args: + data (dict): Dictionary of key, value. Key matches with xgen file. + For example: + {"xgDataPath": "some/path"} + filepath (string): Absolute path of .xgen file. + """ + # Generate regex lookup for line to key basically + # match any of the keys in `\t{key}\t\t` + keys = "|".join(re.escape(key) for key in data.keys()) + re_keys = re.compile("^\t({})\t\t".format(keys)) + lines = [] with open(filepath, "r") as f: - for line in [line.rstrip() for line in f]: - for key, value in data.items(): - if line.startswith("\t{}".format(key)): - line = "\t{}\t\t{}".format(key, value) + for line in f: + match = re_keys.match(line) + if match: + key = match.group(1) + value = data[key] + line = "\t{}\t\t{}\n".format(key, value) lines.append(line) with open(filepath, "w") as f: - f.write("\n".join(lines)) + f.writelines(lines) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index b7adf6edfc..82df85a8be 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -300,36 +300,6 @@ class ReferenceLoader(Loader): str(representation["_id"]), type="string") - # Update any xgen containers. - compound_name = "xgenContainers" - if cmds.objExists("{}.{}".format(node, compound_name)): - import xgenm - container_amount = cmds.getAttr( - "{}.{}".format(node, compound_name), size=True - ) - # loop through all compound children - for i in range(container_amount): - attr = "{}.{}[{}].container".format(node, compound_name, i) - objectset = cmds.listConnections(attr)[0] - reference_node = cmds.sets(objectset, query=True)[0] - palettes = cmds.ls( - cmds.referenceQuery(reference_node, nodes=True), - type="xgmPalette" - ) - for palette in palettes: - for description in xgenm.descriptions(palette): - xgenm.setAttr( - "cacheFileName", - path.replace("\\", "/"), - palette, - description, - "SplinePrimitive" - ) - - # Refresh UI and viewport. - de = xgenm.xgGlobal.DescriptionEditor - de.refresh("Full") - def remove(self, container): """Remove an existing `container` from Maya scene diff --git a/openpype/hosts/maya/plugins/inventory/connect_geometry.py b/openpype/hosts/maya/plugins/inventory/connect_geometry.py index bcfc577104..a12487cf7e 100644 --- a/openpype/hosts/maya/plugins/inventory/connect_geometry.py +++ b/openpype/hosts/maya/plugins/inventory/connect_geometry.py @@ -1,6 +1,7 @@ from maya import cmds from openpype.pipeline import InventoryAction, get_representation_context +from openpype.hosts.maya.api.lib import get_id class ConnectGeometry(InventoryAction): @@ -64,12 +65,12 @@ class ConnectGeometry(InventoryAction): source_data = self.get_container_data(source_object) matches = [] - node_types = [] + node_types = set() for target_container in target_containers: target_data = self.get_container_data( target_container["objectName"] ) - node_types.extend(target_data["node_types"]) + node_types.update(target_data["node_types"]) for id, transform in target_data["ids"].items(): source_match = source_data["ids"].get(id) if source_match: @@ -99,15 +100,30 @@ class ConnectGeometry(InventoryAction): cmds.xgmPreview() def get_container_data(self, container): - data = {"node_types": [], "ids": {}} + """Collects data about the container nodes. + + Args: + container (dict): Container instance. + + Returns: + data (dict): + "node_types": All node types in container nodes. + "ids": If the node is a mesh, we collect its parent transform + id. + """ + data = {"node_types": set(), "ids": {}} ref_node = cmds.sets(container, query=True, nodesOnly=True)[0] for node in cmds.referenceQuery(ref_node, nodes=True): node_type = cmds.nodeType(node) - data["node_types"].append(node_type) - if node_type == "mesh": - transform = cmds.listRelatives(node, parent=True)[0] - id = cmds.getAttr(transform + ".cbId") - data["ids"][id] = transform + data["node_types"].add(node_type) + + # Only interested in mesh transforms for connecting geometry with + # blendshape. + if node_type != "mesh": + continue + + transform = cmds.listRelatives(node, parent=True)[0] + data["ids"][get_id(transform)] = transform return data diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 81a525fe61..5110d4ca05 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -173,3 +173,34 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): super().update(container, representation) xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) + + # Update any xgen containers. + compound_name = "xgenContainers" + import xgenm + container_amount = cmds.getAttr( + "{}.{}".format(container_node, compound_name), size=True + ) + # loop through all compound children + for i in range(container_amount): + attr = "{}.{}[{}].container".format( + container_node, compound_name, i + ) + objectset = cmds.listConnections(attr)[0] + reference_node = cmds.sets(objectset, query=True)[0] + palettes = cmds.ls( + cmds.referenceQuery(reference_node, nodes=True), + type="xgmPalette" + ) + for palette in palettes: + for description in xgenm.descriptions(palette): + xgenm.setAttr( + "cacheFileName", + maya_file.replace("\\", "/"), + palette, + description, + "SplinePrimitive" + ) + + # Refresh UI and viewport. + de = xgenm.xgGlobal.DescriptionEditor + de.refresh("Full") diff --git a/openpype/hosts/maya/plugins/publish/collect_xgen.py b/openpype/hosts/maya/plugins/publish/collect_xgen.py index 5a48b1d221..da0549b2d8 100644 --- a/openpype/hosts/maya/plugins/publish/collect_xgen.py +++ b/openpype/hosts/maya/plugins/publish/collect_xgen.py @@ -1,3 +1,5 @@ +import os + from maya import cmds import pyblish.api @@ -35,5 +37,35 @@ class CollectXgen(pyblish.api.InstancePlugin): input = get_attribute_input("{}.{}".format(node, attr)) data["xgenConnections"][node][attr] = input + # Collect all files under palette root as resources. + import xgenm + + data_path = xgenm.getAttr( + "xgDataPath", data["xgmPalette"].replace("|", "") + ).split(os.pathsep)[0] + data_path = data_path.replace( + "${PROJECT}", + xgenm.getAttr("xgProjectPath", data["xgmPalette"].replace("|", "")) + ) + transfers = [] + + # Since we are duplicating this palette when extracting we predict that + # the name will be the basename without namespaces. + predicted_palette_name = data["xgmPalette"].split(":")[-1] + predicted_palette_name = predicted_palette_name.replace("|", "") + + for root, _, files in os.walk(data_path): + for file in files: + source = os.path.join(root, file).replace("\\", "/") + destination = os.path.join( + instance.data["resourcesDir"], + "collections", + predicted_palette_name, + source.replace(data_path, "")[1:] + ) + transfers.append((source, destination.replace("\\", "/"))) + + data["transfers"] = transfers + self.log.info(data) instance.data.update(data) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index 5847563e5b..fe28427ae7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -5,14 +5,16 @@ import copy from maya import cmds import pyblish.api -from openpype.hosts.maya.api import current_file from openpype.hosts.maya.api.lib import extract_alembic from openpype.pipeline import publish from openpype.lib import StringTemplate class ExtractWorkfileXgen(publish.Extractor): - """Extract Workfile Xgen.""" + """Extract Workfile Xgen. + + When submitting a render, we need to prep Xgen side car files. + """ # Offset to run before workfile scene save. order = pyblish.api.ExtractorOrder - 0.499 @@ -20,13 +22,65 @@ class ExtractWorkfileXgen(publish.Extractor): families = ["workfile"] hosts = ["maya"] + def get_render_max_frame_range(self, context): + """Return start to end frame range including all renderlayers in + context. + + This will return the full frame range which includes all frames of the + renderlayer instances to be published/submitted. + + Args: + context (pyblish.api.Context): Current publishing context. + + Returns: + tuple or None: Start frame, end frame tuple if any renderlayers + found. Otherwise None is returned. + + """ + + def _is_active_renderlayer(i): + """Return whether instance is active renderlayer""" + if not i.data.get("publish", True): + return False + + is_renderlayer = ( + "renderlayer" in i.data.get("families", []) or + i.data["family"] == "renderlayer" + ) + return is_renderlayer + + start_frame = None + end_frame = None + for instance in context: + if not _is_active_renderlayer(instance): + # Only consider renderlyare instances + continue + + render_start_frame = instance.data["frameStart"] + render_end_frame = instance.data["frameStart"] + + if start_frame is None: + start_frame = render_start_frame + else: + start_frame = min(start_frame, render_start_frame) + + if end_frame is None: + end_frame = render_end_frame + else: + end_frame = max(end_frame, render_end_frame) + + if start_frame is None or end_frame is None: + return + + return start_frame, end_frame + def process(self, instance): transfers = [] # Validate there is any palettes in the scene. if not cmds.ls(type="xgmPalette"): self.log.debug( - "No collections found in the scene. Abort Xgen extraction." + "No collections found in the scene. Skipping Xgen extraction." ) return else: @@ -34,44 +88,24 @@ class ExtractWorkfileXgen(publish.Extractor): # Validate to extract only when we are publishing a renderlayer as # well. - renderlayer = False - start_frame = None - end_frame = None - for i in instance.context: - is_renderlayer = ( - "renderlayer" in i.data.get("families", []) or - i.data["family"] == "renderlayer" - ) - if is_renderlayer and i.data["publish"]: - renderlayer = True - - if start_frame is None: - start_frame = i.data["frameStart"] - if end_frame is None: - end_frame = i.data["frameEnd"] - - if i.data["frameStart"] < start_frame: - start_frame = i.data["frameStart"] - if i.data["frameEnd"] > end_frame: - end_frame = i.data["frameEnd"] - - break - - if not renderlayer: + render_range = self.get_render_max_frame_range(instance.context) + if not render_range: self.log.debug( - "No publishable renderlayers found in context. Abort Xgen" + "No publishable renderlayers found in context. Skipping Xgen" " extraction." ) return + start_frame, end_frame = render_range + # We decrement start frame and increment end frame so motion blur will # render correctly. start_frame -= 1 end_frame += 1 # Extract patches alembic. - basename, _ = os.path.splitext(current_file()) - dirname = os.path.dirname(current_file()) + basename, _ = os.path.splitext(instance.context.data["currentFile"]) + dirname = os.path.dirname(instance.context.data["currentFile"]) kwargs = {"attrPrefix": ["xgen"], "stripNamespaces": True} alembic_files = [] for palette in cmds.ls(type="xgmPalette"): @@ -121,8 +155,7 @@ class ExtractWorkfileXgen(publish.Extractor): # Collect Xgen and Delta files. xgen_files = [] sources = [] - file_path = current_file() - current_dir = os.path.dirname(file_path) + current_dir = os.path.dirname(instance.context.data["currentFile"]) attrs = ["xgFileName", "xgBaseFile"] for palette in cmds.ls(type="xgmPalette"): for attr in attrs: diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 9549aba76d..7428fab53f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -7,7 +7,7 @@ import xgenm from openpype.pipeline import publish from openpype.hosts.maya.api.lib import ( - maintained_selection, attribute_values, write_xgen_file + maintained_selection, attribute_values, write_xgen_file, delete_after ) from openpype.lib import StringTemplate @@ -36,48 +36,81 @@ class ExtractXgenCache(publish.Extractor): maya_filename = "{}.{}".format(instance.data["name"], self.scene_type) maya_filepath = os.path.join(staging_dir, maya_filename) - # Collect nodes to export. - duplicate_nodes = [] - for node, connections in instance.data["xgenConnections"].items(): - transform_name = connections["transform"].split(".")[0] - - # Duplicate_transform subd patch geometry. - duplicate_transform = cmds.duplicate(transform_name)[0] - - # Discard the children. - shapes = cmds.listRelatives(duplicate_transform, shapes=True) - children = cmds.listRelatives(duplicate_transform, children=True) - cmds.delete(set(children) - set(shapes)) - - duplicate_transform = cmds.parent( - duplicate_transform, world=True - )[0] - - duplicate_nodes.append(duplicate_transform) - - # Export temp xgen palette files. - temp_xgen_path = os.path.join( - tempfile.gettempdir(), "temp.xgen" - ).replace("\\", "/") - xgenm.exportPalette( - instance.data["xgmPalette"].replace("|", ""), temp_xgen_path - ) - self.log.info("Extracted to {}".format(temp_xgen_path)) - - # Import xgen onto the duplicate. - with maintained_selection(): - cmds.select(duplicate_nodes) - palette = xgenm.importPalette(temp_xgen_path, []) - # Get published xgen file name. template_data = copy.deepcopy(instance.data["anatomyData"]) template_data.update({"ext": "xgen"}) templates = instance.context.data["anatomy"].templates["publish"] xgen_filename = StringTemplate(templates["file"]).format(template_data) - # Export duplicated palette. - xgen_path = os.path.join(staging_dir, xgen_filename).replace("\\", "/") - xgenm.exportPalette(palette, xgen_path) + xgen_path = os.path.join( + self.staging_dir(instance), xgen_filename + ).replace("\\", "/") + type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" + + # Duplicate xgen setup. + with delete_after() as delete_bin: + duplicate_nodes = [] + # Collect nodes to export. + for node, connections in instance.data["xgenConnections"].items(): + transform_name = connections["transform"].split(".")[0] + + # Duplicate_transform subd patch geometry. + duplicate_transform = cmds.duplicate(transform_name)[0] + delete_bin.append(duplicate_transform) + + # Discard the children. + shapes = cmds.listRelatives(duplicate_transform, shapes=True) + children = cmds.listRelatives( + duplicate_transform, children=True + ) + cmds.delete(set(children) - set(shapes)) + + duplicate_transform = cmds.parent( + duplicate_transform, world=True + )[0] + + duplicate_nodes.append(duplicate_transform) + + # Export temp xgen palette files. + temp_xgen_path = os.path.join( + tempfile.gettempdir(), "temp.xgen" + ).replace("\\", "/") + xgenm.exportPalette( + instance.data["xgmPalette"].replace("|", ""), temp_xgen_path + ) + self.log.info("Extracted to {}".format(temp_xgen_path)) + + # Import xgen onto the duplicate. + with maintained_selection(): + cmds.select(duplicate_nodes) + palette = xgenm.importPalette(temp_xgen_path, []) + + delete_bin.append(palette) + + # Export duplicated palettes. + xgenm.exportPalette(palette, xgen_path) + + # Export Maya file. + attribute_data = {"{}.xgFileName".format(palette): xgen_filename} + with attribute_values(attribute_data): + with maintained_selection(): + cmds.select(duplicate_nodes + [palette]) + cmds.file( + maya_filepath, + force=True, + type=type, + exportSelected=True, + preserveReferences=False, + constructionHistory=True, + shader=True, + constraints=True, + expressions=True + ) + + self.log.info("Extracted to {}".format(maya_filepath)) + + if os.path.exists(temp_xgen_path): + os.remove(temp_xgen_path) data = { "xgDataPath": os.path.join( @@ -91,6 +124,7 @@ class ExtractXgenCache(publish.Extractor): } write_xgen_file(data, xgen_path) + # Adding representations. representation = { "name": "xgen", "ext": "xgen", @@ -99,28 +133,6 @@ class ExtractXgenCache(publish.Extractor): } instance.data["representations"].append(representation) - # Export Maya file. - type = "mayaAscii" if self.scene_type == "ma" else "mayaBinary" - attribute_data = { - "{}.xgFileName".format(palette): xgen_filename - } - with attribute_values(attribute_data): - with maintained_selection(): - cmds.select(duplicate_nodes + [palette]) - cmds.file( - maya_filepath, - force=True, - type=type, - exportSelected=True, - preserveReferences=False, - constructionHistory=True, - shader=True, - constraints=True, - expressions=True - ) - - self.log.info("Extracted to {}".format(maya_filepath)) - representation = { "name": self.scene_type, "ext": self.scene_type, @@ -128,31 +140,3 @@ class ExtractXgenCache(publish.Extractor): "stagingDir": staging_dir } instance.data["representations"].append(representation) - - # Clean up. - cmds.delete(duplicate_nodes + [palette]) - os.remove(temp_xgen_path) - - # Collect all files under palette root as resources. - data_path = xgenm.getAttr( - "xgDataPath", instance.data["xgmPalette"].replace("|", "") - ).split(os.pathsep)[0] - data_path = data_path.replace( - "${PROJECT}", - xgenm.getAttr( - "xgProjectPath", instance.data["xgmPalette"].replace("|", "") - ) - ) - transfers = [] - for root, _, files in os.walk(data_path): - for file in files: - source = os.path.join(root, file).replace("\\", "/") - destination = os.path.join( - instance.data["resourcesDir"], - "collections", - palette, - source.replace(data_path, "")[1:] - ) - transfers.append((source, destination.replace("\\", "/"))) - - instance.data["transfers"] = transfers diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 19cf612848..2870909974 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -4,7 +4,7 @@ import maya.cmds as cmds import xgenm import pyblish.api -from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish import PublishValidationError class ValidateXgen(pyblish.api.InstancePlugin): @@ -20,7 +20,7 @@ class ValidateXgen(pyblish.api.InstancePlugin): # Only 1 collection/node per instance. if len(set_members) != 1: - raise KnownPublishError( + raise PublishValidationError( "Only one collection per instance is allowed." " Found:\n{}".format(set_members) ) @@ -28,7 +28,7 @@ class ValidateXgen(pyblish.api.InstancePlugin): # Only xgen palette node is allowed. node_type = cmds.nodeType(set_members[0]) if node_type != "xgmPalette": - raise KnownPublishError( + raise PublishValidationError( "Only node of type \"xgmPalette\" are allowed. Referred to as" " \"collection\" in the Maya UI." " Node type found: {}".format(node_type) @@ -50,10 +50,10 @@ class ValidateXgen(pyblish.api.InstancePlugin): except KeyError: inactive_modifiers[description] = [name] - msg = ( - "There are inactive modifiers on the collection. " - "Please delete these:\n{}".format( - json.dumps(inactive_modifiers, indent=4, sort_keys=True) + if inactive_modifiers: + raise PublishValidationError( + "There are inactive modifiers on the collection. " + "Please delete these:\n{}".format( + json.dumps(inactive_modifiers, indent=4, sort_keys=True) + ) ) - ) - assert not inactive_modifiers, msg From caf26b8ab671ee90977a78fdf9485381b1ae1f68 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 31 Jan 2023 17:49:10 +0000 Subject: [PATCH 076/158] Hound --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 7428fab53f..0719be3a1e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -51,7 +51,7 @@ class ExtractXgenCache(publish.Extractor): with delete_after() as delete_bin: duplicate_nodes = [] # Collect nodes to export. - for node, connections in instance.data["xgenConnections"].items(): + for _, connections in instance.data["xgenConnections"].items(): transform_name = connections["transform"].split(".")[0] # Duplicate_transform subd patch geometry. From 8f83673f7f8dd93c063072437c8c32d8278677df Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:12:02 +0100 Subject: [PATCH 077/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 5ab89e31d8..3db4421181 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -21,7 +21,9 @@ admin, supervisor or production, because OpenPype versions and executables might Installing OpenPype is possible by Installer or by unzipping downloaded ZIP archive to any drive location. -> For more detailed info about installing OpenPype please visit [Installation section](artist_install.md). +:::tip Using the OpenPype Installer +See the [Installation section](artist_install.md) for more information on how to use the OpenPype Installer +::: --- From 7ee305a17329892a28025d65fb906fd376a4d3a3 Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:12:15 +0100 Subject: [PATCH 078/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 3db4421181..b9a75786cd 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -35,7 +35,9 @@ or alternatively by **openpype_console.exe** which is more suitable for **TDs/Admin** for debugging and error reporting. This one runs with opened console window where all the necessary info will appear during user's work session. -> By seeing the "OP" icon in the OS tray user can easily tell OpenPype already running. +:::tip Is OpenPype running? +OpenPype runs in the operating system's tray. If you see the OpenPype icon in the tray you can easily tell OpenPype is currently running. +::: From 4193ab3a9a8bc07f3cd21a03b434fcff5bf9f0dd Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:12:41 +0100 Subject: [PATCH 079/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index b9a75786cd..ef7e682c94 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -44,7 +44,7 @@ OpenPype runs in the operating system's tray. If you see the OpenPype icon in th ## First Launch -When you first start OpenPype, you will be asked to fill in some basic informations. +When you first start OpenPype, you will be asked to fill in some basic information. ### MongoDB From 51ad73c7b3f8b3376693fd946cefb38d716ad180 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 09:12:47 +0000 Subject: [PATCH 080/158] BigRoy feedback --- openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py | 4 ++-- website/docs/artist_hosts_maya_xgen.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index fe28427ae7..49d724960f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -83,8 +83,8 @@ class ExtractWorkfileXgen(publish.Extractor): "No collections found in the scene. Skipping Xgen extraction." ) return - else: - import xgenm + + import xgenm # Validate to extract only when we are publishing a renderlayer as # well. diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index 191826f49c..ec5f2ed921 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -4,6 +4,8 @@ title: Xgen for Maya sidebar_label: Xgen --- +OpenPype supports Xgen classic with the follow workflow. It eases the otherwise cumbersome issues around Xgen's side car files and hidden behaviour inside Maya. The workflow supports publishing, loading and updating of Xgen collections, along with connecting animation from geometry and (guide) curves. + ## Setup ### Settings From 56c7c00bf8ecefbb7330f41f4b50bc3145a9474d Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:12:53 +0100 Subject: [PATCH 081/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index ef7e682c94..0ad98961d5 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -48,8 +48,8 @@ When you first start OpenPype, you will be asked to fill in some basic informati ### MongoDB -In most cases that will only be your studio MongoDB Address. -It's a URL that you should have received from your Studio admin and most often will look like this +In most cases you will only have to supply the MongoDB Address. +It's the database URL you should have received from your Studio admin and often will look like this `mongodb://username:passwword@mongo.mystudiodomain.com:12345` From 1ae99a2d2fae6274115aaeb0cd170587938972cb Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:14:00 +0100 Subject: [PATCH 082/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 0ad98961d5..72a218f77e 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -62,7 +62,7 @@ asks for it, just put it in the corresponding text field and press `install` but ### OpenPype Version Repository -Sometimes your Studio might also ask you to fill in the path to it's version +Sometimes your Studio might also ask you to fill in the path to its version repository. This is a location where OpenPype will be looking for when checking if it's up to date and where updates are installed from automatically. From fae61816c60e2c2b351749ae240f279b00cce538 Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:14:16 +0100 Subject: [PATCH 083/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 72a218f77e..1b32c6a985 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -27,7 +27,7 @@ See the [Installation section](artist_install.md) for more information on how to --- -You can run OpenPype by desktop "OP" icon (if exists after installing) or by directly executing +You can run OpenPype by desktop "OP" icon (if it exists after installing) or by directly executing **openpype_gui.exe** located in the OpenPype folder. This executable being suitable **for artists**. From 022e369d311dc09ca4404e7862f57e6b13323c4f Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:15:36 +0100 Subject: [PATCH 084/158] Update website/docs/artist_getting_started.md Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 1b32c6a985..8350333610 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -63,7 +63,7 @@ asks for it, just put it in the corresponding text field and press `install` but ### OpenPype Version Repository Sometimes your Studio might also ask you to fill in the path to its version -repository. This is a location where OpenPype will be looking for when checking +repository. This is a location where OpenPype will search for the latest versions, check if it's up to date and where updates are installed from automatically. This path is usually taken from the database directly, so you shouldn't need it. From 8351ce0fba1e09689f9e41339e90463a4fde419a Mon Sep 17 00:00:00 2001 From: Libor Batek <112623825+LiborBatek@users.noreply.github.com> Date: Wed, 1 Feb 2023 10:17:04 +0100 Subject: [PATCH 085/158] grammar fixes Co-authored-by: Roy Nieterau --- website/docs/artist_getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 8350333610..14951e599d 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -71,7 +71,7 @@ This path is usually taken from the database directly, so you shouldn't need it. ## Updates -If you're connected to your Studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start, it will go through a quick update installation process, even though you might have just installed it. +If you're connected to your Studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start it can go through a quick update installation process, even though you might have just installed it. ## Advanced Usage From 981d454802dded37955a2c8c573c24173a5d5957 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 11:57:12 +0000 Subject: [PATCH 086/158] Remove old xgen cache plugin --- .../maya/plugins/publish/extract_xgen.py | 2 +- .../plugins/publish/extract_xgen_cache.py | 64 ------------------- 2 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/extract_xgen_cache.py diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 0719be3a1e..0cc842b4ec 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -12,7 +12,7 @@ from openpype.hosts.maya.api.lib import ( from openpype.lib import StringTemplate -class ExtractXgenCache(publish.Extractor): +class ExtractXgen(publish.Extractor): """Extract Xgen Workflow: diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py b/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py deleted file mode 100644 index 77350f343e..0000000000 --- a/openpype/hosts/maya/plugins/publish/extract_xgen_cache.py +++ /dev/null @@ -1,64 +0,0 @@ -import os - -from maya import cmds - -from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import ( - suspended_refresh, - maintained_selection -) - - -class ExtractXgenCache(publish.Extractor): - """Produce an alembic of just xgen interactive groom - - """ - - label = "Extract Xgen ABC Cache" - hosts = ["maya"] - families = ["xgen"] - optional = True - - def process(self, instance): - - # Collect the out set nodes - out_descriptions = [node for node in instance - if cmds.nodeType(node) == "xgmSplineDescription"] - - start = 1 - end = 1 - - self.log.info("Extracting Xgen Cache..") - dirname = self.staging_dir(instance) - - parent_dir = self.staging_dir(instance) - filename = "{name}.abc".format(**instance.data) - path = os.path.join(parent_dir, filename) - - with suspended_refresh(): - with maintained_selection(): - command = ( - '-file ' - + path - + ' -df "ogawa" -fr ' - + str(start) - + ' ' - + str(end) - + ' -step 1 -mxf -wfw' - ) - for desc in out_descriptions: - command += (" -obj " + desc) - cmds.xgmSplineCache(export=True, j=command) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, - "stagingDir": dirname, - } - instance.data["representations"].append(representation) - - self.log.info("Extracted {} to {}".format(instance, dirname)) From c71fc217da17da5e93b7129240ffc6e418d7cd12 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 15:05:15 +0000 Subject: [PATCH 087/158] BigRoy feedback --- .../maya/plugins/publish/extract_workfile_xgen.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index 49d724960f..b95add3306 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -104,7 +104,7 @@ class ExtractWorkfileXgen(publish.Extractor): end_frame += 1 # Extract patches alembic. - basename, _ = os.path.splitext(instance.context.data["currentFile"]) + path_no_ext, _ = os.path.splitext(instance.context.data["currentFile"]) dirname = os.path.dirname(instance.context.data["currentFile"]) kwargs = {"attrPrefix": ["xgen"], "stripNamespaces": True} alembic_files = [] @@ -116,7 +116,9 @@ class ExtractWorkfileXgen(publish.Extractor): alembic_file = os.path.join( dirname, - "{}__{}.abc".format(basename, palette.replace(":", "__ns__")) + "{}__{}.abc".format( + path_no_ext, palette.replace(":", "__ns__") + ) ) extract_alembic( alembic_file, @@ -138,7 +140,9 @@ class ExtractWorkfileXgen(publish.Extractor): for source in alembic_files: destination = os.path.join( os.path.dirname(instance.data["resourcesDir"]), - os.path.basename(source.replace(basename, published_basename)) + os.path.basename( + source.replace(path_no_ext, published_basename) + ) ) transfers.append((source, destination)) From 7a7102337bfe73ab7d7e8e805e681a7ec743fd40 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 15:19:38 +0000 Subject: [PATCH 088/158] BigRoy feedback --- .../hosts/maya/plugins/publish/extract_workfile_xgen.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index b95add3306..c8d0d63344 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -105,7 +105,6 @@ class ExtractWorkfileXgen(publish.Extractor): # Extract patches alembic. path_no_ext, _ = os.path.splitext(instance.context.data["currentFile"]) - dirname = os.path.dirname(instance.context.data["currentFile"]) kwargs = {"attrPrefix": ["xgen"], "stripNamespaces": True} alembic_files = [] for palette in cmds.ls(type="xgmPalette"): @@ -114,11 +113,8 @@ class ExtractWorkfileXgen(publish.Extractor): for name in xgenm.boundGeometry(palette, description): patch_names.append(name) - alembic_file = os.path.join( - dirname, - "{}__{}.abc".format( - path_no_ext, palette.replace(":", "__ns__") - ) + alembic_file = "{}__{}.abc".format( + path_no_ext, palette.replace(":", "__ns__") ) extract_alembic( alembic_file, From dc656050b7184c965774e289c5827c735533ab5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 1 Feb 2023 18:27:44 +0100 Subject: [PATCH 089/158] remove api.pi in openpype root --- openpype/api.py | 112 ------------------------------------------------ 1 file changed, 112 deletions(-) delete mode 100644 openpype/api.py diff --git a/openpype/api.py b/openpype/api.py deleted file mode 100644 index b60cd21d2b..0000000000 --- a/openpype/api.py +++ /dev/null @@ -1,112 +0,0 @@ -from .settings import ( - get_system_settings, - get_project_settings, - get_current_project_settings, - get_anatomy_settings, - - SystemSettings, - ProjectSettings -) -from .lib import ( - PypeLogger, - Logger, - Anatomy, - execute, - run_subprocess, - version_up, - get_asset, - get_workdir_data, - get_version_from_path, - get_last_version_from_path, - get_app_environments_for_context, - source_hash, - get_latest_version, - get_local_site_id, - change_openpype_mongo_url, - create_project_folders, - get_project_basic_paths -) - -from .lib.mongo import ( - get_default_components -) - -from .lib.applications import ( - ApplicationManager -) - -from .lib.avalon_context import ( - BuildWorkfile -) - -from . import resources - -from .plugin import ( - Extractor, - - ValidatePipelineOrder, - ValidateContentsOrder, - ValidateSceneOrder, - ValidateMeshOrder, -) - -# temporary fix, might -from .action import ( - get_errored_instances_from_context, - RepairAction, - RepairContextAction -) - - -__all__ = [ - "get_system_settings", - "get_project_settings", - "get_current_project_settings", - "get_anatomy_settings", - "get_project_basic_paths", - - "SystemSettings", - "ProjectSettings", - - "PypeLogger", - "Logger", - "Anatomy", - "execute", - "get_default_components", - "ApplicationManager", - "BuildWorkfile", - - # Resources - "resources", - - # plugin classes - "Extractor", - # ordering - "ValidatePipelineOrder", - "ValidateContentsOrder", - "ValidateSceneOrder", - "ValidateMeshOrder", - # action - "get_errored_instances_from_context", - "RepairAction", - "RepairContextAction", - - # get contextual data - "version_up", - "get_asset", - "get_workdir_data", - "get_version_from_path", - "get_last_version_from_path", - "get_app_environments_for_context", - "source_hash", - - "run_subprocess", - "get_latest_version", - - "get_local_site_id", - "change_openpype_mongo_url", - - "get_project_basic_paths", - "create_project_folders" - -] From 14ecf3aec18b2f89d6e0a53119d7e4adff3d5c84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 1 Feb 2023 18:33:32 +0100 Subject: [PATCH 090/158] remove last usage of openpype.api --- openpype/hosts/nuke/plugins/publish/collect_context_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_context_data.py b/openpype/hosts/nuke/plugins/publish/collect_context_data.py index 5a1cdcf49e..b487c946f0 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_context_data.py +++ b/openpype/hosts/nuke/plugins/publish/collect_context_data.py @@ -1,7 +1,7 @@ import os import nuke import pyblish.api -import openpype.api as api +from openpype.lib import get_version_from_path import openpype.hosts.nuke.api as napi from openpype.pipeline import KnownPublishError @@ -57,7 +57,7 @@ class CollectContextData(pyblish.api.ContextPlugin): "fps": root_node['fps'].value(), "currentFile": current_file, - "version": int(api.get_version_from_path(current_file)), + "version": int(get_version_from_path(current_file)), "host": pyblish.api.current_host(), "hostVersion": nuke.NUKE_VERSION_STRING From e0f18363cd2017fecab4dae2eceb5f1029c15f21 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 20:57:47 +0000 Subject: [PATCH 091/158] Fix file paths. --- openpype/hosts/maya/plugins/load/load_xgen.py | 6 ++---- .../hosts/maya/plugins/publish/extract_workfile_xgen.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 5110d4ca05..fc86596208 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -73,9 +73,8 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # to ensure paths are setup correctly. project_path = os.path.dirname(current_file()).replace("\\", "/") xgenm.setAttr("xgProjectPath", project_path, xgen_palette) - data_path = "${{PROJECT}}xgen/collections/{}{}{}".format( + data_path = "${{PROJECT}}xgen/collections/{};{}".format( xgen_palette.replace(":", "__ns__"), - os.pathsep, xgenm.getAttr("xgDataPath", xgen_palette) ) xgenm.setAttr("xgDataPath", data_path, xgen_palette) @@ -156,9 +155,8 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): break project_path = os.path.dirname(current_file()).replace("\\", "/") - data_path = "${{PROJECT}}xgen/collections/{}{}{}".format( + data_path = "${{PROJECT}}xgen/collections/{};{}".format( xgen_palette.replace(":", "__ns__"), - os.pathsep, data_path ) data = {"xgProjectPath": project_path, "xgDataPath": data_path} diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index c8d0d63344..20e1bd37d8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -208,7 +208,7 @@ class ExtractWorkfileXgen(publish.Extractor): project_path = xgenm.getAttr("xgProjectPath", palette) data_path = xgenm.getAttr("xgDataPath", palette) data_path = data_path.replace("${PROJECT}", project_path) - for path in data_path.split(os.pathsep): + for path in data_path.split(";"): for root, _, files in os.walk(path): for f in files: source = os.path.join(root, f) From 926f23d39275ece8fbdd04b97541cf874e9b22cd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 20:57:53 +0000 Subject: [PATCH 092/158] Fix updating --- openpype/hosts/maya/api/plugin.py | 33 +++++++++++++++++++ openpype/hosts/maya/plugins/load/load_xgen.py | 31 ----------------- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 82df85a8be..916fddd923 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -300,6 +300,39 @@ class ReferenceLoader(Loader): str(representation["_id"]), type="string") + # When an animation or pointcache gets connected to an Xgen container, + # the compound attribute "xgenContainers" gets created. When animation + # containers gets updated we also need to update the cacheFileName on + # the Xgen collection. + compound_name = "xgenContainers" + if cmds.objExists("{}.{}".format(node, compound_name)): + import xgenm + container_amount = cmds.getAttr( + "{}.{}".format(node, compound_name), size=True + ) + # loop through all compound children + for i in range(container_amount): + attr = "{}.{}[{}].container".format(node, compound_name, i) + objectset = cmds.listConnections(attr)[0] + reference_node = cmds.sets(objectset, query=True)[0] + palettes = cmds.ls( + cmds.referenceQuery(reference_node, nodes=True), + type="xgmPalette" + ) + for palette in palettes: + for description in xgenm.descriptions(palette): + xgenm.setAttr( + "cacheFileName", + path.replace("\\", "/"), + palette, + description, + "SplinePrimitive" + ) + + # Refresh UI and viewport. + de = xgenm.xgGlobal.DescriptionEditor + de.refresh("Full") + def remove(self, container): """Remove an existing `container` from Maya scene diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index fc86596208..1600cd49bd 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -171,34 +171,3 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): super().update(container, representation) xgenm.applyDelta(xgen_palette.replace("|", ""), xgd_file) - - # Update any xgen containers. - compound_name = "xgenContainers" - import xgenm - container_amount = cmds.getAttr( - "{}.{}".format(container_node, compound_name), size=True - ) - # loop through all compound children - for i in range(container_amount): - attr = "{}.{}[{}].container".format( - container_node, compound_name, i - ) - objectset = cmds.listConnections(attr)[0] - reference_node = cmds.sets(objectset, query=True)[0] - palettes = cmds.ls( - cmds.referenceQuery(reference_node, nodes=True), - type="xgmPalette" - ) - for palette in palettes: - for description in xgenm.descriptions(palette): - xgenm.setAttr( - "cacheFileName", - maya_file.replace("\\", "/"), - palette, - description, - "SplinePrimitive" - ) - - # Refresh UI and viewport. - de = xgenm.xgGlobal.DescriptionEditor - de.refresh("Full") From 7217ba6b770d03c77736ba7e72cb2e04ff3b8dff Mon Sep 17 00:00:00 2001 From: Libor Batek Date: Thu, 2 Feb 2023 09:06:01 +0100 Subject: [PATCH 093/158] added pic for systray icon --- website/docs/artist_getting_started.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/website/docs/artist_getting_started.md b/website/docs/artist_getting_started.md index 14951e599d..301a58fa56 100644 --- a/website/docs/artist_getting_started.md +++ b/website/docs/artist_getting_started.md @@ -17,7 +17,7 @@ If this is not the case, please contact your administrator to consult on how to ## Working from home If you are working from **home** though, you'll **need to install** it yourself. You should, however, receive the OpenPype installer files from your studio -admin, supervisor or production, because OpenPype versions and executables might not be compatible between studios. +admin, supervisor or production, because OpenPype versions and executables might not be compatible between studios. Installing OpenPype is possible by Installer or by unzipping downloaded ZIP archive to any drive location. @@ -25,7 +25,6 @@ Installing OpenPype is possible by Installer or by unzipping downloaded ZIP arch See the [Installation section](artist_install.md) for more information on how to use the OpenPype Installer ::: ---- You can run OpenPype by desktop "OP" icon (if it exists after installing) or by directly executing @@ -33,12 +32,15 @@ You can run OpenPype by desktop "OP" icon (if it exists after installing) or by or alternatively by -**openpype_console.exe** which is more suitable for **TDs/Admin** for debugging and error reporting. This one runs with opened console window where all the necessary info will appear during user's work session. +**openpype_console.exe** which is more suitable for **TDs/Admin** for debugging and error reporting. This one runs with +opened console window where all the necessary info will appear during user's work session. :::tip Is OpenPype running? -OpenPype runs in the operating system's tray. If you see the OpenPype icon in the tray you can easily tell OpenPype is currently running. +OpenPype runs in the operating system's tray. If you see turquoise OpenPype icon in the tray you can easily tell OpenPype is currently running. +Keep in mind that on Windows this icon might be hidden by default, in which case, the artist can simply drag the icon down to the tray. ::: +![Systray](assets/artist_systray.png) ## First Launch @@ -49,7 +51,7 @@ When you first start OpenPype, you will be asked to fill in some basic informati ### MongoDB In most cases you will only have to supply the MongoDB Address. -It's the database URL you should have received from your Studio admin and often will look like this +It's the database URL you should have received from your Studio admin and often will look like this `mongodb://username:passwword@mongo.mystudiodomain.com:12345` @@ -64,14 +66,14 @@ asks for it, just put it in the corresponding text field and press `install` but Sometimes your Studio might also ask you to fill in the path to its version repository. This is a location where OpenPype will search for the latest versions, check -if it's up to date and where updates are installed from automatically. +if it's up to date and where updates are installed from automatically. -This path is usually taken from the database directly, so you shouldn't need it. +This path is usually taken from the database directly, so you shouldn't need it. ## Updates -If you're connected to your Studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start it can go through a quick update installation process, even though you might have just installed it. +If you're connected to your Studio, OpenPype will check for, and install updates automatically every time you run it. That's why during the first start it can go through a quick update installation process, even though you might have just installed it. ## Advanced Usage From 0dbb63df944d81d7318f0ea503f00cf7cca1f48d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Feb 2023 11:54:55 +0100 Subject: [PATCH 094/158] modified change item a little --- openpype/pipeline/create/context.py | 324 ++++++++++++++++++++++------ 1 file changed, 254 insertions(+), 70 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 59314b9236..52e43f500e 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -183,92 +183,169 @@ def prepare_failed_creator_operation_info( } -class ChangedItem(object): +_EMPTY_VALUE = object() + + +class TrackChangesItem(object): + """Helper object to track changes in data. + + Has access to full old and new data and will create deep copy of them, + so it is not needed to create copy before passed in. + + Can work as a dictionary if old or new value is a dictionary. In + that case received object is another object of 'TrackChangesItem'. + + Goal is to be able to get old or new value as was or only changed values + or get information about removed/changed keys, and all of that on + any "dictionary level". + + ``` + # Example of possible usages + old_value = { + "key_1": "value_1", + "key_2": { + "key_sub_1": 1, + "key_sub_2": { + "enabled": True + } + }, + "key_3": "value_2" + } + + new_value = { + "key_1": "value_1", + "key_2": { + "key_sub_2": { + "enabled": False + }, + "key_sub_3": 3 + }, + "key_3": "value_3" + } + + changes = TrackChangesItem(old_value, new_value) + print(changes.changed) + >>> True + print(changes.changed_keys) + >>> {"key_2", "key_3"} + print(changes["key_2"]["key_sub_2"]["enabled"].changed) + >>> True + print(changes["key_2"].removed_keys) + >>> {"key_sub_1"} + print(changes["key_2"].available_keys) + >>> {"key_sub_1", "key_sub_2", "key_sub_3"} + print(changes.new_value == new_value) + >>> True + + only_changed_new_values = { + key: changes[key].new_value + for key in changes + } + ``` + + Args: + old_value (Any): Previous value. + new_value (Any): New value. + """ + def __init__(self, old_value, new_value): + self._changed = old_value != new_value + if old_value is _EMPTY_VALUE: + old_value = None + if new_value is _EMPTY_VALUE: + new_value = None self._old_value = copy.deepcopy(old_value) self._new_value = copy.deepcopy(new_value) - self._changed = self._old_value != self._new_value - old_is_dict = isinstance(old_value, dict) - new_is_dict = isinstance(new_value, dict) - children = {} - changed_keys = set() - available_keys = set() - new_keys = set() - old_keys = set() - if old_is_dict and new_is_dict: - old_keys = set(old_value.keys()) - new_keys = set(new_value.keys()) - available_keys = old_keys | new_keys - for key in available_keys: - item = ChangedItem( - old_value.get(key), new_value.get(key) - ) - children[key] = item - if item.changed or key not in old_keys or key not in new_keys: - changed_keys.add(key) + self._old_is_dict = isinstance(old_value, dict) + self._new_is_dict = isinstance(new_value, dict) - elif old_is_dict: - old_keys = set(old_value.keys()) - available_keys = set(old_keys) - changed_keys = set(available_keys) - for key in available_keys: - children[key] = ChangedItem(old_value.get(key), None) + self._old_keys = None + self._new_keys = None + self._available_keys = None + self._removed_keys = None - elif new_is_dict: - new_keys = set(new_value.keys()) - available_keys = set(new_keys) - changed_keys = set(available_keys) - for key in available_keys: - children[key] = ChangedItem(None, new_value.get(key)) + self._changed_keys = None - self._changed_keys = changed_keys - self._available_keys = available_keys - self._children = children - self._old_is_dict = old_is_dict - self._new_is_dict = new_is_dict - self._old_keys = old_keys - self._new_keys = new_keys + self._sub_items = None def __getitem__(self, key): - return self._children[key] + """Getter looks into subitems if object is dictionary.""" + + if self._sub_items is None: + self._prepare_sub_items() + return self._sub_items[key] def __bool__(self): + """Boolean of object is if old and new value are the same.""" + return self._changed - def __iter__(self): - for key in self.changed_keys: - yield key - def get(self, key, default=None): - return self._children.get(key, default) + """Try to get sub item.""" - def keys(self): - return self.changed_keys + if self._sub_items is None: + self._prepare_sub_items() + return self._sub_items.get(key, default) - def items(self): - if not self.is_dict: - yield None, self.changes - else: - for item in self.changes.items(): - yield item + @property + def old_value(self): + """Get copy of old value. + + Returns: + Any: Whatever old value was. + """ + + return copy.deepcopy(self._old_value) + + @property + def new_value(self): + """Get copy of new value. + + Returns: + Any: Whatever new value was. + """ + + return copy.deepcopy(self._new_value) @property def changed(self): + """Value changed. + + Returns: + bool: If data changed. + """ + return self._changed @property def is_dict(self): + """Object can be used as dictionary. + + Returns: + bool: When can be used that way. + """ + return self._old_is_dict or self._new_is_dict @property def changes(self): + """Get changes in raw data. + + This method should be used only if 'is_dict' value is 'True'. + + Returns: + Dict[str, Tuple[Any, Any]]: Changes are by key in tuple + (, ). If 'is_dict' is 'False' then + output is always empty dictionary. + """ + + output = {} if not self.is_dict: - return (self.old_value, self.new_value) + return output old_value = self.old_value new_value = self.new_value - output = {} for key in self.changed_keys: _old = None _new = None @@ -279,29 +356,135 @@ class ChangedItem(object): output[key] = (_old, _new) return output - @property - def changed_keys(self): - return set(self._changed_keys) - - @property - def available_keys(self): - return set(self._available_keys) - + # Methods/properties that can be used when 'is_dict' is 'True' @property def old_keys(self): + """Keys from old value. + + Empty set is returned if old value is not a dict. + + Returns: + Set[str]: Keys from old value. + """ + + if self._old_keys is None: + self._prepare_keys() return set(self._old_keys) @property def new_keys(self): + """Keys from new value. + + Empty set is returned if old value is not a dict. + + Returns: + Set[str]: Keys from new value. + """ + + if self._new_keys is None: + self._prepare_keys() return set(self._new_keys) @property - def old_value(self): - return copy.deepcopy(self._old_value) + def changed_keys(self): + """Keys that has changed from old to new value. + + Empty set is returned if both old and new value are not a dict. + + Returns: + Set[str]: Keys of changed keys. + """ + + if self._changed_keys is None: + self._prepare_sub_items() + return set(self._changed_keys) @property - def new_value(self): - return copy.deepcopy(self._new_value) + def available_keys(self): + """All keys that are available in old and new value. + + Empty set is returned if both old and new value are not a dict. + Output it is Union of 'old_keys' and 'new_keys'. + + Returns: + Set[str]: All keys from old and new value. + """ + + if self._available_keys is None: + self._prepare_keys() + return set(self._available_keys) + + @property + def removed_keys(self): + """Key that are not available in new value but were in old value. + + Returns: + Set[str]: All removed keys. + """ + + if self._removed_keys is None: + self._prepare_sub_items() + return set(self._removed_keys) + + def _prepare_keys(self): + old_keys = set() + new_keys = set() + if self._old_is_dict and self._new_is_dict: + old_keys = set(self._old_value.keys()) + new_keys = set(self._new_value.keys()) + + elif self._old_is_dict: + old_keys = set(self._old_value.keys()) + + elif self._new_is_dict: + new_keys = set(self._new_value.keys()) + + self._old_keys = old_keys + self._new_keys = new_keys + self._available_keys = old_keys | new_keys + self._removed_keys = old_keys - new_keys + + def _prepare_sub_items(self): + sub_items = {} + changed_keys = set() + + old_keys = self.old_keys + new_keys = self.new_keys + new_value = self.new_value + old_value = self.old_value + if self._old_is_dict and self._new_is_dict: + for key in self.available_keys: + item = TrackChangesItem( + old_value.get(key), new_value.get(key) + ) + sub_items[key] = item + if item.changed or key not in old_keys or key not in new_keys: + changed_keys.add(key) + + elif self._old_is_dict: + old_keys = set(old_value.keys()) + available_keys = set(old_keys) + changed_keys = set(available_keys) + for key in available_keys: + # NOTE Use '_EMPTY_VALUE' because old value could be 'None' + # which would result in "unchanged" item + sub_items[key] = TrackChangesItem( + old_value.get(key), _EMPTY_VALUE + ) + + elif self._new_is_dict: + new_keys = set(new_value.keys()) + available_keys = set(new_keys) + changed_keys = set(available_keys) + for key in available_keys: + # NOTE Use '_EMPTY_VALUE' because new value could be 'None' + # which would result in "unchanged" item + sub_items[key] = TrackChangesItem( + _EMPTY_VALUE, new_value.get(key) + ) + + self._sub_items = sub_items + self._changed_keys = changed_keys class InstanceMember: @@ -895,7 +1078,7 @@ class CreatedInstance: def changes(self): """Calculate and return changes.""" - return ChangedItem(self.origin_data, self.data_to_store()) + return TrackChangesItem(self._orig_data, self.data_to_store()) def mark_as_stored(self): """Should be called when instance data are stored. @@ -1519,8 +1702,9 @@ class CreateContext: def context_data_changes(self): """Changes of attributes.""" - old_value = copy.deepcopy(self._original_context_data) - return ChangedItem(old_value, self.context_data_to_store()) + return TrackChangesItem( + self._original_context_data, self.context_data_to_store() + ) def creator_adds_instance(self, instance): """Creator adds new instance to context. From 6ca987fdd259981f05bfcf0e2717a6e91682680e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Feb 2023 11:56:51 +0100 Subject: [PATCH 095/158] modified houdini to use new changes object --- openpype/hosts/houdini/api/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 61127bda57..0b9940a993 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -225,12 +225,12 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): self._add_instance_to_context(created_instance) def update_instances(self, update_list): - for created_inst, _changes in update_list: + for created_inst, changes in update_list: instance_node = hou.node(created_inst.get("instance_node")) new_values = { - key: new_value - for key, (_old_value, new_value) in _changes.items() + key: changes[key].new_value + for key in changes } imprint( instance_node, From 0b65168688be847861d89b4e12547608f30c627c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Feb 2023 12:17:07 +0100 Subject: [PATCH 096/158] fix other places using changes --- openpype/hosts/aftereffects/plugins/create/create_render.py | 4 ++-- openpype/hosts/max/api/plugin.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 8d38288257..5427edb44b 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -47,10 +47,10 @@ class RenderCreator(Creator): for created_inst, _changes in update_list: api.get_stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) - subset_change = _changes.get("subset") + subset_change = _changes["subset"] if subset_change: api.get_stub().rename_item(created_inst.data["members"][0], - subset_change[1]) + subset_change.new_value) def remove_instances(self, instances): for instance in instances: diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 4788bfd383..55603b26a0 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -78,12 +78,12 @@ class MaxCreator(Creator, MaxCreatorBase): self._add_instance_to_context(created_instance) def update_instances(self, update_list): - for created_inst, _changes in update_list: + for created_inst, changes in update_list: instance_node = created_inst.get("instance_node") new_values = { - key: new_value - for key, (_old_value, new_value) in _changes.items() + key: changes[key].new_value + for key in changes } imprint( instance_node, From b83f0ac79948a26f1b757c0b965e4c138c271a11 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Feb 2023 12:46:49 +0100 Subject: [PATCH 097/158] restore file as it was originally --- openpype/pipeline/workfile/workfile_template_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index fd5ac16579..42d8cc82e1 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -525,7 +525,6 @@ class AbstractTemplateBuilder(object): placeholder.scene_identifier: placeholder for placeholder in placeholders } - all_processed = len(placeholders) == 0 # Counter is checked at the ned of a loop so the loop happens at least # once. @@ -566,7 +565,8 @@ class AbstractTemplateBuilder(object): placeholder.set_finished() - # self.clear_shared_populate_data() + # Clear shared data before getting new placeholders + self.clear_shared_populate_data() iter_counter += 1 if iter_counter >= level_limit: @@ -1003,7 +1003,7 @@ class PlaceholderItem(object): return self._log def __repr__(self): - return "< {} {} >".format(self.__class__.__name__, self.data['family']) + return "< {} {} >".format(self.__class__.__name__, self.name) @property def order(self): From 77b55cca517705108e8d8ef7e0c0a577131309c3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Feb 2023 12:46:49 +0100 Subject: [PATCH 098/158] restore file as it was originally --- openpype/pipeline/workfile/workfile_template_builder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index fd5ac16579..582657c735 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -525,7 +525,6 @@ class AbstractTemplateBuilder(object): placeholder.scene_identifier: placeholder for placeholder in placeholders } - all_processed = len(placeholders) == 0 # Counter is checked at the ned of a loop so the loop happens at least # once. @@ -566,14 +565,14 @@ class AbstractTemplateBuilder(object): placeholder.set_finished() - # self.clear_shared_populate_data() + # Clear shared data before getting new placeholders + self.clear_shared_populate_data() iter_counter += 1 if iter_counter >= level_limit: break all_processed = True - collected_placeholders = self.get_placeholders() for placeholder in collected_placeholders: identifier = placeholder.scene_identifier @@ -1003,7 +1002,7 @@ class PlaceholderItem(object): return self._log def __repr__(self): - return "< {} {} >".format(self.__class__.__name__, self.data['family']) + return "< {} {} >".format(self.__class__.__name__, self.name) @property def order(self): From f7fd0a53041cfa9c5a789ededd6b94dc56ba8d72 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 13:01:24 +0100 Subject: [PATCH 099/158] OP-4513 - fix for DL on MacOS This works if DL Openpype plugin Installation Directories is set to level of app bundle (eg. '/Applications/OpenPype 3.15.0.app') --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 984590ddba..33d548d204 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -157,7 +157,7 @@ def get_openpype_version_from_path(path, build=True): # fix path for application bundle on macos if platform.system().lower() == "darwin": - path = os.path.join(path, "Contents", "MacOS", "lib", "Python") + path = os.path.join(path, "MacOS") version_file = os.path.join(path, "openpype", "version.py") if not os.path.isfile(version_file): From 638375c9db7caca607375ed8b3f29eba76e5349d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Feb 2023 13:47:15 +0100 Subject: [PATCH 100/158] add missing attribute usage --- openpype/hosts/houdini/api/plugin.py | 2 +- openpype/hosts/max/api/plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 0b9940a993..f0985973a6 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -230,7 +230,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): new_values = { key: changes[key].new_value - for key in changes + for key in changes.changed_keys } imprint( instance_node, diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 55603b26a0..c16d9e61ec 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -83,7 +83,7 @@ class MaxCreator(Creator, MaxCreatorBase): new_values = { key: changes[key].new_value - for key in changes + for key in changes.changed_keys } imprint( instance_node, From a9e139998ca49b7dc6135858333923f223a92c3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Feb 2023 15:00:56 +0100 Subject: [PATCH 101/158] modified code example to also contain tests --- openpype/pipeline/create/context.py | 76 ++++++++++++++++------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 52e43f500e..f46b4eccdb 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -201,45 +201,53 @@ class TrackChangesItem(object): ``` # Example of possible usages - old_value = { - "key_1": "value_1", - "key_2": { - "key_sub_1": 1, - "key_sub_2": { - "enabled": True - } - }, - "key_3": "value_2" - } + >>> old_value = { + ... "key_1": "value_1", + ... "key_2": { + ... "key_sub_1": 1, + ... "key_sub_2": { + ... "enabled": True + ... } + ... }, + ... "key_3": "value_2" + ... } + >>> new_value = { + ... "key_1": "value_1", + ... "key_2": { + ... "key_sub_2": { + ... "enabled": False + ... }, + ... "key_sub_3": 3 + ... }, + ... "key_3": "value_3" + ... } - new_value = { - "key_1": "value_1", - "key_2": { - "key_sub_2": { - "enabled": False - }, - "key_sub_3": 3 - }, - "key_3": "value_3" - } + >>> changes = TrackChangesItem(old_value, new_value) + >>> changes.changed + True - changes = TrackChangesItem(old_value, new_value) - print(changes.changed) - >>> True - print(changes.changed_keys) - >>> {"key_2", "key_3"} - print(changes["key_2"]["key_sub_2"]["enabled"].changed) - >>> True - print(changes["key_2"].removed_keys) - >>> {"key_sub_1"} - print(changes["key_2"].available_keys) - >>> {"key_sub_1", "key_sub_2", "key_sub_3"} - print(changes.new_value == new_value) - >>> True + >>> changes["key_2"]["key_sub_1"].new_value is None + True + >>> list(sorted(changes.changed_keys)) + ['key_2', 'key_3'] + + >>> changes["key_2"]["key_sub_2"]["enabled"].changed + True + + >>> changes["key_2"].removed_keys + {'key_sub_1'} + + >>> list(sorted(changes["key_2"].available_keys)) + ['key_sub_1', 'key_sub_2', 'key_sub_3'] + + >>> changes.new_value == new_value + True + + # Get only changed values only_changed_new_values = { key: changes[key].new_value - for key in changes + for key in changes.changed_keys } ``` From 4a963e9aac1dc3ce8ada72e006f81fb5a4bf3f21 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 16:10:35 +0100 Subject: [PATCH 102/158] OP-4513 - fix valid logic 0 is false, None check is safer --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 33d548d204..a5e48361c3 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -35,7 +35,7 @@ class OpenPypeVersion: self.prerelease = prerelease is_valid = True - if not major or not minor or not patch: + if major is None or minor is None or patch is None: is_valid = False self.is_valid = is_valid From d1a8744e5b1e72cf5323b58e2489e323967de8cb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 16:11:06 +0100 Subject: [PATCH 103/158] OP-4513 - better logging --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index a5e48361c3..82865ed714 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -218,8 +218,8 @@ def get_requested_openpype_executable( requested_version_obj = OpenPypeVersion.from_string(requested_version) if not requested_version_obj: print(( - ">>> Requested version does not match version regex \"{}\"" - ).format(VERSION_REGEX)) + ">>> Requested version '{}' does not match version regex '{}'" + ).format(requested_version, VERSION_REGEX)) return None print(( From 63912a0772b7ef6c00e134f3bdaaba8508bcd444 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 17:18:10 +0100 Subject: [PATCH 104/158] OP-4513 - fix selection of openpype_console on Mac --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 82865ed714..38eb163306 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -272,7 +272,8 @@ def get_requested_openpype_executable( # Deadline decide. exe_list = [ os.path.join(version_dir, "openpype_console.exe"), - os.path.join(version_dir, "openpype_console") + os.path.join(version_dir, "openpype_console"), + os.path.join(version_dir, "MacOS", "openpype_console") ] return FileUtils.SearchFileList(";".join(exe_list)) From 707c165f4f252afe4693ba71a0ae2dc9a74a62ea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 17:29:37 +0100 Subject: [PATCH 105/158] OP-4513 - fix copy and paste artifact on MacOS clipboarch escapes whitespace with \ on MacOS, so for safety clean it here. Hopefully nobody starts folder name with space on Windows. --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 38eb163306..108c418e7b 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -189,6 +189,12 @@ def get_openpype_executable(): exe_list = config.GetConfigEntryWithDefault("OpenPypeExecutable", "") dir_list = config.GetConfigEntryWithDefault( "OpenPypeInstallationDirs", "") + + # clean '\ ' for MacOS pasting + if exe_list: + exe_list = exe_list.replace("\\ ", " ") + if dir_list: + dir_list = dir_list.replace("\\ ", " ") return exe_list, dir_list From 27773fbbf16fa8c9fa48678f1ec438d5f65f3b8d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 18:23:51 +0100 Subject: [PATCH 106/158] OP-4822 - added profile to disble check to Settings --- .../defaults/project_settings/deadline.json | 3 +- .../schema_project_deadline.json | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index ceb0b2e39a..11bb1aee2e 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -33,7 +33,8 @@ "limit": [], "jobInfo": {}, "pluginInfo": {}, - "scene_patches": [] + "scene_patches": [], + "disable_strict_check_profiles": [] }, "NukeSubmitDeadline": { "enabled": true, 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 08a505bd47..d38358773a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -195,6 +195,36 @@ ] } + }, + { + "type": "list", + "collapsible": true, + "key": "disable_strict_check_profiles", + "label": "Disable Strict Error Check profiles", + "use_label_wrap": true, + "docstring": "Set profile for disabling 'Strict Error Checking'", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + } + ] + } } ] }, From bd77b30c5c3fc3f21d8732d9309ec6c8f5d776f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 18:25:24 +0100 Subject: [PATCH 107/158] OP-4822 - added possibility to disable strict checking DL by default has Strict error checking, but some errors are not fatal. This allows to set profile based on Task and Subset values to temporarily disable Strict Error Checks --- .../plugins/publish/submit_maya_deadline.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 070d4eab18..a59738979c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -35,6 +35,7 @@ from openpype.pipeline import legacy_io from openpype.hosts.maya.api.lib_rendersettings import RenderSettings from openpype.hosts.maya.api.lib import get_attr_in_layer +from openpype.lib.profiles_filtering import filter_profiles from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.tests.lib import is_in_tests @@ -64,6 +65,7 @@ class MayaPluginInfo(object): # Include all lights flag RenderSetupIncludeLights = attr.ib( default="1", validator=_validate_deadline_bool_value) + StrictErrorChecking = attr.ib(default="1") @attr.s @@ -104,6 +106,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): jobInfo = {} pluginInfo = {} group = "none" + disable_strict_check_profiles = [] def get_job_info(self): job_info = DeadlineJobInfo(Plugin="MayaBatch") @@ -219,6 +222,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "renderSetupIncludeLights", default_rs_include_lights) if rs_include_lights not in {"1", "0", True, False}: rs_include_lights = default_rs_include_lights + strict_checking = self._get_strict_checking(instance) plugin_info = MayaPluginInfo( SceneFile=self.scene_path, Version=cmds.about(version=True), @@ -227,6 +231,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): RenderSetupIncludeLights=rs_include_lights, # noqa ProjectPath=context.data["workspaceDir"], UsingRenderLayers=True, + StrictErrorChecking=strict_checking ) plugin_payload = attr.asdict(plugin_info) @@ -748,6 +753,32 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): for file in exp: yield file + def _get_strict_checking(self, plugin_info, instance): + """Find profile to disable Strict Error Checking + + Args: + instance (dict) + Returns: + (int) - 1 if Strict (DL default), 0 for disabled Strict + """ + strict_checking = 1 + if not self.disable_strict_check_profiles: + return strict_checking + + task_data = instance.data["anatomyData"].get("task", {}) + key_values = { + "task_names": task_data.get("name"), + "task_types": task_data.get("type"), + "subsets": instance.data["subset"] + } + profile = filter_profiles(self.profiles, key_values, + logger=self.log) + + if profile: + strict_checking = 0 + + return strict_checking + def _format_tiles( filename, index, tiles_x, tiles_y, From f29c46f7c1cea1b0a9f3cf5b889394ab8bf03afd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 18:44:00 +0100 Subject: [PATCH 108/158] OP-4822 - fix arguments --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a59738979c..a2a80c319b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -753,7 +753,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): for file in exp: yield file - def _get_strict_checking(self, plugin_info, instance): + def _get_strict_checking(self, instance): """Find profile to disable Strict Error Checking Args: From 8a2cf2e6d93d1d9ed0887a84315e13608e859060 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 18:44:24 +0100 Subject: [PATCH 109/158] OP-4822 - fix wrong variable --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a2a80c319b..c353e0b109 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -771,7 +771,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "task_types": task_data.get("type"), "subsets": instance.data["subset"] } - profile = filter_profiles(self.profiles, key_values, + profile = filter_profiles(self.disable_strict_check_profiles, + key_values, logger=self.log) if profile: From 5df388912abba15c5a477aa47f06807cce0cf8fb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 2 Feb 2023 18:44:43 +0100 Subject: [PATCH 110/158] OP-4822 - added logging --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index c353e0b109..3d1b9bcbf6 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -776,6 +776,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): logger=self.log) if profile: + self.log.debug("Disabling Strict Error Checking") strict_checking = 0 return strict_checking From 6a3d981c0fa7d081bee4fe89fd8a0df77c456bdc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 2 Feb 2023 20:45:12 +0000 Subject: [PATCH 111/158] Fix updating VrayProxy --- openpype/hosts/maya/plugins/load/load_vrayproxy.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_vrayproxy.py b/openpype/hosts/maya/plugins/load/load_vrayproxy.py index 720a132aa7..64184f9e7b 100644 --- a/openpype/hosts/maya/plugins/load/load_vrayproxy.py +++ b/openpype/hosts/maya/plugins/load/load_vrayproxy.py @@ -81,10 +81,11 @@ class VRayProxyLoader(load.LoaderPlugin): c = colors.get(family) if c is not None: cmds.setAttr("{0}.useOutlinerColor".format(group_node), 1) - cmds.setAttr("{0}.outlinerColor".format(group_node), - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + cmds.setAttr( + "{0}.outlinerColor".format(group_node), + (float(c[0]) / 255), + (float(c[1]) / 255), + (float(c[2]) / 255) ) return containerise( @@ -101,7 +102,7 @@ class VRayProxyLoader(load.LoaderPlugin): assert cmds.objExists(node), "Missing container" members = cmds.sets(node, query=True) or [] - vraymeshes = cmds.ls(members, type="VRayMesh") + vraymeshes = cmds.ls(members, type="VRayProxy") assert vraymeshes, "Cannot find VRayMesh in container" # get all representations for this version From c0439cae1aec3e7d60a9eb07267ac42b61309edc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 2 Feb 2023 20:48:43 +0000 Subject: [PATCH 112/158] Validate vray plugin is loaded. Otherwise you can publish VrayProxy without needing to enable it and fail at extraction. --- .../maya/plugins/publish/validate_vray.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_vray.py diff --git a/openpype/hosts/maya/plugins/publish/validate_vray.py b/openpype/hosts/maya/plugins/publish/validate_vray.py new file mode 100644 index 0000000000..045ac258a1 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_vray.py @@ -0,0 +1,18 @@ +from maya import cmds + +import pyblish.api +from openpype.pipeline import PublishValidationError + + +class ValidateVray(pyblish.api.InstancePlugin): + """Validate general Vray setup.""" + + order = pyblish.api.ValidatorOrder + label = 'VRay' + hosts = ["maya"] + families = ["vrayproxy"] + + def process(self, instance): + # Validate vray plugin is loaded. + if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): + raise PublishValidationError("Vray plugin is not loaded.") From 2c410e3d5435d661861acab1ea4f93c7de3387aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Feb 2023 12:08:08 +0100 Subject: [PATCH 113/158] gracefully skip if label is not set --- openpype/hosts/houdini/api/shelves.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 3ccab964cd..3a6534d0eb 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -152,9 +152,13 @@ def get_or_create_tool(tool_definition, shelf): Returns: hou.Tool: The tool updated or the new one """ - existing_tools = shelf.tools() - tool_label = tool_definition.get('label') + tool_label = tool_definition.get("label") + if not tool_label: + log.warning("Skipped shelf without label") + return + + existing_tools = shelf.tools() existing_tool = next( (tool for tool in existing_tools if tool.label() == tool_label), None From 2c17a9bb4e866c3e85127acba40ade4f34eafdf6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Feb 2023 12:08:20 +0100 Subject: [PATCH 114/158] fix mandatory keys --- openpype/hosts/houdini/api/shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 3a6534d0eb..efbc0f5b04 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -66,7 +66,7 @@ def generate_shelves(): ) continue - mandatory_attributes = {'name', 'script'} + mandatory_attributes = {'label', 'script'} for tool_definition in shelf_definition.get('tools_list'): # We verify that the name and script attibutes of the tool # are set From 79e7b0e0d5052d5cb180b847fa20e2f2f04a6e3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Feb 2023 12:08:47 +0100 Subject: [PATCH 115/158] check script path and gracefully skip if was not found or not set --- openpype/hosts/houdini/api/shelves.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index efbc0f5b04..f6295ddfe7 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -158,6 +158,11 @@ def get_or_create_tool(tool_definition, shelf): log.warning("Skipped shelf without label") return + script_path = tool_definition["script"] + if not script_path or not os.path.exists(script_path): + log.warning("This path doesn't exist - {}".format(script_path)) + return + existing_tools = shelf.tools() existing_tool = next( (tool for tool in existing_tools if tool.label() == tool_label), From 4c502ffe85af5d66ea2638d839b4487d0dfb2a42 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Feb 2023 12:14:42 +0100 Subject: [PATCH 116/158] change how shelves are created --- openpype/hosts/houdini/api/shelves.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index f6295ddfe7..ebd668e9e4 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -1,4 +1,5 @@ import os +import re import logging import platform @@ -168,24 +169,16 @@ def get_or_create_tool(tool_definition, shelf): (tool for tool in existing_tools if tool.label() == tool_label), None ) + + with open(script_path) as stream: + script = stream.read() + + tool_definition["script"] = script + if existing_tool: - tool_definition.pop('name', None) - tool_definition.pop('label', None) + tool_definition.pop("label", None) existing_tool.setData(**tool_definition) return existing_tool - tool_name = tool_label.replace(' ', '_').lower() - - if not os.path.exists(tool_definition['script']): - log.warning( - "This path doesn't exist - {}".format(tool_definition['script']) - ) - return - - with open(tool_definition['script']) as f: - script = f.read() - tool_definition.update({'script': script}) - - new_tool = hou.shelves.newTool(name=tool_name, **tool_definition) - - return new_tool + tool_name = re.sub(r"[^\w\d]+", "_", tool_label).lower() + return hou.shelves.newTool(name=tool_name, **tool_definition) From 1ac6d9eadb892ca24a7ab2e5c556ae23970c4da9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 16:39:25 +0100 Subject: [PATCH 117/158] OP-4653 - added explicit 'use_layer_name' toggl Similar toggle is in AE, to keep same approach. This allow to create only single instance with layer name (previously only if multiple selected). --- .../hosts/photoshop/plugins/create/create_image.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index ca3bbfd27c..036a1127c1 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -73,13 +73,16 @@ class ImageCreator(Creator): groups_to_create.append(group) layer_name = '' - creating_multiple_groups = len(groups_to_create) > 1 + # use artist chosen option OR force layer if more subsets are created + # to differentiate them + use_layer_name = (pre_create_data.get("use_layer_name") or + len(groups_to_create) > 1) for group in groups_to_create: subset_name = subset_name_from_ui # reset to name from creator UI layer_names_in_hierarchy = [] created_group_name = self._clean_highlights(stub, group.name) - if creating_multiple_groups: + if use_layer_name: layer_name = re.sub( "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), "", @@ -137,7 +140,10 @@ class ImageCreator(Creator): label="Create only for selected"), BoolDef("create_multiple", default=True, - label="Create separate instance for each selected") + label="Create separate instance for each selected"), + BoolDef("use_layer_name", + default=False, + label="Use layer name in subset") ] return output From f0310e99f1799ec06c38e161ecd620cec6a1cbd3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 16:40:49 +0100 Subject: [PATCH 118/158] OP-4653 - updated description, fixed typos --- .../photoshop/plugins/create/create_image.py | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 036a1127c1..d15d7b2ab5 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -13,7 +13,11 @@ from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances class ImageCreator(Creator): - """Creates image instance for publishing.""" + """Creates image instance for publishing. + + Result of 'image' instance is image of all visible layers, or image(s) of + selected layers. + """ identifier = "image" label = "Image" family = "image" @@ -59,9 +63,10 @@ class ImageCreator(Creator): try: group = stub.group_selected_layers(subset_name_from_ui) except: - raise ValueError("Cannot group locked Bakcground layer!") + raise ValueError("Cannot group locked Background layer!") groups_to_create.append(group) + # create empty group if nothing selected if not groups_to_create and not top_layers_to_wrap: group = stub.create_group(subset_name_from_ui) groups_to_create.append(group) @@ -148,7 +153,34 @@ class ImageCreator(Creator): return output def get_detail_description(self): - return """Creator for Image instances""" + return """Creator for Image instances + + Main publishable item in Photoshop will be of `image` family. Result of + this item (instance) is picture that could be loaded and used + in another DCCs (for example as single layer in composition in + AfterEffects, reference in Maya etc). + + There are couple of options what to publish: + - separate image per selected layer (or group of layers) + - one image for all selected layers + - all visible layers (groups) flattened into single image + + In most cases you would like to keep `Create only for selected` + toggled on and select what you would like to publish. + Toggling this option off will allow you to create instance for all + visible layers without a need to select them explicitly. + + Use 'Create separate instance for each selected' to create separate + images per selected layer (group of layers). + + 'Use layer name in subset' will explicitly add layer name into subset + name. Position of this name is configurable in + `project_settings/global/tools/creator/subset_name_profiles`. + If layer placeholder ({layer}) is not used in `subset_name_profiles` + but layer name should be used (set explicitly in UI or implicitly if + multiple images should be created), it is added in capitalized form + as a suffix to subset name. + """ def _handle_legacy(self, instance_data): """Converts old instances to new format.""" From 53c1c842f573622d3c5704bfc06a2c6d56cdde19 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 17:02:33 +0100 Subject: [PATCH 119/158] OP-4653 - standardize use_composition_name Follow more closely login in PS, eg. if {composition} placeholder not present in subset template and should be used, add capitalized. Clean composition name --- .../aftereffects/plugins/create/create_render.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 8d38288257..4ed9192964 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -11,6 +11,7 @@ from openpype.pipeline import ( ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances from openpype.lib import prepare_template_data +from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class RenderCreator(Creator): @@ -82,10 +83,19 @@ class RenderCreator(Creator): "if 'useSelection' or create at least " "one composition." ) - + use_composition_name = (pre_create_data.get("use_composition_name") or + len(comps) > 1) for comp in comps: - if pre_create_data.get("use_composition_name"): - composition_name = comp.name + if use_composition_name: + if "{composition}" not in subset_name.lower(): + subset_name += "{Composition}" + + composition_name = re.sub( + "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), + "", + comp.name + ) + dynamic_fill = prepare_template_data({"composition": composition_name}) subset_name = subset_name_from_ui.format(**dynamic_fill) From f2930ed156aa1e877fe0478e5539e4dd16aaff62 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 17:04:06 +0100 Subject: [PATCH 120/158] OP-4653 - updated description in AE creator --- .../plugins/create/create_render.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 4ed9192964..25dadbecfd 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -15,6 +15,11 @@ from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS class RenderCreator(Creator): + """Creates 'render' instance for publishing. + + Result of 'render' instance is video or sequence of images for particular + composition based of configuration in its RenderQueue. + """ identifier = "render" label = "Render" family = "render" @@ -140,7 +145,32 @@ class RenderCreator(Creator): return output def get_detail_description(self): - return """Creator for Render instances""" + return """Creator for Render instances + + Main publishable item in AfterEffects will be of `render` family. + Result of this item (instance) is picture sequence or video that could + be a final delivery product or loaded and used in another DCCs. + + Select single composition and create instance of 'render' family or + turn off 'Use selection' to create instance for all compositions. + + 'Use composition name in subset' allows to explicitly add composition + name into created subset name. + + Position of composition name could be set in + `project_settings/global/tools/creator/subset_name_profiles` with some + form of '{composition}' placeholder. + + Composition name will be used implicitly if multiple composition should + be handled at same time. + + If {composition} placeholder is not us 'subset_name_profiles' + composition name will be capitalized and set at the end of subset name + if necessary. + + If composition name should be used, it will be cleaned up of characters + that would cause an issue in published file names. + """ def get_dynamic_data(self, variant, task_name, asset_doc, project_name, host_name, instance): From 993145e6f8bf4b829401a89f428d8821108e54a6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 17:16:55 +0100 Subject: [PATCH 121/158] OP-4653 - fix wrong name --- openpype/hosts/aftereffects/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 25dadbecfd..c95270a3bb 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -92,8 +92,8 @@ class RenderCreator(Creator): len(comps) > 1) for comp in comps: if use_composition_name: - if "{composition}" not in subset_name.lower(): - subset_name += "{Composition}" + if "{composition}" not in subset_name_from_ui.lower(): + subset_name_from_ui += "{Composition}" composition_name = re.sub( "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), From 31b137fa1d6257d97e170f475cd6d11fbdbfa8f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 17:17:27 +0100 Subject: [PATCH 122/158] OP-4653 - Hound --- .../plugins/create/create_render.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index c95270a3bb..a5f8dedace 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -146,28 +146,28 @@ class RenderCreator(Creator): def get_detail_description(self): return """Creator for Render instances - - Main publishable item in AfterEffects will be of `render` family. - Result of this item (instance) is picture sequence or video that could + + Main publishable item in AfterEffects will be of `render` family. + Result of this item (instance) is picture sequence or video that could be a final delivery product or loaded and used in another DCCs. - - Select single composition and create instance of 'render' family or + + Select single composition and create instance of 'render' family or turn off 'Use selection' to create instance for all compositions. - + 'Use composition name in subset' allows to explicitly add composition name into created subset name. - - Position of composition name could be set in + + Position of composition name could be set in `project_settings/global/tools/creator/subset_name_profiles` with some form of '{composition}' placeholder. - + Composition name will be used implicitly if multiple composition should be handled at same time. - - If {composition} placeholder is not us 'subset_name_profiles' + + If {composition} placeholder is not us 'subset_name_profiles' composition name will be capitalized and set at the end of subset name if necessary. - + If composition name should be used, it will be cleaned up of characters that would cause an issue in published file names. """ From 59010eb9a6c3a8abbabdd48dbb6c2a8563d9ac27 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 17:19:02 +0100 Subject: [PATCH 123/158] OP-4653 - refactor - move most important method higher --- .../plugins/create/create_render.py | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index a5f8dedace..10ded8b912 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -34,45 +34,6 @@ class RenderCreator(Creator): ["RenderCreator"] ["defaults"]) - def get_icon(self): - return resources.get_openpype_splash_filepath() - - def collect_instances(self): - for instance_data in cache_and_get_instances(self): - # legacy instances have family=='render' or 'renderLocal', use them - creator_id = (instance_data.get("creator_identifier") or - instance_data.get("family", '').replace("Local", '')) - if creator_id == self.identifier: - instance_data = self._handle_legacy(instance_data) - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) - - def update_instances(self, update_list): - for created_inst, _changes in update_list: - api.get_stub().imprint(created_inst.get("instance_id"), - created_inst.data_to_store()) - subset_change = _changes.get("subset") - if subset_change: - api.get_stub().rename_item(created_inst.data["members"][0], - subset_change[1]) - - def remove_instances(self, instances): - for instance in instances: - self._remove_instance_from_context(instance) - self.host.remove_instance(instance) - - subset = instance.data["subset"] - comp_id = instance.data["members"][0] - comp = api.get_stub().get_item(comp_id) - if comp: - new_comp_name = comp.name.replace(subset, '') - if not new_comp_name: - new_comp_name = "dummyCompName" - api.get_stub().rename_item(comp_id, - new_comp_name) - def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up if pre_create_data.get("use_selection"): @@ -144,6 +105,45 @@ class RenderCreator(Creator): ] return output + def get_icon(self): + return resources.get_openpype_splash_filepath() + + def collect_instances(self): + for instance_data in cache_and_get_instances(self): + # legacy instances have family=='render' or 'renderLocal', use them + creator_id = (instance_data.get("creator_identifier") or + instance_data.get("family", '').replace("Local", '')) + if creator_id == self.identifier: + instance_data = self._handle_legacy(instance_data) + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + api.get_stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) + subset_change = _changes.get("subset") + if subset_change: + api.get_stub().rename_item(created_inst.data["members"][0], + subset_change[1]) + + def remove_instances(self, instances): + for instance in instances: + self._remove_instance_from_context(instance) + self.host.remove_instance(instance) + + subset = instance.data["subset"] + comp_id = instance.data["members"][0] + comp = api.get_stub().get_item(comp_id) + if comp: + new_comp_name = comp.name.replace(subset, '') + if not new_comp_name: + new_comp_name = "dummyCompName" + api.get_stub().rename_item(comp_id, + new_comp_name) + def get_detail_description(self): return """Creator for Render instances From 0dd1a376b0463622dfe6aaab08057ce670524ee0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 17:19:52 +0100 Subject: [PATCH 124/158] OP-4653 - refactor - move most important method higher --- .../photoshop/plugins/create/create_image.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index d15d7b2ab5..8a103ea6c2 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -23,21 +23,6 @@ class ImageCreator(Creator): family = "image" description = "Image creator" - def collect_instances(self): - for instance_data in cache_and_get_instances(self): - # legacy instances have family=='image' - creator_id = (instance_data.get("creator_identifier") or - instance_data.get("family")) - - if creator_id == self.identifier: - instance_data = self._handle_legacy(instance_data) - layer = api.stub().get_layer(instance_data["members"][0]) - instance_data["layer"] = layer - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) - def create(self, subset_name_from_ui, data, pre_create_data): groups_to_create = [] top_layers_to_wrap = [] @@ -120,6 +105,21 @@ class ImageCreator(Creator): stub.rename_layer(group.id, stub.PUBLISH_ICON + created_group_name) + def collect_instances(self): + for instance_data in cache_and_get_instances(self): + # legacy instances have family=='image' + creator_id = (instance_data.get("creator_identifier") or + instance_data.get("family")) + + if creator_id == self.identifier: + instance_data = self._handle_legacy(instance_data) + layer = api.stub().get_layer(instance_data["members"][0]) + instance_data["layer"] = layer + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + def update_instances(self, update_list): self.log.debug("update_list:: {}".format(update_list)) for created_inst, _changes in update_list: From 3a8c36e0570e5554d0d9ae7afd073fdde43294ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 18:28:29 +0100 Subject: [PATCH 125/158] OP-4822 - updated settings After discussion profile is too complicated, removed, replaced with simple boolean --- .../defaults/project_settings/deadline.json | 2 +- .../schema_project_deadline.json | 32 +++---------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 11bb1aee2e..0a4318a659 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -34,7 +34,7 @@ "jobInfo": {}, "pluginInfo": {}, "scene_patches": [], - "disable_strict_check_profiles": [] + "strict_error_checking": true }, "NukeSubmitDeadline": { "enabled": true, 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 d38358773a..03f6489a41 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -197,34 +197,10 @@ } }, { - "type": "list", - "collapsible": true, - "key": "disable_strict_check_profiles", - "label": "Disable Strict Error Check profiles", - "use_label_wrap": true, - "docstring": "Set profile for disabling 'Strict Error Checking'", - "object_type": { - "type": "dict", - "children": [ - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "task_names", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "key": "subsets", - "label": "Subset names", - "type": "list", - "object_type": "text" - } - ] - } + "type": "boolean", + "key": "strict_error_checking", + "label": "Strict Error Checking", + "default": true } ] }, From 214ef5104dfe17619c5f04e466d2bb70a9fbd195 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 18:33:40 +0100 Subject: [PATCH 126/158] OP-4822 - changed strict error checking to boolean flag Added boolean flag to render instance to allow artist to change it. --- .../maya/plugins/create/create_render.py | 4 +++ .../plugins/publish/submit_maya_deadline.py | 35 +++---------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 8375149442..387b7321b9 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -54,6 +54,7 @@ class CreateRender(plugin.Creator): tileRendering (bool): Instance is set to tile rendering mode. We won't submit actual render, but we'll make publish job to wait for Tile Assembly job done and then publish. + strict_error_checking (bool): Enable/disable error checking on DL See Also: https://pype.club/docs/artist_hosts_maya#creating-basic-render-setup @@ -271,6 +272,9 @@ class CreateRender(plugin.Creator): secondary_pool = pool_setting["secondary_pool"] self.data["secondaryPool"] = self._set_default_pool(pool_names, secondary_pool) + strict_error_checking = maya_submit_dl.get("strict_error_checking", + True) + self.data["strict_error_checking"] = strict_error_checking if muster_enabled: self.log.info(">>> Loading Muster credentials ...") diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 3d1b9bcbf6..9fef816e1c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -65,7 +65,7 @@ class MayaPluginInfo(object): # Include all lights flag RenderSetupIncludeLights = attr.ib( default="1", validator=_validate_deadline_bool_value) - StrictErrorChecking = attr.ib(default="1") + StrictErrorChecking = attr.ib(default=True) @attr.s @@ -222,7 +222,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "renderSetupIncludeLights", default_rs_include_lights) if rs_include_lights not in {"1", "0", True, False}: rs_include_lights = default_rs_include_lights - strict_checking = self._get_strict_checking(instance) + strict_error_checking = instance.data.get("strict_error_checking", + True) plugin_info = MayaPluginInfo( SceneFile=self.scene_path, Version=cmds.about(version=True), @@ -231,7 +232,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): RenderSetupIncludeLights=rs_include_lights, # noqa ProjectPath=context.data["workspaceDir"], UsingRenderLayers=True, - StrictErrorChecking=strict_checking + StrictErrorChecking=strict_error_checking ) plugin_payload = attr.asdict(plugin_info) @@ -753,34 +754,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): for file in exp: yield file - def _get_strict_checking(self, instance): - """Find profile to disable Strict Error Checking - - Args: - instance (dict) - Returns: - (int) - 1 if Strict (DL default), 0 for disabled Strict - """ - strict_checking = 1 - if not self.disable_strict_check_profiles: - return strict_checking - - task_data = instance.data["anatomyData"].get("task", {}) - key_values = { - "task_names": task_data.get("name"), - "task_types": task_data.get("type"), - "subsets": instance.data["subset"] - } - profile = filter_profiles(self.disable_strict_check_profiles, - key_values, - logger=self.log) - - if profile: - self.log.debug("Disabling Strict Error Checking") - strict_checking = 0 - - return strict_checking - def _format_tiles( filename, index, tiles_x, tiles_y, From 463f770f2d19dcb1bfb184412d1664a8134b7502 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Feb 2023 18:40:53 +0100 Subject: [PATCH 127/158] OP-4822 - Hound --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 9fef816e1c..e7c1899513 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -35,7 +35,6 @@ from openpype.pipeline import legacy_io from openpype.hosts.maya.api.lib_rendersettings import RenderSettings from openpype.hosts.maya.api.lib import get_attr_in_layer -from openpype.lib.profiles_filtering import filter_profiles from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.tests.lib import is_in_tests From 31173abd4fc75c6caca0be712f50faf06a24ed56 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Feb 2023 13:39:04 +0100 Subject: [PATCH 128/158] OP-4702 - fix dirmap remote_site_dir started to return platform dict --- openpype/host/dirmap.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/host/dirmap.py b/openpype/host/dirmap.py index 88d68f27bf..347c5fbf85 100644 --- a/openpype/host/dirmap.py +++ b/openpype/host/dirmap.py @@ -8,6 +8,7 @@ exists is used. import os from abc import ABCMeta, abstractmethod +import platform import six @@ -187,11 +188,19 @@ class HostDirmap(object): self.log.debug("local overrides {}".format(active_overrides)) self.log.debug("remote overrides {}".format(remote_overrides)) + current_platform = platform.system().lower() for root_name, active_site_dir in active_overrides.items(): remote_site_dir = ( remote_overrides.get(root_name) or sync_settings["sites"][remote_site]["root"][root_name] ) + + if isinstance(remote_site_dir, dict): + remote_site_dir = remote_site_dir.get(current_platform) + + if not remote_site_dir: + continue + if os.path.isdir(active_site_dir): if "destination-path" not in mapping: mapping["destination-path"] = [] From 9110f7055415995637039c2da8ac2c1afe2bf9c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:02:09 +0100 Subject: [PATCH 129/158] use safe access to "subset" in AE --- openpype/hosts/aftereffects/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 5427edb44b..f661b3bfd7 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -47,7 +47,7 @@ class RenderCreator(Creator): for created_inst, _changes in update_list: api.get_stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) - subset_change = _changes["subset"] + subset_change = _changes.get("subset") if subset_change: api.get_stub().rename_item(created_inst.data["members"][0], subset_change.new_value) From 22e4b9862b7aaf1e30b1d1d86fcccff2d2ec6799 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:05:27 +0100 Subject: [PATCH 130/158] added 'order' attribute to creators which is used to process them in specific order --- openpype/pipeline/create/creator_plugins.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 1f92056b23..1aba3f8770 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -107,7 +107,11 @@ class SubsetConvertorPlugin(object): @property def create_context(self): - """Quick access to create context.""" + """Quick access to create context. + + Returns: + CreateContext: Context which initialized the plugin. + """ return self._create_context @@ -157,6 +161,10 @@ class BaseCreator: # Cached group label after first call 'get_group_label' _cached_group_label = None + # Order in which will be plugin executed (collect & update instances) + # less == earlier -> Order '90' will be processed before '100' + order = 100 + # Variable to store logger _log = None From fd31d7815a69b51cfe7bc3ecc0d6a48f13c6e817 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:05:46 +0100 Subject: [PATCH 131/158] added helper methods to get sorted creators --- openpype/pipeline/create/context.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index f260e483d9..04db1df790 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1237,6 +1237,37 @@ class CreateContext: """Access to global publish attributes.""" return self._publish_attributes + def get_sorted_creators(self, identifiers=None): + """Sorted creators by 'order' attribute. + + Returns: + List[BaseCreator]: Sorted creator plugins by 'order' value. + """ + + if identifiers is not None: + identifiers = set(identifiers) + creators = [ + creator + for identifier, creator in self.creators.items() + if identifier in identifiers + ] + else: + creators = self.creators.values() + + return sorted( + creators, key=lambda creator: creator.order + ) + + @property + def sorted_creators(self): + return self.get_sorted_creators() + + @property + def sorted_autocreators(self): + return sorted( + self.autocreators.values(), key=lambda creator: creator.order + ) + @classmethod def get_host_misssing_methods(cls, host): """Collect missing methods from host. From 101bbef42d0532dcca235e145cfdae7dc7d22436 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:07:16 +0100 Subject: [PATCH 132/158] added helper method to remove instance --- openpype/pipeline/create/context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 04db1df790..9d8bb63804 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1630,6 +1630,9 @@ class CreateContext: ) ]) + def _remove_instance(self, instance): + self._instances_by_id.pop(instance.id, None) + def creator_removed_instance(self, instance): """When creator removes instance context should be acknowledged. @@ -1641,7 +1644,7 @@ class CreateContext: from scene metadata. """ - self._instances_by_id.pop(instance.id, None) + self._remove_instance(instance) def add_convertor_item(self, convertor_identifier, label): self.convertor_items_by_id[convertor_identifier] = ConvertorItem( From 3316417173b1f0bdd9a49f91cd210ca4f85236ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:07:50 +0100 Subject: [PATCH 133/158] process creators in their order --- openpype/pipeline/create/context.py | 56 ++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 9d8bb63804..5682fb3115 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1688,7 +1688,7 @@ class CreateContext: # Collect instances error_message = "Collection of instances for creator {} failed. {}" failed_info = [] - for creator in self.creators.values(): + for creator in self.sorted_creators: label = creator.label identifier = creator.identifier failed = False @@ -1760,7 +1760,8 @@ class CreateContext: error_message = "Failed to run AutoCreator with identifier \"{}\". {}" failed_info = [] - for identifier, creator in self.autocreators.items(): + for creator in self.sorted_autocreators: + identifier = creator.identifier label = creator.label failed = False add_traceback = False @@ -1865,19 +1866,26 @@ class CreateContext: """Save instance specific values.""" instances_by_identifier = collections.defaultdict(list) for instance in self._instances_by_id.values(): + instance_changes = instance.changes() + if not instance_changes: + continue + identifier = instance.creator_identifier - instances_by_identifier[identifier].append(instance) + instances_by_identifier[identifier].append( + UpdateData(instance, instance_changes) + ) + + if not instances_by_identifier: + return error_message = "Instances update of creator \"{}\" failed. {}" failed_info = [] - for identifier, creator_instances in instances_by_identifier.items(): - update_list = [] - for instance in creator_instances: - instance_changes = instance.changes() - if instance_changes: - update_list.append(UpdateData(instance, instance_changes)) - creator = self.creators[identifier] + for creator in self.get_sorted_creators( + instances_by_identifier.keys() + ): + identifier = creator.identifier + update_list = instances_by_identifier[identifier] if not update_list: continue @@ -1913,9 +1921,13 @@ class CreateContext: def remove_instances(self, instances): """Remove instances from context. + All instances that don't have creator identifier leading to existing + creator are just removed from context. + Args: - instances(list): Instances that should be removed - from context. + instances(List[CreatedInstance]): Instances that should be removed. + Remove logic is done using creator, which may require to + do other cleanup than just remove instance from context. """ instances_by_identifier = collections.defaultdict(list) @@ -1925,8 +1937,13 @@ class CreateContext: error_message = "Instances removement of creator \"{}\" failed. {}" failed_info = [] - for identifier, creator_instances in instances_by_identifier.items(): - creator = self.creators.get(identifier) + # Remove instances by creator plugin order + for creator in self.get_sorted_creators( + instances_by_identifier.keys() + ): + identifier = creator.identifier + creator_instances = instances_by_identifier[identifier] + label = creator.label failed = False add_traceback = False @@ -1969,6 +1986,7 @@ class CreateContext: family(str): Instance family for which should be attribute definitions returned. """ + if family not in self._attr_plugins_by_family: import pyblish.logic @@ -1984,7 +2002,13 @@ class CreateContext: return self._attr_plugins_by_family[family] def _get_publish_plugins_with_attr_for_context(self): - """Publish plugins attributes for Context plugins.""" + """Publish plugins attributes for Context plugins. + + Returns: + List[pyblish.api.Plugin]: Publish plugins that have attribute + definitions for context. + """ + plugins = [] for plugin in self.plugins_with_defs: if not plugin.__instanceEnabled__: @@ -2009,7 +2033,7 @@ class CreateContext: return self._collection_shared_data def run_convertor(self, convertor_identifier): - """Run convertor plugin by it's idenfitifier. + """Run convertor plugin by identifier. Conversion is skipped if convertor is not available. From c5be74156652323a43f05e1d486ed77dfd0f0987 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:08:07 +0100 Subject: [PATCH 134/158] handle instances without available creator --- openpype/pipeline/create/context.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 5682fb3115..c978fcc2e1 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1935,6 +1935,12 @@ class CreateContext: identifier = instance.creator_identifier instances_by_identifier[identifier].append(instance) + # Just remove instances from context if creator is not available + missing_creators = set(instances_by_identifier) - set(self.creators) + for identifier in missing_creators: + for instance in instances_by_identifier[identifier]: + self._remove_instance(instance) + error_message = "Instances removement of creator \"{}\" failed. {}" failed_info = [] # Remove instances by creator plugin order @@ -2046,7 +2052,7 @@ class CreateContext: convertor.convert() def run_convertors(self, convertor_identifiers): - """Run convertor plugins by idenfitifiers. + """Run convertor plugins by identifiers. Conversion is skipped if convertor is not available. It is recommended to trigger reset after conversion to reload instances. From c7f051db20c45ab9ba8b835fea3f858051a93df3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:08:42 +0100 Subject: [PATCH 135/158] added 'show_order' attribute to 'Creator' so show order can be different than processing --- openpype/pipeline/create/creator_plugins.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 1aba3f8770..53acb618ed 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -497,6 +497,17 @@ class Creator(BaseCreator): # - similar to instance attribute definitions pre_create_attr_defs = [] + @property + def show_order(self): + """Order in which is creator shown in UI. + + Returns: + int: Order in which is creator shown (less == earlier). By default + is using Creator's 'order' or processing. + """ + + return self.order + @abstractmethod def create(self, subset_name, instance_data, pre_create_data): """Create new instance and store it. From b67181c4e012ac370a1ca284e2112e92589772d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:09:32 +0100 Subject: [PATCH 136/158] added show order to publisher UI --- openpype/tools/publisher/constants.py | 2 ++ openpype/tools/publisher/control.py | 10 ++++++++-- openpype/tools/publisher/widgets/create_widget.py | 10 +++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index e9fdd4774a..b2bfd7dd5c 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -24,6 +24,7 @@ CREATOR_THUMBNAIL_ENABLED_ROLE = QtCore.Qt.UserRole + 5 FAMILY_ROLE = QtCore.Qt.UserRole + 6 GROUP_ROLE = QtCore.Qt.UserRole + 7 CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 8 +CREATOR_SORT_ROLE = QtCore.Qt.UserRole + 9 __all__ = ( @@ -36,6 +37,7 @@ __all__ = ( "IS_GROUP_ROLE", "CREATOR_IDENTIFIER_ROLE", "CREATOR_THUMBNAIL_ENABLED_ROLE", + "CREATOR_SORT_ROLE", "FAMILY_ROLE", "GROUP_ROLE", "CONVERTER_IDENTIFIER_ROLE", diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7c8da66744..d1ee3ea8aa 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -832,7 +832,8 @@ class CreatorItem: default_variants, create_allow_context_change, create_allow_thumbnail, - pre_create_attributes_defs + show_order, + pre_create_attributes_defs, ): self.identifier = identifier self.creator_type = creator_type @@ -846,6 +847,7 @@ class CreatorItem: self.default_variants = default_variants self.create_allow_context_change = create_allow_context_change self.create_allow_thumbnail = create_allow_thumbnail + self.show_order = show_order self.pre_create_attributes_defs = pre_create_attributes_defs def get_group_label(self): @@ -869,6 +871,7 @@ class CreatorItem: pre_create_attr_defs = None create_allow_context_change = None create_allow_thumbnail = None + show_order = creator.order if creator_type is CreatorTypes.artist: description = creator.get_description() detail_description = creator.get_detail_description() @@ -877,6 +880,7 @@ class CreatorItem: pre_create_attr_defs = creator.get_pre_create_attr_defs() create_allow_context_change = creator.create_allow_context_change create_allow_thumbnail = creator.create_allow_thumbnail + show_order = creator.show_order identifier = creator.identifier return cls( @@ -892,7 +896,8 @@ class CreatorItem: default_variants, create_allow_context_change, create_allow_thumbnail, - pre_create_attr_defs + show_order, + pre_create_attr_defs, ) def to_data(self): @@ -915,6 +920,7 @@ class CreatorItem: "default_variants": self.default_variants, "create_allow_context_change": self.create_allow_context_change, "create_allow_thumbnail": self.create_allow_thumbnail, + "show_order": self.show_order, "pre_create_attributes_defs": pre_create_attributes_defs, } diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 07b124f616..994b9ac912 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -18,9 +18,10 @@ from .tasks_widget import CreateWidgetTasksWidget from .precreate_widget import PreCreateWidget from ..constants import ( VARIANT_TOOLTIP, - CREATOR_IDENTIFIER_ROLE, FAMILY_ROLE, + CREATOR_IDENTIFIER_ROLE, CREATOR_THUMBNAIL_ENABLED_ROLE, + CREATOR_SORT_ROLE, ) SEPARATORS = ("---separator---", "---") @@ -441,7 +442,8 @@ class CreateWidget(QtWidgets.QWidget): # Add new families new_creators = set() - for identifier, creator_item in self._controller.creator_items.items(): + creator_items_by_identifier = self._controller.creator_items + for identifier, creator_item in creator_items_by_identifier.items(): if creator_item.creator_type != "artist": continue @@ -457,6 +459,7 @@ class CreateWidget(QtWidgets.QWidget): self._creators_model.appendRow(item) item.setData(creator_item.label, QtCore.Qt.DisplayRole) + item.setData(creator_item.show_order, CREATOR_SORT_ROLE) item.setData(identifier, CREATOR_IDENTIFIER_ROLE) item.setData( creator_item.create_allow_thumbnail, @@ -482,8 +485,9 @@ class CreateWidget(QtWidgets.QWidget): index = indexes[0] identifier = index.data(CREATOR_IDENTIFIER_ROLE) + create_item = creator_items_by_identifier.get(identifier) - self._set_creator_by_identifier(identifier) + self._set_creator(create_item) def _on_plugins_refresh(self): # Trigger refresh only if is visible From 7e1450e95d1cbc2d3df206235a13f280c3c642cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:09:53 +0100 Subject: [PATCH 137/158] modified proxy filter for sorting of creators --- openpype/tools/publisher/widgets/create_widget.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 994b9ac912..bc1f1ec4f7 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -91,6 +91,15 @@ class CreatorShortDescWidget(QtWidgets.QWidget): self._description_label.setText(description) +class CreatorsProxyModel(QtCore.QSortFilterProxyModel): + def lessThan(self, left, right): + l_show_order = left.data(CREATOR_SORT_ROLE) + r_show_order = right.data(CREATOR_SORT_ROLE) + if l_show_order == r_show_order: + return super(CreatorsProxyModel, self).lessThan(left, right) + return l_show_order < r_show_order + + class CreateWidget(QtWidgets.QWidget): def __init__(self, controller, parent=None): super(CreateWidget, self).__init__(parent) @@ -142,7 +151,7 @@ class CreateWidget(QtWidgets.QWidget): creators_view = QtWidgets.QListView(creators_view_widget) creators_model = QtGui.QStandardItemModel() - creators_sort_model = QtCore.QSortFilterProxyModel() + creators_sort_model = CreatorsProxyModel() creators_sort_model.setSourceModel(creators_model) creators_view.setModel(creators_sort_model) From dfb718741355d5b0c4cad8a9a4947d2dce2eb606 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:10:07 +0100 Subject: [PATCH 138/158] removed window title from creator widget --- openpype/tools/publisher/widgets/create_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index bc1f1ec4f7..dbf075c216 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -104,8 +104,6 @@ class CreateWidget(QtWidgets.QWidget): def __init__(self, controller, parent=None): super(CreateWidget, self).__init__(parent) - self.setWindowTitle("Create new instance") - self._controller = controller self._asset_name = None From 310456ec137c621554b91d2a831f875489059106 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:10:24 +0100 Subject: [PATCH 139/158] fix creator_items cache cleanup --- openpype/tools/publisher/control.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d1ee3ea8aa..435db5fcb3 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1508,9 +1508,6 @@ class BasePublisherController(AbstractPublisherController): def _reset_attributes(self): """Reset most of attributes that can be reset.""" - # Reset creator items - self._creator_items = None - self.publish_is_running = False self.publish_has_validated = False self.publish_has_crashed = False @@ -1766,6 +1763,8 @@ class PublisherController(BasePublisherController): self._resetting_plugins = True self._create_context.reset_plugins() + # Reset creator items + self._creator_items = None self._resetting_plugins = False From cd1b02c59504dab76294663b326851cf70920d7e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:14:53 +0100 Subject: [PATCH 140/158] move batch creator after simple creators --- .../hosts/traypublisher/plugins/create/create_movie_batch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index 1dc4bad9b3..d077131e4c 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -33,6 +33,8 @@ class BatchMovieCreator(TrayPublishCreator): create_allow_context_change = False version_regex = re.compile(r"^(.+)_v([0-9]+)$") + # Position batch creator after simple creators + order = 110 def __init__(self, project_settings, *args, **kwargs): super(BatchMovieCreator, self).__init__(project_settings, From b51a2a72579966e721e268de68ad1d01e62c93a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 7 Feb 2023 19:24:30 +0100 Subject: [PATCH 141/158] added more docstrings --- openpype/pipeline/create/context.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c978fcc2e1..0ee70f39cb 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1240,6 +1240,10 @@ class CreateContext: def get_sorted_creators(self, identifiers=None): """Sorted creators by 'order' attribute. + Args: + identifiers (Iterable[str]): Filter creators by identifiers. All + creators are returned if 'None' is passed. + Returns: List[BaseCreator]: Sorted creator plugins by 'order' value. """ @@ -1260,10 +1264,22 @@ class CreateContext: @property def sorted_creators(self): + """Sorted creators by 'order' attribute. + + Returns: + List[BaseCreator]: Sorted creator plugins by 'order' value. + """ + return self.get_sorted_creators() @property def sorted_autocreators(self): + """Sorted auto-creators by 'order' attribute. + + Returns: + List[AutoCreator]: Sorted plugins by 'order' value. + """ + return sorted( self.autocreators.values(), key=lambda creator: creator.order ) From d819a0c8c8882d325b708529205adf2315099625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 7 Feb 2023 19:37:11 +0100 Subject: [PATCH 142/158] :art: allow underscore in branch names allow underscore in branch names in regex for pre-commit hook - no-commit-branch --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 890df4613e..eec388924e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,4 +9,4 @@ repos: - id: check-yaml - id: check-added-large-files - id: no-commit-to-branch - args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-]+)$).*' ] + args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ] From cba888e66e5e874cfa84add8f396f5a72d1b3e40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Feb 2023 20:02:36 +0100 Subject: [PATCH 143/158] small docstring/comments enhancements --- openpype/pipeline/create/context.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index f46b4eccdb..427cabce67 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -252,12 +252,13 @@ class TrackChangesItem(object): ``` Args: - old_value (Any): Previous value. + old_value (Any): Old value. new_value (Any): New value. """ def __init__(self, old_value, new_value): self._changed = old_value != new_value + # Resolve if value is '_EMPTY_VALUE' after comparison of the values if old_value is _EMPTY_VALUE: old_value = None if new_value is _EMPTY_VALUE: @@ -412,7 +413,7 @@ class TrackChangesItem(object): """All keys that are available in old and new value. Empty set is returned if both old and new value are not a dict. - Output it is Union of 'old_keys' and 'new_keys'. + Output is Union of 'old_keys' and 'new_keys'. Returns: Set[str]: All keys from old and new value. From e1befa7ae3a01be70577539b747445377b09f28f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 8 Feb 2023 03:28:55 +0000 Subject: [PATCH 144/158] [Automated] Bump version --- openpype/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/version.py b/openpype/version.py index 3941912c6e..c65d1197c1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.1-nightly.2" +__version__ = "3.15.1-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 2fc4f6fe39..6e88404700 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.1" # OpenPype +version = "3.15.1-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From e7d60288a12a86e028b692da455ae5ca94ade552 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 12:15:43 +0100 Subject: [PATCH 145/158] Fix - added missed scopes for Slack bot --- openpype/modules/slack/manifest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 7a65cc5915..233c39fbaf 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,6 +19,8 @@ oauth_config: - chat:write.public - files:write - channels:read + - users:read + - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From 1a7f7d310ec02be47e6e95e5e8781d72ba181a33 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 12:21:55 +0100 Subject: [PATCH 146/158] OP-4653 - changed exception Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/photoshop/plugins/create/create_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 8a103ea6c2..9bf47733f5 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -48,7 +48,7 @@ class ImageCreator(Creator): try: group = stub.group_selected_layers(subset_name_from_ui) except: - raise ValueError("Cannot group locked Background layer!") + raise CreatorError("Cannot group locked Background layer!") groups_to_create.append(group) # create empty group if nothing selected From 21d745cc3cc9ad288a1aa0d7eb6329a5aeea7f9e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 12:25:29 +0100 Subject: [PATCH 147/158] OP-4822 - remove unwanted variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index e7c1899513..ed37ff1897 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -105,7 +105,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): jobInfo = {} pluginInfo = {} group = "none" - disable_strict_check_profiles = [] def get_job_info(self): job_info = DeadlineJobInfo(Plugin="MayaBatch") From 3156cc542508949c7786809db94feb0de4c7da35 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 12:39:28 +0100 Subject: [PATCH 148/158] OP-4653 - added missed import for CreatorError --- openpype/hosts/photoshop/plugins/create/create_image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 9bf47733f5..198c1586dc 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -5,7 +5,8 @@ from openpype.lib import BoolDef from openpype.pipeline import ( Creator, CreatedInstance, - legacy_io + legacy_io, + CreatorError ) from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS From b28f91769367ad9b64eadb9d04f18bb2b4529c20 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 13:37:50 +0100 Subject: [PATCH 149/158] Fix - remove minor part in toml Causes issue in create_env and new Poetry --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6e88404700..2fc4f6fe39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.1-nightly.3" # OpenPype +version = "3.15.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 6e55202349b5ea4f62001a586e44e8306143fbee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Feb 2023 14:23:10 +0100 Subject: [PATCH 150/158] change used method names from 'isAlive' to 'is_alive' --- openpype/modules/ftrack/ftrack_server/event_server_cli.py | 6 +++--- openpype/modules/ftrack/tray/login_dialog.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py index 25ebad6658..ad7ffd8e25 100644 --- a/openpype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/ftrack/ftrack_server/event_server_cli.py @@ -316,7 +316,7 @@ def main_loop(ftrack_url): statuser_failed_count = 0 # If thread failed test Ftrack and Mongo connection - elif not statuser_thread.isAlive(): + elif not statuser_thread.is_alive(): statuser_thread.join() statuser_thread = None ftrack_accessible = False @@ -359,7 +359,7 @@ def main_loop(ftrack_url): storer_failed_count = 0 # If thread failed test Ftrack and Mongo connection - elif not storer_thread.isAlive(): + elif not storer_thread.is_alive(): if storer_thread.mongo_error: raise MongoPermissionsError() storer_thread.join() @@ -396,7 +396,7 @@ def main_loop(ftrack_url): processor_failed_count = 0 # If thread failed test Ftrack and Mongo connection - elif not processor_thread.isAlive(): + elif not processor_thread.is_alive(): if processor_thread.mongo_error: raise Exception( "Exiting because have issue with acces to MongoDB" diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index 0e676545f7..f374a71178 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -259,7 +259,7 @@ class CredentialsDialog(QtWidgets.QDialog): # If there is an existing server thread running we need to stop it. if self._login_server_thread: - if self._login_server_thread.isAlive(): + if self._login_server_thread.is_alive(): self._login_server_thread.stop() self._login_server_thread.join() self._login_server_thread = None From 2b78c9404c587e4a3f3f19402e30c4741a07b3db Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 15:01:02 +0100 Subject: [PATCH 151/158] OP-4822 - push property to render instance render instance is created from collected instance explicitly. --- openpype/hosts/maya/plugins/publish/collect_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index b1ad3ca58e..fc297ef612 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -318,7 +318,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 "renderSetupIncludeLights": render_instance.data.get( "renderSetupIncludeLights" - ) + ), + "strict_error_checking": render_instance.data.get( + "strict_error_checking") } # Collect Deadline url if Deadline module is enabled From 91e1d28a9fdc15e7841cd55237f81607dfe0a7b8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 15:09:26 +0100 Subject: [PATCH 152/158] OP-4513 - platform specific logic Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 108c418e7b..b0560ce1e8 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -191,9 +191,8 @@ def get_openpype_executable(): "OpenPypeInstallationDirs", "") # clean '\ ' for MacOS pasting - if exe_list: + if platform.system().lower() == "darwin": exe_list = exe_list.replace("\\ ", " ") - if dir_list: dir_list = dir_list.replace("\\ ", " ") return exe_list, dir_list From f346ace6a12557879cb753c972cead6ea1f4e510 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 17:36:51 +0100 Subject: [PATCH 153/158] OP-4653 - removed obsolete import Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/photoshop/plugins/create/create_image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 198c1586dc..cdea82cb05 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -5,7 +5,6 @@ from openpype.lib import BoolDef from openpype.pipeline import ( Creator, CreatedInstance, - legacy_io, CreatorError ) from openpype.lib import prepare_template_data From 3ac0a1cb6f8c96071f8f5e992bbb0bb868b84e3f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Feb 2023 15:54:20 +0100 Subject: [PATCH 154/158] OP-4513 - fix Openpype plugin for MacOS --- .../repository/custom/plugins/OpenPype/OpenPype.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 6b0f69d98f..ab4a3d5e9b 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -73,7 +73,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): """ # fix path for application bundle on macos if platform.system().lower() == "darwin": - path = os.path.join(path, "Contents", "MacOS", "lib", "Python") + path = os.path.join(path, "MacOS") version_file = os.path.join(path, "openpype", "version.py") if not os.path.isfile(version_file): @@ -107,8 +107,11 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): "Scanning for compatible requested " f"version {requested_version}")) dir_list = self.GetConfigEntry("OpenPypeInstallationDirs") + # clean '\ ' for MacOS pasting + if platform.system().lower() == "darwin": + dir_list = dir_list.replace("\\ ", " ") install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if dir: + if install_dir: sub_dirs = [ f.path for f in os.scandir(install_dir) if f.is_dir() @@ -120,6 +123,9 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): openpype_versions.append((version, subdir)) exe_list = self.GetConfigEntry("OpenPypeExecutable") + # clean '\ ' for MacOS pasting + if platform.system().lower() == "darwin": + exe_list = exe_list.replace("\\ ", " ") exe = FileUtils.SearchFileList(exe_list) if openpype_versions: # if looking for requested compatible version, @@ -161,7 +167,9 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): os.path.join( compatible_versions[-1][1], "openpype_console.exe"), os.path.join( - compatible_versions[-1][1], "openpype_console") + compatible_versions[-1][1], "openpype_console"), + os.path.join( + compatible_versions[-1][1], "MacOS", "openpype_console") ] exe = FileUtils.SearchFileList(";".join(exe_list)) From af83e14dcea910b901969ed7590d6c72e749d2c8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Feb 2023 16:35:14 +0100 Subject: [PATCH 155/158] updating workflows to use YNPUT_BOT_TOKEN also some housecleaning was needed --- .github/workflows/automate-projects.yml | 19 -------- .github/workflows/milestone_assign.yml | 4 +- .github/workflows/milestone_create.yml | 8 ++-- .github/workflows/nightly_merge.yml | 4 +- .github/workflows/prerelease.yml | 40 +++------------- .github/workflows/release.yml | 64 ++++--------------------- .github/workflows/test_build.yml | 26 +--------- 7 files changed, 23 insertions(+), 142 deletions(-) delete mode 100644 .github/workflows/automate-projects.yml diff --git a/.github/workflows/automate-projects.yml b/.github/workflows/automate-projects.yml deleted file mode 100644 index b605071c2d..0000000000 --- a/.github/workflows/automate-projects.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Automate Projects - -on: - issues: - types: [opened, labeled] -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - assign_one_project: - runs-on: ubuntu-latest - name: Assign to One Project - steps: - - name: Assign NEW bugs to triage - uses: srggrs/assign-one-project-github-action@1.2.0 - if: contains(github.event.issue.labels.*.name, 'bug') - with: - project: 'https://github.com/pypeclub/pype/projects/2' - column_name: 'Needs triage' diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml index 4b52dfc30d..d1490b64c0 100644 --- a/.github/workflows/milestone_assign.yml +++ b/.github/workflows/milestone_assign.yml @@ -13,7 +13,7 @@ jobs: if: github.event.pull_request.milestone == null uses: zoispag/action-assign-milestone@v1 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}" milestone: 'next-minor' run_if_develop: @@ -24,5 +24,5 @@ jobs: if: github.event.pull_request.milestone == null uses: zoispag/action-assign-milestone@v1 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}" milestone: 'next-patch' \ No newline at end of file diff --git a/.github/workflows/milestone_create.yml b/.github/workflows/milestone_create.yml index b56ca81dc1..cb459c7ae6 100644 --- a/.github/workflows/milestone_create.yml +++ b/.github/workflows/milestone_create.yml @@ -12,7 +12,7 @@ jobs: uses: "WyriHaximus/github-action-get-milestones@master" id: milestones env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') id: querymilestone @@ -31,7 +31,7 @@ jobs: with: title: 'next-patch' env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" generate-next-minor: runs-on: ubuntu-latest @@ -40,7 +40,7 @@ jobs: uses: "WyriHaximus/github-action-get-milestones@master" id: milestones env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') id: querymilestone @@ -59,4 +59,4 @@ jobs: with: title: 'next-minor' env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml index 1d36c89cc7..a50a5c2828 100644 --- a/.github/workflows/nightly_merge.yml +++ b/.github/workflows/nightly_merge.yml @@ -14,10 +14,10 @@ jobs: - name: ๐Ÿš› Checkout Code uses: actions/checkout@v2 - - name: ๐Ÿ”จ Merge develop to main + - name: ๐Ÿ”จ Merge develop to main uses: everlytic/branch-merge@1.1.0 with: - github_token: ${{ secrets.ADMIN_TOKEN }} + github_token: ${{ secrets.YNPUT_BOT_TOKEN }} source_ref: 'develop' target_branch: 'main' commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 94bbe48156..571b0339e1 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -25,43 +25,15 @@ jobs: - name: ๐Ÿ”Ž Determine next version type id: version_type run: | - TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.GITHUB_TOKEN }}) - - echo ::set-output name=type::$TYPE + TYPE=$(python ./tools/ci_tools.py --bump --github_token ${{ secrets.YNPUT_BOT_TOKEN }}) + echo "type=${TYPE}" >> $GITHUB_OUTPUT - name: ๐Ÿ’‰ Inject new version into files id: version if: steps.version_type.outputs.type != 'skip' run: | - RESULT=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.GITHUB_TOKEN }}) - - echo ::set-output name=next_tag::$RESULT - - # - name: "โœ๏ธ Generate full changelog" - # if: steps.version_type.outputs.type != 'skip' - # id: generate-full-changelog - # uses: heinrichreimer/github-changelog-generator-action@v2.3 - # with: - # token: ${{ secrets.ADMIN_TOKEN }} - # addSections: '{"documentation":{"prefix":"### ๐Ÿ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### โœ… Testing","labels":["tests"]},"feature":{"prefix":"**๐Ÿ†• New features**", "labels":["type: feature"]},"breaking":{"prefix":"**๐Ÿ’ฅ Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**๐Ÿš€ Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**๐Ÿ› Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**โš ๏ธ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**๐Ÿ”€ Refactored code**", "labels":["refactor"]}}' - # issues: false - # issuesWoLabels: false - # sinceTag: "3.12.0" - # maxIssues: 100 - # pullRequests: true - # prWoLabels: false - # author: false - # unreleased: true - # compareLink: true - # stripGeneratorNotice: true - # verbose: true - # unreleasedLabel: ${{ steps.version.outputs.next_tag }} - # excludeTagsRegex: "CI/.+" - # releaseBranch: "main" - - - name: "๐Ÿ–จ๏ธ Print changelog to console" - if: steps.version_type.outputs.type != 'skip' - run: cat CHANGELOG.md + NEW_VERSION_TAG=$(python ./tools/ci_tools.py --nightly --github_token ${{ secrets.YNPUT_BOT_TOKEN }}) + echo "next_tag=${NEW_VERSION_TAG}" >> $GITHUB_OUTPUT - name: ๐Ÿ’พ Commit and Tag id: git_commit @@ -80,7 +52,7 @@ jobs: - name: Push to protected main branch uses: CasperWA/push-protected@v2.10.0 with: - token: ${{ secrets.ADMIN_TOKEN }} + token: ${{ secrets.YNPUT_BOT_TOKEN }} branch: main tags: true unprotect_reviews: true @@ -89,7 +61,7 @@ jobs: uses: everlytic/branch-merge@1.1.0 if: steps.version_type.outputs.type != 'skip' with: - github_token: ${{ secrets.ADMIN_TOKEN }} + github_token: ${{ secrets.YNPUT_BOT_TOKEN }} source_ref: 'main' target_branch: 'develop' commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e3b6eb05c..0b4c8af2c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,34 +26,12 @@ jobs: - name: ๐Ÿ’‰ Inject new version into files id: version run: | - echo ::set-output name=current_version::${GITHUB_REF#refs/*/} - RESULT=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/}) - LASTRELEASE=$(python ./tools/ci_tools.py --lastversion release) + NEW_VERSION=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/}) + LAST_VERSION=$(python ./tools/ci_tools.py --lastversion release) - echo ::set-output name=last_release::$LASTRELEASE - echo ::set-output name=release_tag::$RESULT - - # - name: "โœ๏ธ Generate full changelog" - # if: steps.version.outputs.release_tag != 'skip' - # id: generate-full-changelog - # uses: heinrichreimer/github-changelog-generator-action@v2.3 - # with: - # token: ${{ secrets.ADMIN_TOKEN }} - # addSections: '{"documentation":{"prefix":"### ๐Ÿ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### โœ… Testing","labels":["tests"]},"feature":{"prefix":"**๐Ÿ†• New features**", "labels":["type: feature"]},"breaking":{"prefix":"**๐Ÿ’ฅ Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**๐Ÿš€ Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**๐Ÿ› Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**โš ๏ธ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**๐Ÿ”€ Refactored code**", "labels":["refactor"]}}' - # issues: false - # issuesWoLabels: false - # sinceTag: "3.12.0" - # maxIssues: 100 - # pullRequests: true - # prWoLabels: false - # author: false - # unreleased: true - # compareLink: true - # stripGeneratorNotice: true - # verbose: true - # futureRelease: ${{ steps.version.outputs.release_tag }} - # excludeTagsRegex: "CI/.+" - # releaseBranch: "main" + echo "current_version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT + echo "last_release=${LAST_VERSION}" >> $GITHUB_OUTPUT + echo "release_tag=${NEW_VERSION}" >> $GITHUB_OUTPUT - name: ๐Ÿ’พ Commit and Tag id: git_commit @@ -70,43 +48,17 @@ jobs: if: steps.version.outputs.release_tag != 'skip' uses: CasperWA/push-protected@v2.10.0 with: - token: ${{ secrets.ADMIN_TOKEN }} + token: ${{ secrets.YNPUT_BOT_TOKEN }} branch: main tags: true unprotect_reviews: true - - name: "โœ๏ธ Generate last changelog" - if: steps.version.outputs.release_tag != 'skip' - id: generate-last-changelog - uses: heinrichreimer/github-changelog-generator-action@v2.2 - with: - token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### ๐Ÿ“– Documentation","labels":["type: documentation"]},"tests":{"prefix":"### โœ… Testing","labels":["tests"]},"feature":{"prefix":"**๐Ÿ†• New features**", "labels":["type: feature"]},"breaking":{"prefix":"**๐Ÿ’ฅ Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**๐Ÿš€ Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**๐Ÿ› Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**โš ๏ธ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**๐Ÿ”€ Refactored code**", "labels":["refactor"]}}' - issues: false - issuesWoLabels: false - sinceTag: ${{ steps.version.outputs.last_release }} - maxIssues: 100 - pullRequests: true - prWoLabels: false - author: false - unreleased: true - compareLink: true - stripGeneratorNotice: true - verbose: true - futureRelease: ${{ steps.version.outputs.release_tag }} - excludeTagsRegex: "CI/.+" - releaseBranch: "main" - stripHeaders: true - base: 'none' - - - name: ๐Ÿš€ Github Release if: steps.version.outputs.release_tag != 'skip' uses: ncipollo/release-action@v1 with: - body: ${{ steps.generate-last-changelog.outputs.changelog }} tag: ${{ steps.version.outputs.release_tag }} - token: ${{ secrets.ADMIN_TOKEN }} + token: ${{ secrets.YNPUT_BOT_TOKEN }} - name: โ˜  Delete Pre-release if: steps.version.outputs.release_tag != 'skip' @@ -118,7 +70,7 @@ jobs: if: steps.version.outputs.release_tag != 'skip' uses: everlytic/branch-merge@1.1.0 with: - github_token: ${{ secrets.ADMIN_TOKEN }} + github_token: ${{ secrets.YNPUT_BOT_TOKEN }} source_ref: 'main' target_branch: 'develop' commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}' diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 0e6c242bd6..064a4d47e0 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -28,7 +28,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - + - name: ๐Ÿงต Install Requirements shell: pwsh run: | @@ -64,27 +64,3 @@ jobs: run: | export SKIP_THIRD_PARTY_VALIDATION="1" ./tools/build.sh - - # MacOS-latest: - - # runs-on: macos-latest - # strategy: - # matrix: - # python-version: [3.9] - - # steps: - # - name: ๐Ÿš› Checkout Code - # uses: actions/checkout@v2 - - # - name: Set up Python - # uses: actions/setup-python@v2 - # with: - # python-version: ${{ matrix.python-version }} - - # - name: ๐Ÿงต Install Requirements - # run: | - # ./tools/create_env.sh - - # - name: ๐Ÿ”จ Build - # run: | - # ./tools/build.sh From 240c77c9bbd49d5133c17717b6358769c2c63104 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Feb 2023 16:43:18 +0100 Subject: [PATCH 156/158] fixing nightly versioning --- .github/workflows/milestone_assign.yml | 2 +- .github/workflows/milestone_create.yml | 2 +- .github/workflows/nightly_merge.yml | 2 +- pyproject.toml | 2 +- tools/ci_tools.py | 24 +++++++++++------------- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml index d1490b64c0..3cbee51472 100644 --- a/.github/workflows/milestone_assign.yml +++ b/.github/workflows/milestone_assign.yml @@ -25,4 +25,4 @@ jobs: uses: zoispag/action-assign-milestone@v1 with: repo-token: "${{ secrets.YNPUT_BOT_TOKEN }}" - milestone: 'next-patch' \ No newline at end of file + milestone: 'next-patch' diff --git a/.github/workflows/milestone_create.yml b/.github/workflows/milestone_create.yml index cb459c7ae6..632704e64a 100644 --- a/.github/workflows/milestone_create.yml +++ b/.github/workflows/milestone_create.yml @@ -59,4 +59,4 @@ jobs: with: title: 'next-minor' env: - GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" \ No newline at end of file + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml index a50a5c2828..c5cd9f1466 100644 --- a/.github/workflows/nightly_merge.yml +++ b/.github/workflows/nightly_merge.yml @@ -26,4 +26,4 @@ jobs: uses: benc-uk/workflow-dispatch@v1 with: workflow: Nightly Prerelease - token: ${{ secrets.ADMIN_TOKEN }} \ No newline at end of file + token: ${{ secrets.ADMIN_TOKEN }} diff --git a/pyproject.toml b/pyproject.toml index 6e88404700..a872ed3609 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.1-nightly.3" # OpenPype +version = "3.15.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" diff --git a/tools/ci_tools.py b/tools/ci_tools.py index c8f0cd48b4..750bf8645d 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -7,16 +7,10 @@ from github import Github import os def get_release_type_github(Log, github_token): - # print(Log) minor_labels = ["Bump Minor"] - # patch_labels = [ - # "type: enhancement", - # "type: bug", - # "type: deprecated", - # "type: Feature"] g = Github(github_token) - repo = g.get_repo("pypeclub/OpenPype") + repo = g.get_repo("ynput/OpenPype") labels = set() for line in Log.splitlines(): @@ -35,12 +29,12 @@ def get_release_type_github(Log, github_token): else: return "patch" - # TODO: if all is working fine, this part can be cleaned up eventually + # TODO: if all is working fine, this part can be cleaned up eventually # if any(label in labels for label in patch_labels): # return "patch" - + return None - + def remove_prefix(text, prefix): return text[text.startswith(prefix) and len(prefix):] @@ -93,12 +87,16 @@ def file_regex_replace(filename, regex, version): f.truncate() -def bump_file_versions(version): +def bump_file_versions(version, nightly=False): filename = "./openpype/version.py" regex = "(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?" file_regex_replace(filename, regex, version) + if nightly: + # skip nightly reversion in pyproject.toml + return + # bump pyproject.toml filename = "pyproject.toml" regex = "version = \"(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(\+((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?\" # OpenPype" @@ -196,7 +194,7 @@ def main(): if options.nightly: next_tag_v = calculate_next_nightly(github_token=options.github_token) print(next_tag_v) - bump_file_versions(next_tag_v) + bump_file_versions(next_tag_v, True) if options.finalize: new_release = finalize_prerelease(options.finalize) @@ -222,7 +220,7 @@ def main(): new_prerelease = current_prerelease.bump_prerelease().__str__() print(new_prerelease) bump_file_versions(new_prerelease) - + if options.version: bump_file_versions(options.version) print(f"Injected version {options.version} into the release") From f805e7501d488e82bffcf31aaaab2d63ae15d2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 9 Feb 2023 16:55:56 +0100 Subject: [PATCH 157/158] removing last reference of ADMIN_TOKEN --- .github/workflows/nightly_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml index c5cd9f1466..1776d7a464 100644 --- a/.github/workflows/nightly_merge.yml +++ b/.github/workflows/nightly_merge.yml @@ -26,4 +26,4 @@ jobs: uses: benc-uk/workflow-dispatch@v1 with: workflow: Nightly Prerelease - token: ${{ secrets.ADMIN_TOKEN }} + token: ${{ secrets.YNPUT_BOT_TOKEN }} From 922fe5bdaf17d8e78b30b58b1ca41bbeb54f9307 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 9 Feb 2023 15:59:27 +0000 Subject: [PATCH 158/158] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c65d1197c1..61339fb3dd 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.1-nightly.3" +__version__ = "3.15.1-nightly.4"