From aa600f88d45dcff4452d3f34de53168958cf5172 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 26 Apr 2024 23:20:45 +0300 Subject: [PATCH 001/118] Support HDA Publishing from non object level --- .../hosts/houdini/plugins/create/create_hda.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index d399aa5e15..d747abc738 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -37,16 +37,24 @@ class CreateHDA(plugin.HoudiniCreator): self, folder_path, node_name, parent, node_type="geometry" ): - parent_node = hou.node("/obj") if self.selected_nodes: # if we have `use selection` enabled, and we have some # selected nodes ... + parent_node = self.selected_nodes[0].parent() subnet = parent_node.collapseIntoSubnet( self.selected_nodes, subnet_name="{}_subnet".format(node_name)) subnet.moveToGoodPosition() to_hda = subnet else: + # Use Obj as the default path + parent_node = hou.node("/obj") + # Find and return the NetworkEditor pane tab with the minimum index + pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor) + if isinstance(pane, hou.NetworkEditor): + # Use the NetworkEditor pane path as the parent path. + parent_node = pane.pwd() + to_hda = parent_node.createNode( "subnet", node_name="{}_subnet".format(node_name)) if not to_hda.type().definition(): From 1953a74ae1635c3e0b2159fcec7ca0ec2d6cfef3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 26 Apr 2024 23:21:50 +0300 Subject: [PATCH 002/118] Allow users to specify maximum inputs of the HDA --- .../hosts/houdini/plugins/create/create_hda.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index d747abc738..4538266bce 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -4,6 +4,7 @@ import ayon_api from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin +from ayon_core.lib import NumberDef import hou @@ -16,6 +17,8 @@ class CreateHDA(plugin.HoudiniCreator): icon = "gears" maintain_selection = False + max_num_inputs = 0 + def _check_existing(self, folder_path, product_name): # type: (str, str) -> bool """Check if existing product name versions already exists.""" @@ -66,7 +69,8 @@ class CreateHDA(plugin.HoudiniCreator): hda_node = to_hda.createDigitalAsset( name=node_name, - hda_file_name="$HIP/{}.hda".format(node_name) + hda_file_name="$HIP/{}.hda".format(node_name), + max_num_inputs=self.max_num_inputs ) hda_node.layoutChildren() elif self._check_existing(folder_path, node_name): @@ -83,6 +87,8 @@ class CreateHDA(plugin.HoudiniCreator): def create(self, product_name, instance_data, pre_create_data): instance_data.pop("active", None) + self.max_num_inputs = pre_create_data["max_num_inputs"] + instance = super(CreateHDA, self).create( product_name, instance_data, @@ -94,3 +100,12 @@ class CreateHDA(plugin.HoudiniCreator): return [ hou.objNodeTypeCategory() ] + + def get_pre_create_attr_defs(self): + attrs = super(CreateHDA, self).get_pre_create_attr_defs() + return attrs + [ + NumberDef("max_num_inputs", + label="Maximum Inputs", + default=self.max_num_inputs, + decimals=0) + ] From 079b332e48ad42f2a69d142e35521cf0d97d6610 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 26 Apr 2024 23:23:20 +0300 Subject: [PATCH 003/118] fix a bug with setName --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 4538266bce..1a4457d2d4 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -80,7 +80,11 @@ class CreateHDA(plugin.HoudiniCreator): else: hda_node = to_hda - hda_node.setName(node_name) + # If user tries to create the same HDA instance more than + # once, then all of them will have the same product name and + # point to the same hda_file_name. But, their node names will + # be incremented. + hda_node.setName(node_name, unique_name=True) self.customize_node_look(hda_node) return hda_node From b8dcd4cc315e311a90d7ccaf0c41964e53f2cdd4 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 26 Apr 2024 23:50:34 +0300 Subject: [PATCH 004/118] ignore_external_references when creating a digital asset --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 1a4457d2d4..119afd3edc 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -70,7 +70,8 @@ class CreateHDA(plugin.HoudiniCreator): hda_node = to_hda.createDigitalAsset( name=node_name, hda_file_name="$HIP/{}.hda".format(node_name), - max_num_inputs=self.max_num_inputs + max_num_inputs=self.max_num_inputs, + ignore_external_references=True ) hda_node.layoutChildren() elif self._check_existing(folder_path, node_name): From a41b564f2f5bfc5d18551a9fde7c49da8ccb55dd Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 29 Apr 2024 11:20:02 +0300 Subject: [PATCH 005/118] don't create a subnetwork if user seleted a subnetwork --- .../hosts/houdini/plugins/create/create_hda.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 119afd3edc..6a4ebc324d 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -43,12 +43,16 @@ class CreateHDA(plugin.HoudiniCreator): if self.selected_nodes: # if we have `use selection` enabled, and we have some # selected nodes ... - parent_node = self.selected_nodes[0].parent() - subnet = parent_node.collapseIntoSubnet( - self.selected_nodes, - subnet_name="{}_subnet".format(node_name)) - subnet.moveToGoodPosition() - to_hda = subnet + if self.selected_nodes[0].type().name() == "subnet": + to_hda = self.selected_nodes[0] + to_hda.setName("{}_subnet".format(node_name), unique_name=True) + else: + parent_node = self.selected_nodes[0].parent() + subnet = parent_node.collapseIntoSubnet( + self.selected_nodes, + subnet_name="{}_subnet".format(node_name)) + subnet.moveToGoodPosition() + to_hda = subnet else: # Use Obj as the default path parent_node = hou.node("/obj") From 8399a6832de451e97e5692408aed867a9f796d97 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 29 Apr 2024 11:24:35 +0300 Subject: [PATCH 006/118] Allow users to specify minimum inputs of the HDA --- .../ayon_core/hosts/houdini/plugins/create/create_hda.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 6a4ebc324d..f79b49fb2b 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -17,6 +17,7 @@ class CreateHDA(plugin.HoudiniCreator): icon = "gears" maintain_selection = False + min_num_inputs = 0 max_num_inputs = 0 def _check_existing(self, folder_path, product_name): @@ -74,6 +75,7 @@ class CreateHDA(plugin.HoudiniCreator): hda_node = to_hda.createDigitalAsset( name=node_name, hda_file_name="$HIP/{}.hda".format(node_name), + min_num_inputs=self.min_num_inputs, max_num_inputs=self.max_num_inputs, ignore_external_references=True ) @@ -96,6 +98,7 @@ class CreateHDA(plugin.HoudiniCreator): def create(self, product_name, instance_data, pre_create_data): instance_data.pop("active", None) + self.min_num_inputs = pre_create_data["min_num_inputs"] self.max_num_inputs = pre_create_data["max_num_inputs"] instance = super(CreateHDA, self).create( @@ -113,6 +116,10 @@ class CreateHDA(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super(CreateHDA, self).get_pre_create_attr_defs() return attrs + [ + NumberDef("min_num_inputs", + label="Minimum Inputs", + default=self.min_num_inputs, + decimals=0), NumberDef("max_num_inputs", label="Maximum Inputs", default=self.max_num_inputs, From 23583ab02243ee55cfe6b1424c71e05df90a0e91 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 30 Apr 2024 08:47:50 +0300 Subject: [PATCH 007/118] CreateHDA: update get_network_categories --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index f79b49fb2b..772ff4e255 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -110,7 +110,11 @@ class CreateHDA(plugin.HoudiniCreator): def get_network_categories(self): return [ - hou.objNodeTypeCategory() + category for name, category in hou.nodeTypeCategories().items() + if name in { + "Chop", "Cop2", "Dop", "Driver", "Lop", + "Object", "Shop", "Sop", "Top", "Vop" + } ] def get_pre_create_attr_defs(self): From 05886e882a81fd02c01925ce423bc35dce2d883c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 May 2024 11:51:20 +0200 Subject: [PATCH 008/118] Pass `pre_create_data` to `create_instance_node` --- client/ayon_core/hosts/houdini/api/plugin.py | 7 +++++- .../houdini/plugins/create/create_hda.py | 25 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index a9c8c313b9..75527e0b3f 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -141,7 +141,11 @@ class HoudiniCreatorBase(object): @staticmethod def create_instance_node( - folder_path, node_name, parent, node_type="geometry" + folder_path, + node_name, + parent, + node_type="geometry", + pre_create_data=None ): """Create node representing instance. @@ -150,6 +154,7 @@ class HoudiniCreatorBase(object): node_name (str): Name of the new node. parent (str): Name of the parent node. node_type (str, optional): Type of the node. + pre_create_data (Optional[Dict]): Pre create data. Returns: hou.Node: Newly created instance node. diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 772ff4e255..bc4ad093bf 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -38,8 +38,20 @@ class CreateHDA(plugin.HoudiniCreator): return product_name.lower() in existing_product_names_low def create_instance_node( - self, folder_path, node_name, parent, node_type="geometry" + self, + folder_path, + node_name, + parent, + node_type="geometry", + pre_create_data=None ): + if pre_create_data is None: + pre_create_data = {} + + min_num_inputs = pre_create_data.get("min_num_inputs", + self.min_num_inputs) + max_num_inputs = pre_create_data.get("min_num_inputs", + self.max_num_inputs) if self.selected_nodes: # if we have `use selection` enabled, and we have some @@ -75,8 +87,8 @@ class CreateHDA(plugin.HoudiniCreator): hda_node = to_hda.createDigitalAsset( name=node_name, hda_file_name="$HIP/{}.hda".format(node_name), - min_num_inputs=self.min_num_inputs, - max_num_inputs=self.max_num_inputs, + min_num_inputs=min_num_inputs, + max_num_inputs=max_num_inputs, ignore_external_references=True ) hda_node.layoutChildren() @@ -98,16 +110,11 @@ class CreateHDA(plugin.HoudiniCreator): def create(self, product_name, instance_data, pre_create_data): instance_data.pop("active", None) - self.min_num_inputs = pre_create_data["min_num_inputs"] - self.max_num_inputs = pre_create_data["max_num_inputs"] - - instance = super(CreateHDA, self).create( + return super(CreateHDA, self).create( product_name, instance_data, pre_create_data) - return instance - def get_network_categories(self): return [ category for name, category in hou.nodeTypeCategories().items() From 2a654bcfe82f6e5df8a156823a7a85cc97957102 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 May 2024 19:38:18 +0300 Subject: [PATCH 009/118] pass pre_create_data argument to self.create_instance_node --- client/ayon_core/hosts/houdini/api/plugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index 75527e0b3f..6c878867c0 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -189,7 +189,12 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): folder_path = instance_data["folderPath"] instance_node = self.create_instance_node( - folder_path, product_name, "/out", node_type) + folder_path, + product_name, + "/out", + node_type, + pre_create_data + ) self.customize_node_look(instance_node) From 689c87b6d9ddf5605825767eef8f0446d1931c75 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 May 2024 19:38:58 +0300 Subject: [PATCH 010/118] fix pre_create_data key --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index bc4ad093bf..9566a6c5f3 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -50,7 +50,7 @@ class CreateHDA(plugin.HoudiniCreator): min_num_inputs = pre_create_data.get("min_num_inputs", self.min_num_inputs) - max_num_inputs = pre_create_data.get("min_num_inputs", + max_num_inputs = pre_create_data.get("max_num_inputs", self.max_num_inputs) if self.selected_nodes: From 79139ce0dd4b31c32c98d26403a2f78c6e345f14 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 3 May 2024 20:09:02 +0300 Subject: [PATCH 011/118] use explicit network categories --- .../hosts/houdini/plugins/create/create_hda.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 9566a6c5f3..0eb2b5dd52 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -116,12 +116,20 @@ class CreateHDA(plugin.HoudiniCreator): pre_create_data) def get_network_categories(self): + # Houdini allows creating sub-network nodes inside + # these categories. + # Therefore this plugin can work in these categories. return [ - category for name, category in hou.nodeTypeCategories().items() - if name in { - "Chop", "Cop2", "Dop", "Driver", "Lop", - "Object", "Shop", "Sop", "Top", "Vop" - } + hou.chopNodeTypeCategory(), + hou.cop2NodeTypeCategory(), + hou.dopNodeTypeCategory(), + hou.ropNodeTypeCategory(), + hou.lopNodeTypeCategory(), + hou.objNodeTypeCategory(), + hou.shopNodeTypeCategory(), + hou.sopNodeTypeCategory(), + hou.topNodeTypeCategory(), + hou.vopNodeTypeCategory() ] def get_pre_create_attr_defs(self): From 618bab2c281936a9a2ec9982f98f96513d7b2e8c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 May 2024 01:30:49 +0300 Subject: [PATCH 012/118] add more options for HDA creator --- .../houdini/plugins/create/create_hda.py | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 0eb2b5dd52..ff56a63132 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- """Creator plugin for creating publishable Houdini Digital Assets.""" import ayon_api - +import getpass from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin -from ayon_core.lib import NumberDef +from ayon_core.lib import NumberDef, BoolDef import hou +from ayon_core.resources import get_ayon_icon_filepath class CreateHDA(plugin.HoudiniCreator): @@ -19,6 +20,7 @@ class CreateHDA(plugin.HoudiniCreator): min_num_inputs = 0 max_num_inputs = 0 + max_num_outputs = 1 def _check_existing(self, folder_path, product_name): # type: (str, str) -> bool @@ -52,7 +54,8 @@ class CreateHDA(plugin.HoudiniCreator): self.min_num_inputs) max_num_inputs = pre_create_data.get("max_num_inputs", self.max_num_inputs) - + max_num_outputs = pre_create_data.get("max_num_outputs", + self.max_num_outputs) if self.selected_nodes: # if we have `use selection` enabled, and we have some # selected nodes ... @@ -87,8 +90,6 @@ class CreateHDA(plugin.HoudiniCreator): hda_node = to_hda.createDigitalAsset( name=node_name, hda_file_name="$HIP/{}.hda".format(node_name), - min_num_inputs=min_num_inputs, - max_num_inputs=max_num_inputs, ignore_external_references=True ) hda_node.layoutChildren() @@ -105,6 +106,24 @@ class CreateHDA(plugin.HoudiniCreator): # be incremented. hda_node.setName(node_name, unique_name=True) self.customize_node_look(hda_node) + + # Set Custom settings. + hda_def = hda_node.type().definition() + hda_def.setMinNumInputs(min_num_inputs) + hda_def.setMaxNumInputs(max_num_inputs) + hda_def.setMaxNumOutputs(max_num_outputs) + + if pre_create_data.get("use_ayon_icon"): + hda_def.setIcon(get_ayon_icon_filepath()) + + if pre_create_data.get("set_user"): + hda_def.setUserInfo(getpass.getuser()) + + if pre_create_data.get("use_project"): + tool_name = hou.shelves.defaultToolName( + hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) + hou.shelves.tool(tool_name).setToolLocations((self.project_name,)) + return hda_node def create(self, product_name, instance_data, pre_create_data): @@ -142,5 +161,21 @@ class CreateHDA(plugin.HoudiniCreator): NumberDef("max_num_inputs", label="Maximum Inputs", default=self.max_num_inputs, - decimals=0) + decimals=0), + NumberDef("max_num_outputs", + label="Maximum Outputs", + default=self.max_num_outputs, + decimals=0), + BoolDef("use_ayon_icon", + tooltip="Use Ayon icon for the digital asset.", + default=True, + label="Use AYON Icon"), + BoolDef("set_user", + tooltip="Set current user as the author of the HDA", + default=True, + label="Set Current User"), + BoolDef("use_project", + tooltip="Use project name as tab submenu path", + default=True, + label="Use Project as menu entry"), ] From 2ab0e240f8ca0e119bbb317452e8d78247e7f296 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 May 2024 16:09:27 +0300 Subject: [PATCH 013/118] remove shop from network categories --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index ff56a63132..d4dcd245ec 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -145,7 +145,6 @@ class CreateHDA(plugin.HoudiniCreator): hou.ropNodeTypeCategory(), hou.lopNodeTypeCategory(), hou.objNodeTypeCategory(), - hou.shopNodeTypeCategory(), hou.sopNodeTypeCategory(), hou.topNodeTypeCategory(), hou.vopNodeTypeCategory() From 7d37953011078a0d07b3a0c086507891329ec1d3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 May 2024 16:15:44 +0300 Subject: [PATCH 014/118] remove setting number of inputs and outputs --- .../houdini/plugins/create/create_hda.py | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index d4dcd245ec..0e0436d981 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -4,7 +4,7 @@ import ayon_api import getpass from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin -from ayon_core.lib import NumberDef, BoolDef +from ayon_core.lib import BoolDef import hou from ayon_core.resources import get_ayon_icon_filepath @@ -18,10 +18,6 @@ class CreateHDA(plugin.HoudiniCreator): icon = "gears" maintain_selection = False - min_num_inputs = 0 - max_num_inputs = 0 - max_num_outputs = 1 - def _check_existing(self, folder_path, product_name): # type: (str, str) -> bool """Check if existing product name versions already exists.""" @@ -50,12 +46,6 @@ class CreateHDA(plugin.HoudiniCreator): if pre_create_data is None: pre_create_data = {} - min_num_inputs = pre_create_data.get("min_num_inputs", - self.min_num_inputs) - max_num_inputs = pre_create_data.get("max_num_inputs", - self.max_num_inputs) - max_num_outputs = pre_create_data.get("max_num_outputs", - self.max_num_outputs) if self.selected_nodes: # if we have `use selection` enabled, and we have some # selected nodes ... @@ -109,9 +99,6 @@ class CreateHDA(plugin.HoudiniCreator): # Set Custom settings. hda_def = hda_node.type().definition() - hda_def.setMinNumInputs(min_num_inputs) - hda_def.setMaxNumInputs(max_num_inputs) - hda_def.setMaxNumOutputs(max_num_outputs) if pre_create_data.get("use_ayon_icon"): hda_def.setIcon(get_ayon_icon_filepath()) @@ -153,18 +140,6 @@ class CreateHDA(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super(CreateHDA, self).get_pre_create_attr_defs() return attrs + [ - NumberDef("min_num_inputs", - label="Minimum Inputs", - default=self.min_num_inputs, - decimals=0), - NumberDef("max_num_inputs", - label="Maximum Inputs", - default=self.max_num_inputs, - decimals=0), - NumberDef("max_num_outputs", - label="Maximum Outputs", - default=self.max_num_outputs, - decimals=0), BoolDef("use_ayon_icon", tooltip="Use Ayon icon for the digital asset.", default=True, From 99179cc6453e968741313dd946671ff5aa56939f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 7 May 2024 16:21:07 +0300 Subject: [PATCH 015/118] update default values of attr defs --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 0e0436d981..a810f06283 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -142,11 +142,11 @@ class CreateHDA(plugin.HoudiniCreator): return attrs + [ BoolDef("use_ayon_icon", tooltip="Use Ayon icon for the digital asset.", - default=True, + default=False, label="Use AYON Icon"), BoolDef("set_user", tooltip="Set current user as the author of the HDA", - default=True, + default=False, label="Set Current User"), BoolDef("use_project", tooltip="Use project name as tab submenu path", From 8f1dd6acf6f16f2f1f608fced6be7e416de747ee Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 8 May 2024 22:50:06 +0300 Subject: [PATCH 016/118] support loading HDAs from non object level --- .../hosts/houdini/plugins/load/load_hda.py | 131 +++++++++++++++--- 1 file changed, 115 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py index 10fc03be03..b2d2337266 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py @@ -3,10 +3,28 @@ import os from ayon_core.pipeline import ( load, get_representation_path, + AVALON_CONTAINER_ID ) -from ayon_core.hosts.houdini.api import pipeline +from ayon_core.hosts.houdini.api import lib, pipeline +import hou +def get_avalon_container(): + path = pipeline.AVALON_CONTAINERS + avalon_container = hou.node(path) + if not avalon_container: + # Let's create avalon container secretly + # but make sure the pipeline still is built the + # way we anticipate it was built, asserting it. + assert path == "/obj/AVALON_CONTAINERS" + + parent = hou.node("/obj") + avalon_container = parent.createNode( + "subnet", node_name="AVALON_CONTAINERS" + ) + + return avalon_container + class HdaLoader(load.LoaderPlugin): """Load Houdini Digital Asset file.""" @@ -18,38 +36,44 @@ class HdaLoader(load.LoaderPlugin): color = "orange" def load(self, context, name=None, namespace=None, data=None): - import hou # Format file name, Houdini only wants forward slashes file_path = self.filepath_from_context(context) file_path = os.path.normpath(file_path) file_path = file_path.replace("\\", "/") - # Get the root node - obj = hou.node("/obj") - # Create a unique name counter = 1 namespace = namespace or context["folder"]["name"] formatted = "{}_{}".format(namespace, name) if namespace else name node_name = "{0}_{1:03d}".format(formatted, counter) + hda_defs = hou.hda.definitionsInFile(file_path) + if not hda_defs: + raise RuntimeError ("No HDA definitions found!") + + hda_def = hda_defs[0] + parent_node = self._create_dedicated_parent_node(hda_def) + hou.hda.installFile(file_path) - hda_node = obj.createNode(name, node_name) + hda_node = parent_node.createNode(name, node_name) + hda_node.moveToGoodPosition() - self[:] = [hda_node] + # Imprint it manually + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": node_name, + "namespace": namespace, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["id"], + } - return pipeline.containerise( - node_name, - namespace, - [hda_node], - context, - self.__class__.__name__, - suffix="", - ) + lib.imprint(hda_node, data) + + return hda_node def update(self, container, context): - import hou repre_entity = context["representation"] hda_node = container["node"] @@ -66,4 +90,79 @@ class HdaLoader(load.LoaderPlugin): def remove(self, container): node = container["node"] + parent = node.parent() node.destroy() + + if parent.type().category() == hou.objNodeTypeCategory(): + return + + # Remove parent if empty. + if not parent.children(): + parent.destroy() + + def _create_dedicated_parent_node(self, hda_def): + + # Get the root node + parent_node = get_avalon_container() + node = None + node_type = None + if hda_def.nodeTypeCategory() == hou.objNodeTypeCategory(): + return parent_node + elif hda_def.nodeTypeCategory() == hou.chopNodeTypeCategory(): + node_type, node_name = "chopnet", "MOTION" + elif hda_def.nodeTypeCategory() == hou.cop2NodeTypeCategory(): + node_type, node_name = "cop2net", "IMAGES" + elif hda_def.nodeTypeCategory() == hou.dopNodeTypeCategory(): + node_type, node_name = "dopnet", "DOPS" + elif hda_def.nodeTypeCategory() == hou.ropNodeTypeCategory(): + node_type, node_name = "ropnet", "ROPS" + elif hda_def.nodeTypeCategory() == hou.lopNodeTypeCategory(): + node_type, node_name = "lopnet", "LOPS" + elif hda_def.nodeTypeCategory() == hou.sopNodeTypeCategory(): + node_type, node_name = "geo", "SOPS" + elif hda_def.nodeTypeCategory() == hou.topNodeTypeCategory(): + node_type, node_name = "topnet", "TOPS" + # TODO: Create a dedicated parent node based on Vop Node vex context. + elif hda_def.nodeTypeCategory() == hou.vopNodeTypeCategory(): + parent_node, node_type, node_name = self._get_dedicated_parent_for_vop(parent_node, hda_def) + + node = parent_node.node(node_name) + if not node: + node = parent_node.createNode(node_type, node_name) + + node.moveToGoodPosition() + return node + + def _get_dedicated_parent_for_vop(self, parent_node, hda_def): + """create_dedicated_parent_for_vop + + VOPs are a little special because they can exist + in different contexts. + e.g. + - Material Network + - SOPs > VOP nodes + - Chop > VOP nodes + - Cop2 > VOP nodes + + Get the vex context of the HDA and return + dedicated node_type and node_name. + + It creates intermediate nodes if needed. + """ + vex_context = hou.vexContextForNodeTypeCategory(hda_def.nodeTypeCategory()) + + if vex_context: + new_parent = parent_node.node("VOPS") + if not new_parent: + new_parent = parent_node.createNode("vopnet", "VOPS") + new_parent.moveToGoodPosition() + + if vex_context.name() == "Cop2": + return new_parent, "cop2filter", "COP_VOP" + elif vex_context.name() == "Chop": + return new_parent, "chop", "CHOP_VOP" + elif vex_context.name() == "Sop": + return new_parent, "sop", "SOP_VOP" + + # Fall to material net if no vex context found + return parent_node, "matnet", "MATS" From 01906888d959df4d3445b78f6e3bd29276bc5704 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 16:28:14 +0300 Subject: [PATCH 017/118] update ToolLocation --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index a810f06283..e6aeba270c 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -109,7 +109,8 @@ class CreateHDA(plugin.HoudiniCreator): if pre_create_data.get("use_project"): tool_name = hou.shelves.defaultToolName( hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) - hou.shelves.tool(tool_name).setToolLocations((self.project_name,)) + hou.shelves.tool(tool_name).setToolLocations( + ("AYON/{}".format(self.project_name),)) return hda_node From f8d07c0045d8437838f2e4d56b0e0e6ad1bc1c74 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 16:32:15 +0300 Subject: [PATCH 018/118] update Tooltip --- client/ayon_core/hosts/houdini/plugins/create/create_hda.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index e6aeba270c..3ca82ccb60 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -150,7 +150,9 @@ class CreateHDA(plugin.HoudiniCreator): default=False, label="Set Current User"), BoolDef("use_project", - tooltip="Use project name as tab submenu path", + tooltip="Use project name as tab submenu path.\n" + "The location in TAB Menu will be\n" + "'AYON/project_name/your_HDA_name'", default=True, label="Use Project as menu entry"), ] From 4f532560f3c958fb6431e2a7cad22340ece7700b Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 18:09:23 +0300 Subject: [PATCH 019/118] add use_project to creator_attributes --- .../ayon_core/hosts/houdini/plugins/create/create_hda.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 3ca82ccb60..462ae5eb7c 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -117,6 +117,13 @@ class CreateHDA(plugin.HoudiniCreator): def create(self, product_name, instance_data, pre_create_data): instance_data.pop("active", None) + # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + for key in {"use_project"}: + if key in pre_create_data: + creator_attributes[key] = pre_create_data[key] + return super(CreateHDA, self).create( product_name, instance_data, From 9b4d36eee848b9dc8704b03f263a6c70c4d1e7a2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 18:09:50 +0300 Subject: [PATCH 020/118] set HDA TAB menu loaction interactively --- client/ayon_core/hosts/houdini/plugins/load/load_hda.py | 7 +++++++ .../hosts/houdini/plugins/publish/extract_hda.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py index b2d2337266..2faa403858 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py @@ -59,6 +59,13 @@ class HdaLoader(load.LoaderPlugin): hda_node = parent_node.createNode(name, node_name) hda_node.moveToGoodPosition() + # Set TAB Menu location interactively + # This shouldn't be needed if the Tool Location is saved in the HDA. + tool_name = hou.shelves.defaultToolName( + hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) + hou.shelves.tool(tool_name).setToolLocations( + ("AYON/{}".format(context["project"]["name"]),)) + # Imprint it manually data = { "schema": "openpype:container-2.0", diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py index 5fe83e0dcf..5ac102332a 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py @@ -26,6 +26,14 @@ class ExtractHDA(publish.Extractor): hda_def.setOptions(hda_options) hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options) + if instance.data["creator_attributes"].get("use_project"): + # Set TAB Menu location interactively + # This shouldn't be needed if the Tool Location is saved in the HDA. + tool_name = hou.shelves.defaultToolName( + hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) + hou.shelves.tool(tool_name).setToolLocations( + ("AYON/{}".format(self.project_name),)) + if "representations" not in instance.data: instance.data["representations"] = [] From a5e664619457c2a41f4c97abd16ad4d7f1e4d57e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 18:13:55 +0300 Subject: [PATCH 021/118] use matnet as the parent node to load VOP HDAs --- .../hosts/houdini/plugins/load/load_hda.py | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py index 2faa403858..9368c8d2fb 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py @@ -131,7 +131,7 @@ class HdaLoader(load.LoaderPlugin): node_type, node_name = "topnet", "TOPS" # TODO: Create a dedicated parent node based on Vop Node vex context. elif hda_def.nodeTypeCategory() == hou.vopNodeTypeCategory(): - parent_node, node_type, node_name = self._get_dedicated_parent_for_vop(parent_node, hda_def) + node_type, node_name = "matnet", "MATSandVOPS" node = parent_node.node(node_name) if not node: @@ -139,37 +139,3 @@ class HdaLoader(load.LoaderPlugin): node.moveToGoodPosition() return node - - def _get_dedicated_parent_for_vop(self, parent_node, hda_def): - """create_dedicated_parent_for_vop - - VOPs are a little special because they can exist - in different contexts. - e.g. - - Material Network - - SOPs > VOP nodes - - Chop > VOP nodes - - Cop2 > VOP nodes - - Get the vex context of the HDA and return - dedicated node_type and node_name. - - It creates intermediate nodes if needed. - """ - vex_context = hou.vexContextForNodeTypeCategory(hda_def.nodeTypeCategory()) - - if vex_context: - new_parent = parent_node.node("VOPS") - if not new_parent: - new_parent = parent_node.createNode("vopnet", "VOPS") - new_parent.moveToGoodPosition() - - if vex_context.name() == "Cop2": - return new_parent, "cop2filter", "COP_VOP" - elif vex_context.name() == "Chop": - return new_parent, "chop", "CHOP_VOP" - elif vex_context.name() == "Sop": - return new_parent, "sop", "SOP_VOP" - - # Fall to material net if no vex context found - return parent_node, "matnet", "MATS" From 943a36130f56f54aba49ba3e48b887344fea3ba4 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 22:04:54 +0300 Subject: [PATCH 022/118] Update 'creator>product_name_profiles': add Houdini HDA template to default values --- client/ayon_core/version.py | 2 +- package.py | 3 +-- server/settings/tools.py | 11 +++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a60de0493a..a0def120c4 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.1-dev.1" +__version__ = "0.3.1-dev.2" diff --git a/package.py b/package.py index 79450d029f..2a84c68f35 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.3.1-dev.1" +version = "0.3.1-dev.2" client_dir = "ayon_core" @@ -8,4 +8,3 @@ plugin_for = ["ayon_server"] requires = [ "~ayon_server-1.0.3+<2.0.0", ] - diff --git a/server/settings/tools.py b/server/settings/tools.py index fb8430a71c..ce3ceaea6e 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -403,6 +403,17 @@ DEFAULT_TOOLS_VALUES = { "task_types": [], "tasks": [], "template": "SK_{folder[name]}{variant}" + }, + { + "product_types": [ + "hda" + ], + "hosts": [ + "houdini" + ], + "task_types": [], + "tasks": [], + "template": "{folder[label]}_{variant}" } ] }, From 95f33b2da2d175e29198855322d1f876bed25d28 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 22:06:22 +0300 Subject: [PATCH 023/118] add folder name and label to dynamic data --- .../houdini/plugins/create/create_hda.py | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 462ae5eb7c..957da8f230 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -51,12 +51,12 @@ class CreateHDA(plugin.HoudiniCreator): # selected nodes ... if self.selected_nodes[0].type().name() == "subnet": to_hda = self.selected_nodes[0] - to_hda.setName("{}_subnet".format(node_name), unique_name=True) + to_hda.setName("{}_HDA".format(node_name), unique_name=True) else: parent_node = self.selected_nodes[0].parent() subnet = parent_node.collapseIntoSubnet( self.selected_nodes, - subnet_name="{}_subnet".format(node_name)) + subnet_name="{}_HDA".format(node_name)) subnet.moveToGoodPosition() to_hda = subnet else: @@ -69,7 +69,7 @@ class CreateHDA(plugin.HoudiniCreator): parent_node = pane.pwd() to_hda = parent_node.createNode( - "subnet", node_name="{}_subnet".format(node_name)) + "subnet", node_name="{}_HDA".format(node_name)) if not to_hda.type().definition(): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. @@ -163,3 +163,30 @@ class CreateHDA(plugin.HoudiniCreator): default=True, label="Use Project as menu entry"), ] + + def get_dynamic_data( + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance + ): + """ + Pass product name from product name templates as dynamic data. + """ + dynamic_data = super(CreateHDA, self).get_dynamic_data( + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance + ) + + dynamic_data["folder"] = { + "label": folder_entity["label"], + "name": folder_entity["name"] + } + return dynamic_data From 389288a50649526c1c438cea5b720e32a0c7f1d7 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 9 May 2024 22:06:40 +0300 Subject: [PATCH 024/118] add hda to families --- .../hosts/houdini/plugins/publish/validate_subset_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py index 0481929824..1bcd2dada2 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py @@ -25,7 +25,7 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, """ - families = ["staticMesh"] + families = ["staticMesh", "hda"] hosts = ["houdini"] label = "Validate Product Name" order = ValidateContentsOrder + 0.1 From fa8b4e808b8f45a1f1672f015bf8d46ea61bd4a6 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 10 May 2024 12:04:33 +0300 Subject: [PATCH 025/118] fux bug, get project name from context --- client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py index 5ac102332a..1a3bcacc38 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_hda.py @@ -32,7 +32,7 @@ class ExtractHDA(publish.Extractor): tool_name = hou.shelves.defaultToolName( hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) hou.shelves.tool(tool_name).setToolLocations( - ("AYON/{}".format(self.project_name),)) + ("AYON/{}".format(instance.context.data["projectName"]),)) if "representations" not in instance.data: instance.data["representations"] = [] From bc2c9479d557464be6d8b001f73bc6d66e9a35cf Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 10 May 2024 12:05:17 +0300 Subject: [PATCH 026/118] add missing dynamic data keys --- .../hosts/houdini/plugins/publish/validate_subset_name.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py index 1bcd2dada2..6c79687d88 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py @@ -67,7 +67,13 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, instance.context.data["hostName"], instance.data["productType"], variant=instance.data["variant"], - dynamic_data={"asset": folder_entity["name"]} + dynamic_data={ + "asset": folder_entity["name"], + "folder": { + "label": folder_entity["label"], + "name": folder_entity["name"] + } + } ) if instance.data.get("productName") != product_name: From e95fcbe995ce018564cf235cf8e3da795bd384d6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 10 May 2024 23:39:51 +0800 Subject: [PATCH 027/118] add joint into accepted_controllers --- .../hosts/maya/plugins/publish/validate_animated_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py index 2ba2bff6fc..c9dcc662af 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py @@ -16,7 +16,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin, hosts = ["maya"] families = ["animation.fbx"] label = "Animated Reference Rig" - accepted_controllers = ["transform", "locator"] + accepted_controllers = ["transform", "locator", "joint"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] optional = False From c02d5522f3d6617c27fd4fe9da2e0c5de999f503 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 10 May 2024 23:58:34 +0800 Subject: [PATCH 028/118] make sure the animation doesnt get resample --- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py b/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py index ee66ed2fb7..36dc1b1544 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -36,6 +36,7 @@ class ExtractFBXAnimation(publish.Extractor): out_members = instance.data.get("animated_skeleton", []) # Export instance.data["constraints"] = True + instance.data["bakeResampleAnimation"] = False instance.data["skeletonDefinitions"] = True instance.data["referencedAssetsContent"] = True fbx_exporter.set_options_from_instance(instance) From 1b70c0a6a4bd08815ab680549b7dd99f199c85ec Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 10 May 2024 23:59:19 +0800 Subject: [PATCH 029/118] revert commits --- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 1 - .../hosts/maya/plugins/publish/validate_animated_reference.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py b/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py index 36dc1b1544..ee66ed2fb7 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -36,7 +36,6 @@ class ExtractFBXAnimation(publish.Extractor): out_members = instance.data.get("animated_skeleton", []) # Export instance.data["constraints"] = True - instance.data["bakeResampleAnimation"] = False instance.data["skeletonDefinitions"] = True instance.data["referencedAssetsContent"] = True fbx_exporter.set_options_from_instance(instance) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py index c9dcc662af..2ba2bff6fc 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py @@ -16,7 +16,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin, hosts = ["maya"] families = ["animation.fbx"] label = "Animated Reference Rig" - accepted_controllers = ["transform", "locator", "joint"] + accepted_controllers = ["transform", "locator"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] optional = False From 888c4ff35434588fc28cc468753a1ac9ce05001f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 23 May 2024 12:18:36 +0300 Subject: [PATCH 030/118] revert changes to ayon-core version --- client/ayon_core/version.py | 2 +- package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 28731bcb47..275e1b1dd6 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.3.2-dev.2" +__version__ = "0.3.2-dev.1" diff --git a/package.py b/package.py index f5d1ee3d03..b7b8d2dae6 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.3.2-dev.2" +version = "0.3.2-dev.1" client_dir = "ayon_core" From d54ddf2210d5f023837abf5ddcf26df760600158 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 23 May 2024 12:25:39 +0300 Subject: [PATCH 031/118] replace getpass.getuser() with get_ayon_username() --- .../ayon_core/hosts/houdini/plugins/create/create_hda.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 957da8f230..7f270b5cc2 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- """Creator plugin for creating publishable Houdini Digital Assets.""" import ayon_api -import getpass from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin -from ayon_core.lib import BoolDef +from ayon_core.lib import ( + get_ayon_username, + BoolDef +) import hou from ayon_core.resources import get_ayon_icon_filepath @@ -104,7 +106,7 @@ class CreateHDA(plugin.HoudiniCreator): hda_def.setIcon(get_ayon_icon_filepath()) if pre_create_data.get("set_user"): - hda_def.setUserInfo(getpass.getuser()) + hda_def.setUserInfo(get_ayon_username()) if pre_create_data.get("use_project"): tool_name = hou.shelves.defaultToolName( From 0d49b83b33d172988808ab699fed4837ba61e6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 31 May 2024 12:49:42 +0200 Subject: [PATCH 032/118] :recycle: check for invalid reference compatible with maya 2022 --- server_addon/maya/client/ayon_maya/api/lib.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/api/lib.py b/server_addon/maya/client/ayon_maya/api/lib.py index 2b41ffc06c..b9b0795ebf 100644 --- a/server_addon/maya/client/ayon_maya/api/lib.py +++ b/server_addon/maya/client/ayon_maya/api/lib.py @@ -1721,7 +1721,7 @@ def is_valid_reference_node(reference_node): Reference node 'reference_node' is not associated with a reference file. Note that this does *not* check whether the reference node points to an - existing file. Instead it only returns whether maya considers it valid + existing file. Instead, it only returns whether maya considers it valid and thus is not an unassociated reference node Arguments: @@ -1734,6 +1734,15 @@ def is_valid_reference_node(reference_node): sel = OpenMaya.MSelectionList() sel.add(reference_node) depend_node = sel.getDependNode(0) + # maya 2022 is missing `isValidReference` so the check needs to be + # done in different way. + if cmds.about(version=True) < 2023: + try: + cmds.referenceQuery(reference_node, filename=True) + return True + except RuntimeError: + return False + return OpenMaya.MFnReference(depend_node).isValidReference() From f482c9e788364fc4f434be1dd2c0f3c8e4390cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 31 May 2024 17:34:18 +0200 Subject: [PATCH 033/118] Update server_addon/maya/client/ayon_maya/api/lib.py Co-authored-by: Roy Nieterau --- server_addon/maya/client/ayon_maya/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/api/lib.py b/server_addon/maya/client/ayon_maya/api/lib.py index b9b0795ebf..3b351ec1f0 100644 --- a/server_addon/maya/client/ayon_maya/api/lib.py +++ b/server_addon/maya/client/ayon_maya/api/lib.py @@ -1731,9 +1731,6 @@ def is_valid_reference_node(reference_node): bool: Whether reference node is a valid reference """ - sel = OpenMaya.MSelectionList() - sel.add(reference_node) - depend_node = sel.getDependNode(0) # maya 2022 is missing `isValidReference` so the check needs to be # done in different way. if cmds.about(version=True) < 2023: @@ -1742,6 +1739,9 @@ def is_valid_reference_node(reference_node): return True except RuntimeError: return False + sel = OpenMaya.MSelectionList() + sel.add(reference_node) + depend_node = sel.getDependNode(0) return OpenMaya.MFnReference(depend_node).isValidReference() From 0cccc9a7e8d52abcf7a03750a90d323c945d0d54 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 3 Jun 2024 21:02:43 +0300 Subject: [PATCH 034/118] add missing dynamic data keys in repair action --- .../ayon_houdini/plugins/publish/validate_subset_name.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py index 999dcc3981..4f15f193fc 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py @@ -103,7 +103,13 @@ class ValidateSubsetName(plugin.HoudiniInstancePlugin, instance.context.data["hostName"], instance.data["productType"], variant=instance.data["variant"], - dynamic_data={"asset": folder_entity["name"]} + dynamic_data={ + "asset": folder_entity["name"], + "folder": { + "label": folder_entity["label"], + "name": folder_entity["name"] + } + } ) instance.data["productName"] = product_name From 17ec19f5069860be184733254db79f4a86308de5 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 12 Jun 2024 22:24:37 +0300 Subject: [PATCH 035/118] set tab shelf location properly and revert unnecessary logic --- .../client/ayon_houdini/plugins/create/create_hda.py | 7 +++---- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 7 ------- .../client/ayon_houdini/plugins/publish/extract_hda.py | 8 -------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index c727fe0b09..e0929e16f7 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Creator plugin for creating publishable Houdini Digital Assets.""" import hou +from assettools import setToolSubmenu + import ayon_api from ayon_core.pipeline import CreatorError from ayon_core.lib import ( @@ -109,10 +111,7 @@ class CreateHDA(plugin.HoudiniCreator): hda_def.setUserInfo(get_ayon_username()) if pre_create_data.get("use_project"): - tool_name = hou.shelves.defaultToolName( - hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) - hou.shelves.tool(tool_name).setToolLocations( - ("AYON/{}".format(self.project_name),)) + setToolSubmenu(hda_def, "AYON/{}".format(self.project_name)) return hda_node diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index 3ee487f496..85477965cd 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -62,13 +62,6 @@ class HdaLoader(plugin.HoudiniLoader): hda_node = parent_node.createNode(name, node_name) hda_node.moveToGoodPosition() - # Set TAB Menu location interactively - # This shouldn't be needed if the Tool Location is saved in the HDA. - tool_name = hou.shelves.defaultToolName( - hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) - hou.shelves.tool(tool_name).setToolLocations( - ("AYON/{}".format(context["project"]["name"]),)) - # Imprint it manually data = { "schema": "openpype:container-2.0", diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_hda.py index 0ff7948a88..e4449d11f8 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_hda.py @@ -25,14 +25,6 @@ class ExtractHDA(plugin.HoudiniExtractorPlugin): hda_def.setOptions(hda_options) hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options) - if instance.data["creator_attributes"].get("use_project"): - # Set TAB Menu location interactively - # This shouldn't be needed if the Tool Location is saved in the HDA. - tool_name = hou.shelves.defaultToolName( - hda_def.nodeTypeCategory().name(), hda_def.nodeTypeName()) - hou.shelves.tool(tool_name).setToolLocations( - ("AYON/{}".format(instance.context.data["projectName"]),)) - if "representations" not in instance.data: instance.data["representations"] = [] From 45f72649464dc6071e6c92789de2176c411133cf Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 12 Jun 2024 22:47:36 +0300 Subject: [PATCH 036/118] revert changes in subnetwork naming, use postfix _subnet --- .../client/ayon_houdini/plugins/create/create_hda.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index e0929e16f7..18b26bb533 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -55,12 +55,12 @@ class CreateHDA(plugin.HoudiniCreator): # selected nodes ... if self.selected_nodes[0].type().name() == "subnet": to_hda = self.selected_nodes[0] - to_hda.setName("{}_HDA".format(node_name), unique_name=True) + to_hda.setName("{}_subnet".format(node_name), unique_name=True) else: parent_node = self.selected_nodes[0].parent() subnet = parent_node.collapseIntoSubnet( self.selected_nodes, - subnet_name="{}_HDA".format(node_name)) + subnet_name="{}_subnet".format(node_name)) subnet.moveToGoodPosition() to_hda = subnet else: @@ -73,7 +73,7 @@ class CreateHDA(plugin.HoudiniCreator): parent_node = pane.pwd() to_hda = parent_node.createNode( - "subnet", node_name="{}_HDA".format(node_name)) + "subnet", node_name="{}_subnet".format(node_name)) if not to_hda.type().definition(): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. From 65e8b0383ac4233d7d625ac7c6bfcae231e899f3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 13 Jun 2024 22:29:52 +0300 Subject: [PATCH 037/118] update default hda product name profile --- server/settings/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index f73287eafd..3ed12d3d0a 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -458,7 +458,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "tasks": [], - "template": "{folder[label]}_{variant}" + "template": "{folder[name]}_{variant}" } ], "filter_creator_profiles": [] From 503e8bce3d30ea03d622bea1196e6ff3ff963cb3 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Thu, 13 Jun 2024 22:36:29 +0300 Subject: [PATCH 038/118] skip str casting Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index 85477965cd..46fcb74b2c 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -68,7 +68,7 @@ class HdaLoader(plugin.HoudiniLoader): "id": AVALON_CONTAINER_ID, "name": node_name, "namespace": namespace, - "loader": str(self.__class__.__name__), + "loader": self.__class__.__name__, "representation": context["representation"]["id"], } From 121fa8a98b1f204b0d20d004e4178c414c69d772 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 14 Jun 2024 16:50:19 +0300 Subject: [PATCH 039/118] revert 'use ayon icon as HDA icon' --- .../client/ayon_houdini/plugins/create/create_hda.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index 18b26bb533..1b9ffb1b5e 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -9,7 +9,7 @@ from ayon_core.lib import ( get_ayon_username, BoolDef ) -from ayon_core.resources import get_ayon_icon_filepath + from ayon_houdini.api import plugin @@ -104,9 +104,6 @@ class CreateHDA(plugin.HoudiniCreator): # Set Custom settings. hda_def = hda_node.type().definition() - if pre_create_data.get("use_ayon_icon"): - hda_def.setIcon(get_ayon_icon_filepath()) - if pre_create_data.get("set_user"): hda_def.setUserInfo(get_ayon_username()) @@ -149,10 +146,6 @@ class CreateHDA(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super(CreateHDA, self).get_pre_create_attr_defs() return attrs + [ - BoolDef("use_ayon_icon", - tooltip="Use Ayon icon for the digital asset.", - default=False, - label="Use AYON Icon"), BoolDef("set_user", tooltip="Set current user as the author of the HDA", default=False, From eea56697995c664107eeeb9358c096021ef06e52 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 14 Jun 2024 17:31:10 +0300 Subject: [PATCH 040/118] don't transfere creator attribute --- .../client/ayon_houdini/plugins/create/create_hda.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index 72107bef72..e03f290ae8 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -128,13 +128,6 @@ class CreateHDA(plugin.HoudiniCreator): def create(self, product_name, instance_data, pre_create_data): instance_data.pop("active", None) - # Transfer settings from pre create to instance - creator_attributes = instance_data.setdefault( - "creator_attributes", dict()) - for key in {"use_project"}: - if key in pre_create_data: - creator_attributes[key] = pre_create_data[key] - return super(CreateHDA, self).create( product_name, instance_data, From 71be4e99a7e04356ea3c3912ab9706dbf99b3333 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 14 Jun 2024 17:40:06 +0300 Subject: [PATCH 041/118] use the last HDA Def --- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index 289b8beb69..eb9a74a7ad 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -55,10 +55,10 @@ class HdaLoader(plugin.HoudiniLoader): if not hda_defs: raise LoadError(f"No HDA definitions found in file: {file_path}") - parent_node = self._create_dedicated_parent_node(hda_defs[0]) + parent_node = self._create_dedicated_parent_node(hda_defs[-1]) # Get the type name from the HDA definition. - type_name = hda_defs[0].nodeTypeName() + type_name = hda_defs[-1].nodeTypeName() hda_node = parent_node.createNode(type_name, node_name) hda_node.moveToGoodPosition() From a2ba61c6615d19fe085e439b11b8ffe3cc0e3247 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:37:58 +0200 Subject: [PATCH 042/118] prepare helper classes to track csv data --- .../plugins/create/create_csv_ingest.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index 5a5deeada8..3d804f6189 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -16,6 +16,198 @@ from ayon_core.pipeline.create import CreatorError from ayon_traypublisher.api.plugin import TrayPublishCreator +def _get_row_value_with_validation( + columns_config: Dict[str, Any], + column_name: str, + row_data: Dict[str, Any], +): + """Get row value with validation""" + + # get column data from column config + column_data = None + for column in columns_config["columns"]: + if column["name"] == column_name: + column_data = column + break + + if not column_data: + raise CreatorError( + f"Column '{column_name}' not found in column config." + ) + + # get column value from row + column_value = row_data.get(column_name) + column_required = column_data["required_column"] + + # check if column value is not empty string and column is required + if column_value == "" and column_required: + raise CreatorError( + f"Value in column '{column_name}' is required." + ) + + # get column type + column_type = column_data["type"] + # get column validation regex + column_validation = column_data["validation_pattern"] + # get column default value + column_default = column_data["default"] + + if column_type in ["number", "decimal"] and column_default == 0: + column_default = None + + # check if column value is not empty string + if column_value == "": + # set default value if column value is empty string + column_value = column_default + + # set column value to correct type following column type + if column_type == "number" and column_value is not None: + column_value = int(column_value) + elif column_type == "decimal" and column_value is not None: + column_value = float(column_value) + elif column_type == "bool": + column_value = column_value in ["true", "True"] + + # check if column value matches validation regex + if ( + column_value is not None and + not re.match(str(column_validation), str(column_value)) + ): + raise CreatorError( + f"Column '{column_name}' value '{column_value}'" + f" does not match validation regex '{column_validation}'" + f"\nRow data: {row_data}" + f"\nColumn data: {column_data}" + ) + + return column_value + + +class RepreItem: + def __init__( + self, + name, + filepath, + frame_start, + frame_end, + handle_start, + handle_end, + fps, + thumbnail_path, + colorspace, + comment, + slate_exists, + tags, + ): + self.name = name + self.filepath = filepath + self.frame_start = frame_start + self.frame_end = frame_end + self.handle_start = handle_start + self.handle_end = handle_end + self.fps = fps + self.thumbnail_path = thumbnail_path + self.colorspace = colorspace + self.comment = comment + self.slate_exists = slate_exists + self.tags = tags + + @classmethod + def from_csv_row(cls, columns_config, repre_config, row): + kwargs = { + dst_key: _get_row_value_with_validation( + columns_config, column_name, row + ) + for column_name, dst_key in ( + # Representation information + ("filepath", "File Path"), + ("frame_start", "Frame Start"), + ("frame_end", "Frame End"), + ("handle_start", "Handle Start"), + ("handle_end", "Handle End"), + ("fps", "FPS"), + + # Optional representation information + ("thumbnail_path", "Version Thumbnail"), + ("colorspace", "Representation Colorspace"), + ("comment", "Version Comment"), + ("name", "Representation"), + ("slate_exists", "Slate Exists"), + ("repre_tags", "Representation Tags"), + ) + } + + # Should the 'int' and 'float' conversion happen? + # - looks like '_get_row_value_with_validation' is already handling it + for key in {"frame_start", "frame_end", "handle_start", "handle_end"}: + kwargs[key] = int(kwargs[key]) + + kwargs["fps"] = float(kwargs["fps"]) + + # Convert tags value to list + tags_list = copy(repre_config["default_tags"]) + repre_tags: Optional[str] = kwargs.pop("repre_tags") + if repre_tags: + tags_list = [] + tags_delimiter = repre_config["tags_delimiter"] + # strip spaces from repre_tags + if tags_delimiter in repre_tags: + tags = repre_tags.split(tags_delimiter) + for _tag in tags: + tags_list.append(_tag.strip().lower()) + else: + tags_list.append(repre_tags) + kwargs["tags"] = tags_list + return cls(**kwargs) + + +class ProductItem: + def __init__( + self, + folder_path: str, + task_name: str, + version: int, + variant: str, + product_type: str, + pre_product_name: str, + task_type: Optional[str] = None, + ): + self.folder_path = folder_path + self.task_name = task_name + self.task_type = task_type + self.version = version + self.variant = variant + self.product_type = product_type + self.pre_product_name = pre_product_name + self.repre_items: List[RepreItem] = [] + + def add_repre_item(self, repre_item: RepreItem): + self.repre_items.append(repre_item) + + @classmethod + def from_csv_row(cls, columns_config, row): + kwargs = { + dst_key: _get_row_value_with_validation( + columns_config, column_name, row + ) + for column_name, dst_key in ( + # Context information + ("folder_path", "Folder Path"), + ("task_name", "Task Name"), + ("version", "Version"), + ("variant", "Variant"), + ("product_type", "Product Type"), + ) + } + + kwargs["pre_product_name"] = ( + "{task_name}{variant}{product_type}{version}" + .format(**kwargs) + .replace(" ", "").lower() + ) + return cls(**kwargs) + + class IngestCSV(TrayPublishCreator): """CSV ingest creator class""" From 17b56c2578bad54394db57583ae6dfe492978665 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:40:45 +0200 Subject: [PATCH 043/118] refactored the code to use new items --- .../plugins/create/create_csv_ingest.py | 1186 ++++++++--------- 1 file changed, 524 insertions(+), 662 deletions(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index 3d804f6189..963263a049 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -1,11 +1,14 @@ import os import re import csv -import clique +import collections from io import StringIO from copy import deepcopy, copy +from typing import Optional, List, Set, Dict, Union, Any + +import clique +import ayon_api -from ayon_api import get_folder_by_path, get_task_by_name from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline import CreatedInstance from ayon_core.lib import FileDef, BoolDef @@ -232,658 +235,64 @@ configuration in project settings. columns_config = {} representations_config = {} - def create(self, subset_name, instance_data, pre_create_data): - """Create an product from each row found in the CSV. + def get_instance_attr_defs(self): + return [ + BoolDef( + "add_review_family", + default=True, + label="Review" + ) + ] + + def get_pre_create_attr_defs(self): + """Creating pre-create attributes at creator plugin. + + Returns: + list: list of attribute object instances + """ + # Use same attributes as for instance attributes + return [ + FileDef( + "csv_filepath_data", + folders=False, + extensions=[".csv"], + allow_sequences=False, + single_item=True, + label="CSV File", + ), + ] + + def create( + self, + product_name: str, + instance_data: Dict[str, Any], + pre_create_data: Dict[str, Any] + ): + """Create product from each row found in the CSV. Args: - subset_name (str): The subset name. + product_name (str): The subset name. instance_data (dict): The instance data. pre_create_data (dict): """ csv_filepath_data = pre_create_data.get("csv_filepath_data", {}) - folder = csv_filepath_data.get("directory", "") - if not os.path.exists(folder): + csv_dir = csv_filepath_data.get("directory", "") + if not os.path.exists(csv_dir): raise CreatorError( - f"Directory '{folder}' does not exist." + f"Directory '{csv_dir}' does not exist." ) filename = csv_filepath_data.get("filenames", []) - self._process_csv_file(subset_name, instance_data, folder, filename[0]) - - def _process_csv_file( - self, subset_name, instance_data, staging_dir, filename): - """Process CSV file. - - Args: - subset_name (str): The subset name. - instance_data (dict): The instance data. - staging_dir (str): The staging directory. - filename (str): The filename. - """ - - # create new instance from the csv file via self function - self._pass_data_to_csv_instance( - instance_data, - staging_dir, - filename + self._process_csv_file( + product_name, instance_data, csv_dir, filename[0] ) - csv_instance = CreatedInstance( - self.product_type, subset_name, instance_data, self - ) - self._store_new_instance(csv_instance) - - csv_instance["csvFileData"] = { - "filename": filename, - "staging_dir": staging_dir, - } - - # from special function get all data from csv file and convert them - # to new instances - csv_data_for_instances = self._get_data_from_csv( - staging_dir, filename) - - # create instances from csv data via self function - self._create_instances_from_csv_data( - csv_data_for_instances, staging_dir - ) - - def _create_instances_from_csv_data( - self, - csv_data_for_instances, - staging_dir - ): - """Create instances from csv data""" - - for folder_path, prepared_data in csv_data_for_instances.items(): - project_name = self.create_context.get_current_project_name() - products = prepared_data["products"] - - for instance_name, product_data in products.items(): - # get important instance variables - task_name = product_data["task_name"] - task_type = product_data["task_type"] - variant = product_data["variant"] - product_type = product_data["product_type"] - version = product_data["version"] - - # create subset/product name - product_name = get_product_name( - project_name, - task_name, - task_type, - self.host_name, - product_type, - variant - ) - - # make sure frame start/end is inherited from csv columns - # expected frame range data are handles excluded - for _, repre_data in product_data["representations"].items(): # noqa: E501 - frame_start = repre_data["frameStart"] - frame_end = repre_data["frameEnd"] - handle_start = repre_data["handleStart"] - handle_end = repre_data["handleEnd"] - fps = repre_data["fps"] - break - - # try to find any version comment in representation data - version_comment = next( - iter( - repre_data["comment"] - for repre_data in product_data["representations"].values() # noqa: E501 - if repre_data["comment"] - ), - None - ) - - # try to find any slate switch in representation data - slate_exists = any( - repre_data["slate"] - for _, repre_data in product_data["representations"].items() # noqa: E501 - ) - - # get representations from product data - representations = product_data["representations"] - label = f"{folder_path}_{product_name}_v{version:>03}" - - families = ["csv_ingest"] - if slate_exists: - # adding slate to families mainly for loaders to be able - # to filter out slates - families.append("slate") - - # make product data - product_data = { - "name": instance_name, - "folderPath": folder_path, - "families": families, - "label": label, - "task": task_name, - "variant": variant, - "source": "csv", - "frameStart": frame_start, - "frameEnd": frame_end, - "handleStart": handle_start, - "handleEnd": handle_end, - "fps": fps, - "version": version, - "comment": version_comment, - } - - # create new instance - new_instance = CreatedInstance( - product_type, product_name, product_data, self - ) - self._store_new_instance(new_instance) - - if not new_instance.get("prepared_data_for_repres"): - new_instance["prepared_data_for_repres"] = [] - - base_thumbnail_repre_data = { - "name": "thumbnail", - "ext": None, - "files": None, - "stagingDir": None, - "stagingDir_persistent": True, - "tags": ["thumbnail", "delete"], - } - # need to populate all thumbnails for all representations - # so we can check if unique thumbnail per representation - # is needed - thumbnails = [ - repre_data["thumbnailPath"] - for repre_data in representations.values() - if repre_data["thumbnailPath"] - ] - multiple_thumbnails = len(set(thumbnails)) > 1 - explicit_output_name = None - thumbnails_processed = False - for filepath, repre_data in representations.items(): - # check if any review derivate tag is present - reviewable = any( - tag for tag in repre_data.get("tags", []) - # tag can be `ftrackreview` or `review` - if "review" in tag - ) - # since we need to populate multiple thumbnails as - # representation with outputName for (Ftrack instance - # integrator) pairing with reviewable video representations - if ( - thumbnails - and multiple_thumbnails - and reviewable - ): - # multiple unique thumbnails per representation needs - # grouping by outputName - # mainly used in Ftrack instance integrator - explicit_output_name = repre_data["representationName"] - relative_thumbnail_path = repre_data["thumbnailPath"] - # representation might not have thumbnail path - # so ignore this one - if not relative_thumbnail_path: - continue - thumb_dir, thumb_file = \ - self._get_refactor_thumbnail_path( - staging_dir, relative_thumbnail_path) - filename, ext = os.path.splitext(thumb_file) - thumbnail_repr_data = deepcopy( - base_thumbnail_repre_data) - thumbnail_repr_data.update({ - "name": "thumbnail_{}".format(filename), - "ext": ext[1:], - "files": thumb_file, - "stagingDir": thumb_dir, - "outputName": explicit_output_name, - }) - new_instance["prepared_data_for_repres"].append({ - "type": "thumbnail", - "colorspace": None, - "representation": thumbnail_repr_data, - }) - # also add thumbnailPath for ayon to integrate - if not new_instance.get("thumbnailPath"): - new_instance["thumbnailPath"] = ( - os.path.join(thumb_dir, thumb_file) - ) - elif ( - thumbnails - and not multiple_thumbnails - and not thumbnails_processed - or not reviewable - ): - """ - For case where we have only one thumbnail - and not reviewable medias. This needs to be processed - only once per instance. - """ - if not thumbnails: - continue - # here we will use only one thumbnail for - # all representations - relative_thumbnail_path = repre_data["thumbnailPath"] - # popping last thumbnail from list since it is only one - # and we do not need to iterate again over it - if not relative_thumbnail_path: - relative_thumbnail_path = thumbnails.pop() - thumb_dir, thumb_file = \ - self._get_refactor_thumbnail_path( - staging_dir, relative_thumbnail_path) - _, ext = os.path.splitext(thumb_file) - thumbnail_repr_data = deepcopy( - base_thumbnail_repre_data) - thumbnail_repr_data.update({ - "ext": ext[1:], - "files": thumb_file, - "stagingDir": thumb_dir - }) - new_instance["prepared_data_for_repres"].append({ - "type": "thumbnail", - "colorspace": None, - "representation": thumbnail_repr_data, - }) - # also add thumbnailPath for ayon to integrate - if not new_instance.get("thumbnailPath"): - new_instance["thumbnailPath"] = ( - os.path.join(thumb_dir, thumb_file) - ) - - thumbnails_processed = True - - # get representation data - representation_data = self._get_representation_data( - filepath, repre_data, staging_dir, - explicit_output_name - ) - - new_instance["prepared_data_for_repres"].append({ - "type": "media", - "colorspace": repre_data["colorspace"], - "representation": representation_data, - }) - - def _get_refactor_thumbnail_path( - self, staging_dir, relative_thumbnail_path): - thumbnail_abs_path = os.path.join( - staging_dir, relative_thumbnail_path) - return os.path.split( - thumbnail_abs_path) - - def _get_representation_data( - self, filepath, repre_data, staging_dir, explicit_output_name=None - ): - """Get representation data - - Args: - filepath (str): Filepath to representation file. - repre_data (dict): Representation data from CSV file. - staging_dir (str): Staging directory. - explicit_output_name (Optional[str]): Explicit output name. - For grouping purposes with reviewable components. - Defaults to None. - """ - - # get extension of file - basename = os.path.basename(filepath) - extension = os.path.splitext(filepath)[-1].lower() - - # validate filepath is having correct extension based on output - repre_name = repre_data["representationName"] - repre_config_data = None - for repre in self.representations_config["representations"]: - if repre["name"] == repre_name: - repre_config_data = repre - break - - if not repre_config_data: - raise CreatorError( - f"Representation '{repre_name}' not found " - "in config representation data." - ) - - validate_extensions = repre_config_data["extensions"] - if extension not in validate_extensions: - raise CreatorError( - f"File extension '{extension}' not valid for " - f"output '{validate_extensions}'." - ) - - is_sequence = (extension in IMAGE_EXTENSIONS) - # convert ### string in file name to %03d - # this is for correct frame range validation - # example: file.###.exr -> file.%03d.exr - if "#" in basename: - padding = len(basename.split("#")) - 1 - basename = basename.replace("#" * padding, f"%0{padding}d") - is_sequence = True - - # make absolute path to file - absfilepath = os.path.normpath(os.path.join(staging_dir, filepath)) - dirname = os.path.dirname(absfilepath) - - # check if dirname exists - if not os.path.isdir(dirname): - raise CreatorError( - f"Directory '{dirname}' does not exist." - ) - - # collect all data from dirname - paths_for_collection = [] - for file in os.listdir(dirname): - filepath = os.path.join(dirname, file) - paths_for_collection.append(filepath) - - collections, _ = clique.assemble(paths_for_collection) - - if collections: - collections = collections[0] - else: - if is_sequence: - raise CreatorError( - f"No collections found in directory '{dirname}'." - ) - - frame_start = None - frame_end = None - if is_sequence: - files = [os.path.basename(file) for file in collections] - frame_start = list(collections.indexes)[0] - frame_end = list(collections.indexes)[-1] - else: - files = basename - - tags = deepcopy(repre_data["tags"]) - # if slate in repre_data is True then remove one frame from start - if repre_data["slate"]: - tags.append("has_slate") - - # get representation data - representation_data = { - "name": repre_name, - "ext": extension[1:], - "files": files, - "stagingDir": dirname, - "stagingDir_persistent": True, - "tags": tags, - } - if extension in VIDEO_EXTENSIONS: - representation_data.update({ - "fps": repre_data["fps"], - "outputName": repre_name, - }) - - if explicit_output_name: - representation_data["outputName"] = explicit_output_name - - if frame_start: - representation_data["frameStart"] = frame_start - if frame_end: - representation_data["frameEnd"] = frame_end - - return representation_data - - def _get_data_from_csv( - self, package_dir, filename - ): - """Generate instances from the csv file""" - # get current project name and code from context.data - project_name = self.create_context.get_current_project_name() - - csv_file_path = os.path.join( - package_dir, filename - ) - - # make sure csv file contains columns from following list - required_columns = [ - column["name"] for column in self.columns_config["columns"] - if column["required_column"] - ] - - # read csv file - with open(csv_file_path, "r") as csv_file: - csv_content = csv_file.read() - - # read csv file with DictReader - csv_reader = csv.DictReader( - StringIO(csv_content), - delimiter=self.columns_config["csv_delimiter"] - ) - - # fix fieldnames - # sometimes someone can keep extra space at the start or end of - # the column name - all_columns = [ - " ".join(column.rsplit()) for column in csv_reader.fieldnames] - - # return back fixed fieldnames - csv_reader.fieldnames = all_columns - - # check if csv file contains all required columns - if any(column not in all_columns for column in required_columns): - raise CreatorError( - f"Missing required columns: {required_columns}" - ) - - csv_data = {} - # get data from csv file - for row in csv_reader: - # Get required columns first - # TODO: will need to be folder path in CSV - # TODO: `context_asset_name` is now `folder_path` - folder_path = self._get_row_value_with_validation( - "Folder Path", row) - task_name = self._get_row_value_with_validation( - "Task Name", row) - version = self._get_row_value_with_validation( - "Version", row) - - # Get optional columns - variant = self._get_row_value_with_validation( - "Variant", row) - product_type = self._get_row_value_with_validation( - "Product Type", row) - - pre_product_name = ( - f"{task_name}{variant}{product_type}" - f"{version}".replace(" ", "").lower() - ) - - # get representation data - filename, representation_data = \ - self._get_representation_row_data(row) - - # TODO: batch query of all folder paths and task names - - # get folder entity from folder path - folder_entity = get_folder_by_path( - project_name, folder_path) - - # make sure asset exists - if not folder_entity: - raise CreatorError( - f"Asset '{folder_path}' not found." - ) - - # first get all tasks on the folder entity and then find - task_entity = get_task_by_name( - project_name, folder_entity["id"], task_name) - - # check if task name is valid task in asset doc - if not task_entity: - raise CreatorError( - f"Task '{task_name}' not found in asset doc." - ) - - # get all csv data into one dict and make sure there are no - # duplicates data are already validated and sorted under - # correct existing asset also check if asset exists and if - # task name is valid task in asset doc and representations - # are distributed under products following variants - if folder_path not in csv_data: - csv_data[folder_path] = { - "folder_entity": folder_entity, - "products": { - pre_product_name: { - "task_name": task_name, - "task_type": task_entity["taskType"], - "variant": variant, - "product_type": product_type, - "version": version, - "representations": { - filename: representation_data, - }, - } - } - } - else: - csv_products = csv_data[folder_path]["products"] - if pre_product_name not in csv_products: - csv_products[pre_product_name] = { - "task_name": task_name, - "task_type": task_entity["taskType"], - "variant": variant, - "product_type": product_type, - "version": version, - "representations": { - filename: representation_data, - }, - } - else: - csv_representations = \ - csv_products[pre_product_name]["representations"] - if filename in csv_representations: - raise CreatorError( - f"Duplicate filename '{filename}' in csv file." - ) - csv_representations[filename] = representation_data - - return csv_data - - def _get_representation_row_data(self, row_data): - """Get representation row data""" - # Get required columns first - file_path = self._get_row_value_with_validation( - "File Path", row_data) - frame_start = self._get_row_value_with_validation( - "Frame Start", row_data) - frame_end = self._get_row_value_with_validation( - "Frame End", row_data) - handle_start = self._get_row_value_with_validation( - "Handle Start", row_data) - handle_end = self._get_row_value_with_validation( - "Handle End", row_data) - fps = self._get_row_value_with_validation( - "FPS", row_data) - - # Get optional columns - thumbnail_path = self._get_row_value_with_validation( - "Version Thumbnail", row_data) - colorspace = self._get_row_value_with_validation( - "Representation Colorspace", row_data) - comment = self._get_row_value_with_validation( - "Version Comment", row_data) - repre = self._get_row_value_with_validation( - "Representation", row_data) - slate_exists = self._get_row_value_with_validation( - "Slate Exists", row_data) - repre_tags = self._get_row_value_with_validation( - "Representation Tags", row_data) - - # convert tags value to list - tags_list = copy(self.representations_config["default_tags"]) - if repre_tags: - tags_list = [] - tags_delimiter = self.representations_config["tags_delimiter"] - # strip spaces from repre_tags - if tags_delimiter in repre_tags: - tags = repre_tags.split(tags_delimiter) - for _tag in tags: - tags_list.append(("".join(_tag.strip())).lower()) - else: - tags_list.append(repre_tags) - - representation_data = { - "colorspace": colorspace, - "comment": comment, - "representationName": repre, - "slate": slate_exists, - "tags": tags_list, - "thumbnailPath": thumbnail_path, - "frameStart": int(frame_start), - "frameEnd": int(frame_end), - "handleStart": int(handle_start), - "handleEnd": int(handle_end), - "fps": float(fps), - } - return file_path, representation_data - - def _get_row_value_with_validation( - self, column_name, row_data, default_value=None - ): - """Get row value with validation""" - - # get column data from column config - column_data = None - for column in self.columns_config["columns"]: - if column["name"] == column_name: - column_data = column - break - - if not column_data: - raise CreatorError( - f"Column '{column_name}' not found in column config." - ) - - # get column value from row - column_value = row_data.get(column_name) - column_required = column_data["required_column"] - - # check if column value is not empty string and column is required - if column_value == "" and column_required: - raise CreatorError( - f"Value in column '{column_name}' is required." - ) - - # get column type - column_type = column_data["type"] - # get column validation regex - column_validation = column_data["validation_pattern"] - # get column default value - column_default = default_value or column_data["default"] - - if column_type in ["number", "decimal"] and column_default == 0: - column_default = None - - # check if column value is not empty string - if column_value == "": - # set default value if column value is empty string - column_value = column_default - - # set column value to correct type following column type - if column_type == "number" and column_value is not None: - column_value = int(column_value) - elif column_type == "decimal" and column_value is not None: - column_value = float(column_value) - elif column_type == "bool": - column_value = column_value in ["true", "True"] - - # check if column value matches validation regex - if ( - column_value is not None and - not re.match(str(column_validation), str(column_value)) - ): - raise CreatorError( - f"Column '{column_name}' value '{column_value}' " - f"does not match validation regex '{column_validation}' \n" - f"Row data: {row_data} \n" - f"Column data: {column_data}" - ) - - return column_value - def _pass_data_to_csv_instance( - self, instance_data, staging_dir, filename + self, + instance_data: Dict[str, Any], + staging_dir: str, + filename: str ): """Pass CSV representation file to instance data""" @@ -902,30 +311,483 @@ configuration in project settings. "stagingDir_persistent": True, }) - def get_instance_attr_defs(self): - return [ - BoolDef( - "add_review_family", - default=True, - label="Review" - ) + def _process_csv_file( + self, + product_name: str, + instance_data: Dict[str, Any], + csv_dir: str, + filename: str + ): + """Process CSV file. + + Args: + product_name (str): The subset name. + instance_data (dict): The instance data. + csv_dir (str): The csv directory. + filename (str): The filename. + + """ + # create new instance from the csv file via self function + self._pass_data_to_csv_instance( + instance_data, + csv_dir, + filename + ) + + csv_instance = CreatedInstance( + self.product_type, product_name, instance_data, self + ) + self._store_new_instance(csv_instance) + + csv_instance["csvFileData"] = { + "filename": filename, + "staging_dir": csv_dir, + } + + # create instances from csv data via self function + self._create_instances_from_csv_data(csv_dir, filename) + + def _resolve_repre_path( + self, csv_dir: str, filepath: Union[str, None] + ) -> Union[str, None]: + if not filepath: + return filepath + + # Validate only existence of file directory as filename + # may contain frame specific char (e.g. '%04d' or '####'). + filedir, filename = os.path.split(filepath) + if not filedir or filedir == ".": + # If filedir is empty or "." then use same directory as + # csv path + filepath = os.path.join(csv_dir, filepath) + + elif not os.path.exists(filedir): + # If filepath does not exist, first try to find it in the + # same directory as the csv file is, but keep original + # value otherwise. + new_filedir = os.path.join(csv_dir, filedir) + if os.path.exists(new_filedir): + filepath = os.path.join(new_filedir, filename) + + return filepath + + def _get_data_from_csv( + self, csv_dir: str, filename: str + ) -> Dict[str, ProductItem]: + """Generate instances from the csv file""" + # get current project name and code from context.data + project_name = self.create_context.get_current_project_name() + csv_path = os.path.join(csv_dir, filename) + + # make sure csv file contains columns from following list + required_columns = [ + column["name"] + for column in self.columns_config["columns"] + if column["required_column"] ] - def get_pre_create_attr_defs(self): - """Creating pre-create attributes at creator plugin. + # read csv file + with open(csv_path, "r") as csv_file: + csv_content = csv_file.read() + + # read csv file with DictReader + csv_reader = csv.DictReader( + StringIO(csv_content), + delimiter=self.columns_config["csv_delimiter"] + ) + + # fix fieldnames + # sometimes someone can keep extra space at the start or end of + # the column name + all_columns = [ + " ".join(column.rsplit()) + for column in csv_reader.fieldnames + ] + + # return back fixed fieldnames + csv_reader.fieldnames = all_columns + + # check if csv file contains all required columns + if any(column not in all_columns for column in required_columns): + raise CreatorError( + f"Missing required columns: {required_columns}" + ) + + product_items_by_name: Dict[str, ProductItem] = {} + for row in csv_reader: + _product_item: ProductItem = ProductItem.from_csv_row( + self.columns_config, row + ) + name = _product_item.pre_product_name + if name not in product_items_by_name: + product_items_by_name[name] = _product_item + product_item: ProductItem = product_items_by_name[name] + product_item.add_repre_item( + RepreItem.from_csv_row( + self.columns_config, + self.representations_config, + row + ) + ) + + folder_paths: Set[str] = { + product_item.folder_path + for product_item in product_items_by_name.values() + } + folder_ids_by_path: Dict[str, str] = { + folder_entity["path"]: folder_entity["id"] + for folder_entity in ayon_api.get_folders( + project_name, folder_paths=folder_paths, fields={"id", "path"} + ) + } + missing_paths: Set[str] = folder_paths - set(folder_ids_by_path.keys()) + if missing_paths: + ending = "" if len(missing_paths) == 1 else "s" + joined_paths = "\n".join(sorted(missing_paths)) + raise CreatorError( + f"Folder{ending} not found.\n{joined_paths}" + ) + + task_names: Set[str] = { + product_item.task_name + for product_item in product_items_by_name.values() + } + task_entities_by_folder_id = collections.defaultdict(list) + for task_entity in ayon_api.get_tasks( + project_name, + folder_ids=set(folder_ids_by_path.values()), + task_names=task_names, + fields={"folderId", "name", "taskType"} + ): + folder_id = task_entity["folderId"] + task_entities_by_folder_id[folder_id].append(task_entity) + + missing_tasks: Set[str] = set() + for product_item in product_items_by_name.values(): + folder_path = product_item.folder_path + task_name = product_item.task_name + folder_id = folder_ids_by_path[folder_path] + task_entities = task_entities_by_folder_id[folder_id] + task_entity = next( + ( + task_entity + for task_entity in task_entities + if task_entity["name"] == task_name + ), + None + ) + if task_entity is None: + missing_tasks.add("/".join([folder_path, task_name])) + else: + product_item.task_type = task_entity["taskType"] + + if missing_tasks: + ending = "" if len(missing_tasks) == 1 else "s" + joined_paths = "\n".join(sorted(missing_tasks)) + raise CreatorError( + f"Task{ending} not found.\n{joined_paths}" + ) + + for product_item in product_items_by_name.values(): + repre_paths: Set[str] = set() + duplicated_paths: Set[str] = set() + for repre_item in product_item.repre_items: + # Resolve relative paths in csv file + repre_item.filepath = self._resolve_repre_path( + csv_dir, repre_item.filepath + ) + repre_item.thumbnail_path = self._resolve_repre_path( + csv_dir, repre_item.thumbnail_path + ) + + filepath = repre_item.filepath + if filepath in repre_paths: + duplicated_paths.add(filepath) + repre_paths.add(filepath) + + if duplicated_paths: + ending = "" if len(duplicated_paths) == 1 else "s" + joined_names = "\n".join(sorted(duplicated_paths)) + raise CreatorError( + f"Duplicate filename{ending} in csv file.\n{joined_names}" + ) + + return product_items_by_name + + def _add_thumbnail_repre( + self, + thumbnails: Set[str], + instance: CreatedInstance, + repre_item: RepreItem, + multiple_thumbnails: bool, + ) -> Union[str, None]: + """Add thumbnail to instance. + + Add thumbnail as representation and set 'thumbnailPath' if is not set + yet. + + Args: + thumbnails (Set[str]): Set of all thumbnail paths that should + create representation. + instance (CreatedInstance): Instance from create plugin. + repre_item (RepreItem): Representation item. + multiple_thumbnails (bool): There are multiple representations + with thumbnail. Returns: - list: list of attribute object instances + Uniom[str, None]: Explicit output name for thumbnail + representation. + """ - # Use same attributes as for instance attributes - attr_defs = [ - FileDef( - "csv_filepath_data", - folders=False, - extensions=[".csv"], - allow_sequences=False, - single_item=True, - label="CSV File", - ), - ] - return attr_defs + if not thumbnails: + return None + + thumbnail_path = repre_item.thumbnail_path + if not thumbnail_path or thumbnail_path not in thumbnails: + return None + + thumbnails.remove(thumbnail_path) + + thumb_dir, thumb_file = os.path.split(thumbnail_path) + thumb_basename, thumb_ext = os.path.splitext(thumb_file) + + # NOTE 'explicit_output_name' and custom repre name was set only + # when 'multiple_thumbnails' is True and 'review' tag is present. + # That was changed to set 'explicit_output_name' is set when + # 'multiple_thumbnails' is True. + # is_reviewable = "review" in repre_item.tags + + repre_name = "thumbnail" + explicit_output_name = None + if multiple_thumbnails: + repre_name = f"thumbnail_{thumb_basename}" + explicit_output_name = repre_item.name + + thumbnail_repre_data = { + "name": repre_name, + "ext": thumb_ext.lstrip("."), + "files": thumb_file, + "stagingDir": thumb_dir, + "stagingDir_persistent": True, + "tags": ["thumbnail", "delete"], + } + if explicit_output_name: + thumbnail_repre_data["outputName"] = explicit_output_name + + instance["prepared_data_for_repres"].append({ + "type": "thumbnail", + "colorspace": None, + "representation": thumbnail_repre_data, + }) + # also add thumbnailPath for ayon to integrate + if not instance.get("thumbnailPath"): + instance["thumbnailPath"] = thumbnail_path + + return explicit_output_name + + def _add_representation( + self, + instance: CreatedInstance, + repre_item: RepreItem, + explicit_output_name: Optional[str] = None + ): + """Get representation data + + Args: + repre_item (RepreItem): Representation item based on csv row. + explicit_output_name (Optional[str]): Explicit output name. + For grouping purposes with reviewable components. + + """ + # get extension of file + basename: str = os.path.basename(repre_item.filepath) + extension: str = os.path.splitext(basename)[-1].lower() + + # validate filepath is having correct extension based on output + repre_config_data: Union[Dict[str, Any], None] = None + for repre in self.representations_config["representations"]: + if repre["name"] == repre_item.name: + repre_config_data = repre + break + + if not repre_config_data: + raise CreatorError( + f"Representation '{repre_item.name}' not found " + "in config representation data." + ) + + validate_extensions: List[str] = repre_config_data["extensions"] + if extension not in validate_extensions: + raise CreatorError( + f"File extension '{extension}' not valid for " + f"output '{validate_extensions}'." + ) + + is_sequence: bool = extension in IMAGE_EXTENSIONS + # convert ### string in file name to %03d + # this is for correct frame range validation + # example: file.###.exr -> file.%03d.exr + if "#" in basename: + padding = len(basename.split("#")) - 1 + basename = basename.replace("#" * padding, f"%0{padding}d") + is_sequence = True + + # make absolute path to file + dirname: str = os.path.dirname(repre_item.filepath) + + # check if dirname exists + if not os.path.isdir(dirname): + raise CreatorError( + f"Directory '{dirname}' does not exist." + ) + + frame_start: Union[int, None] = None + frame_end: Union[int, None] = None + files: Union[str, List[str]] = basename + if is_sequence: + # collect all data from dirname + cols, _ = clique.assemble(list(os.listdir(dirname))) + if not cols: + raise CreatorError( + f"No collections found in directory '{dirname}'." + ) + + col = cols[0] + files = list(col) + frame_start = min(col.indexes) + frame_end = max(col.indexes) + + tags: List[str] = deepcopy(repre_item.tags) + # if slate in repre_data is True then remove one frame from start + if repre_item.slate_exists: + tags.append("has_slate") + + # get representation data + representation_data: Dict[str, Any] = { + "name": repre_item.name, + "ext": extension[1:], + "files": files, + "stagingDir": dirname, + "stagingDir_persistent": True, + "tags": tags, + } + if extension in VIDEO_EXTENSIONS: + representation_data.update({ + "fps": repre_item.fps, + "outputName": repre_item.name, + }) + + if explicit_output_name: + representation_data["outputName"] = explicit_output_name + + if frame_start: + representation_data["frameStart"] = frame_start + if frame_end: + representation_data["frameEnd"] = frame_end + + instance["prepared_data_for_repres"].append({ + "type": "media", + "colorspace": repre_item.colorspace, + "representation": representation_data, + }) + + def _prepare_representations( + self, product_item: ProductItem, instance: CreatedInstance + ): + # Collect thumbnail paths from all representation items + # to check if multiple thumbnails are present. + # Once representation is created for certain thumbnail it is removed + # from the set. + thumbnails: Set[str] = { + repre_item.thumbnail_path + for repre_item in product_item.repre_items + if repre_item.thumbnail_path + } + multiple_thumbnails: bool = len(thumbnails) > 1 + + for repre_item in product_item.repre_items: + explicit_output_name = self._add_thumbnail_repre( + thumbnails, + instance, + repre_item, + multiple_thumbnails, + ) + + # get representation data + self._add_representation( + instance, + repre_item, + explicit_output_name + ) + + def _create_instances_from_csv_data(self, csv_dir: str, filename: str): + """Create instances from csv data""" + # from special function get all data from csv file and convert them + # to new instances + product_items_by_name: Dict[str, ProductItem] = ( + self._get_data_from_csv(csv_dir, filename) + ) + + project_name: str = self.create_context.get_current_project_name() + for instance_name, product_item in product_items_by_name.items(): + folder_path: str = product_item.folder_path + version: int = product_item.version + product_name: str = get_product_name( + project_name, + product_item.task_name, + product_item.task_type, + self.host_name, + product_item.product_type, + product_item.variant + ) + label: str = f"{folder_path}_{product_name}_v{version:>03}" + + repre_items: List[RepreItem] = product_item.repre_items + first_repre_item: RepreItem = repre_items[0] + version_comment: Union[str, None] = next( + ( + repre_item.comment + for repre_item in repre_items + if repre_item.comment + ), + None + ) + slate_exists: bool = any( + repre_item.slate_exists + for repre_item in repre_items + ) + + families: List[str] = ["csv_ingest"] + if slate_exists: + # adding slate to families mainly for loaders to be able + # to filter out slates + families.append("slate") + + instance_data = { + "name": instance_name, + "folderPath": folder_path, + "families": families, + "label": label, + "task": product_item.task_name, + "variant": product_item.variant, + "source": "csv", + "frameStart": first_repre_item.frame_start, + "frameEnd": first_repre_item.frame_end, + "handleStart": first_repre_item.handle_start, + "handleEnd": first_repre_item.handle_end, + "fps": first_repre_item.fps, + "version": version, + "comment": version_comment, + "prepared_data_for_repres": [] + } + + # create new instance + new_instance: CreatedInstance = CreatedInstance( + product_item.product_type, + product_name, + instance_data, + self + ) + self._prepare_representations(product_item, new_instance) + + self._store_new_instance(new_instance) From 451cff966718dea1a35d23127e45845fafb04f9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:12:57 +0200 Subject: [PATCH 044/118] fix columns mapping --- .../ayon_traypublisher/plugins/create/create_csv_ingest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index 963263a049..071ee7decd 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -121,7 +121,7 @@ class RepreItem: dst_key: _get_row_value_with_validation( columns_config, column_name, row ) - for column_name, dst_key in ( + for dst_key, column_name in ( # Representation information ("filepath", "File Path"), ("frame_start", "Frame Start"), @@ -193,7 +193,7 @@ class ProductItem: dst_key: _get_row_value_with_validation( columns_config, column_name, row ) - for column_name, dst_key in ( + for dst_key, column_name in ( # Context information ("folder_path", "Folder Path"), ("task_name", "Task Name"), From 8a59e688b3be8073ed254818f3ab3fa5f79ecd8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:13:22 +0200 Subject: [PATCH 045/118] create instances only if are all prepared --- .../plugins/create/create_csv_ingest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index 071ee7decd..0a8c876590 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -337,7 +337,6 @@ configuration in project settings. csv_instance = CreatedInstance( self.product_type, product_name, instance_data, self ) - self._store_new_instance(csv_instance) csv_instance["csvFileData"] = { "filename": filename, @@ -345,7 +344,10 @@ configuration in project settings. } # create instances from csv data via self function - self._create_instances_from_csv_data(csv_dir, filename) + instances = self._create_instances_from_csv_data(csv_dir, filename) + for instance in instances: + self._store_new_instance(instance) + self._store_new_instance(csv_instance) def _resolve_repre_path( self, csv_dir: str, filepath: Union[str, None] @@ -728,6 +730,7 @@ configuration in project settings. self._get_data_from_csv(csv_dir, filename) ) + instances = [] project_name: str = self.create_context.get_current_project_name() for instance_name, product_item in product_items_by_name.items(): folder_path: str = product_item.folder_path @@ -789,5 +792,6 @@ configuration in project settings. self ) self._prepare_representations(product_item, new_instance) + instances.append(new_instance) - self._store_new_instance(new_instance) + return instances From f22f0462e436c972501fc4e5840bc7c080486935 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Jun 2024 18:13:38 +0200 Subject: [PATCH 046/118] use folder path in unique product name --- .../plugins/create/create_csv_ingest.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index 0a8c876590..a8f95fbde8 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -172,7 +172,6 @@ class ProductItem: version: int, variant: str, product_type: str, - pre_product_name: str, task_type: Optional[str] = None, ): self.folder_path = folder_path @@ -181,8 +180,20 @@ class ProductItem: self.version = version self.variant = variant self.product_type = product_type - self.pre_product_name = pre_product_name self.repre_items: List[RepreItem] = [] + self._unique_name = None + + @property + def unique_name(self) -> str: + if self._unique_name is None: + self._unique_name = "/".join([ + self.folder_path, + self.task_name, + f"{self.variant}{self.product_type}{self.version}".replace( + " ", "" + ).lower() + ]) + return self._unique_name def add_repre_item(self, repre_item: RepreItem): self.repre_items.append(repre_item) @@ -202,12 +213,6 @@ class ProductItem: ("product_type", "Product Type"), ) } - - kwargs["pre_product_name"] = ( - "{task_name}{variant}{product_type}{version}" - .format(**kwargs) - .replace(" ", "").lower() - ) return cls(**kwargs) @@ -420,10 +425,10 @@ configuration in project settings. _product_item: ProductItem = ProductItem.from_csv_row( self.columns_config, row ) - name = _product_item.pre_product_name - if name not in product_items_by_name: - product_items_by_name[name] = _product_item - product_item: ProductItem = product_items_by_name[name] + unique_name = _product_item.unique_name + if unique_name not in product_items_by_name: + product_items_by_name[unique_name] = _product_item + product_item: ProductItem = product_items_by_name[unique_name] product_item.add_repre_item( RepreItem.from_csv_row( self.columns_config, From e9cf6a5e2dd04691faa31130ffe3fcd7e613b47c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:20:31 +0200 Subject: [PATCH 047/118] dont add folder path to instance name --- .../plugins/create/create_csv_ingest.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py index a8f95fbde8..fd4dedd48e 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/plugins/create/create_csv_ingest.py @@ -182,6 +182,7 @@ class ProductItem: self.product_type = product_type self.repre_items: List[RepreItem] = [] self._unique_name = None + self._pre_product_name = None @property def unique_name(self) -> str: @@ -195,6 +196,15 @@ class ProductItem: ]) return self._unique_name + @property + def instance_name(self): + if self._pre_product_name is None: + self._pre_product_name = ( + f"{self.task_name}{self.variant}" + f"{self.product_type}{self.version}" + ).replace(" ", "").lower() + return self._pre_product_name + def add_repre_item(self, repre_item: RepreItem): self.repre_items.append(repre_item) @@ -737,7 +747,7 @@ configuration in project settings. instances = [] project_name: str = self.create_context.get_current_project_name() - for instance_name, product_item in product_items_by_name.items(): + for product_item in product_items_by_name.values(): folder_path: str = product_item.folder_path version: int = product_item.version product_name: str = get_product_name( @@ -772,7 +782,7 @@ configuration in project settings. families.append("slate") instance_data = { - "name": instance_name, + "name": product_item.instance_name, "folderPath": folder_path, "families": families, "label": label, From e037e8c8d4c563991700e6953e1485481550e86b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 19 Jun 2024 16:43:12 +0800 Subject: [PATCH 048/118] fix the TypeError found in the scriptmenu when launching maya 2025 --- client/ayon_core/vendor/python/scriptsmenu/launchformaya.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py index 01880b94d7..c8b0c777de 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py +++ b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py @@ -130,7 +130,10 @@ def main(title="Scripts", parent=None, objectName=None): # Register control + shift callback to add to shelf (maya behavior) modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - menu.register_callback(int(modifiers), to_shelf) + if int(cmds.about(version=True)) <= 2025: + modifiers = int(modifiers) + + menu.register_callback(modifiers, to_shelf) menu.register_callback(0, register_repeat_last) From 819664cf0561956a729accc531f81dc8b20bbf67 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 19 Jun 2024 22:26:02 +0800 Subject: [PATCH 049/118] add option to stripe shader assignment --- .../maya/client/ayon_maya/plugins/create/create_setdress.py | 3 +++ .../client/ayon_maya/plugins/publish/extract_maya_scene_raw.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py b/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py index 12532e0724..98d6de867f 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py @@ -15,5 +15,8 @@ class CreateSetDress(plugin.MayaCreator): return [ BoolDef("exactSetMembersOnly", label="Exact Set Members Only", + default=True), + BoolDef("shader", + label="Include shader", default=True) ] diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py index 6e66353c7a..f0d0c70ae4 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py @@ -96,7 +96,7 @@ class ExtractMayaSceneRaw(plugin.MayaExtractorPlugin, AYONPyblishPluginMixin): "preserve_references" ], constructionHistory=True, - shader=True, + shader=True if instance.data.get("shader", False) else False, constraints=True, expressions=True) From 4265c92ceaaa0c0d3d60dd9123a5e792467f1439 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 19 Jun 2024 22:27:15 +0800 Subject: [PATCH 050/118] make sure shader is always true if it is not in the instance.data --- .../client/ayon_maya/plugins/publish/extract_maya_scene_raw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py index f0d0c70ae4..fde48afb8f 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py @@ -96,7 +96,7 @@ class ExtractMayaSceneRaw(plugin.MayaExtractorPlugin, AYONPyblishPluginMixin): "preserve_references" ], constructionHistory=True, - shader=True if instance.data.get("shader", False) else False, + shader=True if instance.data.get("shader", True) else False, constraints=True, expressions=True) From fbcd56eb0d2b1c5c4d3032b7737da34cf900b47e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 17:52:21 +0200 Subject: [PATCH 051/118] Support single frame publishes - fix #672 --- .../houdini/client/ayon_houdini/api/plugin.py | 20 +++++++++++++++++++ .../plugins/publish/extract_alembic.py | 11 ++-------- .../plugins/publish/extract_ass.py | 13 ++---------- .../plugins/publish/extract_bgeo.py | 10 ++++------ .../plugins/publish/extract_composite.py | 7 +++---- .../plugins/publish/extract_mantra_ifd.py | 14 ++----------- .../plugins/publish/extract_opengl.py | 4 +--- .../plugins/publish/extract_redshift_proxy.py | 4 +--- .../plugins/publish/extract_vdb_cache.py | 4 +--- 9 files changed, 36 insertions(+), 51 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/plugin.py b/server_addon/houdini/client/ayon_houdini/api/plugin.py index 9c6bba925a..0e2308e948 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Houdini specific Avalon/Pyblish plugin definitions.""" +import os import sys from abc import ( ABCMeta @@ -392,3 +393,22 @@ class HoudiniExtractorPlugin(publish.Extractor): hosts = ["houdini"] settings_category = SETTINGS_CATEGORY + + def validate_expected_frames(self, instance, staging_dir): + """ + Validate all expected files in `instance.data["frames"]` exist in + the staging directory. + """ + filenames = instance.data["frames"] + if isinstance(filenames, str): + # Single frame + filenames = [filenames] + + missing_filenames = [] + for filename in filenames: + path = os.path.join(staging_dir, filename) + if not os.path.isfile(path): + missing_filenames.append(filename) + if missing_filenames: + raise RuntimeError(f"Missing frames: {missing_filenames}") + diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py index e82f07284a..07216a491c 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py @@ -26,15 +26,8 @@ class ExtractAlembic(plugin.HoudiniExtractorPlugin): staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir - if instance.data.get("frames"): - # list of files - files = instance.data["frames"] - else: - # single file - files = os.path.basename(output) - # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (files, + self.log.info("Writing alembic '%s' to '%s'" % (output, staging_dir)) render_rop(ropnode) @@ -45,7 +38,7 @@ class ExtractAlembic(plugin.HoudiniExtractorPlugin): representation = { 'name': 'abc', 'ext': 'abc', - 'files': files, + 'files': instance.data["frames"], "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py index a796bbf4b3..befa6d0d49 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py @@ -35,16 +35,7 @@ class ExtractAss(plugin.HoudiniExtractorPlugin): # Unfortunately user interrupting the extraction does not raise an # error and thus still continues to the integrator. To capture that # we make sure all files exist - files = instance.data["frames"] - missing = [] - for file_name in files: - full_path = os.path.normpath(os.path.join(staging_dir, file_name)) - if not os.path.exists(full_path): - missing.append(full_path) - - if missing: - raise RuntimeError("Failed to complete Arnold ass extraction. " - "Missing output files: {}".format(missing)) + self.validate_expected_frames(instance, staging_dir) if "representations" not in instance.data: instance.data["representations"] = [] @@ -55,7 +46,7 @@ class ExtractAss(plugin.HoudiniExtractorPlugin): representation = { 'name': 'ass', 'ext': ext, - "files": files, + "files": instance.data["frames"], "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py index ab8837065d..180812ab87 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py @@ -19,8 +19,8 @@ class ExtractBGEO(plugin.HoudiniExtractorPlugin): ropnode = hou.node(instance.data["instance_node"]) # Get the filename from the filename parameter - output = ropnode.evalParm("sopoutput") - staging_dir, file_name = os.path.split(output) + sop_output = ropnode.evalParm("sopoutput") + staging_dir, file_name = os.path.split(sop_output) instance.data["stagingDir"] = staging_dir # We run the render @@ -30,10 +30,8 @@ class ExtractBGEO(plugin.HoudiniExtractorPlugin): # write files lib.render_rop(ropnode) - output = instance.data["frames"] - _, ext = lib.splitext( - output[0], allowed_multidot_extensions=[ + sop_output, allowed_multidot_extensions=[ ".ass.gz", ".bgeo.sc", ".bgeo.gz", ".bgeo.lzma", ".bgeo.bz2"]) @@ -43,7 +41,7 @@ class ExtractBGEO(plugin.HoudiniExtractorPlugin): representation = { "name": "bgeo", "ext": ext.lstrip("."), - "files": output, + "files": instance.data["frames"], "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"] diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py index cab462aef6..84f03e5d1c 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py @@ -20,17 +20,16 @@ class ExtractComposite(plugin.HoudiniExtractorPlugin, # Get the filename from the copoutput parameter # `.evalParm(parameter)` will make sure all tokens are resolved - output = ropnode.evalParm("copoutput") - staging_dir = os.path.dirname(output) + cop_output = ropnode.evalParm("copoutput") + staging_dir, file_name = os.path.split(cop_output) instance.data["stagingDir"] = staging_dir - file_name = os.path.basename(output) self.log.info("Writing comp '%s' to '%s'" % (file_name, staging_dir)) render_rop(ropnode) output = instance.data["frames"] - _, ext = splitext(output[0], []) + _, ext = splitext(file_name, []) ext = ext.lstrip(".") if "representations" not in instance.data: diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py index b424f2e452..f0f402fa64 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py @@ -23,17 +23,7 @@ class ExtractMantraIFD(plugin.HoudiniExtractorPlugin): staging_dir = os.path.dirname(output) instance.data["stagingDir"] = staging_dir - files = instance.data["frames"] - missing_frames = [ - frame - for frame in instance.data["frames"] - if not os.path.exists( - os.path.normpath(os.path.join(staging_dir, frame))) - ] - if missing_frames: - raise RuntimeError("Failed to complete Mantra ifd extraction. " - "Missing output files: {}".format( - missing_frames)) + self.validate_expected_frames(instance, staging_dir) if "representations" not in instance.data: instance.data["representations"] = [] @@ -41,7 +31,7 @@ class ExtractMantraIFD(plugin.HoudiniExtractorPlugin): representation = { 'name': 'ifd', 'ext': 'ifd', - 'files': files, + 'files': instance.data["frames"], "stagingDir": staging_dir, "frameStart": instance.data["frameStart"], "frameEnd": instance.data["frameEnd"], diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py index bee1bf871f..934f98a9f3 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py @@ -38,8 +38,6 @@ class ExtractOpenGL(plugin.HoudiniExtractorPlugin, render_rop(ropnode) - output = instance.data["frames"] - tags = ["review"] if not instance.data.get("keepImages"): tags.append("delete") @@ -47,7 +45,7 @@ class ExtractOpenGL(plugin.HoudiniExtractorPlugin, representation = { "name": instance.data["imageFormat"], "ext": instance.data["imageFormat"], - "files": output, + "files": instance.data["frames"], "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py index 3e8a79df00..8c3cdd5ef9 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py @@ -32,15 +32,13 @@ class ExtractRedshiftProxy(plugin.HoudiniExtractorPlugin): render_rop(ropnode) - output = instance.data["frames"] - if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "rs", "ext": "rs", - "files": output, + "files": instance.data["frames"], "stagingDir": staging_dir, } diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py index a944d81e9b..8f0c070ff1 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py @@ -30,15 +30,13 @@ class ExtractVDBCache(plugin.HoudiniExtractorPlugin): render_rop(ropnode) - output = instance.data["frames"] - if "representations" not in instance.data: instance.data["representations"] = [] representation = { "name": "vdb", "ext": "vdb", - "files": output, + "files": instance.data["frames"], "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], From 0bec3f0d2e2616471102b905785cc739724ee031 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 17:52:38 +0200 Subject: [PATCH 052/118] Simplify setting instance data "frames" --- .../client/ayon_houdini/plugins/publish/collect_frames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py index 3378657bfd..82f986ee13 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py @@ -60,7 +60,7 @@ class CollectFrames(plugin.HoudiniInstancePlugin): # todo: `frames` currently conflicts with "explicit frames" for a # for a custom frame list. So this should be refactored. - instance.data.update({"frames": result}) + instance.data["frames"] = result @staticmethod def create_file_list(match, start_frame, end_frame): From f56f0cc3d761c52330637dd2e2f0f0c10ca58a24 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 18:17:00 +0200 Subject: [PATCH 053/118] Move variable closer to its usage --- .../client/ayon_houdini/plugins/publish/extract_composite.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py index 84f03e5d1c..9830b2ea84 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py @@ -28,7 +28,6 @@ class ExtractComposite(plugin.HoudiniExtractorPlugin, render_rop(ropnode) - output = instance.data["frames"] _, ext = splitext(file_name, []) ext = ext.lstrip(".") @@ -38,7 +37,7 @@ class ExtractComposite(plugin.HoudiniExtractorPlugin, representation = { "name": ext, "ext": ext, - "files": output, + "files": instance.data["frames"], "stagingDir": staging_dir, "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], From 320f97c9c68ff5acd220c5da8f632258f935c2dd Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Jun 2024 20:32:07 +0300 Subject: [PATCH 054/118] be more explicit about which node to skip --- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index eb9a74a7ad..2f8d6aae49 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -96,7 +96,7 @@ class HdaLoader(plugin.HoudiniLoader): parent = node.parent() node.destroy() - if parent.type().category() == hou.objNodeTypeCategory(): + if parent.path() == pipeline.AVALON_CONTAINERS: return # Remove parent if empty. From 2ce50870377748a9a6e5206542c3d5326c419cd5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 19:39:16 +0200 Subject: [PATCH 055/118] Implement more generic ExtractROP extractor --- .../plugins/publish/extract_alembic.py | 44 ------ .../plugins/publish/extract_ass.py | 54 ------- .../plugins/publish/extract_bgeo.py | 49 ------- .../plugins/publish/extract_composite.py | 56 -------- .../plugins/publish/extract_fbx.py | 51 ------- .../plugins/publish/extract_mantra_ifd.py | 39 ----- .../plugins/publish/extract_opengl.py | 67 --------- .../plugins/publish/extract_redshift_proxy.py | 50 ------- .../plugins/publish/extract_rop.py | 134 ++++++++++++++++++ .../plugins/publish/extract_vdb_cache.py | 44 ------ 10 files changed, 134 insertions(+), 454 deletions(-) delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_fbx.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py create mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py deleted file mode 100644 index 07216a491c..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_alembic.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop - - -class ExtractAlembic(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder - label = "Extract Alembic" - families = ["abc", "camera"] - targets = ["local", "remote"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - - ropnode = hou.node(instance.data["instance_node"]) - - # Get the filename from the filename parameter - output = ropnode.evalParm("filename") - staging_dir = os.path.dirname(output) - instance.data["stagingDir"] = staging_dir - - # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (output, - staging_dir)) - - render_rop(ropnode) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': instance.data["frames"], - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py deleted file mode 100644 index befa6d0d49..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_ass.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop - - -class ExtractAss(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder + 0.1 - label = "Extract Ass" - families = ["ass"] - targets = ["local", "remote"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - ropnode = hou.node(instance.data["instance_node"]) - - # Get the filename from the filename parameter - # `.evalParm(parameter)` will make sure all tokens are resolved - output = ropnode.evalParm("ar_ass_file") - staging_dir = os.path.dirname(output) - instance.data["stagingDir"] = staging_dir - file_name = os.path.basename(output) - - # We run the render - self.log.info("Writing ASS '%s' to '%s'" % (file_name, staging_dir)) - - render_rop(ropnode) - - # Unfortunately user interrupting the extraction does not raise an - # error and thus still continues to the integrator. To capture that - # we make sure all files exist - self.validate_expected_frames(instance, staging_dir) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - # Allow ass.gz extension as well - ext = "ass.gz" if file_name.endswith(".ass.gz") else "ass" - - representation = { - 'name': 'ass', - 'ext': ext, - "files": instance.data["frames"], - "stagingDir": staging_dir, - "frameStart": instance.data["frameStartHandle"], - "frameEnd": instance.data["frameEndHandle"], - } - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py deleted file mode 100644 index 180812ab87..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_bgeo.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import lib, plugin - - -class ExtractBGEO(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder - label = "Extract BGEO" - families = ["bgeo"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - ropnode = hou.node(instance.data["instance_node"]) - - # Get the filename from the filename parameter - sop_output = ropnode.evalParm("sopoutput") - staging_dir, file_name = os.path.split(sop_output) - instance.data["stagingDir"] = staging_dir - - # We run the render - self.log.info("Writing bgeo files '{}' to '{}'.".format( - file_name, staging_dir)) - - # write files - lib.render_rop(ropnode) - - _, ext = lib.splitext( - sop_output, allowed_multidot_extensions=[ - ".ass.gz", ".bgeo.sc", ".bgeo.gz", - ".bgeo.lzma", ".bgeo.bz2"]) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": "bgeo", - "ext": ext.lstrip("."), - "files": instance.data["frames"], - "stagingDir": staging_dir, - "frameStart": instance.data["frameStartHandle"], - "frameEnd": instance.data["frameEndHandle"] - } - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py deleted file mode 100644 index 9830b2ea84..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_composite.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -import hou -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop, splitext - - -class ExtractComposite(plugin.HoudiniExtractorPlugin, - publish.ColormanagedPyblishPluginMixin): - - order = pyblish.api.ExtractorOrder - label = "Extract Composite (Image Sequence)" - families = ["imagesequence"] - - def process(self, instance): - - ropnode = hou.node(instance.data["instance_node"]) - - # Get the filename from the copoutput parameter - # `.evalParm(parameter)` will make sure all tokens are resolved - cop_output = ropnode.evalParm("copoutput") - staging_dir, file_name = os.path.split(cop_output) - instance.data["stagingDir"] = staging_dir - - self.log.info("Writing comp '%s' to '%s'" % (file_name, staging_dir)) - - render_rop(ropnode) - - _, ext = splitext(file_name, []) - ext = ext.lstrip(".") - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": ext, - "ext": ext, - "files": instance.data["frames"], - "stagingDir": staging_dir, - "frameStart": instance.data["frameStartHandle"], - "frameEnd": instance.data["frameEndHandle"], - } - - if ext.lower() == "exr": - # Inject colorspace with 'scene_linear' as that's the - # default Houdini working colorspace and all extracted - # OpenEXR images should be in that colorspace. - # https://www.sidefx.com/docs/houdini/render/linear.html#image-formats - self.set_representation_colorspace( - representation, instance.context, - colorspace="scene_linear" - ) - - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_fbx.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_fbx.py deleted file mode 100644 index 49b3fa07ca..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_fbx.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -"""Fbx Extractor for houdini. """ - -import os -import hou -import pyblish.api -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop - - -class ExtractFBX(plugin.HoudiniExtractorPlugin): - - label = "Extract FBX" - families = ["fbx"] - - order = pyblish.api.ExtractorOrder + 0.1 - - def process(self, instance): - - # get rop node - ropnode = hou.node(instance.data.get("instance_node")) - output_file = ropnode.evalParm("sopoutput") - - # get staging_dir and file_name - staging_dir = os.path.normpath(os.path.dirname(output_file)) - file_name = os.path.basename(output_file) - - # render rop - self.log.debug("Writing FBX '%s' to '%s'", file_name, staging_dir) - render_rop(ropnode) - - # prepare representation - representation = { - "name": "fbx", - "ext": "fbx", - "files": file_name, - "stagingDir": staging_dir - } - - # A single frame may also be rendered without start/end frame. - if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa - representation["frameStart"] = instance.data["frameStartHandle"] - representation["frameEnd"] = instance.data["frameEndHandle"] - - # set value type for 'representations' key to list - if "representations" not in instance.data: - instance.data["representations"] = [] - - # update instance data - instance.data["stagingDir"] = staging_dir - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py deleted file mode 100644 index f0f402fa64..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import plugin - - -class ExtractMantraIFD(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder - label = "Extract Mantra ifd" - families = ["mantraifd"] - targets = ["local", "remote"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - - ropnode = hou.node(instance.data.get("instance_node")) - output = ropnode.evalParm("soho_diskfile") - staging_dir = os.path.dirname(output) - instance.data["stagingDir"] = staging_dir - - self.validate_expected_frames(instance, staging_dir) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'ifd', - 'ext': 'ifd', - 'files': instance.data["frames"], - "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - } - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py deleted file mode 100644 index 934f98a9f3..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_opengl.py +++ /dev/null @@ -1,67 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_core.pipeline import publish -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop - - -class ExtractOpenGL(plugin.HoudiniExtractorPlugin, - publish.ColormanagedPyblishPluginMixin): - - order = pyblish.api.ExtractorOrder - 0.01 - label = "Extract OpenGL" - families = ["review"] - - def process(self, instance): - ropnode = hou.node(instance.data.get("instance_node")) - - # This plugin is triggered when marking render as reviewable. - # Therefore, this plugin will run on over wrong instances. - # TODO: Don't run this plugin on wrong instances. - # This plugin should run only on review product type - # with instance node of opengl type. - if ropnode.type().name() != "opengl": - self.log.debug("Skipping OpenGl extraction. Rop node {} " - "is not an OpenGl node.".format(ropnode.path())) - return - - output = ropnode.evalParm("picture") - staging_dir = os.path.normpath(os.path.dirname(output)) - instance.data["stagingDir"] = staging_dir - file_name = os.path.basename(output) - - self.log.info("Extracting '%s' to '%s'" % (file_name, - staging_dir)) - - render_rop(ropnode) - - tags = ["review"] - if not instance.data.get("keepImages"): - tags.append("delete") - - representation = { - "name": instance.data["imageFormat"], - "ext": instance.data["imageFormat"], - "files": instance.data["frames"], - "stagingDir": staging_dir, - "frameStart": instance.data["frameStartHandle"], - "frameEnd": instance.data["frameEndHandle"], - "tags": tags, - "preview": True, - "camera_name": instance.data.get("review_camera") - } - - if ropnode.evalParm("colorcorrect") == 2: # OpenColorIO enabled - colorspace = ropnode.evalParm("ociocolorspace") - # inject colorspace data - self.set_representation_colorspace( - representation, instance.context, - colorspace=colorspace - ) - - if "representations" not in instance.data: - instance.data["representations"] = [] - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py deleted file mode 100644 index 8c3cdd5ef9..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_redshift_proxy.py +++ /dev/null @@ -1,50 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop - - -class ExtractRedshiftProxy(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder + 0.1 - label = "Extract Redshift Proxy" - families = ["redshiftproxy"] - targets = ["local", "remote"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - ropnode = hou.node(instance.data.get("instance_node")) - - # Get the filename from the filename parameter - # `.evalParm(parameter)` will make sure all tokens are resolved - output = ropnode.evalParm("RS_archive_file") - staging_dir = os.path.normpath(os.path.dirname(output)) - instance.data["stagingDir"] = staging_dir - file_name = os.path.basename(output) - - self.log.info("Writing Redshift Proxy '%s' to '%s'" % (file_name, - staging_dir)) - - render_rop(ropnode) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": "rs", - "ext": "rs", - "files": instance.data["frames"], - "stagingDir": staging_dir, - } - - # A single frame may also be rendered without start/end frame. - if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa - representation["frameStart"] = instance.data["frameStartHandle"] - representation["frameEnd"] = instance.data["frameEndHandle"] - - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py new file mode 100644 index 0000000000..7b9e389f79 --- /dev/null +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -0,0 +1,134 @@ +import os +import hou + +import pyblish.api + +from ayon_core.pipeline import publish +from ayon_houdini.api import plugin +from ayon_houdini.api.lib import render_rop, get_output_parameter, splitext + + +class ExtractROP(plugin.HoudiniExtractorPlugin): + """Generic Extractor for any ROP node.""" + label = "Extract ROP" + order = pyblish.api.ExtractorOrder + + families = ["abc", "camera", "bgeo", "pointcache", "fbx", "imagesequence", + "vdbcache", "ass", "redshiftproxy", "mantraifd"] + targets = ["local", "remote"] + + def process(self, instance: pyblish.api.Instance): + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return + + rop_node = hou.node(instance.data["instance_node"]) + + parm = get_output_parameter(rop_node) + filepath = parm.eval() + staging_dir = os.path.dirname(filepath) + _, ext = splitext( + filepath, allowed_multidot_extensions=[ + ".ass.gz", ".bgeo.sc", ".bgeo.gz", + ".bgeo.lzma", ".bgeo.bz2"] + ) + + render_rop(rop_node) + self.validate_expected_frames(instance, staging_dir) + + # In some cases representation name is not the the extension + # TODO: Preferably we remove this very specific naming + product_type = instance.data["productType"] + name = { + "bgeo": "bgeo", + "rs": "rs", + "ass": "ass", + }.get(product_type, ext) + + representation = { + "name": name, + "ext": ext, + "files": instance.data["frames"], + "stagingDir": staging_dir, + "frameStart": instance.data["frameStartHandle"], + "frameEnd": instance.data["frameEndHandle"], + } + representation = self.update_representation_data(instance, + representation) + instance.data.setdefault("representations", []).append(representation) + instance.data["stagingDir"] = staging_dir + + def update_representation_data(self, + instance: pyblish.api.Instance, + representation: dict): + """Allow subclass to override the representation data in-place""" + pass + + +class ExtractOpenGL(ExtractROP, + publish.ColormanagedPyblishPluginMixin): + + order = pyblish.api.ExtractorOrder - 0.01 + label = "Extract OpenGL" + families = ["review"] + + def process(self, instance): + # This plugin is triggered when marking render as reviewable. + # Therefore, this plugin will run over wrong instances. + # TODO: Don't run this plugin on wrong instances. + # This plugin should run only on review product type + # with instance node of opengl type. + instance_node = instance.data.get("instance_node") + if not instance_node: + self.log.debug("Skipping instance without instance node.") + return + + rop_node = hou.node(instance_node) + if rop_node.type().name() != "opengl": + self.log.debug("Skipping OpenGl extraction. Rop node {} " + "is not an OpenGl node.".format(rop_node.path())) + return + + super(ExtractOpenGL, self).process(instance) + + def update_representation_data(self, + instance: pyblish.api.Instance, + representation: dict): + + tags = ["review"] + if not instance.data.get("keepImages"): + tags.append("delete") + + representation.update({ + # TODO: Avoid this override? + "name": instance.data["imageFormat"], + "ext": instance.data["imageFormat"], + + "tags": tags, + "preview": True, + "camera_name": instance.data.get("review_camera") + }) + return representation + + +class ExtractComposite(ExtractROP, + publish.ColormanagedPyblishPluginMixin): + + label = "Extract Composite (Image Sequence)" + families = ["imagesequence"] + + def update_representation_data(self, + instance: pyblish.api.Instance, + representation: dict): + + if representation["ext"].lower() != "exr": + return + + # Inject colorspace with 'scene_linear' as that's the + # default Houdini working colorspace and all extracted + # OpenEXR images should be in that colorspace. + # https://www.sidefx.com/docs/houdini/render/linear.html#image-formats + self.set_representation_colorspace( + representation, instance.context, + colorspace="scene_linear" + ) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py deleted file mode 100644 index 8f0c070ff1..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_vdb_cache.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop - - -class ExtractVDBCache(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder + 0.1 - label = "Extract VDB Cache" - families = ["vdbcache"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - ropnode = hou.node(instance.data["instance_node"]) - - # Get the filename from the filename parameter - # `.evalParm(parameter)` will make sure all tokens are resolved - sop_output = ropnode.evalParm("sopoutput") - staging_dir = os.path.normpath(os.path.dirname(sop_output)) - instance.data["stagingDir"] = staging_dir - file_name = os.path.basename(sop_output) - - self.log.info("Writing VDB '%s' to '%s'" % (file_name, staging_dir)) - - render_rop(ropnode) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": "vdb", - "ext": "vdb", - "files": instance.data["frames"], - "stagingDir": staging_dir, - "frameStart": instance.data["frameStartHandle"], - "frameEnd": instance.data["frameEndHandle"], - } - instance.data["representations"].append(representation) From 640a409729f808a40bcec549a545fda060ed14b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 19:44:29 +0200 Subject: [PATCH 056/118] Move method to where it's used --- .../houdini/client/ayon_houdini/api/plugin.py | 20 ------------------- .../plugins/publish/extract_rop.py | 18 +++++++++++++++++ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/plugin.py b/server_addon/houdini/client/ayon_houdini/api/plugin.py index 0e2308e948..9c6bba925a 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Houdini specific Avalon/Pyblish plugin definitions.""" -import os import sys from abc import ( ABCMeta @@ -393,22 +392,3 @@ class HoudiniExtractorPlugin(publish.Extractor): hosts = ["houdini"] settings_category = SETTINGS_CATEGORY - - def validate_expected_frames(self, instance, staging_dir): - """ - Validate all expected files in `instance.data["frames"]` exist in - the staging directory. - """ - filenames = instance.data["frames"] - if isinstance(filenames, str): - # Single frame - filenames = [filenames] - - missing_filenames = [] - for filename in filenames: - path = os.path.join(staging_dir, filename) - if not os.path.isfile(path): - missing_filenames.append(filename) - if missing_filenames: - raise RuntimeError(f"Missing frames: {missing_filenames}") - diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 7b9e389f79..ed0c3f9855 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -58,6 +58,24 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): instance.data.setdefault("representations", []).append(representation) instance.data["stagingDir"] = staging_dir + def validate_expected_frames(self, instance, staging_dir): + """ + Validate all expected files in `instance.data["frames"]` exist in + the staging directory. + """ + filenames = instance.data["frames"] + if isinstance(filenames, str): + # Single frame + filenames = [filenames] + + missing_filenames = [] + for filename in filenames: + path = os.path.join(staging_dir, filename) + if not os.path.isfile(path): + missing_filenames.append(filename) + if missing_filenames: + raise RuntimeError(f"Missing frames: {missing_filenames}") + def update_representation_data(self, instance: pyblish.api.Instance, representation: dict): From c45144a1cf7631137187ed0d5a2b98f620b741d3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 19:46:02 +0200 Subject: [PATCH 057/118] Avoid redundant `return` --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index ed0c3f9855..fdabe4c713 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -112,7 +112,6 @@ class ExtractOpenGL(ExtractROP, def update_representation_data(self, instance: pyblish.api.Instance, representation: dict): - tags = ["review"] if not instance.data.get("keepImages"): tags.append("delete") @@ -126,7 +125,6 @@ class ExtractOpenGL(ExtractROP, "preview": True, "camera_name": instance.data.get("review_camera") }) - return representation class ExtractComposite(ExtractROP, From cde63892a8efa184b908b551dc3924902a845773 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 19:46:50 +0200 Subject: [PATCH 058/118] Do not use extension to base `usd` representation name on --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index fdabe4c713..6b05b468d5 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -43,6 +43,7 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): "bgeo": "bgeo", "rs": "rs", "ass": "ass", + "usd": "usd" }.get(product_type, ext) representation = { From 3de37d0507429a1b5a7d6b3544cd91cabefa349c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 19:47:34 +0200 Subject: [PATCH 059/118] Revert: Do not use extension to base `usd` representation name on --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 6b05b468d5..265f4c1538 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -42,8 +42,7 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): name = { "bgeo": "bgeo", "rs": "rs", - "ass": "ass", - "usd": "usd" + "ass": "ass" }.get(product_type, ext) representation = { From 928115f25a64f98a83b7593fadfd7142e2d3e35b Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Wed, 19 Jun 2024 20:53:05 +0300 Subject: [PATCH 060/118] remove redundant attribute Co-authored-by: Roy Nieterau --- .../client/ayon_houdini/plugins/publish/validate_subset_name.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py index 4f15f193fc..a63a4f16c7 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_subset_name.py @@ -26,7 +26,6 @@ class ValidateSubsetName(plugin.HoudiniInstancePlugin, """ families = ["staticMesh", "hda"] - hosts = ["houdini"] label = "Validate Product Name" order = ValidateContentsOrder + 0.1 actions = [FixProductNameAction, SelectInvalidAction] From 336d38b21835f02bd04b5a2a4e317d07e5bb94b1 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 19 Jun 2024 20:56:36 +0300 Subject: [PATCH 061/118] move get_avalon_container to ayon_houdini.api.pipeline --- .../client/ayon_houdini/api/pipeline.py | 25 ++++++++++++++----- .../ayon_houdini/plugins/load/load_hda.py | 18 +------------ 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 6af4993d25..0ca7de23bd 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -221,12 +221,8 @@ def containerise(name, """ - # Ensure AVALON_CONTAINERS subnet exists - subnet = hou.node(AVALON_CONTAINERS) - if subnet is None: - obj_network = hou.node("/obj") - subnet = obj_network.createNode("subnet", - node_name="AVALON_CONTAINERS") + # Get AVALON_CONTAINERS subnet + subnet = get_avalon_container() # Create proper container name container_name = "{}_{}".format(name, suffix or "CON") @@ -401,6 +397,23 @@ def on_new(): _enforce_start_frame() +def get_avalon_container(): + path = AVALON_CONTAINERS + avalon_container = hou.node(path) + if not avalon_container: + # Let's create avalon container secretly + # but make sure the pipeline still is built the + # way we anticipate it was built, asserting it. + assert path == "/obj/AVALON_CONTAINERS" + + parent = hou.node("/obj") + avalon_container = parent.createNode( + "subnet", node_name="AVALON_CONTAINERS" + ) + + return avalon_container + + def _set_context_settings(): """Apply the project settings from the project definition diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index 2f8d6aae49..d7268da39e 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -12,22 +12,6 @@ from ayon_houdini.api import ( plugin ) -def get_avalon_container(): - path = pipeline.AVALON_CONTAINERS - avalon_container = hou.node(path) - if not avalon_container: - # Let's create avalon container secretly - # but make sure the pipeline still is built the - # way we anticipate it was built, asserting it. - assert path == "/obj/AVALON_CONTAINERS" - - parent = hou.node("/obj") - avalon_container = parent.createNode( - "subnet", node_name="AVALON_CONTAINERS" - ) - - return avalon_container - class HdaLoader(plugin.HoudiniLoader): """Load Houdini Digital Asset file.""" @@ -106,7 +90,7 @@ class HdaLoader(plugin.HoudiniLoader): def _create_dedicated_parent_node(self, hda_def): # Get the root node - parent_node = get_avalon_container() + parent_node = pipeline.get_avalon_container() node = None node_type = None if hda_def.nodeTypeCategory() == hou.objNodeTypeCategory(): From 020abb84c678f068e5f57fd86fdffe3c63cd690f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 19:57:15 +0200 Subject: [PATCH 062/118] Mimic generic ExtractROP logic more from #542 --- .../plugins/publish/collect_frames.py | 5 +++- .../plugins/publish/extract_rop.py | 24 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py index 82f986ee13..94feb7532c 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py @@ -60,7 +60,10 @@ class CollectFrames(plugin.HoudiniInstancePlugin): # todo: `frames` currently conflicts with "explicit frames" for a # for a custom frame list. So this should be refactored. - instance.data["frames"] = result + instance.data.update({ + "frames": result, + "stagingDir": os.path.dirname(output) + }) @staticmethod def create_file_list(match, start_frame, end_frame): diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 265f4c1538..4075db3a2c 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -24,17 +24,16 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): rop_node = hou.node(instance.data["instance_node"]) - parm = get_output_parameter(rop_node) - filepath = parm.eval() - staging_dir = os.path.dirname(filepath) + files = instance.data["frames"] + first_file = files[0] if isinstance(files, (list, tuple)) else files _, ext = splitext( - filepath, allowed_multidot_extensions=[ + first_file, allowed_multidot_extensions=[ ".ass.gz", ".bgeo.sc", ".bgeo.gz", ".bgeo.lzma", ".bgeo.bz2"] ) render_rop(rop_node) - self.validate_expected_frames(instance, staging_dir) + self.validate_expected_frames(instance) # In some cases representation name is not the the extension # TODO: Preferably we remove this very specific naming @@ -49,30 +48,29 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): "name": name, "ext": ext, "files": instance.data["frames"], - "stagingDir": staging_dir, + "stagingDir": instance.data["stagingDir"], "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], } representation = self.update_representation_data(instance, representation) instance.data.setdefault("representations", []).append(representation) - instance.data["stagingDir"] = staging_dir - def validate_expected_frames(self, instance, staging_dir): + def validate_expected_frames(self, instance: pyblish.api.Instance): """ Validate all expected files in `instance.data["frames"]` exist in the staging directory. """ filenames = instance.data["frames"] + staging_dir = instance.data["stagingDir"] if isinstance(filenames, str): # Single frame filenames = [filenames] - missing_filenames = [] - for filename in filenames: - path = os.path.join(staging_dir, filename) - if not os.path.isfile(path): - missing_filenames.append(filename) + missing_filenames = [ + filename for filename in filenames + if not os.path.isfile(os.path.join(staging_dir, filename)) + ] if missing_filenames: raise RuntimeError(f"Missing frames: {missing_filenames}") From a45b68f20fdf13bd64d3641abeddf989de646c9f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 20:28:57 +0200 Subject: [PATCH 063/118] Expect update in-place --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 4075db3a2c..37e45e68d5 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -52,8 +52,7 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], } - representation = self.update_representation_data(instance, - representation) + self.update_representation_data(instance, representation) instance.data.setdefault("representations", []).append(representation) def validate_expected_frames(self, instance: pyblish.api.Instance): From a6de293bb6ece60983c9f4277bd356e3e8a8cbb7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 20:29:28 +0200 Subject: [PATCH 064/118] Remove dot from `ext` --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 37e45e68d5..784a4f161d 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -31,6 +31,7 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): ".ass.gz", ".bgeo.sc", ".bgeo.gz", ".bgeo.lzma", ".bgeo.bz2"] ) + ext = ext.lstrip(".") render_rop(rop_node) self.validate_expected_frames(instance) From d0d45abfbad76a7dd24307441761cc5cfce1ddbe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 20:33:16 +0200 Subject: [PATCH 065/118] Add some debug logs --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 784a4f161d..215d4bf213 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -33,6 +33,8 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): ) ext = ext.lstrip(".") + self.log.debug(f"Rendering {rop_node.path()} to {first_file}..") + render_rop(rop_node) self.validate_expected_frames(instance) From 836554d6f6b0d61b53f3dc543023613ba3877870 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Jun 2024 22:11:24 +0200 Subject: [PATCH 066/118] Remove `imagesequence` because it has its own dedicated plug-in lower down in the file --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 215d4bf213..23657b66c9 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -13,7 +13,7 @@ class ExtractROP(plugin.HoudiniExtractorPlugin): label = "Extract ROP" order = pyblish.api.ExtractorOrder - families = ["abc", "camera", "bgeo", "pointcache", "fbx", "imagesequence", + families = ["abc", "camera", "bgeo", "pointcache", "fbx", "vdbcache", "ass", "redshiftproxy", "mantraifd"] targets = ["local", "remote"] From 2a49064c0b1b4dec0379e1f379ef496be367520f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Jun 2024 12:27:27 +0200 Subject: [PATCH 067/118] Integrate Hero Version: Disable usage of hardlinks - but allow enabling via settings --- .../plugins/publish/integrate_hero_version.py | 30 +++++++++++-------- server/settings/publish_plugins.py | 11 ++++++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 8c36719b77..95b9f418f9 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -87,7 +87,9 @@ class IntegrateHeroVersion( ] # QUESTION/TODO this process should happen on server if crashed due to # permissions error on files (files were used or user didn't have perms) - # *but all other plugins must be sucessfully completed + # *but all other plugins must be successfully completed + + use_hardlinks = False def process(self, instance): if not self.is_active(instance.data): @@ -621,19 +623,21 @@ class IntegrateHeroVersion( src_path, dst_path )) - # First try hardlink and copy if paths are cross drive - try: - create_hard_link(src_path, dst_path) - # Return when successful - return + if self.use_hardlinks: + # First try hardlink and copy if paths are cross drive + try: + create_hard_link(src_path, dst_path) + # Return when successful + return - except OSError as exc: - # re-raise exception if different than - # EXDEV - cross drive path - # EINVAL - wrong format, must be NTFS - self.log.debug("Hardlink failed with errno:'{}'".format(exc.errno)) - if exc.errno not in [errno.EXDEV, errno.EINVAL]: - raise + except OSError as exc: + # re-raise exception if different than + # EXDEV - cross drive path + # EINVAL - wrong format, must be NTFS + self.log.debug( + "Hardlink failed with errno:'{}'".format(exc.errno)) + if exc.errno not in [errno.EXDEV, errno.EINVAL]: + raise shutil.copy(src_path, dst_path) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index b37be1afe6..1b3d382f01 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -743,6 +743,14 @@ class IntegrateHeroVersionModel(BaseSettingsModel): optional: bool = SettingsField(False, title="Optional") active: bool = SettingsField(True, title="Active") families: list[str] = SettingsField(default_factory=list, title="Families") + use_hardlinks: bool = SettingsField( + False, title="Use Hardlinks", + description="When enabled first try to make a hardlink of the version " + "instead of a copy. This helps reduce disk usage, but may " + "create issues.\nFor example there are known issues on " + "Windows being unable to delete any of the hardlinks if " + "any of the links is in use creating issues with updating " + "hero versions.") class CleanUpModel(BaseSettingsModel): @@ -1136,7 +1144,8 @@ DEFAULT_PUBLISH_VALUES = { "layout", "mayaScene", "simpleUnrealTexture" - ] + ], + "use_hardlinks": False }, "CleanUp": { "paterns": [], From 0ddfaa5b91a2a6d35dec5f6df70bb6094db582a7 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Thu, 20 Jun 2024 16:04:40 +0300 Subject: [PATCH 068/118] refactor `get_avalon_container` to `get_or_create_avalon_container` Co-authored-by: Roy Nieterau --- .../client/ayon_houdini/api/pipeline.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 0ca7de23bd..ba7804cb02 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -397,21 +397,16 @@ def on_new(): _enforce_start_frame() -def get_avalon_container(): - path = AVALON_CONTAINERS - avalon_container = hou.node(path) - if not avalon_container: - # Let's create avalon container secretly - # but make sure the pipeline still is built the - # way we anticipate it was built, asserting it. - assert path == "/obj/AVALON_CONTAINERS" +def get_or_create_avalon_container() -> "hou.OpNode": + avalon_container = hou.node(AVALON_CONTAINERS) + if avalon_container: + return avalon_container - parent = hou.node("/obj") - avalon_container = parent.createNode( - "subnet", node_name="AVALON_CONTAINERS" - ) - - return avalon_container + parent_path, name = AVALON_CONTAINERS.rsplit("/", 1) + parent = hou.node(parent_path) + return parent.createNode( + "subnet", node_name=name + ) def _set_context_settings(): From 9757f276a092a119453b21266ad7c34c3c8b66b9 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 20 Jun 2024 16:12:00 +0300 Subject: [PATCH 069/118] replace `get_avalon_container` calls with `get_or_create_avalon_container` --- server_addon/houdini/client/ayon_houdini/api/pipeline.py | 2 +- .../houdini/client/ayon_houdini/plugins/load/load_hda.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index ba7804cb02..be8901d1f9 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -222,7 +222,7 @@ def containerise(name, """ # Get AVALON_CONTAINERS subnet - subnet = get_avalon_container() + subnet = get_or_create_avalon_container() # Create proper container name container_name = "{}_{}".format(name, suffix or "CON") diff --git a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py index d7268da39e..fcf0e834f8 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/load/load_hda.py @@ -90,7 +90,7 @@ class HdaLoader(plugin.HoudiniLoader): def _create_dedicated_parent_node(self, hda_def): # Get the root node - parent_node = pipeline.get_avalon_container() + parent_node = pipeline.get_or_create_avalon_container() node = None node_type = None if hda_def.nodeTypeCategory() == hou.objNodeTypeCategory(): From 6c5fe9476162b174d294fe42069bcee1afaac372 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 20 Jun 2024 16:41:28 +0300 Subject: [PATCH 070/118] update dynamic data in CreateHDA --- .../ayon_houdini/plugins/create/create_hda.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py index e03f290ae8..179a6c2b00 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_hda.py @@ -185,8 +185,14 @@ class CreateHDA(plugin.HoudiniCreator): instance ) - dynamic_data["folder"] = { - "label": folder_entity["label"], - "name": folder_entity["name"] - } + dynamic_data.update( + { + "asset": folder_entity["name"], + "folder": { + "label": folder_entity["label"], + "name": folder_entity["name"] + } + } + ) + return dynamic_data From 64c57d4f6a07394dd7f4871ec731bd94abe29131 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 00:56:26 +0200 Subject: [PATCH 071/118] Remove Houdini `Mantra IDF` product, fix #673 --- .../plugins/create/create_mantra_ifd.py | 55 ------------------- .../plugins/publish/collect_cache_farm.py | 4 +- .../plugins/publish/collect_chunk_size.py | 4 +- .../plugins/publish/collect_frames.py | 3 +- .../plugins/publish/extract_mantra_ifd.py | 49 ----------------- .../houdini/server/settings/create.py | 7 --- 6 files changed, 3 insertions(+), 119 deletions(-) delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/create/create_mantra_ifd.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_mantra_ifd.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_mantra_ifd.py deleted file mode 100644 index fc5c4819d0..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_mantra_ifd.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator plugin for creating pointcache alembics.""" -from ayon_houdini.api import plugin -from ayon_core.lib import BoolDef - - -class CreateMantraIFD(plugin.HoudiniCreator): - """Mantra .ifd Archive""" - identifier = "io.openpype.creators.houdini.mantraifd" - label = "Mantra IFD" - product_type = "mantraifd" - icon = "gears" - - def create(self, product_name, instance_data, pre_create_data): - import hou - instance_data.pop("active", None) - instance_data.update({"node_type": "ifd"}) - creator_attributes = instance_data.setdefault( - "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] - instance = super(CreateMantraIFD, self).create( - product_name, - instance_data, - pre_create_data) - - instance_node = hou.node(instance.get("instance_node")) - - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.ifd".format(product_name)) - parms = { - # Render frame range - "trange": 1, - # Arnold ROP settings - "soho_diskfile": filepath, - "soho_outputmode": 1 - } - - instance_node.setParms(parms) - - # Lock any parameters in this list - to_lock = ["soho_outputmode", "productType", "id"] - self.lock_parameters(instance_node, to_lock) - - def get_instance_attr_defs(self): - return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) - ] - - def get_pre_create_attr_defs(self): - attrs = super().get_pre_create_attr_defs() - # Use same attributes as for instance attributes - return attrs + self.get_instance_attr_defs() diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_cache_farm.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_cache_farm.py index ecfebccfef..c558f35208 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_cache_farm.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_cache_farm.py @@ -12,9 +12,7 @@ class CollectDataforCache(plugin.HoudiniInstancePlugin): # Run after Collect Frames order = pyblish.api.CollectorOrder + 0.11 - families = ["ass", "pointcache", - "mantraifd", "redshiftproxy", - "vdbcache", "model"] + families = ["ass", "pointcache", "redshiftproxy", "vdbcache", "model"] targets = ["local", "remote"] label = "Collect Data for Cache" diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_chunk_size.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_chunk_size.py index 6ff53b7695..cd94827ba7 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_chunk_size.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_chunk_size.py @@ -9,9 +9,7 @@ class CollectChunkSize(plugin.HoudiniInstancePlugin, """Collect chunk size for cache submission to Deadline.""" order = pyblish.api.CollectorOrder + 0.05 - families = ["ass", "pointcache", - "vdbcache", "mantraifd", - "redshiftproxy", "model"] + families = ["ass", "pointcache", "vdbcache", "redshiftproxy", "model"] targets = ["local", "remote"] label = "Collect Chunk Size" chunk_size = 999999 diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py index 3378657bfd..5b85023123 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py @@ -16,8 +16,7 @@ class CollectFrames(plugin.HoudiniInstancePlugin): order = pyblish.api.CollectorOrder + 0.1 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", - "mantraifd", "redshiftproxy", "review", - "pointcache"] + "redshiftproxy", "review", "pointcache"] def process(self, instance): diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py deleted file mode 100644 index b424f2e452..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_mantra_ifd.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import hou - -import pyblish.api - -from ayon_houdini.api import plugin - - -class ExtractMantraIFD(plugin.HoudiniExtractorPlugin): - - order = pyblish.api.ExtractorOrder - label = "Extract Mantra ifd" - families = ["mantraifd"] - targets = ["local", "remote"] - - def process(self, instance): - if instance.data.get("farm"): - self.log.debug("Should be processed on farm, skipping.") - return - - ropnode = hou.node(instance.data.get("instance_node")) - output = ropnode.evalParm("soho_diskfile") - staging_dir = os.path.dirname(output) - instance.data["stagingDir"] = staging_dir - - files = instance.data["frames"] - missing_frames = [ - frame - for frame in instance.data["frames"] - if not os.path.exists( - os.path.normpath(os.path.join(staging_dir, frame))) - ] - if missing_frames: - raise RuntimeError("Failed to complete Mantra ifd extraction. " - "Missing output files: {}".format( - missing_frames)) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'ifd', - 'ext': 'ifd', - 'files': files, - "stagingDir": staging_dir, - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - } - instance.data["representations"].append(representation) diff --git a/server_addon/houdini/server/settings/create.py b/server_addon/houdini/server/settings/create.py index cd1e110c23..02fb052a36 100644 --- a/server_addon/houdini/server/settings/create.py +++ b/server_addon/houdini/server/settings/create.py @@ -51,9 +51,6 @@ class CreatePluginsModel(BaseSettingsModel): CreateKarmaROP: CreatorModel = SettingsField( default_factory=CreatorModel, title="Create Karma ROP") - CreateMantraIFD: CreatorModel = SettingsField( - default_factory=CreatorModel, - title="Create Mantra IFD") CreateMantraROP: CreatorModel = SettingsField( default_factory=CreatorModel, title="Create Mantra ROP") @@ -119,10 +116,6 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "enabled": True, "default_variants": ["Main"] }, - "CreateMantraIFD": { - "enabled": True, - "default_variants": ["Main"] - }, "CreateMantraROP": { "enabled": True, "default_variants": ["Main"] From 2a847b1e55750b64eea69da6eb11e07a743ad1d7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 15:37:11 +0200 Subject: [PATCH 072/118] Bump houdini addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index 10d1478249..af2c4557db 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.2" +__version__ = "0.3.3" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 1f7879483e..da13bee9c7 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.2" +version = "0.3.3" client_dir = "ayon_houdini" From 8336a0ff1843c14beffe3ff405aa31591f8be8cd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jun 2024 21:42:41 +0800 Subject: [PATCH 073/118] add the setdress setting into ayon setting --- .../ayon_maya/plugins/create/create_setdress.py | 6 ++++-- server_addon/maya/client/ayon_maya/version.py | 2 +- server_addon/maya/package.py | 2 +- server_addon/maya/server/settings/creators.py | 14 ++++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py b/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py index 98d6de867f..6e1c4e1c4f 100644 --- a/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py +++ b/server_addon/maya/client/ayon_maya/plugins/create/create_setdress.py @@ -9,14 +9,16 @@ class CreateSetDress(plugin.MayaCreator): label = "Set Dress" product_type = "setdress" icon = "cubes" + exactSetMembersOnly = True + shader = True default_variants = ["Main", "Anim"] def get_instance_attr_defs(self): return [ BoolDef("exactSetMembersOnly", label="Exact Set Members Only", - default=True), + default=self.exactSetMembersOnly), BoolDef("shader", label="Include shader", - default=True) + default=self.shader) ] diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index 37f9026945..df66e3f399 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.4" +__version__ = "0.2.5" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 17614ed9c1..3dd863a1b3 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.4" +version = "0.2.5" client_dir = "ayon_maya" ayon_required_addons = { diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 5f3b850a1f..ede33b6eec 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -124,6 +124,14 @@ class CreateVrayProxyModel(BaseSettingsModel): default_factory=list, title="Default Products") +class CreateSetDressModel(BaseSettingsModel): + enabled: bool = SettingsField(True) + exactSetMembersOnly: bool = SettingsField(title="Exact Set Members Only") + shader: bool = SettingsField(title="Include shader") + default_variants: list[str] = SettingsField( + default_factory=list, title="Default Products") + + class CreateMultishotLayout(BasicCreatorModel): shotParent: str = SettingsField(title="Shot Parent Folder") groupLoadedAssets: bool = SettingsField(title="Group Loaded Assets") @@ -217,8 +225,8 @@ class CreatorsModel(BaseSettingsModel): default_factory=BasicCreatorModel, title="Create Rig" ) - CreateSetDress: BasicCreatorModel = SettingsField( - default_factory=BasicCreatorModel, + CreateSetDress: CreateSetDressModel = SettingsField( + default_factory=CreateSetDressModel, title="Create Set Dress" ) CreateVrayProxy: CreateVrayProxyModel = SettingsField( @@ -396,6 +404,8 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateSetDress": { "enabled": True, + "exactSetMembersOnly": True, + "shader": True, "default_variants": [ "Main", "Anim" From f6c7d508d9fe62976f3027bad9311f374b2e1874 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jun 2024 21:49:11 +0800 Subject: [PATCH 074/118] add the setdress setting into ayon setting --- .../client/ayon_maya/plugins/publish/extract_maya_scene_raw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py index fde48afb8f..2052abe648 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py @@ -96,7 +96,7 @@ class ExtractMayaSceneRaw(plugin.MayaExtractorPlugin, AYONPyblishPluginMixin): "preserve_references" ], constructionHistory=True, - shader=True if instance.data.get("shader", True) else False, + shader=instance.data.get("shader", True), constraints=True, expressions=True) From 187b2e98c53e49819c807eab9829f436b16af676 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 16:23:50 +0200 Subject: [PATCH 075/118] Fix FBX export --- .../client/ayon_houdini/plugins/publish/collect_frames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py index 94feb7532c..64bf0c8f46 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py @@ -17,7 +17,7 @@ class CollectFrames(plugin.HoudiniInstancePlugin): label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "mantraifd", "redshiftproxy", "review", - "pointcache"] + "pointcache", "fbx"] def process(self, instance): From 89eb86a140a8195b45bce495244d7cfd21ebc5f6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jun 2024 22:34:41 +0800 Subject: [PATCH 076/118] add support to assign default shaders to the published setdress --- .../plugins/publish/extract_maya_scene_raw.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py index 2052abe648..7253dc38c2 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py @@ -5,7 +5,7 @@ import os from ayon_core.lib import BoolDef from ayon_core.pipeline import AVALON_CONTAINER_ID, AYON_CONTAINER_ID from ayon_core.pipeline.publish import AYONPyblishPluginMixin -from ayon_maya.api.lib import maintained_selection +from ayon_maya.api.lib import maintained_selection, shader from ayon_maya.api import plugin from maya import cmds @@ -88,17 +88,30 @@ class ExtractMayaSceneRaw(plugin.MayaExtractorPlugin, AYONPyblishPluginMixin): ) with maintained_selection(): cmds.select(selection, noExpand=True) - cmds.file(path, - force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 - exportSelected=True, - preserveReferences=attribute_values[ - "preserve_references" - ], - constructionHistory=True, - shader=instance.data.get("shader", True), - constraints=True, - expressions=True) + if instance.data.get("shader", True): + with shader(selection, shadingEngine="initialShadingGroup"): + cmds.file(path, + force=True, + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 + exportSelected=True, + preserveReferences=attribute_values[ + "preserve_references" + ], + constructionHistory=True, + shader=instance.data.get("shader", True), + expressions=True) + else: + cmds.file(path, + force=True, + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 + exportSelected=True, + preserveReferences=attribute_values[ + "preserve_references" + ], + constructionHistory=True, + shader=True, + constraints=True, + expressions=True) if "representations" not in instance.data: instance.data["representations"] = [] From 32ebf9c34f91e8f4cc88ed6f98d97c2d6d600fd5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 17:08:05 +0200 Subject: [PATCH 077/118] Fix `camera` product type export --- .../client/ayon_houdini/plugins/publish/collect_frames.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py index 64bf0c8f46..9aceb5a1d0 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_frames.py @@ -15,9 +15,8 @@ class CollectFrames(plugin.HoudiniInstancePlugin): # this plugin runs after CollectRopFrameRange order = pyblish.api.CollectorOrder + 0.1 label = "Collect Frames" - families = ["vdbcache", "imagesequence", "ass", - "mantraifd", "redshiftproxy", "review", - "pointcache", "fbx"] + families = ["camera", "vdbcache", "imagesequence", "ass", "mantraifd", + "redshiftproxy", "review", "pointcache", "fbx"] def process(self, instance): From 9b02eed4c6c4620f3b1c08944c0e41f4b8e0fe89 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jun 2024 23:08:30 +0800 Subject: [PATCH 078/118] add exit stack contextlib for default shader assignment for meshes in the published scene --- .../plugins/publish/extract_maya_scene_raw.py | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py index 7253dc38c2..047b7f6e6c 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/extract_maya_scene_raw.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Extract data as Maya scene (raw).""" import os - +import contextlib from ayon_core.lib import BoolDef from ayon_core.pipeline import AVALON_CONTAINER_ID, AYON_CONTAINER_ID from ayon_core.pipeline.publish import AYONPyblishPluginMixin @@ -88,28 +88,19 @@ class ExtractMayaSceneRaw(plugin.MayaExtractorPlugin, AYONPyblishPluginMixin): ) with maintained_selection(): cmds.select(selection, noExpand=True) - if instance.data.get("shader", True): - with shader(selection, shadingEngine="initialShadingGroup"): - cmds.file(path, - force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 - exportSelected=True, - preserveReferences=attribute_values[ - "preserve_references" - ], - constructionHistory=True, - shader=instance.data.get("shader", True), - expressions=True) - else: + with contextlib.ExitStack() as stack: + if not instance.data.get("shader", True): + # Fix bug where export without shader may import the geometry 'green' + # due to the lack of any shader on import. + stack.enter_context(shader(selection, shadingEngine="initialShadingGroup")) + cmds.file(path, force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", exportSelected=True, - preserveReferences=attribute_values[ - "preserve_references" - ], + preserveReferences=attribute_values["preserve_references"], constructionHistory=True, - shader=True, + shader=instance.data.get("shader", True), constraints=True, expressions=True) From b61bb2dfc8aaf0452e0548ba8559ef6378d0c93b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 18:00:08 +0200 Subject: [PATCH 079/118] Bump houdini addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index 10d1478249..af2c4557db 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.2" +__version__ = "0.3.3" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 1f7879483e..da13bee9c7 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.2" +version = "0.3.3" client_dir = "ayon_houdini" From 24a508a7040af06c7ded60d0dfae76e6482ffda8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 22 Jun 2024 00:23:28 +0800 Subject: [PATCH 080/118] remove unused function in extract rop --- .../houdini/client/ayon_houdini/plugins/publish/extract_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index 23657b66c9..62a38c0b93 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -5,7 +5,7 @@ import pyblish.api from ayon_core.pipeline import publish from ayon_houdini.api import plugin -from ayon_houdini.api.lib import render_rop, get_output_parameter, splitext +from ayon_houdini.api.lib import render_rop, splitext class ExtractROP(plugin.HoudiniExtractorPlugin): From 7e6af882bd42a24cd8b438f7d091a4e2c2c080fe Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 21 Jun 2024 22:13:06 +0300 Subject: [PATCH 081/118] support_opening_workfile_on_launching_houdini --- .../houdini/client/ayon_houdini/api/lib.py | 50 ++++++++++++++++++- .../client/ayon_houdini/api/pipeline.py | 5 +- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index 671265fae9..eefe895b8f 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -8,6 +8,7 @@ import json from contextlib import contextmanager import six +from qtpy import QtCore, QtWidgets import ayon_api from ayon_core.lib import StringTemplate @@ -23,7 +24,11 @@ from ayon_core.pipeline import ( from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup +from ayon_core.tools.utils import ( + PopupUpdateKeys, + SimplePopup, + host_tools +) from ayon_core.tools.utils.host_tools import get_tool_by_name import hou @@ -1193,3 +1198,46 @@ def prompt_reset_context(): update_content_on_context_change() dialog.deleteLater() + + +def wait_startup_launch_workfiles_app(): + """Show workfiles tool on Houdini launch. + + Trigger to show workfiles tool on application launch. Can be executed only + once all other calls are ignored. + + Workfiles tool show is deferred after application initialization using + QTimer. + + Basically, it should wait till the app finish starting up. + """ + + # Show workfiles tool using timer + # - this will be probably triggered during initialization in that case + # the application is not be able to show uis so it must be + # deferred using timer + # - timer should be processed when initialization ends + # When applications starts to process events. + timer = QtCore.QTimer() + timer.timeout.connect(lambda: _launch_workfile_app(timer)) + timer.setInterval(100) + timer.start() + + +def _launch_workfile_app(timer): + # Safeguard to not show window when application is still starting up + # or is already closing down. + closing_down = QtWidgets.QApplication.closingDown() + starting_up = QtWidgets.QApplication.startingUp() + + # Stop the timer if application finished start up of is closing down + if closing_down or not starting_up: + timer.stop() + + # Skip if application is starting up or closing down + if starting_up or closing_down: + return + + # Make sure on top is enabled on first show so the window is not hidden + # under main nuke window + host_tools.show_workfiles(parent=hou.qt.mainWindow(), on_top=True) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 6af4993d25..9d420a92d3 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -85,10 +85,9 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # initialization during start up delays Houdini UI by minutes # making it extremely slow to launch. hdefereval.executeDeferred(shelves.generate_shelves) - - if not IS_HEADLESS: - import hdefereval # noqa, hdefereval is only available in ui mode hdefereval.executeDeferred(creator_node_shelves.install) + if os.environ.get("AYON_WORKFILE_TOOL_ON_START"): + hdefereval.executeDeferred(lib.wait_startup_launch_workfiles_app) def workfile_has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() From e6bb56b088ba001859ff02b61d577638f8d61ce1 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 21 Jun 2024 22:56:53 +0300 Subject: [PATCH 082/118] refactor launching the workfile - remove redundant code --- .../houdini/client/ayon_houdini/api/lib.py | 50 +------------------ .../client/ayon_houdini/api/pipeline.py | 11 +++- 2 files changed, 10 insertions(+), 51 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index eefe895b8f..671265fae9 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -8,7 +8,6 @@ import json from contextlib import contextmanager import six -from qtpy import QtCore, QtWidgets import ayon_api from ayon_core.lib import StringTemplate @@ -24,11 +23,7 @@ from ayon_core.pipeline import ( from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.tools.utils import ( - PopupUpdateKeys, - SimplePopup, - host_tools -) +from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup from ayon_core.tools.utils.host_tools import get_tool_by_name import hou @@ -1198,46 +1193,3 @@ def prompt_reset_context(): update_content_on_context_change() dialog.deleteLater() - - -def wait_startup_launch_workfiles_app(): - """Show workfiles tool on Houdini launch. - - Trigger to show workfiles tool on application launch. Can be executed only - once all other calls are ignored. - - Workfiles tool show is deferred after application initialization using - QTimer. - - Basically, it should wait till the app finish starting up. - """ - - # Show workfiles tool using timer - # - this will be probably triggered during initialization in that case - # the application is not be able to show uis so it must be - # deferred using timer - # - timer should be processed when initialization ends - # When applications starts to process events. - timer = QtCore.QTimer() - timer.timeout.connect(lambda: _launch_workfile_app(timer)) - timer.setInterval(100) - timer.start() - - -def _launch_workfile_app(timer): - # Safeguard to not show window when application is still starting up - # or is already closing down. - closing_down = QtWidgets.QApplication.closingDown() - starting_up = QtWidgets.QApplication.startingUp() - - # Stop the timer if application finished start up of is closing down - if closing_down or not starting_up: - timer.stop() - - # Skip if application is starting up or closing down - if starting_up or closing_down: - return - - # Make sure on top is enabled on first show so the window is not hidden - # under main nuke window - host_tools.show_workfiles(parent=hou.qt.mainWindow(), on_top=True) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 9d420a92d3..2c28e33929 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -6,7 +6,7 @@ import logging import hou # noqa from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost - +from ayon_core.tools.utils import host_tools import pyblish.api from ayon_core.pipeline import ( @@ -25,6 +25,13 @@ from ayon_core.lib import ( emit_event, ) +def show_workfiles_tool(): + # Make sure on top is enabled on first show so the + # window is not hidden under main nuke window + print("showing workfiles tool..") + from ayon_core.tools.utils import host_tools + host_tools.show_workfiles(parent=hou.qt.mainWindow(), + on_top=True) log = logging.getLogger("ayon_houdini") @@ -87,7 +94,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): hdefereval.executeDeferred(shelves.generate_shelves) hdefereval.executeDeferred(creator_node_shelves.install) if os.environ.get("AYON_WORKFILE_TOOL_ON_START"): - hdefereval.executeDeferred(lib.wait_startup_launch_workfiles_app) + hdefereval.executeDeferred(lambda: host_tools.show_workfiles(parent=hou.qt.mainWindow())) def workfile_has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() From d88c36b01ec50a706bb544f09574dadc963e762e Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Fri, 21 Jun 2024 23:05:34 +0300 Subject: [PATCH 083/118] remove redundant code Co-authored-by: Roy Nieterau --- server_addon/houdini/client/ayon_houdini/api/pipeline.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 2c28e33929..463191f787 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -25,13 +25,6 @@ from ayon_core.lib import ( emit_event, ) -def show_workfiles_tool(): - # Make sure on top is enabled on first show so the - # window is not hidden under main nuke window - print("showing workfiles tool..") - from ayon_core.tools.utils import host_tools - host_tools.show_workfiles(parent=hou.qt.mainWindow(), - on_top=True) log = logging.getLogger("ayon_houdini") From 583bc8f86b596ed0e6fc27b97e46b20d5b9929b5 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 21 Jun 2024 23:07:52 +0300 Subject: [PATCH 084/118] use env_value_to_bool instead of os.environ.get --- server_addon/houdini/client/ayon_houdini/api/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/pipeline.py b/server_addon/houdini/client/ayon_houdini/api/pipeline.py index 463191f787..22a15605c7 100644 --- a/server_addon/houdini/client/ayon_houdini/api/pipeline.py +++ b/server_addon/houdini/client/ayon_houdini/api/pipeline.py @@ -23,6 +23,7 @@ from ayon_houdini.api import lib, shelves, creator_node_shelves from ayon_core.lib import ( register_event_callback, emit_event, + env_value_to_bool, ) @@ -86,7 +87,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # making it extremely slow to launch. hdefereval.executeDeferred(shelves.generate_shelves) hdefereval.executeDeferred(creator_node_shelves.install) - if os.environ.get("AYON_WORKFILE_TOOL_ON_START"): + if env_value_to_bool("AYON_WORKFILE_TOOL_ON_START"): hdefereval.executeDeferred(lambda: host_tools.show_workfiles(parent=hou.qt.mainWindow())) def workfile_has_unsaved_changes(self): From 51d63d849335257098d0791703ca0484db8f9ed6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 09:29:06 +0200 Subject: [PATCH 085/118] Houdini: Remove the legacy creator from before new publisher UI --- .../houdini/client/ayon_houdini/api/plugin.py | 82 ++----------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/plugin.py b/server_addon/houdini/client/ayon_houdini/api/plugin.py index 9c6bba925a..c78d85924e 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -10,8 +10,7 @@ import hou import pyblish.api from ayon_core.pipeline import ( CreatorError, - LegacyCreator, - Creator as NewCreator, + Creator, CreatedInstance, AYON_INSTANCE_ID, AVALON_INSTANCE_ID, @@ -22,84 +21,13 @@ from ayon_core.lib import BoolDef from .lib import imprint, read, lsattr, add_self_publish_button +# Backwards compatibility +NewCreator = Creator + SETTINGS_CATEGORY = "houdini" -class Creator(LegacyCreator): - """Creator plugin to create instances in Houdini - - To support the wide range of node types for render output (Alembic, VDB, - Mantra) the Creator needs a node type to create the correct instance - - By default, if none is given, is `geometry`. An example of accepted node - types: geometry, alembic, ifd (mantra) - - Please check the Houdini documentation for more node types. - - Tip: to find the exact node type to create press the `i` left of the node - when hovering over a node. The information is visible under the name of - the node. - - Deprecated: - This creator is deprecated and will be removed in future version. - - """ - defaults = ['Main'] - - def __init__(self, *args, **kwargs): - super(Creator, self).__init__(*args, **kwargs) - self.nodes = [] - - def process(self): - """This is the base functionality to create instances in Houdini - - The selected nodes are stored in self to be used in an override method. - This is currently necessary in order to support the multiple output - types in Houdini which can only be rendered through their own node. - - Default node type if none is given is `geometry` - - It also makes it easier to apply custom settings per instance type - - Example of override method for Alembic: - - def process(self): - instance = super(CreateEpicNode, self, process() - # Set parameters for Alembic node - instance.setParms( - {"sop_path": "$HIP/%s.abc" % self.nodes[0]} - ) - - Returns: - hou.Node - - """ - try: - if (self.options or {}).get("useSelection"): - self.nodes = hou.selectedNodes() - - # Get the node type and remove it from the data, not needed - node_type = self.data.pop("node_type", None) - if node_type is None: - node_type = "geometry" - - # Get out node - out = hou.node("/out") - instance = out.createNode(node_type, node_name=self.name) - instance.moveToGoodPosition() - - imprint(instance, self.data) - - self._process(instance) - - except hou.Error as er: - six.reraise( - CreatorError, - CreatorError("Creator error: {}".format(er)), - sys.exc_info()[2]) - - class HoudiniCreatorBase(object): @staticmethod def cache_instance_data(shared_data): @@ -170,7 +98,7 @@ class HoudiniCreatorBase(object): @six.add_metaclass(ABCMeta) -class HoudiniCreator(NewCreator, HoudiniCreatorBase): +class HoudiniCreator(Creator, HoudiniCreatorBase): """Base class for most of the Houdini creator plugins.""" selected_nodes = [] settings_name = None From fc4df957e6351d120b8ac7cc6e566d762729a3e8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 09:59:35 +0200 Subject: [PATCH 086/118] Maya: Remove the legacy creator from before new publisher UI --- .../maya/client/ayon_maya/api/__init__.py | 2 -- .../maya/client/ayon_maya/api/plugin.py | 26 +++++-------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/api/__init__.py b/server_addon/maya/client/ayon_maya/api/__init__.py index 0948282f57..8783fbeeb7 100644 --- a/server_addon/maya/client/ayon_maya/api/__init__.py +++ b/server_addon/maya/client/ayon_maya/api/__init__.py @@ -12,7 +12,6 @@ from .pipeline import ( MayaHost, ) from .plugin import ( - Creator, Loader ) @@ -45,7 +44,6 @@ __all__ = [ "containerise", "MayaHost", - "Creator", "Loader", # Workfiles API diff --git a/server_addon/maya/client/ayon_maya/api/plugin.py b/server_addon/maya/client/ayon_maya/api/plugin.py index b8d9748ef1..6ff428567e 100644 --- a/server_addon/maya/client/ayon_maya/api/plugin.py +++ b/server_addon/maya/client/ayon_maya/api/plugin.py @@ -15,10 +15,9 @@ from ayon_core.pipeline import ( Anatomy, AutoCreator, CreatedInstance, - Creator as NewCreator, + Creator, CreatorError, HiddenCreator, - LegacyCreator, LoaderPlugin, get_current_project_name, get_representation_path, @@ -35,6 +34,9 @@ from . import lib from .lib import imprint, read from .pipeline import containerise +# Backwards compatibility +NewCreator = Creator + log = Logger.get_logger() SETTINGS_CATEGORY = "maya" @@ -70,22 +72,6 @@ def get_reference_node_parents(*args, **kwargs): return lib.get_reference_node_parents(*args, **kwargs) -class Creator(LegacyCreator): - defaults = ['Main'] - - def process(self): - nodes = list() - - with lib.undo_chunk(): - if (self.options or {}).get("useSelection"): - nodes = cmds.ls(selection=True) - - instance = cmds.sets(nodes, name=self.name) - lib.imprint(instance, self.data) - - return instance - - @six.add_metaclass(ABCMeta) class MayaCreatorBase(object): @@ -274,7 +260,7 @@ class MayaCreatorBase(object): @six.add_metaclass(ABCMeta) -class MayaCreator(NewCreator, MayaCreatorBase): +class MayaCreator(Creator, MayaCreatorBase): settings_category = "maya" @@ -381,7 +367,7 @@ def ensure_namespace(namespace): return cmds.namespace(add=namespace) -class RenderlayerCreator(NewCreator, MayaCreatorBase): +class RenderlayerCreator(Creator, MayaCreatorBase): """Creator which creates an instance per renderlayer in the workfile. Create and manages renderlayer product per renderLayer in workfile. From 61074623d498f6191f19934df546d4586239b133 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 10:01:55 +0200 Subject: [PATCH 087/118] Remove import of `Creator` which isn't actually the Houdini specific Creator anyway --- server_addon/houdini/client/ayon_houdini/api/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/__init__.py b/server_addon/houdini/client/ayon_houdini/api/__init__.py index 2663a55f6f..358113a555 100644 --- a/server_addon/houdini/client/ayon_houdini/api/__init__.py +++ b/server_addon/houdini/client/ayon_houdini/api/__init__.py @@ -4,10 +4,6 @@ from .pipeline import ( containerise ) -from .plugin import ( - Creator, -) - from .lib import ( lsattr, lsattrs, @@ -23,8 +19,6 @@ __all__ = [ "ls", "containerise", - "Creator", - # Utility functions "lsattr", "lsattrs", From a01f047e2a6761ffb2cef86c499dbb8779cc584e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:42:11 +0200 Subject: [PATCH 088/118] convert version string to integer --- server_addon/maya/client/ayon_maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/api/lib.py b/server_addon/maya/client/ayon_maya/api/lib.py index 3b351ec1f0..0242dafc0b 100644 --- a/server_addon/maya/client/ayon_maya/api/lib.py +++ b/server_addon/maya/client/ayon_maya/api/lib.py @@ -1733,7 +1733,7 @@ def is_valid_reference_node(reference_node): """ # maya 2022 is missing `isValidReference` so the check needs to be # done in different way. - if cmds.about(version=True) < 2023: + if int(cmds.about(version=True)) < 2023: try: cmds.referenceQuery(reference_node, filename=True) return True From 0ab3653f360e530402d17bc1c78fbeea4f2cd9fd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 10:45:33 +0200 Subject: [PATCH 089/118] Report whether it's hardlinking or copying; also report whether hardlinking failed. --- .../plugins/publish/integrate_hero_version.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 95b9f418f9..4fb8b886a9 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -619,12 +619,11 @@ class IntegrateHeroVersion( self.log.debug("Folder already exists: \"{}\"".format(dirname)) - self.log.debug("Copying file \"{}\" to \"{}\"".format( - src_path, dst_path - )) - if self.use_hardlinks: # First try hardlink and copy if paths are cross drive + self.log.debug("Hardlinking file \"{}\" to \"{}\"".format( + src_path, dst_path + )) try: create_hard_link(src_path, dst_path) # Return when successful @@ -639,6 +638,13 @@ class IntegrateHeroVersion( if exc.errno not in [errno.EXDEV, errno.EINVAL]: raise + self.log.debug( + "Hardlinking failed, falling back to regular copy...") + + self.log.debug("Copying file \"{}\" to \"{}\"".format( + src_path, dst_path + )) + shutil.copy(src_path, dst_path) def version_from_representations(self, project_name, repres): From 0fdb63bc6779e3956c5b2f240a0d6fceb664b641 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:01:15 +0200 Subject: [PATCH 090/118] bump version to '0.2.5' --- server_addon/maya/client/ayon_maya/version.py | 2 +- server_addon/maya/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index 37f9026945..df66e3f399 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.4" +__version__ = "0.2.5" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 17614ed9c1..3dd863a1b3 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.4" +version = "0.2.5" client_dir = "ayon_maya" ayon_required_addons = { From 2e8d578d8f4999fb3e58ec473470c77ba62a2d3f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 11:22:29 +0200 Subject: [PATCH 091/118] Update server_addon/maya/client/ayon_maya/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/maya/client/ayon_maya/api/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/api/plugin.py b/server_addon/maya/client/ayon_maya/api/plugin.py index 6ff428567e..45b2151e26 100644 --- a/server_addon/maya/client/ayon_maya/api/plugin.py +++ b/server_addon/maya/client/ayon_maya/api/plugin.py @@ -34,8 +34,6 @@ from . import lib from .lib import imprint, read from .pipeline import containerise -# Backwards compatibility -NewCreator = Creator log = Logger.get_logger() SETTINGS_CATEGORY = "maya" From b0701dc6d5b54178ebbeb7c8d38a792c96bde6f5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 11:23:36 +0200 Subject: [PATCH 092/118] Remove backwards compatibility --- server_addon/houdini/client/ayon_houdini/api/plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/plugin.py b/server_addon/houdini/client/ayon_houdini/api/plugin.py index c78d85924e..b841b57617 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -21,9 +21,6 @@ from ayon_core.lib import BoolDef from .lib import imprint, read, lsattr, add_self_publish_button -# Backwards compatibility -NewCreator = Creator - SETTINGS_CATEGORY = "houdini" From 48a9077acb7c3ef5d5c4bc8912e2f5023823833d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 11:24:39 +0200 Subject: [PATCH 093/118] Bump Maya addon version --- server_addon/maya/client/ayon_maya/version.py | 2 +- server_addon/maya/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index df66e3f399..c5fbef58fe 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.5" +__version__ = "0.2.6" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 3dd863a1b3..2f70b630d5 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.5" +version = "0.2.6" client_dir = "ayon_maya" ayon_required_addons = { From 79ccd665944bc7546f40a8695b06671760d3b895 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 11:25:39 +0200 Subject: [PATCH 094/118] Cosmetics --- server_addon/maya/client/ayon_maya/api/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server_addon/maya/client/ayon_maya/api/plugin.py b/server_addon/maya/client/ayon_maya/api/plugin.py index 45b2151e26..d2678e2100 100644 --- a/server_addon/maya/client/ayon_maya/api/plugin.py +++ b/server_addon/maya/client/ayon_maya/api/plugin.py @@ -34,7 +34,6 @@ from . import lib from .lib import imprint, read from .pipeline import containerise - log = Logger.get_logger() SETTINGS_CATEGORY = "maya" From 7daf688533e3db83cf32d1c138808d49fc45f58b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 11:28:20 +0200 Subject: [PATCH 095/118] Bump houdini addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index af2c4557db..66f3ac59e7 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.3" +__version__ = "0.3.4" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index da13bee9c7..0c1b1fcf9b 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.3" +version = "0.3.4" client_dir = "ayon_houdini" From 2834c16dec124522bdd68914192823de61ff8675 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 12:25:47 +0200 Subject: [PATCH 096/118] Skip viewers that are not currently visible --- .../houdini/client/ayon_houdini/api/lib.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index 671265fae9..29fb038de0 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -1038,17 +1038,25 @@ def add_self_publish_button(node): node.setParmTemplateGroup(template) -def get_scene_viewer(): +def get_scene_viewer(visible_only=True): """ Return an instance of a visible viewport. There may be many, some could be closed, any visible are current + Arguments: + visible_only (Optional[bool]): Only return viewers that currently + are the active tab (and hence are visible). + Returns: Optional[hou.SceneViewer]: A scene viewer, if any. """ panes = hou.ui.paneTabs() panes = [x for x in panes if x.type() == hou.paneTabType.SceneViewer] + + if visible_only: + return next((pane for pane in panes if pane.isCurrentTab()), None) + panes = sorted(panes, key=lambda x: x.isCurrentTab()) if panes: return panes[-1] @@ -1067,12 +1075,10 @@ def sceneview_snapshot( So, it's capable of generating snapshots image sequence. It works in different Houdini context e.g. Objects, Solaris - Example: - This is how the function can be used:: - - from ayon_houdini.api import lib - sceneview = hou.ui.paneTabOfType(hou.paneTabType.SceneViewer) - lib.sceneview_snapshot(sceneview) + Example:: + >>> from ayon_houdini.api import lib + >>> sceneview = hou.ui.paneTabOfType(hou.paneTabType.SceneViewer) + >>> lib.sceneview_snapshot(sceneview) Notes: .png output will render poorly, so use .jpg. From 201d36589935b92c137a2a4810e2a406ad0d9be5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 12:31:29 +0200 Subject: [PATCH 097/118] Cosmetics (fix docstring triple quotes) --- .../client/ayon_aftereffects/api/workfile_template_builder.py | 2 +- .../client/ayon_blender/plugins/publish/extract_thumbnail.py | 2 +- .../client/ayon_hiero/vendor/google/protobuf/text_format.py | 2 +- .../nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py b/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py index 77fd1059b5..a4d90be548 100644 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py +++ b/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py @@ -92,7 +92,7 @@ class AEPlaceholderPlugin(PlaceholderPlugin): return None, None def _collect_scene_placeholders(self): - """" Cache placeholder data to shared data. + """ Cache placeholder data to shared data. Returns: (list) of dicts """ diff --git a/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py b/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py index 40097aaa89..e3bce8bf73 100644 --- a/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py +++ b/server_addon/blender/client/ayon_blender/plugins/publish/extract_thumbnail.py @@ -83,7 +83,7 @@ class ExtractThumbnail(plugin.BlenderExtractor): instance.data["representations"].append(representation) def _fix_output_path(self, filepath): - """"Workaround to return correct filepath. + """Workaround to return correct filepath. To workaround this we just glob.glob() for any file extensions and assume the latest modified file is the correct file and return it. diff --git a/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py b/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py index 412385c26f..39739e21fb 100644 --- a/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py +++ b/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py @@ -548,7 +548,7 @@ class _Printer(object): self.out.write(' ' if self.as_one_line else '\n') def _PrintShortRepeatedPrimitivesValue(self, field, value): - """"Prints short repeated primitives value.""" + """Prints short repeated primitives value.""" # Note: this is called only when value has at least one element. self._PrintFieldName(field) self.out.write(' [') diff --git a/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py index 412385c26f..39739e21fb 100644 --- a/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py +++ b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py @@ -548,7 +548,7 @@ class _Printer(object): self.out.write(' ' if self.as_one_line else '\n') def _PrintShortRepeatedPrimitivesValue(self, field, value): - """"Prints short repeated primitives value.""" + """Prints short repeated primitives value.""" # Note: this is called only when value has at least one element. self._PrintFieldName(field) self.out.write(' [') From 28dede482fedddebf272696c85b21db8fca909f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 14:37:48 +0200 Subject: [PATCH 098/118] Update server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../client/ayon_aftereffects/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py b/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py index a4d90be548..7fbd469851 100644 --- a/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py +++ b/server_addon/aftereffects/client/ayon_aftereffects/api/workfile_template_builder.py @@ -92,7 +92,7 @@ class AEPlaceholderPlugin(PlaceholderPlugin): return None, None def _collect_scene_placeholders(self): - """ Cache placeholder data to shared data. + """Cache placeholder data to shared data. Returns: (list) of dicts """ From e342f9fe0d6594e930f28ed83617ee61c33a7602 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 14:49:23 +0200 Subject: [PATCH 099/118] Revert changes to vendorized files Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../client/ayon_hiero/vendor/google/protobuf/text_format.py | 2 +- .../nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py b/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py index 39739e21fb..412385c26f 100644 --- a/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py +++ b/server_addon/hiero/client/ayon_hiero/vendor/google/protobuf/text_format.py @@ -548,7 +548,7 @@ class _Printer(object): self.out.write(' ' if self.as_one_line else '\n') def _PrintShortRepeatedPrimitivesValue(self, field, value): - """Prints short repeated primitives value.""" + """"Prints short repeated primitives value.""" # Note: this is called only when value has at least one element. self._PrintFieldName(field) self.out.write(' [') diff --git a/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py index 39739e21fb..412385c26f 100644 --- a/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py +++ b/server_addon/nuke/client/ayon_nuke/vendor/google/protobuf/text_format.py @@ -548,7 +548,7 @@ class _Printer(object): self.out.write(' ' if self.as_one_line else '\n') def _PrintShortRepeatedPrimitivesValue(self, field, value): - """Prints short repeated primitives value.""" + """"Prints short repeated primitives value.""" # Note: this is called only when value has at least one element. self._PrintFieldName(field) self.out.write(' [') From 016ad0e0299ce8c411823c9f0b54aba31456f5b8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 15:43:13 +0200 Subject: [PATCH 100/118] Bump addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index af2c4557db..66f3ac59e7 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.3" +__version__ = "0.3.4" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index da13bee9c7..0c1b1fcf9b 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.3" +version = "0.3.4" client_dir = "ayon_houdini" From cb80c8fa1a59da05cb131511f4575d7e22560830 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 15:44:19 +0200 Subject: [PATCH 101/118] Bump addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index 66f3ac59e7..b6b644f30e 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.4" +__version__ = "0.3.5" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 0c1b1fcf9b..5bdde038c2 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.4" +version = "0.3.5" client_dir = "ayon_houdini" From 34bdab72ee28d27567fa81c0b21cb1eb58fedf95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:29:16 +0200 Subject: [PATCH 102/118] Don't use six to define ABC class --- client/ayon_core/tools/common_models/__init__.py | 4 ++++ client/ayon_core/tools/common_models/projects.py | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/common_models/__init__.py b/client/ayon_core/tools/common_models/__init__.py index f09edfeab2..40394c4732 100644 --- a/client/ayon_core/tools/common_models/__init__.py +++ b/client/ayon_core/tools/common_models/__init__.py @@ -2,6 +2,8 @@ from .cache import CacheItem, NestedCacheItem from .projects import ( + StatusItem, + StatusStates, ProjectItem, ProjectsModel, PROJECTS_MODEL_SENDER, @@ -21,6 +23,8 @@ __all__ = ( "CacheItem", "NestedCacheItem", + "StatusItem", + "StatusStates", "ProjectItem", "ProjectsModel", "PROJECTS_MODEL_SENDER", diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 4e8925388d..1c455a9619 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,8 +1,7 @@ import contextlib -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod import ayon_api -import six from ayon_core.style import get_default_entity_icon_color from ayon_core.lib import CacheItem, NestedCacheItem @@ -10,8 +9,7 @@ from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" -@six.add_metaclass(ABCMeta) -class AbstractHierarchyController: +class AbstractHierarchyController(ABC): @abstractmethod def emit_event(self, topic, data, source): pass From 386a627abe9e71783d67929da23f076503940eb7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:31:58 +0200 Subject: [PATCH 103/118] Added helper classes and hints for state --- .../ayon_core/tools/common_models/projects.py | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 1c455a9619..17599c27f6 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,5 +1,6 @@ import contextlib from abc import ABC, abstractmethod +from typing import Literal, Dict, Any import ayon_api @@ -7,6 +8,14 @@ from ayon_core.style import get_default_entity_icon_color from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" +StatusStatesType = Literal["not_started", "in_progress", "done", "blocked"] + + +class StatusStates: + not_started = "not_started" + in_progress = "in_progress" + done = "done" + blocked = "blocked" class AbstractHierarchyController(ABC): @@ -23,18 +32,24 @@ class StatusItem: color (str): Status color in hex ("#434a56"). short (str): Short status name ("NRD"). icon (str): Icon name in MaterialIcons ("fiber_new"). - state (Literal["not_started", "in_progress", "done", "blocked"]): - Status state. + state (StatusStatesType): Status state. """ - def __init__(self, name, color, short, icon, state): - self.name = name - self.color = color - self.short = short - self.icon = icon - self.state = state + def __init__( + self, + name: str, + color: str, + short: str, + icon: str, + state: StatusStatesType + ): + self.name: str = name + self.color: str = color + self.short: str = short + self.icon: str = icon + self.state: StatusStatesType = state - def to_data(self): + def to_data(self) -> Dict[str, Any]: return { "name": self.name, "color": self.color, From 14fc4ae187eb48ab169bc94d8b66b3a475d2028f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:32:21 +0200 Subject: [PATCH 104/118] added 'is_last_approved' attribute to 'VersionItem' --- .../tools/sceneinventory/models/containers.py | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 95c5322343..c3881ea40d 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -3,7 +3,9 @@ import collections import ayon_api from ayon_api.graphql import GraphQlQuery + from ayon_core.host import ILoadHost +from ayon_core.tools.common_models.projects import StatusStates # --- Implementation that should be in ayon-python-api --- @@ -149,26 +151,35 @@ class RepresentationInfo: class VersionItem: - def __init__(self, version_id, product_id, version, status, is_latest): - self.version = version - self.version_id = version_id - self.product_id = product_id - self.version = version - self.status = status - self.is_latest = is_latest + def __init__( + self, + version_id: str, + product_id: str, + version: int, + status: str, + is_latest: bool, + is_last_approved: bool, + ): + self.version_id: str = version_id + self.product_id: str = product_id + self.version: int = version + self.status: str = status + self.is_latest: bool = is_latest + self.is_last_approved: bool = is_last_approved @property def is_hero(self): return self.version < 0 @classmethod - def from_entity(cls, version_entity, is_latest): + def from_entity(cls, version_entity, is_latest, is_last_approved): return cls( version_id=version_entity["id"], product_id=version_entity["productId"], version=version_entity["version"], status=version_entity["status"], is_latest=is_latest, + is_last_approved=is_last_approved, ) @@ -275,6 +286,11 @@ class ContainersModel: if product_id not in self._version_items_by_product_id } if missing_ids: + status_items_by_name = { + status_item.name: status_item + for status_item in self._controller.get_project_status_items() + } + def version_sorted(entity): return entity["version"] @@ -300,9 +316,21 @@ class ContainersModel: version_entities_by_product_id.items() ): last_version = abs(version_entities[-1]["version"]) + last_approved_id = None + for version_entity in version_entities: + status_item = status_items_by_name.get( + version_entity["status"] + ) + if status_item is None: + continue + if status_item.state == StatusStates.done: + last_approved_id = version_entity["id"] + version_items_by_id = { entity["id"]: VersionItem.from_entity( - entity, abs(entity["version"]) == last_version + entity, + abs(entity["version"]) == last_version, + entity["id"] == last_approved_id ) for entity in version_entities } From db3f5c60c68e47b210dcd644ea69f754c554b8ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:38:18 +0200 Subject: [PATCH 105/118] implemented logic to update to latest approved --- client/ayon_core/tools/sceneinventory/view.py | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index c8cc3299a2..017a9597f1 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -233,10 +233,18 @@ class SceneInventoryView(QtWidgets.QTreeView): has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False - for version_items_by_id in version_items_by_product_id.values(): + has_outdated_approved = False + last_version_by_product_id = {} + for product_id, version_items_by_id in ( + version_items_by_product_id.items() + ): + _has_outdated_approved = False + _last_approved_version_item = None for version_item in version_items_by_id.values(): if version_item.is_hero: has_available_hero_version = True + if version_item.is_last_approved: + _last_approved_version_item = version_item if version_item.version_id not in version_ids: continue @@ -245,6 +253,17 @@ class SceneInventoryView(QtWidgets.QTreeView): elif not version_item.is_latest: has_outdated = True + elif not version_item.is_last_approved: + _has_outdated_approved = True + + if ( + _has_outdated_approved + and _last_approved_version_item is not None + ): + last_version_by_product_id[product_id] = ( + _last_approved_version_item + ) + has_outdated_approved = True switch_to_versioned = None if has_loaded_hero_versions: @@ -261,6 +280,41 @@ class SceneInventoryView(QtWidgets.QTreeView): lambda: self._on_switch_to_versioned(item_ids) ) + update_to_last_approved_action = None + if has_outdated_approved: + approved_version_by_item_id = {} + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + repre_info = repre_info_by_id.get(repre_id) + if not repre_info or not repre_info.is_valid: + continue + version_item = last_version_by_product_id.get( + repre_info.product_id + ) + if ( + version_item is None + or version_item.id == repre_info.version_id + ): + continue + approved_version_by_item_id[container_item.item_id] = ( + version_item.version + ) + + update_icon = qtawesome.icon( + "fa.angle-double-up", + color="#00f0b4" + ) + update_to_last_approved_action = QtWidgets.QAction( + update_icon, + "Update to last approved", + menu + ) + update_to_last_approved_action.triggered.connect( + lambda: self._update_containers_to_approved_versions( + approved_version_by_item_id + ) + ) + update_to_latest_action = None if has_outdated or has_loaded_hero_versions: update_icon = qtawesome.icon( @@ -299,7 +353,9 @@ class SceneInventoryView(QtWidgets.QTreeView): # set version set_version_action = None if active_repre_id is not None: - set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR) + set_version_icon = qtawesome.icon( + "fa.hashtag", color=DEFAULT_COLOR + ) set_version_action = QtWidgets.QAction( set_version_icon, "Set version", @@ -323,6 +379,9 @@ class SceneInventoryView(QtWidgets.QTreeView): if switch_to_versioned: menu.addAction(switch_to_versioned) + if update_to_last_approved_action: + menu.addAction(update_to_last_approved_action) + if update_to_latest_action: menu.addAction(update_to_latest_action) @@ -970,3 +1029,24 @@ class SceneInventoryView(QtWidgets.QTreeView): """ versions = [version for _ in range(len(item_ids))] self._update_containers(item_ids, versions) + + def _update_containers_to_approved_versions( + self, approved_version_by_item_id + ): + """Helper to update items to given version (or version per item) + + If at least one item is specified this will always try to refresh + the inventory even if errors occurred on any of the items. + + Arguments: + approved_version_by_item_id (Dict[str, int]): Version to set by + item id. + + """ + versions = [] + item_ids = [] + for item_id, version in approved_version_by_item_id.items(): + item_ids.append(item_id) + versions.append(version) + + self._update_containers(item_ids, versions) From b710af2662b2df77f672d4d8c84261433caeab5b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:00:54 +0200 Subject: [PATCH 106/118] removed 'Literal' --- client/ayon_core/tools/common_models/projects.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 17599c27f6..7ec941e6bd 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,6 +1,6 @@ import contextlib from abc import ABC, abstractmethod -from typing import Literal, Dict, Any +from typing import Dict, Any import ayon_api @@ -8,7 +8,6 @@ from ayon_core.style import get_default_entity_icon_color from ayon_core.lib import CacheItem, NestedCacheItem PROJECTS_MODEL_SENDER = "projects.model" -StatusStatesType = Literal["not_started", "in_progress", "done", "blocked"] class StatusStates: @@ -32,7 +31,7 @@ class StatusItem: color (str): Status color in hex ("#434a56"). short (str): Short status name ("NRD"). icon (str): Icon name in MaterialIcons ("fiber_new"). - state (StatusStatesType): Status state. + state (str): Status state. """ def __init__( @@ -41,13 +40,13 @@ class StatusItem: color: str, short: str, icon: str, - state: StatusStatesType + state: str ): self.name: str = name self.color: str = color self.short: str = short self.icon: str = icon - self.state: StatusStatesType = state + self.state: str = state def to_data(self) -> Dict[str, Any]: return { From 47fafbdc05f726d2d11ad2971ae393acbc28f6f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:15:04 +0200 Subject: [PATCH 107/118] fix logic of action discovery --- client/ayon_core/tools/sceneinventory/view.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 017a9597f1..22ba15fda8 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -243,18 +243,18 @@ class SceneInventoryView(QtWidgets.QTreeView): for version_item in version_items_by_id.values(): if version_item.is_hero: has_available_hero_version = True - if version_item.is_last_approved: + + elif version_item.is_last_approved: _last_approved_version_item = version_item + _has_outdated_approved = True if version_item.version_id not in version_ids: continue + if version_item.is_hero: has_loaded_hero_versions = True - elif not version_item.is_latest: has_outdated = True - elif not version_item.is_last_approved: - _has_outdated_approved = True if ( _has_outdated_approved @@ -281,8 +281,8 @@ class SceneInventoryView(QtWidgets.QTreeView): ) update_to_last_approved_action = None + approved_version_by_item_id = {} if has_outdated_approved: - approved_version_by_item_id = {} for container_item in container_items_by_id.values(): repre_id = container_item.representation_id repre_info = repre_info_by_id.get(repre_id) @@ -293,13 +293,14 @@ class SceneInventoryView(QtWidgets.QTreeView): ) if ( version_item is None - or version_item.id == repre_info.version_id + or version_item.version_id == repre_info.version_id ): continue approved_version_by_item_id[container_item.item_id] = ( version_item.version ) + if approved_version_by_item_id: update_icon = qtawesome.icon( "fa.angle-double-up", color="#00f0b4" From 403442b281b0645952fc3b9f6513e8c8c27ba88c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Jun 2024 18:20:40 +0200 Subject: [PATCH 108/118] fix color of loaded version --- client/ayon_core/tools/sceneinventory/model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 3e0c361535..335df87b95 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -217,7 +217,9 @@ class InventoryModel(QtGui.QStandardItemModel): version_label = format_version(version_item.version) is_hero = version_item.version < 0 is_latest = version_item.is_latest - if not is_latest: + # TODO maybe use different colors for last approved and last + # version? Or don't care about color at all? + if not is_latest and not version_item.is_last_approved: version_color = self.OUTDATED_COLOR status_name = version_item.status From 4583a447776ed1af544255b024656aafaa8c9d43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 22:56:33 +0200 Subject: [PATCH 109/118] Remove unused "remote publish" workflow for Houdini This likely was once a 'straight' copy from `colorbleed` codebase back in the Avalon-era. --- .../houdini/client/ayon_houdini/api/lib.py | 83 ------------------- .../plugins/publish/collect_remote_publish.py | 29 ------- .../publish/validate_remote_publish.py | 50 ----------- .../validate_remote_publish_enabled.py | 41 --------- 4 files changed, 203 deletions(-) delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/collect_remote_publish.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish.py delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish_enabled.py diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index 671265fae9..d23edcf1df 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -148,89 +148,6 @@ def validate_fps(): return True -def create_remote_publish_node(force=True): - """Function to create a remote publish node in /out - - This is a hacked "Shell" node that does *nothing* except for triggering - `colorbleed.lib.publish_remote()` as pre-render script. - - All default attributes of the Shell node are hidden to the Artist to - avoid confusion. - - Additionally some custom attributes are added that can be collected - by a Collector to set specific settings for the publish, e.g. whether - to separate the jobs per instance or process in one single job. - - """ - - cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()" - - existing = hou.node("/out/REMOTE_PUBLISH") - if existing: - if force: - log.warning("Removing existing '/out/REMOTE_PUBLISH' node..") - existing.destroy() - else: - raise RuntimeError("Node already exists /out/REMOTE_PUBLISH. " - "Please remove manually or set `force` to " - "True.") - - # Create the shell node - out = hou.node("/out") - node = out.createNode("shell", node_name="REMOTE_PUBLISH") - node.moveToGoodPosition() - - # Set color make it stand out (avalon/pyblish color) - node.setColor(hou.Color(0.439, 0.709, 0.933)) - - # Set the pre-render script - node.setParms({ - "prerender": cmd, - "lprerender": "python" # command language - }) - - # Lock the attributes to ensure artists won't easily mess things up. - node.parm("prerender").lock(True) - node.parm("lprerender").lock(True) - - # Lock up the actual shell command - command_parm = node.parm("command") - command_parm.set("") - command_parm.lock(True) - shellexec_parm = node.parm("shellexec") - shellexec_parm.set(False) - shellexec_parm.lock(True) - - # Get the node's parm template group so we can customize it - template = node.parmTemplateGroup() - - # Hide default tabs - template.hideFolder("Shell", True) - template.hideFolder("Scripts", True) - - # Hide default settings - template.hide("execute", True) - template.hide("renderdialog", True) - template.hide("trange", True) - template.hide("f", True) - template.hide("take", True) - - # Add custom settings to this node. - parm_folder = hou.FolderParmTemplate("folder", "Submission Settings") - - # Separate Jobs per Instance - parm = hou.ToggleParmTemplate(name="separateJobPerInstance", - label="Separate Job per Instance", - default_value=False) - parm_folder.addParmTemplate(parm) - - # Add our custom Submission Settings folder - template.append(parm_folder) - - # Apply template back to the node - node.setParmTemplateGroup(template) - - def render_rop(ropnode): """Render ROP node utility for Publishing. diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_remote_publish.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_remote_publish.py deleted file mode 100644 index e695b57518..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_remote_publish.py +++ /dev/null @@ -1,29 +0,0 @@ -import hou -import pyblish.api - -from ayon_core.pipeline.publish import RepairAction -from ayon_houdini.api import lib, plugin - - -class CollectRemotePublishSettings(plugin.HoudiniContextPlugin): - """Collect custom settings of the Remote Publish node.""" - - order = pyblish.api.CollectorOrder - families = ["*"] - targets = ["deadline"] - label = "Remote Publish Submission Settings" - actions = [RepairAction] - - def process(self, context): - - node = hou.node("/out/REMOTE_PUBLISH") - if not node: - return - - attributes = lib.read(node) - - # Debug the settings we have collected - for key, value in sorted(attributes.items()): - self.log.debug("Collected %s: %s" % (key, value)) - - context.data.update(attributes) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish.py deleted file mode 100644 index 08597c0a6f..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*-coding: utf-8 -*- -import hou - -import pyblish.api -from ayon_core.pipeline.publish import RepairContextAction -from ayon_core.pipeline import PublishValidationError - -from ayon_houdini.api import lib, plugin - - -class ValidateRemotePublishOutNode(plugin.HoudiniContextPlugin): - """Validate the remote publish out node exists for Deadline to trigger.""" - - order = pyblish.api.ValidatorOrder - 0.4 - families = ["*"] - targets = ["deadline"] - label = "Remote Publish ROP node" - actions = [RepairContextAction] - - def process(self, context): - - cmd = "import colorbleed.lib; colorbleed.lib.publish_remote()" - - node = hou.node("/out/REMOTE_PUBLISH") - if not node: - raise RuntimeError("Missing REMOTE_PUBLISH node.") - - # We ensure it's a shell node and that it has the pre-render script - # set correctly. Plus the shell script it will trigger should be - # completely empty (doing nothing) - if node.type().name() != "shell": - self.raise_error("Must be shell ROP node") - if node.parm("command").eval() != "": - self.raise_error("Must have no command") - if node.parm("shellexec").eval(): - self.raise_error("Must not execute in shell") - if node.parm("prerender").eval() != cmd: - self.raise_error("REMOTE_PUBLISH node does not have " - "correct prerender script.") - if node.parm("lprerender").eval() != "python": - self.raise_error("REMOTE_PUBLISH node prerender script " - "type not set to 'python'") - - @classmethod - def repair(cls, context): - """(Re)create the node if it fails to pass validation.""" - lib.create_remote_publish_node(force=True) - - def raise_error(self, message): - raise PublishValidationError(message) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish_enabled.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish_enabled.py deleted file mode 100644 index dc5666609f..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/validate_remote_publish_enabled.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -import hou - -import pyblish.api -from ayon_core.pipeline.publish import RepairContextAction -from ayon_core.pipeline import PublishValidationError - -from ayon_houdini.api import plugin - - -class ValidateRemotePublishEnabled(plugin.HoudiniContextPlugin): - """Validate the remote publish node is *not* bypassed.""" - - order = pyblish.api.ValidatorOrder - 0.39 - families = ["*"] - targets = ["deadline"] - label = "Remote Publish ROP enabled" - actions = [RepairContextAction] - - def process(self, context): - - node = hou.node("/out/REMOTE_PUBLISH") - if not node: - raise PublishValidationError( - "Missing REMOTE_PUBLISH node.", title=self.label) - - if node.isBypassed(): - raise PublishValidationError( - "REMOTE_PUBLISH must not be bypassed.", title=self.label) - - @classmethod - def repair(cls, context): - """(Re)create the node if it fails to pass validation.""" - - node = hou.node("/out/REMOTE_PUBLISH") - if not node: - raise PublishValidationError( - "Missing REMOTE_PUBLISH node.", title=cls.label) - - cls.log.info("Disabling bypass on /out/REMOTE_PUBLISH") - node.bypass(False) From ee0d3c91e2f8456bb34ab82258b4893af48bef54 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Jun 2024 11:45:03 +0200 Subject: [PATCH 110/118] Bump Houdini addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index b6b644f30e..5c32b4860e 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.5" +__version__ = "0.3.6" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index 5bdde038c2..fb345dab51 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.5" +version = "0.3.6" client_dir = "ayon_houdini" From 32f6d4941fb8106f0fb71cf3cb9f113d893198e1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Jun 2024 15:06:52 +0200 Subject: [PATCH 111/118] Bump houdini addon version --- server_addon/houdini/client/ayon_houdini/version.py | 2 +- server_addon/houdini/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/version.py b/server_addon/houdini/client/ayon_houdini/version.py index 5c32b4860e..3dbbb4c23e 100644 --- a/server_addon/houdini/client/ayon_houdini/version.py +++ b/server_addon/houdini/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.6" +__version__ = "0.3.7" diff --git a/server_addon/houdini/package.py b/server_addon/houdini/package.py index fb345dab51..c01cc6044d 100644 --- a/server_addon/houdini/package.py +++ b/server_addon/houdini/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.6" +version = "0.3.7" client_dir = "ayon_houdini" From b6a4047efae94af8567a20407e4a8dad0b4704cf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 25 Jun 2024 21:50:40 +0800 Subject: [PATCH 112/118] supports to export fbx in model family in Maya --- .../plugins/publish/collect_fbx_model.py | 29 +++++++++++++++++++ server_addon/maya/client/ayon_maya/version.py | 2 +- server_addon/maya/package.py | 2 +- .../maya/server/settings/publishers.py | 12 ++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py b/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py new file mode 100644 index 0000000000..a5da0419a9 --- /dev/null +++ b/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py @@ -0,0 +1,29 @@ +import pyblish.api +from ayon_core.pipeline import OptionalPyblishPluginMixin +from ayon_maya.api import plugin + + + +class CollectFbxModel(plugin.MayaInstancePlugin, + OptionalPyblishPluginMixin): + """Collect Camera for FBX export.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Model for FBX export" + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + if not instance.data.get("families"): + instance.data["families"] = [] + + if "fbx" not in instance.data["families"]: + instance.data["families"].append("fbx") + + for key in { + "bakeComplexAnimation", "bakeResampleAnimation", + "skins", "constraints", "lights"}: + instance.data[key] = False diff --git a/server_addon/maya/client/ayon_maya/version.py b/server_addon/maya/client/ayon_maya/version.py index 1f53dfa492..80af287e97 100644 --- a/server_addon/maya/client/ayon_maya/version.py +++ b/server_addon/maya/client/ayon_maya/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'maya' version.""" -__version__ = "0.2.7" +__version__ = "0.2.8" diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py index 47aa8a4c0d..b2d0622493 100644 --- a/server_addon/maya/package.py +++ b/server_addon/maya/package.py @@ -1,6 +1,6 @@ name = "maya" title = "Maya" -version = "0.2.7" +version = "0.2.8" client_dir = "ayon_maya" ayon_required_addons = { diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 9c552e17fa..4834ed839a 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -184,6 +184,11 @@ class CollectFbxCameraModel(BaseSettingsModel): enabled: bool = SettingsField(title="CollectFbxCamera") + +class CollectFbxModelModel(BaseSettingsModel): + enabled: bool = SettingsField(title="CollectFbxModel") + + class CollectGLTFModel(BaseSettingsModel): enabled: bool = SettingsField(title="CollectGLTF") @@ -625,6 +630,10 @@ class PublishersModel(BaseSettingsModel): default_factory=CollectFbxCameraModel, title="Collect Camera for FBX export", ) + CollectFbxModel: CollectFbxModelModel = SettingsField( + default_factory=CollectFbxModelModel, + title="Collect Model for FBX export", + ) CollectGLTF: CollectGLTFModel = SettingsField( default_factory=CollectGLTFModel, title="Collect Assets for GLB/GLTF export" @@ -1047,6 +1056,9 @@ DEFAULT_PUBLISH_SETTINGS = { "CollectFbxCamera": { "enabled": False }, + "CollectFbxModel": { + "enabled": False + }, "CollectGLTF": { "enabled": False }, From f866949669f302a837dc7f09ae23f96ee393d545 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 25 Jun 2024 22:37:58 +0800 Subject: [PATCH 113/118] add optional and enable settings for collect fbx model in ayon settings --- .../ayon_maya/plugins/publish/collect_fbx_model.py | 2 +- server_addon/maya/server/settings/publishers.py | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py b/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py index a5da0419a9..f3902a2868 100644 --- a/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py +++ b/server_addon/maya/client/ayon_maya/plugins/publish/collect_fbx_model.py @@ -9,7 +9,7 @@ class CollectFbxModel(plugin.MayaInstancePlugin, """Collect Camera for FBX export.""" order = pyblish.api.CollectorOrder + 0.2 - label = "Collect Model for FBX export" + label = "Collect Fbx Model" families = ["model"] optional = True diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 4834ed839a..6a127cc998 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -184,11 +184,6 @@ class CollectFbxCameraModel(BaseSettingsModel): enabled: bool = SettingsField(title="CollectFbxCamera") - -class CollectFbxModelModel(BaseSettingsModel): - enabled: bool = SettingsField(title="CollectFbxModel") - - class CollectGLTFModel(BaseSettingsModel): enabled: bool = SettingsField(title="CollectGLTF") @@ -630,8 +625,8 @@ class PublishersModel(BaseSettingsModel): default_factory=CollectFbxCameraModel, title="Collect Camera for FBX export", ) - CollectFbxModel: CollectFbxModelModel = SettingsField( - default_factory=CollectFbxModelModel, + CollectFbxModel: BasicValidateModel = SettingsField( + default_factory=BasicValidateModel, title="Collect Model for FBX export", ) CollectGLTF: CollectGLTFModel = SettingsField( @@ -1057,7 +1052,9 @@ DEFAULT_PUBLISH_SETTINGS = { "enabled": False }, "CollectFbxModel": { - "enabled": False + "enabled": False, + "optional": True, + "active": True }, "CollectGLTF": { "enabled": False From a98e55f8e5f186d90201ce1b4df35d6579667d8e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:19:23 +0200 Subject: [PATCH 114/118] fix deafults --- .../server/settings/creator_plugins.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/server_addon/traypublisher/server/settings/creator_plugins.py b/server_addon/traypublisher/server/settings/creator_plugins.py index 1ff14002aa..0e7963a33e 100644 --- a/server_addon/traypublisher/server/settings/creator_plugins.py +++ b/server_addon/traypublisher/server/settings/creator_plugins.py @@ -182,7 +182,7 @@ DEFAULT_CREATORS = { "type": "text", "default": "", "required_column": True, - "validation_pattern": "^([a-z0-9#._\\/]*)$" + "validation_pattern": "^([a-zA-Z\\:\\ 0-9#._\\\\/]*)$" }, { "name": "Folder Path", @@ -215,7 +215,7 @@ DEFAULT_CREATORS = { { "name": "Version", "type": "number", - "default": 1, + "default": "1", "required_column": True, "validation_pattern": "^(\\d{1,3})$" }, @@ -231,47 +231,47 @@ DEFAULT_CREATORS = { "type": "text", "default": "", "required_column": False, - "validation_pattern": "^([a-zA-Z0-9#._\\/]*)$" + "validation_pattern": "^([a-zA-Z\\:\\ 0-9#._\\\\/]*)$" }, { "name": "Frame Start", "type": "number", - "default": 0, + "default": "0", "required_column": True, "validation_pattern": "^(\\d{1,8})$" }, { "name": "Frame End", "type": "number", - "default": 0, + "default": "0", "required_column": True, "validation_pattern": "^(\\d{1,8})$" }, { "name": "Handle Start", "type": "number", - "default": 0, + "default": "0", "required_column": True, "validation_pattern": "^(\\d)$" }, { "name": "Handle End", "type": "number", - "default": 0, + "default": "0", "required_column": True, "validation_pattern": "^(\\d)$" }, { "name": "FPS", "type": "decimal", - "default": 0.0, + "default": "0.0", "required_column": True, "validation_pattern": "^[0-9]*\\.[0-9]+$|^[0-9]+$" }, { "name": "Slate Exists", "type": "bool", - "default": True, + "default": "True", "required_column": False, "validation_pattern": "(True|False)" }, From ee8c64a863b2db361452ceb2607adc0bd4dbac6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Jun 2024 16:21:51 +0200 Subject: [PATCH 115/118] bump traypublisher to '0.2.5' --- server_addon/traypublisher/client/ayon_traypublisher/version.py | 2 +- server_addon/traypublisher/package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/traypublisher/client/ayon_traypublisher/version.py b/server_addon/traypublisher/client/ayon_traypublisher/version.py index 5a2d84194c..16e0ec1829 100644 --- a/server_addon/traypublisher/client/ayon_traypublisher/version.py +++ b/server_addon/traypublisher/client/ayon_traypublisher/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'traypublisher' version.""" -__version__ = "0.2.4" +__version__ = "0.2.5" diff --git a/server_addon/traypublisher/package.py b/server_addon/traypublisher/package.py index 5475033d22..6a9ecdb0be 100644 --- a/server_addon/traypublisher/package.py +++ b/server_addon/traypublisher/package.py @@ -1,6 +1,6 @@ name = "traypublisher" title = "TrayPublisher" -version = "0.2.4" +version = "0.2.5" client_dir = "ayon_traypublisher" From 1333499df6e36c4a7813ee2787794cad626b5717 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Jun 2024 00:43:14 +0200 Subject: [PATCH 116/118] Remove dots from end of plug-in titles --- .../server/settings/publish_plugins.py | 4 ++-- .../houdini/server/settings/publish.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 85a93d49cd..a041a08ded 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -375,11 +375,11 @@ class PublishPluginsModel(BaseSettingsModel): title="Nuke Submit to deadline") ProcessSubmittedCacheJobOnFarm: ProcessCacheJobFarmModel = SettingsField( default_factory=ProcessCacheJobFarmModel, - title="Process submitted cache Job on farm.", + title="Process submitted cache Job on farm", section="Publish Jobs") ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = SettingsField( default_factory=ProcessSubmittedJobOnFarmModel, - title="Process submitted job on farm.") + title="Process submitted job on farm") DEFAULT_DEADLINE_PLUGINS_SETTINGS = { diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 336de8e046..847147c27a 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -66,36 +66,36 @@ class BasicValidateModel(BaseSettingsModel): class PublishPluginsModel(BaseSettingsModel): CollectAssetHandles: CollectAssetHandlesModel = SettingsField( default_factory=CollectAssetHandlesModel, - title="Collect Asset Handles.", + title="Collect Asset Handles", section="Collectors" ) CollectChunkSize: CollectChunkSizeModel = SettingsField( default_factory=CollectChunkSizeModel, - title="Collect Chunk Size." + title="Collect Chunk Size" ) CollectLocalRenderInstances: CollectLocalRenderInstancesModel = SettingsField( default_factory=CollectLocalRenderInstancesModel, - title="Collect Local Render Instances." + title="Collect Local Render Instances" ) ValidateInstanceInContextHoudini: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, - title="Validate Instance is in same Context.", + title="Validate Instance is in same Context", section="Validators") ValidateMeshIsStatic: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, - title="Validate Mesh is Static.") + title="Validate Mesh is Static") ValidateReviewColorspace: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, - title="Validate Review Colorspace.") + title="Validate Review Colorspace") ValidateSubsetName: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, - title="Validate Subset Name.") + title="Validate Subset Name") ValidateUnrealStaticMeshName: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, - title="Validate Unreal Static Mesh Name.") + title="Validate Unreal Static Mesh Name") ValidateWorkfilePaths: ValidateWorkfilePathsModel = SettingsField( default_factory=ValidateWorkfilePathsModel, - title="Validate workfile paths settings.") + title="Validate workfile paths settings") DEFAULT_HOUDINI_PUBLISH_SETTINGS = { From 61c1acaa9503b1f1fba997cb63e09bcab6797ecb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Jun 2024 00:44:09 +0200 Subject: [PATCH 117/118] Fix indentations --- server_addon/deadline/server/settings/publish_plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index a041a08ded..1cf699db23 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -153,8 +153,8 @@ class FusionSubmitDeadlineModel(BaseSettingsModel): ) group: str = SettingsField("", title="Group Name") plugin: str = SettingsField("Fusion", - enum_resolver=fusion_deadline_plugin_enum, - title="Deadline Plugin") + enum_resolver=fusion_deadline_plugin_enum, + title="Deadline Plugin") class NukeSubmitDeadlineModel(BaseSettingsModel): @@ -376,7 +376,7 @@ class PublishPluginsModel(BaseSettingsModel): ProcessSubmittedCacheJobOnFarm: ProcessCacheJobFarmModel = SettingsField( default_factory=ProcessCacheJobFarmModel, title="Process submitted cache Job on farm", - section="Publish Jobs") + section="Publish Jobs") ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = SettingsField( default_factory=ProcessSubmittedJobOnFarmModel, title="Process submitted job on farm") From 31f11684e1ad52e75e3733b51889f307c9eb6810 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Jun 2024 00:45:22 +0200 Subject: [PATCH 118/118] Cosmetics --- server_addon/houdini/server/settings/publish.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server_addon/houdini/server/settings/publish.py b/server_addon/houdini/server/settings/publish.py index 847147c27a..4f324f0726 100644 --- a/server_addon/houdini/server/settings/publish.py +++ b/server_addon/houdini/server/settings/publish.py @@ -31,6 +31,7 @@ class AOVFilterSubmodel(BaseSettingsModel): title="AOV regex" ) + class CollectLocalRenderInstancesModel(BaseSettingsModel): use_deadline_aov_filter: bool = SettingsField( @@ -109,7 +110,7 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { }, "CollectLocalRenderInstances": { "use_deadline_aov_filter": False, - "aov_filter" : { + "aov_filter": { "host_name": "houdini", "value": [ ".*([Bb]eauty).*"