diff --git a/client/ayon_houdini/plugins/load/load_filepath.py b/client/ayon_houdini/plugins/load/load_filepath.py index 2ce9bd7ffb..dddc26e32a 100644 --- a/client/ayon_houdini/plugins/load/load_filepath.py +++ b/client/ayon_houdini/plugins/load/load_filepath.py @@ -2,12 +2,35 @@ import re import hou +from ayon_core.pipeline import Anatomy +from ayon_core.lib import StringTemplate from ayon_houdini.api import ( pipeline, plugin ) +def remove_format_spec(template: str, key: str) -> str: + """Remove format specifier from a format token in formatting string. + + For example, change `{frame:0>4d}` into `{frame}` + + Examples: + >>> remove_format_spec("{frame:0>4d}", "frame") + '{frame}' + >>> remove_format_spec("{digit:04d}/{frame:0>4d}", "frame") + '{digit:04d}/{udim}_{frame}' + >>> remove_format_spec("{a: >4}/{aa: >4}", "a") + '{a}/{aa: >4}' + + """ + # Find all {key:foobar} and remove the `:foobar` + # Pattern will be like `({key):[^}]+(})` where we use the captured groups + # to keep those parts in the resulting string + pattern = f"({{{key}):[^}}]+(}})" + return re.sub(pattern, r"\1\2", template) + + class FilePathLoader(plugin.HoudiniLoader): """Load a managed filepath to a null node. @@ -42,10 +65,7 @@ def load(self, context, name=None, namespace=None, data=None): node.destroy() # Add filepath attribute, set value as default value - filepath = self.format_path( - path=self.filepath_from_context(context), - representation=context["representation"] - ) + filepath = self.filepath_from_context(context) parm_template_group = container.parmTemplateGroup() attr_folder = hou.FolderParmTemplate("attributes_folder", "Attributes") parm = hou.StringParmTemplate(name="filepath", @@ -85,21 +105,18 @@ def update(self, container, context): # Update the file path representation_entity = context["representation"] - file_path = self.format_path( - path=self.filepath_from_context(context), - representation=representation_entity - ) + filepath = self.filepath_from_context(context) node = container["node"] node.setParms({ - "filepath": file_path, + "filepath": filepath, "representation": str(representation_entity["id"]) }) # Update the parameter default value (cosmetics) parm_template_group = node.parmTemplateGroup() parm = parm_template_group.find("filepath") - parm.setDefaultValue((file_path,)) + parm.setDefaultValue((filepath,)) parm_template_group.replace(parm_template_group.find("filepath"), parm) node.setParmTemplateGroup(parm_template_group) @@ -112,19 +129,29 @@ def remove(self, container): node = container["node"] node.destroy() - @staticmethod - def format_path(path: str, representation: dict) -> str: - """Format file path for sequence with $F.""" - if not os.path.exists(path): - raise RuntimeError("Path does not exist: %s" % path) - + def filepath_from_context(self, context: dict) -> str: + """Format file path for sequence with $F or .""" # The path is either a single file or sequence in a folder. + # Format frame as $F and udim as + representation = context["representation"] frame = representation["context"].get("frame") - if frame is not None: - # Substitute frame number in sequence with $F with padding - ext = representation.get("ext", representation["name"]) - token = "$F{}".format(len(frame)) # e.g. $F4 - pattern = r"\.(\d+)\.{ext}$".format(ext=re.escape(ext)) - path = re.sub(pattern, ".{}.{}".format(token, ext), path) + udim = representation["context"].get("udim") + if frame is not None or udim is not None: + template: str = representation["attrib"]["template"] + repre_context: dict = representation["context"] + if udim is not None: + repre_context["udim"] = "" + template = remove_format_spec(template, "udim") + if frame is not None: + # Substitute frame number in sequence with $F with padding + repre_context["frame"] = "$F{}".format(len(frame)) # e.g. $F4 + template = remove_format_spec(template, "frame") + + project_name: str = repre_context["project"]["name"] + anatomy = Anatomy(project_name, project_entity=context["project"]) + repre_context["root"] = anatomy.roots + path = StringTemplate(template).format(repre_context) + else: + path = super().filepath_from_context(context) return os.path.normpath(path).replace("\\", "/")