From 6ee7780972d2c37ccbf0279e5b7c21471afdba97 Mon Sep 17 00:00:00 2001 From: skacmazbelhaine Date: Tue, 6 Jun 2023 15:51:20 +0200 Subject: [PATCH 001/175] Update local settings --- website/docs/admin_settings_local.md | 27 +++++++++++++++++- .../assets/settings/settings_local_02.png | Bin 0 -> 11181 bytes 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/settings/settings_local_02.png diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index b254beb53b..214615f442 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -11,18 +11,43 @@ OpenPype stores some of it's settings and configuration in local file system. Th **Local Settings** GUI can be started from the tray menu. -![Local Settings](assets/settings/settings_local.png) +![Local Settings](assets/settings/settings_local_02.png) ## Categories + + ### OpenPype Mongo URL +The **Mongo URL** is the database URL given by your Studio. More details [here](https://openpype.io/docs/artist_getting_started#mongodb). + +
### General +**OpenPype Username** : enter your username (it can also take by default the computer session username). It signs your actions on **OpenPype**. + +
+ +**Admin permissions** : on checked, no need to enter a password (if defined) to access to the **Admin** section. + +
### Experimental tools +Futur version of existing tools or new ones. + +
+ +### Environments +**Environments** data of each software and there extra in-house needed to be loaded correctly. More details [here](https://openpype.io/docs/pype2/admin_config/#environments). + +
### Applications +Location of the softwares and there versions. More details [here](https://openpype.io/docs/admin_settings_system/#applications). + +
### Project Settings +The **Project Settings** allows to determine the root folder. More details [here](https://openpype.io/docs/module_site_sync/#project-settings). + diff --git a/website/docs/assets/settings/settings_local_02.png b/website/docs/assets/settings/settings_local_02.png new file mode 100644 index 0000000000000000000000000000000000000000..725c3327472cae5dc6271586aa4a1de2495104ca GIT binary patch literal 11181 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 T+AsLMj#fcN6 Date: Tue, 6 Jun 2023 15:51:32 +0200 Subject: [PATCH 002/175] Update local settings --- website/docs/admin_settings_local.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index 214615f442..222ee41515 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -36,7 +36,7 @@ Futur version of existing tools or new ones.
-### Environments +### Environments **Environments** data of each software and there extra in-house needed to be loaded correctly. More details [here](https://openpype.io/docs/pype2/admin_config/#environments).
@@ -48,6 +48,3 @@ Location of the softwares and there versions. More details [here](https://openpy ### Project Settings The **Project Settings** allows to determine the root folder. More details [here](https://openpype.io/docs/module_site_sync/#project-settings). - - - From ead57a2cafe2d451e99189d7d1aa94678a032033 Mon Sep 17 00:00:00 2001 From: skacmazbelhaine Date: Tue, 6 Jun 2023 16:30:11 +0200 Subject: [PATCH 003/175] Update local settings --- website/docs/admin_settings_local.md | 2 +- .../docs/assets/settings/settings_local.png | Bin 7212 -> 11181 bytes .../assets/settings/settings_local_02.png | Bin 11181 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 website/docs/assets/settings/settings_local_02.png diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index 222ee41515..177c32451b 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -11,7 +11,7 @@ OpenPype stores some of it's settings and configuration in local file system. Th **Local Settings** GUI can be started from the tray menu. -![Local Settings](assets/settings/settings_local_02.png) +![Local Settings](assets/settings/settings_local.png) ## Categories diff --git a/website/docs/assets/settings/settings_local.png b/website/docs/assets/settings/settings_local.png index d2cf1c920d07e98bb53570f6b76ef039e642e741..725c3327472cae5dc6271586aa4a1de2495104ca 100644 GIT binary patch literal 11181 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 T+AsLMj#fcN6V{?TXm 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 Tu$%%u=7ApE)zK(Xvkdw#D_=3L diff --git a/website/docs/assets/settings/settings_local_02.png b/website/docs/assets/settings/settings_local_02.png deleted file mode 100644 index 725c3327472cae5dc6271586aa4a1de2495104ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11181 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 T+AsLMj#fcN6 Date: Wed, 7 Jun 2023 10:57:27 +0200 Subject: [PATCH 004/175] update after comments - local settings --- website/docs/admin_settings_local.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index 177c32451b..99e27731d0 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -18,7 +18,7 @@ OpenPype stores some of it's settings and configuration in local file system. Th ### OpenPype Mongo URL -The **Mongo URL** is the database URL given by your Studio. More details [here](https://openpype.io/docs/artist_getting_started#mongodb). +The **Mongo URL** is the database URL given by your Studio. More details [here](artist_getting_started#mongodb).
@@ -37,14 +37,14 @@ Futur version of existing tools or new ones.
### Environments -**Environments** data of each software and there extra in-house needed to be loaded correctly. More details [here](https://openpype.io/docs/pype2/admin_config/#environments). +Local replacement of the environment data of each software and additional internal data necessary to be loaded correctly.
### Applications -Location of the softwares and there versions. More details [here](https://openpype.io/docs/admin_settings_system/#applications). +Local override of software paths and their versions. More details [here](admin_settings_system/#applications).
### Project Settings -The **Project Settings** allows to determine the root folder. More details [here](https://openpype.io/docs/module_site_sync/#project-settings). +The **Project Settings** allows to determine the root folder. More details [here](module_site_sync/#local-settings). From 85af2013726edb61a76faff9122a28cd1f1749dc Mon Sep 17 00:00:00 2001 From: skacmazbelhaine Date: Thu, 8 Jun 2023 10:16:04 +0200 Subject: [PATCH 005/175] fix link path --- website/docs/admin_settings_local.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index 99e27731d0..33e0e374ce 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -18,7 +18,7 @@ OpenPype stores some of it's settings and configuration in local file system. Th ### OpenPype Mongo URL -The **Mongo URL** is the database URL given by your Studio. More details [here](artist_getting_started#mongodb). +The **Mongo URL** is the database URL given by your Studio. More details [here](artist_getting_started.md#mongodb).
@@ -42,9 +42,9 @@ Local replacement of the environment data of each software and additional intern
### Applications -Local override of software paths and their versions. More details [here](admin_settings_system/#applications). +Local override of software paths and their versions. More details [here](admin_settings_system.md#applications).
### Project Settings -The **Project Settings** allows to determine the root folder. More details [here](module_site_sync/#local-settings). +The **Project Settings** allows to determine the root folder. More details [here](module_site_sync.md#local-settings). From 54bdd152f6b3d67e240d487b264f1a674a75f876 Mon Sep 17 00:00:00 2001 From: skacmazbelhaine Date: Thu, 8 Jun 2023 11:03:19 +0200 Subject: [PATCH 006/175] Correction of line breaks --- website/docs/admin_settings_local.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index 33e0e374ce..142238f14f 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -15,36 +15,22 @@ OpenPype stores some of it's settings and configuration in local file system. Th ## Categories - - ### OpenPype Mongo URL The **Mongo URL** is the database URL given by your Studio. More details [here](artist_getting_started.md#mongodb). -
- ### General **OpenPype Username** : enter your username (it can also take by default the computer session username). It signs your actions on **OpenPype**. -
- **Admin permissions** : on checked, no need to enter a password (if defined) to access to the **Admin** section. -
- ### Experimental tools Futur version of existing tools or new ones. -
- ### Environments Local replacement of the environment data of each software and additional internal data necessary to be loaded correctly. -
- ### Applications Local override of software paths and their versions. More details [here](admin_settings_system.md#applications). -
- ### Project Settings The **Project Settings** allows to determine the root folder. More details [here](module_site_sync.md#local-settings). From aea1adab65ed3707a8963543313e0e6976082046 Mon Sep 17 00:00:00 2001 From: skacmazbelhaine Date: Tue, 13 Jun 2023 17:03:51 +0200 Subject: [PATCH 007/175] update local settings for global settings PR --- website/docs/admin_settings_local.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/website/docs/admin_settings_local.md b/website/docs/admin_settings_local.md index 142238f14f..8935b29fb5 100644 --- a/website/docs/admin_settings_local.md +++ b/website/docs/admin_settings_local.md @@ -19,18 +19,16 @@ OpenPype stores some of it's settings and configuration in local file system. Th The **Mongo URL** is the database URL given by your Studio. More details [here](artist_getting_started.md#mongodb). ### General -**OpenPype Username** : enter your username (it can also take by default the computer session username). It signs your actions on **OpenPype**. - -**Admin permissions** : on checked, no need to enter a password (if defined) to access to the **Admin** section. +**OpenPype Username** : enter your username (if not provided, it uses computer session username by default). This username is used to sign your actions on **OpenPype**, for example the "author" on a publish. +**Admin permissions** : When enabled you do not need to enter a password (if defined in Studio Settings) to access to the **Admin** section. ### Experimental tools -Futur version of existing tools or new ones. - +Future version of existing tools or new ones. ### Environments Local replacement of the environment data of each software and additional internal data necessary to be loaded correctly. ### Applications -Local override of software paths and their versions. More details [here](admin_settings_system.md#applications). +Local override of software executable paths for each version. More details [here](admin_settings_system.md#applications). ### Project Settings The **Project Settings** allows to determine the root folder. More details [here](module_site_sync.md#local-settings). From 8dea1724f9d13c1bb11f6840fb73228eb95b7086 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 23 Jun 2023 16:01:24 +0100 Subject: [PATCH 008/175] Check custom staging dir for Maya images folder. --- .../publish/validate_render_image_rule.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 78bb022785..96a57ee5d2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -1,3 +1,5 @@ +import os + from maya import cmds import pyblish.api @@ -24,8 +26,12 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): - required_images_rule = self.get_default_render_image_folder(instance) - current_images_rule = cmds.workspace(fileRuleEntry="images") + required_images_rule = os.path.normpath( + self.get_default_render_image_folder(self, instance) + ) + current_images_rule = os.path.normpath( + cmds.workspace(fileRuleEntry="images") + ) assert current_images_rule == required_images_rule, ( "Invalid workspace `images` file rule value: '{}'. " @@ -37,7 +43,9 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - required_images_rule = cls.get_default_render_image_folder(instance) + required_images_rule = cls.get_default_render_image_folder( + cls, instance + ) current_images_rule = cmds.workspace(fileRuleEntry="images") if current_images_rule != required_images_rule: @@ -45,7 +53,16 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): cmds.workspace(saveWorkspace=True) @staticmethod - def get_default_render_image_folder(instance): + def get_default_render_image_folder(cls, instance): + staging_dir = instance.data.get("stagingDir") + if staging_dir: + cls.log.debug( + "Staging dir found: \"{}\". Ignoring setting from " + "`project_settings/maya/RenderSettings/" + "default_render_image_folder`.".format(staging_dir) + ) + return staging_dir + return instance.context.data.get('project_settings')\ .get('maya') \ .get('RenderSettings') \ From 7dba2378844a2268ed1f8dc7f3cc6a8a3d47d02e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 23 Jun 2023 16:21:45 +0100 Subject: [PATCH 009/175] Docs and setting note. --- .../schemas/schema_maya_render_settings.json | 2 +- .../project_settings/settings_project_global.md | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json index 636dfa114c..fc4e750e3b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_render_settings.json @@ -12,7 +12,7 @@ { "type": "text", "key": "default_render_image_folder", - "label": "Default render image folder" + "label": "Default render image folder. This setting can be\noverwritten by custom staging directory profile;\n\"project_settings/global/tools/publish\n/custom_staging_dir_profiles\"." }, { "type": "boolean", diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 5ddf247d98..e0481a8717 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -192,7 +192,7 @@ A profile may generate multiple outputs from a single input. Each output must de - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataMov/outputs/baking/add_custom_tags` - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input. - + ### Extract Burnin Plugin is responsible for adding burnins into review representations. @@ -226,13 +226,13 @@ A burnin profile may set multiple burnin outputs from one input. The burnin's na | **Bottom Centered** | Bottom center content. | str | "{username}" | | **Bottom Right** | Bottom right corner content. | str | "{frame_start}-{current_frame}-{frame_end}" | -Each burnin profile can be configured with additional family filtering and can -add additional tags to the burnin representation, these can be configured under +Each burnin profile can be configured with additional family filtering and can +add additional tags to the burnin representation, these can be configured under the profile's **Additional filtering** section. :::note Filename suffix -The filename suffix is appended to filename of the source representation. For -example, if the source representation has suffix **"h264"** and the burnin +The filename suffix is appended to filename of the source representation. For +example, if the source representation has suffix **"h264"** and the burnin suffix is **"client"** then the final suffix is **"h264_client"**. ::: @@ -343,6 +343,10 @@ One of the key advantages of this feature is that it allows users to choose the In some cases, these DCCs (Nuke, Houdini, Maya) automatically add a rendering path during the creation stage, which is then used in publishing. Creators and extractors of such DCCs need to use these profiles to fill paths in DCC's nodes to use this functionality. +:::note +Maya's setting `project_settings/maya/RenderSettings/default_render_image_folder` is be overwritten by the custom staging dir. +::: + The custom staging folder uses a path template configured in `project_anatomy/templates/others` with `transient` being a default example path that could be used. The template requires a 'folder' key for it to be usable as custom staging folder. ##### Known issues From 49dc54c64767c62c7c71718cf047a489886f84ab Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 25 Jun 2023 15:37:52 +0100 Subject: [PATCH 010/175] Fix getting render paths. --- openpype/hosts/maya/plugins/publish/collect_render.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index babd494758..ac318dfbf7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -201,10 +201,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # append full path aov_dict = {} - default_render_file = context.data.get('project_settings')\ - .get('maya')\ - .get('RenderSettings')\ - .get('default_render_image_folder') or "" + image_directory = os.path.join( + cmds.workspace(query=True, rootDirectory=True), + cmds.workspace(fileRuleEntry="images") + ) # replace relative paths with absolute. Render products are # returned as list of dictionaries. publish_meta_path = None @@ -212,8 +212,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_paths = [] aov_first_key = list(aov.keys())[0] for file in aov[aov_first_key]: - full_path = os.path.join(workspace, default_render_file, - file) + full_path = os.path.join(image_directory, file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) From 96d796c4f5bdb51bd0461ad0f9a984019e48664c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 25 Jun 2023 15:38:26 +0100 Subject: [PATCH 011/175] Account for custom staging directory persistentcy. --- .../hosts/maya/plugins/publish/extract_pointcache.py | 3 ++- .../hosts/maya/plugins/publish/extract_proxy_abc.py | 3 ++- .../hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- .../deadline/plugins/publish/submit_publish_job.py | 7 +++++-- openpype/plugins/publish/collect_rendered_files.py | 11 ++++++++--- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index f44c13767c..f0d914fd7a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -108,7 +108,8 @@ class ExtractAlembic(publish.Extractor): } instance.data["representations"].append(representation) - instance.context.data["cleanupFullPaths"].append(path) + if not instance.data.get("stagingDir_persistent", False): + instance.context.data["cleanupFullPaths"].append(path) self.log.info("Extracted {} to {}".format(instance, dirname)) diff --git a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py index cf6351fdca..5894907795 100644 --- a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py @@ -80,7 +80,8 @@ class ExtractProxyAlembic(publish.Extractor): } instance.data["representations"].append(representation) - instance.context.data["cleanupFullPaths"].append(path) + if not instance.data.get("stagingDir_persistent", False): + instance.context.data["cleanupFullPaths"].append(path) self.log.info("Extracted {} to {}".format(instance, dirname)) # remove the bounding box diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 4160ac4cb2..3c7277121c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -92,7 +92,8 @@ class ExtractThumbnail(publish.Extractor): "Create temp directory {} for thumbnail".format(dst_staging) ) # Store new staging to cleanup paths - instance.context.data["cleanupFullPaths"].append(dst_staging) + if not instance.data.get("stagingDir_persistent", False): + instance.context.data["cleanupFullPaths"].append(dst_staging) filename = "{0}".format(instance.name) path = os.path.join(dst_staging, filename) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 69e9fb6449..7c29a68dc7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -844,7 +844,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, do_not_add_review = False if data.get("review"): families.append("review") - elif data.get("review") == False: + elif data.get("review") is False: self.log.debug("Instance has review explicitly disabled.") do_not_add_review = True @@ -872,7 +872,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "useSequenceForReview": data.get("useSequenceForReview", True), # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))), - "colorspace": instance.data.get("colorspace") + "colorspace": instance.data.get("colorspace"), + "stagingDir_persistent": instance.data.get( + "stagingDir_persistent", False + ) } # skip locking version if we are creating v01 diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 6c8d1e9ca5..4b95d8ac44 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -124,6 +124,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): self.log.info( f"Adding audio to instance: {instance.data['audio']}") + return instance.data.get("stagingDir_persistent", False) + def process(self, context): self._context = context @@ -160,9 +162,12 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): legacy_io.Session.update(session_data) os.environ.update(session_data) session_is_set = True - self._process_path(data, anatomy) - context.data["cleanupFullPaths"].append(path) - context.data["cleanupEmptyDirs"].append(os.path.dirname(path)) + staging_dir_persistent = self._process_path(data, anatomy) + if not staging_dir_persistent: + context.data["cleanupFullPaths"].append(path) + context.data["cleanupEmptyDirs"].append( + os.path.dirname(path) + ) except Exception as e: self.log.error(e, exc_info=True) raise Exception("Error") from e From 1f6934afbfc18816c53e64d9484405c34ac6c97f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 26 Jun 2023 16:38:29 +0100 Subject: [PATCH 012/175] staticmethod > classmethod --- .../hosts/maya/plugins/publish/validate_render_image_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index 96a57ee5d2..fdb069ae43 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -52,7 +52,7 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): cmds.workspace(fileRule=("images", required_images_rule)) cmds.workspace(saveWorkspace=True) - @staticmethod + @classmethod def get_default_render_image_folder(cls, instance): staging_dir = instance.data.get("stagingDir") if staging_dir: From 8492491f8382acc9372017f8ae316117e11cb6ca Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 27 Jun 2023 10:40:51 +0100 Subject: [PATCH 013/175] Fix get_default_render_image_folder arguments. --- .../maya/plugins/publish/validate_render_image_rule.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index fdb069ae43..f8c848e08b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -27,7 +27,7 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): required_images_rule = os.path.normpath( - self.get_default_render_image_folder(self, instance) + self.get_default_render_image_folder(instance) ) current_images_rule = os.path.normpath( cmds.workspace(fileRuleEntry="images") @@ -43,9 +43,7 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - required_images_rule = cls.get_default_render_image_folder( - cls, instance - ) + required_images_rule = cls.get_default_render_image_folder(instance) current_images_rule = cmds.workspace(fileRuleEntry="images") if current_images_rule != required_images_rule: From a6727800a36be1d8abb25f4ffe19858f552c0d94 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 3 Jul 2023 12:10:39 +0100 Subject: [PATCH 014/175] Dont change thumbnail extraction --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 3c7277121c..a4e5d4f8df 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -92,8 +92,6 @@ class ExtractThumbnail(publish.Extractor): "Create temp directory {} for thumbnail".format(dst_staging) ) # Store new staging to cleanup paths - if not instance.data.get("stagingDir_persistent", False): - instance.context.data["cleanupFullPaths"].append(dst_staging) filename = "{0}".format(instance.name) path = os.path.join(dst_staging, filename) From 5c44f12b37374b48c8f905834ff55e1bec99dee4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 00:51:48 +0200 Subject: [PATCH 015/175] Cleanup code - Refactor `get_file_node_files` because popping from `paths` by index should have been done in reversed order anyway. It's now changed to not need popping at all. - Removed unused `RENDERER_NODE_TYPES` and if-branch which collected `node_attrs` list which was unused + collected members which was also done outside of the if branch and thus generated no extra data. - Collected all materials from look set attributes at once instead of multiple queries - Collected all file nodes in history from a single query instead of per type - Restructured assignment of `instance.data["resources"]` to be more readable - Cached `PXR_NODES` only ones (Note: plugin load is checked on discovery of the collect look plugin) instead of querying plugin load and its nodes per file node per attribute - Removed some debug logs or combined some messages --- .../maya/plugins/publish/collect_look.py | 192 ++++++------------ 1 file changed, 64 insertions(+), 128 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 287ddc228b..6832411190 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -17,11 +17,6 @@ SHAPE_ATTRS = ["castsShadows", "visibleInRefractions", "doubleSided", "opposite"] - -RENDERER_NODE_TYPES = [ - # redshift - "RedshiftMeshParameters" -] SHAPE_ATTRS = set(SHAPE_ATTRS) @@ -36,12 +31,13 @@ def get_pxr_multitexture_file_attrs(node): FILE_NODES = { + # maya "file": "fileTextureName", - + # arnold (mtoa) "aiImage": "filename", - + # redshift "RedshiftNormalMap": "tex0", - + # renderman "PxrBump": "filename", "PxrNormalMap": "filename", "PxrMultiTexture": get_pxr_multitexture_file_attrs, @@ -49,6 +45,15 @@ FILE_NODES = { "PxrTexture": "filename" } +# Cache pixar dependency node types so we can perform a type lookup against it +PXR_NODES = set() +if cmds.pluginInfo("RenderMan_for_Maya", query=True, loaded=True): + PXR_NODES = set( + cmds.pluginInfo("RenderMan_for_Maya", + query=True, + dependNode=True) + ) + def get_attributes(dictionary, attr, node=None): # type: (dict, str, str) -> list @@ -232,20 +237,17 @@ def get_file_node_files(node): """ paths = get_file_node_paths(node) - sequences = [] - replaces = [] + + # For sequences get all files and filter to only existing files + result = [] for index, path in enumerate(paths): if node_uses_image_sequence(node, path): glob_pattern = seq_to_glob(path) - sequences.extend(glob.glob(glob_pattern)) - replaces.append(index) + result.extend(glob.glob(glob_pattern)) + elif os.path.exists(path): + result.append(path) - for index in replaces: - paths.pop(index) - - paths.extend(sequences) - - return [p for p in paths if os.path.exists(p)] + return result class CollectLook(pyblish.api.InstancePlugin): @@ -260,7 +262,7 @@ class CollectLook(pyblish.api.InstancePlugin): membership relations. Collects: - lookAttribtutes (list): Nodes in instance with their altered attributes + lookAttributes (list): Nodes in instance with their altered attributes lookSetRelations (list): Sets and their memberships lookSets (list): List of set names included in the look @@ -285,76 +287,31 @@ class CollectLook(pyblish.api.InstancePlugin): instance: Instance to collect. """ - self.log.info("Looking for look associations " - "for %s" % instance.data['name']) - - # Discover related object sets - self.log.info("Gathering sets ...") - sets = self.collect_sets(instance) + self.log.debug("Looking for look associations " + "for %s" % instance.data['name']) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) - self.log.info("Gathering set relations ...") - # Ensure iteration happen in a list so we can remove keys from the + # Discover related object sets + self.log.debug("Gathering sets ...") + sets = self.collect_sets(instance) + + # Ensure iteration happen in a list to allow removing keys from the # dict within the loop - - # skipped types of attribute on render specific nodes - disabled_types = ["message", "TdataCompound"] - + self.log.info("Gathering set relations ...") for obj_set in list(sets): self.log.debug("From {}".format(obj_set)) - - # if node is specified as renderer node type, it will be - # serialized with its attributes. - if cmds.nodeType(obj_set) in RENDERER_NODE_TYPES: - self.log.info("- {} is {}".format( - obj_set, cmds.nodeType(obj_set))) - - node_attrs = [] - - # serialize its attributes so they can be recreated on look - # load. - for attr in cmds.listAttr(obj_set): - # skip publishedNodeInfo attributes as they break - # getAttr() and we don't need them anyway - if attr.startswith("publishedNodeInfo"): - continue - - # skip attributes types defined in 'disabled_type' list - if cmds.getAttr("{}.{}".format(obj_set, attr), type=True) in disabled_types: # noqa - continue - - node_attrs.append(( - attr, - cmds.getAttr("{}.{}".format(obj_set, attr)), - cmds.getAttr( - "{}.{}".format(obj_set, attr), type=True) - )) - - for member in cmds.ls( - cmds.sets(obj_set, query=True), long=True): - member_data = self.collect_member_data(member, - instance_lookup) - if not member_data: - continue - - # Add information of the node to the members list - sets[obj_set]["members"].append(member_data) - # Get all nodes of the current objectSet (shadingEngine) for member in cmds.ls(cmds.sets(obj_set, query=True), long=True): member_data = self.collect_member_data(member, instance_lookup) - if not member_data: - continue - - # Add information of the node to the members list - sets[obj_set]["members"].append(member_data) + if member_data: + # Add information of the node to the members list + sets[obj_set]["members"].append(member_data) # Remove sets that didn't have any members assigned in the end # Thus the data will be limited to only what we need. - self.log.info("obj_set {}".format(sets[obj_set])) if not sets[obj_set]["members"]: self.log.info( "Removing redundant set information: {}".format(obj_set)) @@ -382,35 +339,26 @@ class CollectLook(pyblish.api.InstancePlugin): "rman__displacement" ] if look_sets: - materials = [] + self.log.debug("Found look sets:\n{}".format(look_sets)) + # Get all material attrs for all look sets to retrieve their inputs + existing_attrs = [] for look in look_sets: - for at in shader_attrs: - try: - con = cmds.listConnections("{}.{}".format(look, at)) - except ValueError: - # skip attributes that are invalid in current - # context. For example in the case where - # Arnold is not enabled. - continue - if con: - materials.extend(con) + for attr in shader_attrs: + if cmds.attributeQuery(attr, node=look_sets, exists=True): + existing_attrs.append("{}.{}".format(look, attr)) + materials = cmds.listConnections(existing_attrs, + source=True, + destination=False) or [] + self.log.debug("Found materials:\n{}".format(materials)) - self.log.info("Found materials:\n{}".format(materials)) - - self.log.info("Found the following sets:\n{}".format(look_sets)) # Get the entire node chain of the look sets - # history = cmds.listHistory(look_sets) - history = [] - for material in materials: - history.extend(cmds.listHistory(material, ac=True)) - - # handle VrayPluginNodeMtl node - see #1397 - vray_plugin_nodes = cmds.ls( - history, type="VRayPluginNodeMtl", long=True) - for vray_node in vray_plugin_nodes: - history.extend(cmds.listHistory(vray_node, ac=True)) + # history = cmds.listHistory(look_sets, allConnections=True) + history = cmds.listHistory(materials, allConnections=True) + # Since we retrieved history only of the connected materials + # connected to the look sets above we now add direct history + # for some of the look sets directly # handling render attribute sets render_set_types = [ "VRayDisplacement", @@ -428,20 +376,17 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) - all_supported_nodes = FILE_NODES.keys() - files = [] - for node_type in all_supported_nodes: - files.extend(cmds.ls(history, type=node_type, long=True)) + files = cmds.ls(history, + type=list(FILE_NODES.keys()), + long=True) - self.log.info("Collected file nodes:\n{}".format(files)) + self.log.info("Collected file nodes:{}".format(files)) # Collect textures if any file nodes are found - instance.data["resources"] = [] - for n in files: - for res in self.collect_resources(n): - instance.data["resources"].append(res) - - self.log.info("Collected resources: {}".format( - instance.data["resources"])) + resources = [] + for node in files: + resources.extend(self.collect_resources(node)) + instance.data["resources"] = resources + self.log.debug("Collected resources: {}".format(resources)) # Log warning when no relevant sets were retrieved for the look. if ( @@ -456,7 +401,7 @@ class CollectLook(pyblish.api.InstancePlugin): instance.extend(shader for shader in look_sets if shader not in instance_lookup) - self.log.info("Collected look for %s" % instance) + self.log.debug("Collected look for %s" % instance) def collect_sets(self, instance): """Collect all objectSets which are of importance for publishing @@ -536,14 +481,14 @@ class CollectLook(pyblish.api.InstancePlugin): # Collect changes to "custom" attributes node_attrs = get_look_attrs(node) - self.log.info( - "Node \"{0}\" attributes: {1}".format(node, node_attrs) - ) - # Only include if there are any properties we care about if not node_attrs: continue + self.log.debug( + "Node \"{0}\" attributes: {1}".format(node, node_attrs) + ) + node_attributes = {} for attr in node_attrs: if not cmds.attributeQuery(attr, node=node, exists=True): @@ -574,14 +519,12 @@ class CollectLook(pyblish.api.InstancePlugin): Returns: dict """ - self.log.debug("processing: {}".format(node)) - all_supported_nodes = FILE_NODES.keys() - if cmds.nodeType(node) not in all_supported_nodes: + if cmds.nodeType(node) not in FILE_NODES: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") - self.log.debug(" - got {}".format(cmds.nodeType(node))) + self.log.debug("processing: {} ({})".format(node, cmds.nodeType(node))) attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node) for attribute in attributes: @@ -613,14 +556,7 @@ class CollectLook(pyblish.api.InstancePlugin): # renderman allows nodes to have filename attribute empty while # you can have another incoming connection from different node. - pxr_nodes = set() - if cmds.pluginInfo("RenderMan_for_Maya", query=True, loaded=True): - pxr_nodes = set( - cmds.pluginInfo("RenderMan_for_Maya", - query=True, - dependNode=True) - ) - if not source and cmds.nodeType(node) in pxr_nodes: + if not source and cmds.nodeType(node) in PXR_NODES: self.log.info("Renderman: source is empty, skipping...") continue # We replace backslashes with forward slashes because V-Ray From fab6c0d9c603e1edfcdd86cc007cd7d0776a7ef5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 01:24:00 +0200 Subject: [PATCH 016/175] Fix typo --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 6832411190..e08bf88872 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -345,7 +345,7 @@ class CollectLook(pyblish.api.InstancePlugin): existing_attrs = [] for look in look_sets: for attr in shader_attrs: - if cmds.attributeQuery(attr, node=look_sets, exists=True): + if cmds.attributeQuery(attr, node=look, exists=True): existing_attrs.append("{}.{}".format(look, attr)) materials = cmds.listConnections(existing_attrs, source=True, From 40dc747f9ae1ba867e8afc14735c1e0d3a40ea0e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 01:24:33 +0200 Subject: [PATCH 017/175] Fix file nodes filtering --- openpype/hosts/maya/plugins/publish/collect_look.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index e08bf88872..21e3d83d13 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -45,6 +45,13 @@ FILE_NODES = { "PxrTexture": "filename" } +# Keep only node types that actually exist +all_node_types = set(cmds.allNodeTypes()) +for node_type in list(FILE_NODES.keys()): + if node_type not in all_node_types: + FILE_NODES.pop(node_type) +del all_node_types + # Cache pixar dependency node types so we can perform a type lookup against it PXR_NODES = set() if cmds.pluginInfo("RenderMan_for_Maya", query=True, loaded=True): @@ -377,10 +384,13 @@ class CollectLook(pyblish.api.InstancePlugin): ) files = cmds.ls(history, + # It's important only node types are passed that + # exist (e.g. for loaded plugins) because otherwise + # the result will turn back empty type=list(FILE_NODES.keys()), long=True) - self.log.info("Collected file nodes:{}".format(files)) + self.log.info("Collected file nodes: {}".format(files)) # Collect textures if any file nodes are found resources = [] for node in files: From 38ab207e06b63eec371e8ef1cf83c90b73d6630d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 01:30:09 +0200 Subject: [PATCH 018/175] Cosmetics: remove new line in log --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 21e3d83d13..0104c4e1dd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -357,7 +357,7 @@ class CollectLook(pyblish.api.InstancePlugin): materials = cmds.listConnections(existing_attrs, source=True, destination=False) or [] - self.log.debug("Found materials:\n{}".format(materials)) + self.log.debug("Found materials: {}".format(materials)) # Get the entire node chain of the look sets # history = cmds.listHistory(look_sets, allConnections=True) From ae3364a833efc39ce82d073a4402d95b2d78735d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 01:30:28 +0200 Subject: [PATCH 019/175] Cosmetics: remove new line in logs --- openpype/hosts/maya/plugins/publish/collect_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 0104c4e1dd..f8c5ccf4ee 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -346,7 +346,7 @@ class CollectLook(pyblish.api.InstancePlugin): "rman__displacement" ] if look_sets: - self.log.debug("Found look sets:\n{}".format(look_sets)) + self.log.debug("Found look sets: {}".format(look_sets)) # Get all material attrs for all look sets to retrieve their inputs existing_attrs = [] From 58217d88c57700e7e645d5b7dbca2bc6e5f92bf3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 11:11:59 +0200 Subject: [PATCH 020/175] Ensure file nodes are processed only once --- openpype/hosts/maya/plugins/publish/collect_look.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index f8c5ccf4ee..80c4b61de5 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -383,6 +383,9 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) + # Ensure unique entries only + history = list(set(history)) + files = cmds.ls(history, # It's important only node types are passed that # exist (e.g. for loaded plugins) because otherwise @@ -390,10 +393,13 @@ class CollectLook(pyblish.api.InstancePlugin): type=list(FILE_NODES.keys()), long=True) + # Sort for log readability + files.sort() + self.log.info("Collected file nodes: {}".format(files)) # Collect textures if any file nodes are found resources = [] - for node in files: + for node in files: # sort for log readability resources.extend(self.collect_resources(node)) instance.data["resources"] = resources self.log.debug("Collected resources: {}".format(resources)) From 95e763dee4d592d94180cf9047eb9210fae9246c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 11:12:46 +0200 Subject: [PATCH 021/175] Clarify that computed source is only used for logging by moving logic closer together --- .../maya/plugins/publish/collect_look.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 80c4b61de5..321df3e61c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -548,27 +548,28 @@ class CollectLook(pyblish.api.InstancePlugin): node, attribute )) - computed_attribute = "{}.{}".format(node, attribute) - if attribute == "fileTextureName": - computed_attribute = node + ".computedFileTextureNamePattern" - self.log.info(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) try: color_space = cmds.getAttr(color_space_attr) except ValueError: # node doesn't have colorspace attribute color_space = "Raw" + # Compare with the computed file path, e.g. the one with # the pattern in it, to generate some logging information # about this difference - computed_source = cmds.getAttr(computed_attribute) - if source != computed_source: - self.log.debug("Detected computed file pattern difference " - "from original pattern: {0} " - "({1} -> {2})".format(node, - source, - computed_source)) + # Only for file nodes with `fileTextureName` attribute + if attribute == "fileTextureName": + computed_source = cmds.getAttr( + "{}.computedFileTextureNamePattern".format(node) + ) + if source != computed_source: + self.log.debug("Detected computed file pattern difference " + "from original pattern: {0} " + "({1} -> {2})".format(node, + source, + computed_source)) # renderman allows nodes to have filename attribute empty while # you can have another incoming connection from different node. From a54cddf0f378eb80bb4d2bd2c7386415ed69f034 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Jul 2023 11:13:12 +0200 Subject: [PATCH 022/175] Clarify in log that it's processing a resource --- openpype/hosts/maya/plugins/publish/collect_look.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 321df3e61c..bbe25c559e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -540,7 +540,9 @@ class CollectLook(pyblish.api.InstancePlugin): "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") - self.log.debug("processing: {} ({})".format(node, cmds.nodeType(node))) + self.log.debug( + "Collecting resource: {} ({})".format(node, cmds.nodeType(node)) + ) attributes = get_attributes(FILE_NODES, cmds.nodeType(node), node) for attribute in attributes: From f7c368892b6a34a808b21a0f4da51b9f74b3228f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Jul 2023 15:49:16 +0100 Subject: [PATCH 023/175] Fix Maya Deadline submit plugin --- .../deadline/plugins/publish/submit_maya_deadline.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 159ac43289..8193ca2734 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -289,7 +289,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def process_submission(self): instance = self._instance - context = instance.context filepath = self.scene_path # publish if `use_publish` else workfile @@ -306,13 +305,11 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, self._patch_workfile() # Gather needed data ------------------------------------------------ - workspace = context.data["workspaceDir"] - default_render_file = instance.context.data.get('project_settings')\ - .get('maya')\ - .get('RenderSettings')\ - .get('default_render_image_folder') filename = os.path.basename(filepath) - dirname = os.path.join(workspace, default_render_file) + dirname = os.path.join( + cmds.workspace(query=True, rootDirectory=True), + cmds.workspace(fileRuleEntry="images") + ) # Fill in common data to payload ------------------------------------ # TODO: Replace these with collected data from CollectRender From a2e18323ff1103861891cf84dd9e2ac3b6efab53 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 27 Jul 2023 22:32:43 +0300 Subject: [PATCH 024/175] update fbx creator --- .../houdini/plugins/create/create_fbx.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/create/create_fbx.py diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py new file mode 100644 index 0000000000..65f613bdea --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating fbx.""" +from openpype.hosts.houdini.api import plugin + +import hou + + +class CreateFilmboxFBX(plugin.HoudiniCreator): + """Filmbox FBX Driver""" + identifier = "io.openpype.creators.houdini.filmboxfbx" + label = "Filmbox FBX" + family = "filmboxfbx" + icon = "fa5s.cubes" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data.pop("active", None) + instance_data.update({"node_type": "filmboxfbx"}) + + instance = super(CreateFilmboxFBX, self).create( + subset_name, + instance_data, + pre_create_data) + + instance_node = hou.node(instance.get("instance_node")) + output_path = hou.text.expandString( + "$HIP/pyblish/{}.fbx".format(subset_name)) + + parms = { + "sopoutput": output_path + } + + if self.selected_nodes: + selected_node = self.selected_nodes[0] + + # Although Houdini allows ObjNode path on `startnode` for the + # the ROP node we prefer it set to the SopNode path explicitly + + # Allow sop level paths (e.g. /obj/geo1/box1) + if isinstance(selected_node, hou.SopNode): + parms["startnode"] = selected_node.path() + self.log.debug( + "Valid SopNode selection, 'Export' in filmboxfbx" + " will be set to '%s'." + % selected_node + ) + + # Allow object level paths to Geometry nodes (e.g. /obj/geo1) + # but do not allow other object level nodes types like cameras, etc. + elif isinstance(selected_node, hou.ObjNode) and \ + selected_node.type().name() in ["geo"]: + + # get the output node with the minimum + # 'outputidx' or the node with display flag + sop_path = self.get_obj_output(selected_node) + + if sop_path: + parms["startnode"] = sop_path.path() + self.log.debug( + "Valid ObjNode selection, 'Export' in filmboxfbx " + "will be set to the child path '%s'." + % sop_path + ) + + if not parms.get("startnode", None): + self.log.debug( + "Selection isn't valid. 'Export' in filmboxfbx will be empty." + ) + else: + self.log.debug( + "No Selection. 'Export' in filmboxfbx will be empty." + ) + + instance_node.setParms(parms) + + # Lock any parameters in this list + to_lock = [] + self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] + + def get_obj_output(self, obj_node): + """Find output node with the smallest 'outputidx'.""" + + outputs = obj_node.subnetOutputs() + + # if obj_node is empty + if not outputs: + return + + # if obj_node has one output child whether its + # sop output node or a node with the render flag + elif len(outputs) == 1: + return outputs[0] + + # if there are more than one, then it have multiple ouput nodes + # return the one with the minimum 'outputidx' + else: + return min(outputs, + key=lambda node: node.evalParm('outputidx')) From 5a9281823d3d18cc9857875b796f7e65d9c747f1 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 28 Jul 2023 00:06:55 +0300 Subject: [PATCH 025/175] update create_fbx --- .../houdini/plugins/create/create_fbx.py | 62 ++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 65f613bdea..b59a8ccfc6 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -12,23 +12,61 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): family = "filmboxfbx" icon = "fa5s.cubes" + # Overrides HoudiniCreator.create() def create(self, subset_name, instance_data, pre_create_data): - instance_data.pop("active", None) + # instance_data.pop("active", None) + + # set node type instance_data.update({"node_type": "filmboxfbx"}) + # set essential extra parameters instance = super(CreateFilmboxFBX, self).create( subset_name, instance_data, pre_create_data) + # get the created node instance_node = hou.node(instance.get("instance_node")) + + # get output path output_path = hou.text.expandString( "$HIP/pyblish/{}.fbx".format(subset_name)) + # get selection + selection = self.get_selection() + + # parms dictionary parms = { + "startnode" : selection, "sopoutput": output_path } + # set parms + instance_node.setParms(parms) + + # Lock any parameters in this list + to_lock = [] + self.lock_parameters(instance_node, to_lock) + + # Overrides HoudiniCreator.get_network_categories() + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] + + def get_selection(self): + """Selection Logic. + + how self.selected_nodes should be processed to get + the desirable node from selection. + + Returns: + str : node path + """ + + selection = "" + if self.selected_nodes: selected_node = self.selected_nodes[0] @@ -37,7 +75,7 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # Allow sop level paths (e.g. /obj/geo1/box1) if isinstance(selected_node, hou.SopNode): - parms["startnode"] = selected_node.path() + selection = selected_node.path() self.log.debug( "Valid SopNode selection, 'Export' in filmboxfbx" " will be set to '%s'." @@ -54,14 +92,14 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): sop_path = self.get_obj_output(selected_node) if sop_path: - parms["startnode"] = sop_path.path() + selection = sop_path.path() self.log.debug( "Valid ObjNode selection, 'Export' in filmboxfbx " "will be set to the child path '%s'." % sop_path ) - if not parms.get("startnode", None): + if not selection: self.log.debug( "Selection isn't valid. 'Export' in filmboxfbx will be empty." ) @@ -70,20 +108,12 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): "No Selection. 'Export' in filmboxfbx will be empty." ) - instance_node.setParms(parms) - - # Lock any parameters in this list - to_lock = [] - self.lock_parameters(instance_node, to_lock) - - def get_network_categories(self): - return [ - hou.ropNodeTypeCategory(), - hou.sopNodeTypeCategory() - ] + return selection def get_obj_output(self, obj_node): - """Find output node with the smallest 'outputidx'.""" + """Find output node with the smallest 'outputidx' + or return tje node with the render flag instead. + """ outputs = obj_node.subnetOutputs() From 9724ea4c84f51e68785cbd54c1d3eef87cdd4a3f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 28 Jul 2023 00:08:23 +0300 Subject: [PATCH 026/175] update create_fbx --- openpype/hosts/houdini/plugins/create/create_fbx.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index b59a8ccfc6..d4594edcbe 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- -"""Creator plugin for creating fbx.""" +"""Creator plugin for creating fbx. + +It was made to pratice publish process. +""" from openpype.hosts.houdini.api import plugin import hou From 8a431f2d44c967daab56dce3ed06fdfd7bcae0c5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 28 Jul 2023 00:18:47 +0300 Subject: [PATCH 027/175] update create_fbx --- openpype/hosts/houdini/plugins/create/create_fbx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index d4594edcbe..30cc98ddb9 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -31,11 +31,12 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # get the created node instance_node = hou.node(instance.get("instance_node")) - # get output path + # Get this node specific parms + # 1. get output path output_path = hou.text.expandString( "$HIP/pyblish/{}.fbx".format(subset_name)) - # get selection + # 2. get selection selection = self.get_selection() # parms dictionary From e4c9275ac36e1900d48a3605f2693a189ab92158 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 28 Jul 2023 20:08:20 +0300 Subject: [PATCH 028/175] update default_variants --- openpype/hosts/houdini/plugins/create/create_fbx.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 30cc98ddb9..18fd879ac7 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -15,6 +15,9 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): family = "filmboxfbx" icon = "fa5s.cubes" + default_variant = "FBX" + default_variants = ["FBX", "Main", "Test"] + # Overrides HoudiniCreator.create() def create(self, subset_name, instance_data, pre_create_data): # instance_data.pop("active", None) From 919247b3d4675b5febbb3ebebdc2b92075312399 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 28 Jul 2023 21:17:38 +0300 Subject: [PATCH 029/175] update creator --- .../houdini/plugins/create/create_fbx.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 18fd879ac7..fed1ad0562 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -4,6 +4,7 @@ It was made to pratice publish process. """ from openpype.hosts.houdini.api import plugin +from openpype.lib import EnumDef import hou @@ -42,10 +43,14 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # 2. get selection selection = self.get_selection() + # 3. get Vertex Cache Format + vcformat = pre_create_data.get("vcformat") + # parms dictionary parms = { "startnode" : selection, - "sopoutput": output_path + "sopoutput": output_path, + "vcformat" : vcformat } # set parms @@ -62,6 +67,19 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): hou.sopNodeTypeCategory() ] + # Overrides HoudiniCreator.get_pre_create_attr_defs() + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + vcformat = EnumDef("vcformat", + items={ + 0: "Maya Compatible (MC)", + 1: "3DS MAX Compatible (PC2)" + }, + default=0, + label="Vertex Cache Format") + + return attrs + [vcformat] + def get_selection(self): """Selection Logic. From 7918668e72ec743c63d966cbf1db3f60159292ef Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 28 Jul 2023 22:15:59 +0300 Subject: [PATCH 030/175] add Valid Frame Range --- .../hosts/houdini/plugins/create/create_fbx.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index fed1ad0562..0345e07ea6 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -46,11 +46,15 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # 3. get Vertex Cache Format vcformat = pre_create_data.get("vcformat") + # 3. get Valid Frame Range + trange = pre_create_data.get("trange") + # parms dictionary parms = { "startnode" : selection, "sopoutput": output_path, - "vcformat" : vcformat + "vcformat" : vcformat, + "trange" : trange } # set parms @@ -77,8 +81,15 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): }, default=0, label="Vertex Cache Format") + trange = EnumDef("trange", + items={ + 0: "Render Current Frame", + 1: "Render Frame Range" + }, + default=0, + label="Valid Frame Range") - return attrs + [vcformat] + return attrs + [vcformat, trange] def get_selection(self): """Selection Logic. From ed2a48e2cd9af28fdd45237cecd9d1d9c17eab7e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Sat, 29 Jul 2023 02:28:02 +0300 Subject: [PATCH 031/175] add fbx collector --- .../plugins/publish/collect_fbx_type.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_fbx_type.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py new file mode 100644 index 0000000000..f25dbf3a5b --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -0,0 +1,56 @@ +"""Collector for filmboxfbx types. + +A Collector can act as a preprocessor for the validation stage. +It is used mainly to update instance.data + +P.S. + There are some collectors that run by default for all types. +""" +import pyblish.api + + +class CollectFilmboxfbxType(pyblish.api.InstancePlugin): + """Collect data type for filmboxfbx instance.""" + + order = pyblish.api.CollectorOrder + hosts = ["houdini"] + families = ["filmboxfbx"] + label = "Collect type of filmboxfbx" + + def process(self, instance): + + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.filmboxfbx": # noqa: E501 + # such a condition can be used to differentiate between + # instances by identifier even if they have the same type. + pass + + # Update instance.data with ouptut_node + out_node = self.get_output_node(instance) + + if out_node: + instance.data["output_node"] = out_node + + # Disclaimer : As a convntin we use collect_output_node.py + # to Update instance.data with ouptut_node of different types + # however, we use this collector instead for demonstration + + + def get_output_node(self, instance): + """Getting output_node Logic. + + It's moved here so that it become easier to focus on + process method. + """ + + import hou + + # get output node + node = hou.node(instance.data["instance_node"]) + out_node = node.parm("startnode").evalAsNode() + + if not out_node: + self.log.warning("No output node collected.") + return + + self.log.debug("Output node: %s" % out_node.path()) + return out_node From 051f03f4fa4e4718c6be56878ae2f8b07b5a3a11 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 31 Jul 2023 21:00:16 +0300 Subject: [PATCH 032/175] add export validator --- .../publish/validate_fbx_export_node.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py new file mode 100644 index 0000000000..f9f2461415 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Validator plugin for Export node in filmbox instance.""" +import pyblish.api +from openpype.pipeline import PublishValidationError + + +class ValidateNoExportPath(pyblish.api.InstancePlugin): + """Validate if Export node in filmboxfbx instance exists.""" + + order = pyblish.api.ValidatorOrder + families = ["filmboxfbx"] + label = "Validate Filmbox Export Node" + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + "Export node is incorrect", + title="Invalid Export Node" + ) + + @classmethod + def get_invalid(cls, instance): + + import hou + + fbx_rop = hou.node(instance.data.get("instance_node")) + export_node = fbx_rop.parm("startnode").evalAsNode() + + if not export_node: + cls.log.error( + ("Empty Export ('Export' parameter) found in " + "the filmbox instance - {}".format(fbx_rop.path())) + ) + return [fbx_rop] + + if not isinstance(export_node, hou.SopNode): + cls.log.error( + "Export node '{}' is not pointing to valid SOP" + " node".format(export_node.path()) + ) + return [export_node] From ecc583f8f2eec059e8589e6c45934540085aad34 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 1 Aug 2023 22:04:04 +0300 Subject: [PATCH 033/175] update create collect validate --- .../houdini/plugins/create/create_fbx.py | 11 +- .../plugins/publish/collect_fbx_type.py | 1 + .../publish/validate_fbx_export_node.py | 43 ---- .../publish/validate_fbx_hierarchy_path.py | 220 ++++++++++++++++++ .../publish/validate_sop_output_node.py | 2 +- 5 files changed, 231 insertions(+), 46 deletions(-) delete mode 100644 openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py create mode 100644 openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 0345e07ea6..ee2fdcd73f 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -2,7 +2,14 @@ """Creator plugin for creating fbx. It was made to pratice publish process. + +Filmbox by default expects an ObjNode +it's by default selects the output sop with mimimum idx +or the node with render flag isntead. + +to eleminate any confusion, we set the sop node explictly. """ + from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef @@ -16,8 +23,8 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): family = "filmboxfbx" icon = "fa5s.cubes" - default_variant = "FBX" - default_variants = ["FBX", "Main", "Test"] + default_variant = "Main" + default_variants = ["Main", "Test"] # Overrides HoudiniCreator.create() def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index f25dbf3a5b..05a0af659f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -17,6 +17,7 @@ class CollectFilmboxfbxType(pyblish.api.InstancePlugin): families = ["filmboxfbx"] label = "Collect type of filmboxfbx" + # overrides InstancePlugin.process() def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.filmboxfbx": # noqa: E501 diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py deleted file mode 100644 index f9f2461415..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_export_node.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validator plugin for Export node in filmbox instance.""" -import pyblish.api -from openpype.pipeline import PublishValidationError - - -class ValidateNoExportPath(pyblish.api.InstancePlugin): - """Validate if Export node in filmboxfbx instance exists.""" - - order = pyblish.api.ValidatorOrder - families = ["filmboxfbx"] - label = "Validate Filmbox Export Node" - - def process(self, instance): - - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError( - "Export node is incorrect", - title="Invalid Export Node" - ) - - @classmethod - def get_invalid(cls, instance): - - import hou - - fbx_rop = hou.node(instance.data.get("instance_node")) - export_node = fbx_rop.parm("startnode").evalAsNode() - - if not export_node: - cls.log.error( - ("Empty Export ('Export' parameter) found in " - "the filmbox instance - {}".format(fbx_rop.path())) - ) - return [fbx_rop] - - if not isinstance(export_node, hou.SopNode): - cls.log.error( - "Export node '{}' is not pointing to valid SOP" - " node".format(export_node.path()) - ) - return [export_node] diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py new file mode 100644 index 0000000000..24f88b384f --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +"""It's almost the same as +'validate_primitive_hierarchy_paths.py' +however this one includes more comments for demonstration. + +FYI, path for fbx behaves a little differently. +In maya terms: +in Filmbox FBX: it sets the name of the object +in Alembic ROP: it sets the name of the shape +""" + +import pyblish.api +from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import ( + ValidateContentsOrder, + RepairAction, +) +from openpype.hosts.houdini.api.action import ( + SelectInvalidAction, + SelectROPAction, +) + +import hou + +# Each validation can have a single repair action +# which calls the repair method +class AddDefaultPathAction(RepairAction): + label = "Add a default path" + icon = "mdi.pencil-plus-outline" + + +class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): + """Validate all primitives build hierarchy from attribute + when enabled. + + The name of the attribute must exist on the prims and have the + same name as Build Hierarchy from Attribute's `Path Attribute` + value on the FilmBox node. + This validation enables 'Build Hierarchy from Attribute' + by default. + """ + + # Usually you will this value by default + order = ValidateContentsOrder + 0.1 + families = ["filmboxfbx"] + hosts = ["houdini"] + label = "Validate FBX Hierarchy Path" + + # Validation can have as many actions as you want + # all of these actions are defined in a seperate place + # unlike the repair action + actions = [SelectInvalidAction, AddDefaultPathAction, + SelectROPAction] + + # overrides InstancePlugin.process() + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.name() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes), + title=self.label + ) + + # This method was named get_invalid as a convention + # it's also used by SelectInvalidAction to select + # the returned node + @classmethod + def get_invalid(cls, instance): + + output_node = instance.data.get("output_node") + rop_node = hou.node(instance.data["instance_node"]) + + if output_node is None: + cls.log.error( + "SOP Output node in '%s' does not exist. " + "Ensure a valid SOP output path is set.", + rop_node.path() + ) + + return [rop_node] + + build_from_path = rop_node.parm("buildfrompath").eval() + if not build_from_path: + cls.log.debug( + "Filmbox FBX has 'Build from Path' disabled. " + "Enbaling it as default." + ) + rop_node.parm("buildfrompath").set(1) + + path_attr = rop_node.parm("pathattrib").eval() + if not path_attr: + cls.log.debug( + "Filmbox FBX node has no Path Attribute" + "value set, setting it to 'path' as default." + ) + rop_node.parm("pathattrib").set("path") + + cls.log.debug("Checking for attribute: %s", path_attr) + + if not hasattr(output_node, "geometry"): + # In the case someone has explicitly set an Object + # node instead of a SOP node in Geometry context + # then for now we ignore - this allows us to also + # export object transforms. + cls.log.warning("No geometry output node found," + " skipping check..") + return + + # Check if the primitive attribute exists + frame = instance.data.get("frameStart", 0) + geo = output_node.geometryAtFrame(frame) + + # If there are no primitives on the current frame then + # we can't check whether the path names are correct. + # So we'll just issue a warning that the check can't + # be done consistently and skip validation. + + if len(geo.iterPrims()) == 0: + cls.log.warning( + "No primitives found on current frame." + " Validation for primitive hierarchy" + " paths will be skipped," + " thus can't be validated." + ) + return + + # Check if there are any values for the primitives + attrib = geo.findPrimAttrib(path_attr) + if not attrib: + cls.log.info( + "Geometry Primitives are missing " + "path attribute: `%s`", path_attr + ) + return [output_node] + + # Ensure at least a single string value is present + if not attrib.strings(): + cls.log.info( + "Primitive path attribute has no " + "string values: %s", path_attr + ) + return [output_node] + + paths = geo.primStringAttribValues(path_attr) + # Ensure all primitives are set to a valid path + # Collect all invalid primitive numbers + invalid_prims = [i for i, path in enumerate(paths) if not path] + if invalid_prims: + num_prims = len(geo.iterPrims()) # faster than len(geo.prims()) + cls.log.info( + "Prims have no value for attribute `%s` " + "(%s of %s prims)", + path_attr, len(invalid_prims), num_prims + ) + return [output_node] + + # what repair action expects to find and call + @classmethod + def repair(cls, instance): + """Add a default path attribute Action. + + It is a helper action more than a repair action, + used to add a default single value for the path. + """ + + rop_node = hou.node(instance.data["instance_node"]) + # I'm doing so because an artist may change output node + # before clicking the button. + output_node = rop_node.parm("startnode").evalAsNode() + + if not output_node: + cls.log.debug( + "Action isn't performed, invalid SOP Path on %s", + rop_node + ) + return + + # This check to prevent the action from running multiple times. + # git_invalid only returns [output_node] when + # path attribute is the problem + if cls.get_invalid(instance) != [output_node]: + return + + path_attr = rop_node.parm("pathattrib").eval() + + path_node = output_node.parent().createNode("name", + "AUTO_PATH") + path_node.parm("attribname").set(path_attr) + path_node.parm("name1").set('`opname("..")`_GEO') + + cls.log.debug( + "'%s' was created. It adds '%s' with a default" + " single value", path_node, path_attr + ) + + path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True) + path_node.setComment( + 'Auto path node was created automatically by ' + '"Add a default path attribute"' + '\nFeel free to modify or replace it.' + ) + + if output_node.type().name() in ["null", "output"]: + # Connect before + path_node.setFirstInput(output_node.input(0)) + path_node.moveToGoodPosition() + output_node.setFirstInput(path_node) + output_node.moveToGoodPosition() + else: + # Connect after + path_node.setFirstInput(output_node) + rop_node.parm("startnode").set(path_node.path()) + path_node.moveToGoodPosition() + + cls.log.debug( + "SOP path on '%s' updated to new output node '%s'", + rop_node, path_node + ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index d9dee38680..da9752505a 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -22,7 +22,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["pointcache", "vdbcache"] + families = ["pointcache", "vdbcache", "filmboxfbx"] hosts = ["houdini"] label = "Validate Output Node" actions = [SelectROPAction, SelectInvalidAction] From 0a7d6aa31845a172dea4899f98a99bac66d806d8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 1 Aug 2023 22:09:01 +0300 Subject: [PATCH 034/175] update validate --- .../houdini/plugins/publish/validate_fbx_hierarchy_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index 24f88b384f..7e890db58e 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -56,7 +56,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - nodes = [n.name() for n in invalid] + nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes), From 39225ee0479c0bdf257d02fcfff236d105806fd6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 1 Aug 2023 22:54:05 +0300 Subject: [PATCH 035/175] add fbx extractor --- .../houdini/plugins/publish/extract_fbx.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/extract_fbx.py diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py new file mode 100644 index 0000000000..70bf3e1017 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -0,0 +1,50 @@ +import os + +import pyblish.api + +from openpype.pipeline import publish +from openpype.hosts.houdini.api.lib import render_rop + +import hou + + +class ExtractRedshiftProxy(publish.Extractor): + + order = pyblish.api.ExtractorOrder + 0.1 + label = "Extract FilmBox FBX" + families = ["filmboxfbx"] + hosts = ["houdini"] + + # overrides InstancePlugin.process() + def process(self, instance): + + 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("sopoutput") + staging_dir = os.path.normpath(os.path.dirname(output)) + instance.data["stagingDir"] = staging_dir + file_name = os.path.basename(output) + + self.log.info("Writing FBX '%s' to '%s'" % (file_name, + staging_dir)) + + render_rop(ropnode) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + "name": "fbx", + "ext": "fbx", + "files": file_name, + "stagingDir": staging_dir, + } + + # A single frame may also be rendered without start/end frame. + if "frameStart" in instance.data and "frameEnd" in instance.data: + representation["frameStart"] = instance.data["frameStart"] + representation["frameEnd"] = instance.data["frameEnd"] + + instance.data["representations"].append(representation) From b507c2d52b2b2ad23ec1d300e8f5d8cbbd97fb58 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 1 Aug 2023 23:03:55 +0300 Subject: [PATCH 036/175] register filmbox family in integrate.py --- openpype/plugins/publish/integrate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index ffb9acf4a7..05ffe0bd3d 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -138,7 +138,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "simpleUnrealTexture", "online", "uasset", - "blendScene" + "blendScene", + "filmboxfbx" ] default_template_name = "publish" From 686ba073cef895b17bc6393e4a4dd9bbb4bf73f5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 1 Aug 2023 23:06:40 +0300 Subject: [PATCH 037/175] update comments --- openpype/hosts/houdini/plugins/publish/collect_fbx_type.py | 1 + openpype/hosts/houdini/plugins/publish/extract_fbx.py | 3 ++- .../houdini/plugins/publish/validate_fbx_hierarchy_path.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index 05a0af659f..c665852bb6 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -12,6 +12,7 @@ import pyblish.api class CollectFilmboxfbxType(pyblish.api.InstancePlugin): """Collect data type for filmboxfbx instance.""" + # Usually you will use this value as default order = pyblish.api.CollectorOrder hosts = ["houdini"] families = ["filmboxfbx"] diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 70bf3e1017..6a4b541c33 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -10,12 +10,13 @@ import hou class ExtractRedshiftProxy(publish.Extractor): + # Usually you will use this value as default order = pyblish.api.ExtractorOrder + 0.1 label = "Extract FilmBox FBX" families = ["filmboxfbx"] hosts = ["houdini"] - # overrides InstancePlugin.process() + # overrides Extractor.process() def process(self, instance): ropnode = hou.node(instance.data.get("instance_node")) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index 7e890db58e..871b347155 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -40,7 +40,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): by default. """ - # Usually you will this value by default + # Usually you will use this value as default order = ValidateContentsOrder + 0.1 families = ["filmboxfbx"] hosts = ["houdini"] From 7a74608b11bfd891d52b92ae2bdf78ebf3dcd468 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 3 Aug 2023 21:42:26 +0300 Subject: [PATCH 038/175] add loader --- .../hosts/houdini/plugins/load/load_fbx.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/load/load_fbx.py diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py new file mode 100644 index 0000000000..e69bdbb10a --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +"""Fbx Loader for houdini. + +It's an exact copy of +'load_bgeo.py' +however this one includes extra comments for demonstration. + +This plugin is part of publish process guide. +""" +import os +import re + +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.hosts.houdini.api import pipeline + + +class FbxLoader(load.LoaderPlugin): + """Load fbx files to Houdini.""" + + label = "Load FBX" + families = ["filmboxfbx", "fbx"] + representations = ["fbx"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import hou + + # Get the root node + obj = hou.node("/obj") + + # Define node name + namespace = namespace if namespace else context["asset"]["name"] + node_name = "{}_{}".format(namespace, name) if namespace else name + + # Create a new geo node + container = obj.createNode("geo", node_name=node_name) + is_sequence = bool(context["representation"]["context"].get("frame")) + + # Remove the file node, it only loads static meshes + # Houdini 17 has removed the file node from the geo node + file_node = container.node("file1") + if file_node: + file_node.destroy() + + # Explicitly create a file node + path = self.filepath_from_context(context) + file_node = container.createNode("file", node_name=node_name) + file_node.setParms( + {"file": self.format_path(path, context["representation"])}) + + # Set display on last node + file_node.setDisplayFlag(True) + + nodes = [container, file_node] + self[:] = nodes + + return pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + @staticmethod + def format_path(path, representation): + """Format file path correctly for single bgeo or bgeo sequence.""" + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) + + is_sequence = bool(representation["context"].get("frame")) + # The path is either a single file or sequence in a folder. + if not is_sequence: + filename = path + else: + filename = re.sub(r"(.*)\.(\d+)\.(bgeo.*)", "\\1.$F4.\\3", path) + + filename = os.path.join(path, filename) + + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + + return filename + + def update(self, container, representation): + + node = container["node"] + try: + file_node = next( + n for n in node.children() if n.type().name() == "file" + ) + except StopIteration: + self.log.error("Could not find node of type `file`") + return + + # Update the file path + file_path = get_representation_path(representation) + file_path = self.format_path(file_path, representation) + + file_node.setParms({"file": file_path}) + + # Update attribute + node.setParms({"representation": str(representation["_id"])}) + + def remove(self, container): + + node = container["node"] + node.destroy() + + def switch(self, container, representation): + self.update(container, representation) From a1b0c409aca7af1f9d44563bc4c797d884b67b5a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 3 Aug 2023 21:43:50 +0300 Subject: [PATCH 039/175] update doc strings --- openpype/hosts/houdini/plugins/create/create_fbx.py | 2 ++ .../hosts/houdini/plugins/publish/collect_fbx_type.py | 2 ++ .../plugins/publish/validate_fbx_hierarchy_path.py | 8 ++++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index ee2fdcd73f..ac76dc0441 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -8,6 +8,8 @@ it's by default selects the output sop with mimimum idx or the node with render flag isntead. to eleminate any confusion, we set the sop node explictly. + +This plugin is part of publish process guide. """ from openpype.hosts.houdini.api import plugin diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index c665852bb6..f1665b27f0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -5,6 +5,8 @@ It is used mainly to update instance.data P.S. There are some collectors that run by default for all types. + +This plugin is part of publish process guide. """ import pyblish.api diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index 871b347155..6a2bda1bca 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- -"""It's almost the same as +"""Validate path attribute for all primitives. + +It's almost the same as 'validate_primitive_hierarchy_paths.py' -however this one includes more comments for demonstration. +however this one includes extra comments for demonstration. FYI, path for fbx behaves a little differently. In maya terms: in Filmbox FBX: it sets the name of the object in Alembic ROP: it sets the name of the shape + +This plugin is part of publish process guide. """ import pyblish.api From 08fdbf1283151e2109f1649321700ea8697c261d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 3 Aug 2023 23:34:32 +0300 Subject: [PATCH 040/175] make loader look prettier --- .../hosts/houdini/plugins/load/load_fbx.py | 151 +++++++++++------- 1 file changed, 96 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index e69bdbb10a..5afd39ea99 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -23,71 +23,25 @@ class FbxLoader(load.LoaderPlugin): label = "Load FBX" families = ["filmboxfbx", "fbx"] representations = ["fbx"] - order = -10 - icon = "code-fork" - color = "orange" + order = -10 # you can use this by default. + icon = "code-fork" # you can use this by default. + color = "orange" # you can use this by default. def load(self, context, name=None, namespace=None, data=None): - import hou + file_path = self.get_file_path(context) - # Get the root node - obj = hou.node("/obj") + namespace, node_name = self.get_node_name(context, name, namespace) - # Define node name - namespace = namespace if namespace else context["asset"]["name"] - node_name = "{}_{}".format(namespace, name) if namespace else name + nodes = self.create_load_node_tree(file_path, node_name, name) - # Create a new geo node - container = obj.createNode("geo", node_name=node_name) - is_sequence = bool(context["representation"]["context"].get("frame")) - - # Remove the file node, it only loads static meshes - # Houdini 17 has removed the file node from the geo node - file_node = container.node("file1") - if file_node: - file_node.destroy() - - # Explicitly create a file node - path = self.filepath_from_context(context) - file_node = container.createNode("file", node_name=node_name) - file_node.setParms( - {"file": self.format_path(path, context["representation"])}) - - # Set display on last node - file_node.setDisplayFlag(True) - - nodes = [container, file_node] self[:] = nodes - return pipeline.containerise( - node_name, - namespace, - nodes, - context, - self.__class__.__name__, - suffix="", + containerised_nodes = self.get_containerised_nodes( + nodes, context, node_name, namespace ) - @staticmethod - def format_path(path, representation): - """Format file path correctly for single bgeo or bgeo sequence.""" - if not os.path.exists(path): - raise RuntimeError("Path does not exist: %s" % path) - - is_sequence = bool(representation["context"].get("frame")) - # The path is either a single file or sequence in a folder. - if not is_sequence: - filename = path - else: - filename = re.sub(r"(.*)\.(\d+)\.(bgeo.*)", "\\1.$F4.\\3", path) - - filename = os.path.join(path, filename) - - filename = os.path.normpath(filename) - filename = filename.replace("\\", "/") - - return filename + return containerised_nodes def update(self, container, representation): @@ -116,3 +70,90 @@ class FbxLoader(load.LoaderPlugin): def switch(self, container, representation): self.update(container, representation) + + def get_file_path(self, context): + """Return formatted file path.""" + + # 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("\\", "/") + + return file_path + + def get_node_name(self, context, name=None, namespace=None): + """Define node name.""" + + if not namespace: + namespace = context["asset"]["name"] + + if namespace: + node_name = "{}_{}".format(namespace, name) + else: + node_name = name + + return namespace, node_name + + def create_load_node_tree(self, file_path, node_name, subset_name): + """Create Load node network. + + you can start building your tree at any obj level. + it'll be much easier to build it in the root obj level. + + Afterwards, your tree will be automatically moved to + '/obj/AVALON_CONTAINERS' subnetwork. + """ + import hou + + # Get the root obj level + obj = hou.node("/obj") + + # Create a new obj geo node + parent_node = obj.createNode("geo", node_name=node_name) + + # In older houdini, + # when reating a new obj geo node, a default file node will be + # automatically created. + # so, we will delete it if exists. + file_node = parent_node.node("file1") + if file_node: + file_node.destroy() + + # Create a new file node + file_node = parent_node.createNode("file", node_name= node_name) + file_node.setParms({"file":file_path}) + + # Create attribute delete + attribdelete_name = "attribdelete_{}".format(subset_name) + attribdelete = parent_node.createNode("attribdelete", + node_name= attribdelete_name) + attribdelete.setParms({"ptdel":"fbx_*"}) + attribdelete.setInput(0, file_node) + + # Create a Null node + null_name = "OUT_{}".format(subset_name) + null = parent_node.createNode("null", node_name= null_name) + null.setInput(0, attribdelete) + + # Ensure display flag is on the file_node input node and not on the OUT + # node to optimize "debug" displaying in the viewport. + file_node.setDisplayFlag(True) + + # Set new position for unpack node else it gets cluttered + nodes = [parent_node, file_node, attribdelete, null] + for nr, node in enumerate(nodes): + node.setPosition([0, (0 - nr)]) + + return nodes + + def get_containerised_nodes(self, nodes, context, node_name, namespace): + containerised_nodes = pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + return containerised_nodes From de56b53ac6c48f0ca73b9111ce0edbf73d02d950 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 3 Aug 2023 23:35:28 +0300 Subject: [PATCH 041/175] update doc string --- openpype/hosts/houdini/plugins/load/load_fbx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 5afd39ea99..6bac7a7cec 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """Fbx Loader for houdini. -It's an exact copy of -'load_bgeo.py' +It's almost a copy of +'load_bgeo.py'and 'load_alembic.py' however this one includes extra comments for demonstration. This plugin is part of publish process guide. From ba5f7eb417ccf86edccb4a3b1ef2b6dfe64fecca Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 3 Aug 2023 23:36:33 +0300 Subject: [PATCH 042/175] remove un-necessary import --- openpype/hosts/houdini/plugins/load/load_fbx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 6bac7a7cec..3f66c8e88f 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -8,7 +8,6 @@ however this one includes extra comments for demonstration. This plugin is part of publish process guide. """ import os -import re from openpype.pipeline import ( load, From 31f8c4cd13ef4240566f8dbf98af86f2488d3936 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 7 Aug 2023 18:59:04 +0300 Subject: [PATCH 043/175] update fbx creator --- .../houdini/plugins/create/create_fbx.py | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index ac76dc0441..9e1dbea2f4 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -4,10 +4,12 @@ It was made to pratice publish process. Filmbox by default expects an ObjNode -it's by default selects the output sop with mimimum idx -or the node with render flag isntead. +however, we set the sop node explictly +to eleminate any confusion. -to eleminate any confusion, we set the sop node explictly. +This creator by default will select +the output sop with mimimum idx +or the node with render flag isntead. This plugin is part of publish process guide. """ @@ -30,12 +32,11 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # Overrides HoudiniCreator.create() def create(self, subset_name, instance_data, pre_create_data): - # instance_data.pop("active", None) # set node type instance_data.update({"node_type": "filmboxfbx"}) - # set essential extra parameters + # create instance (calls super create method) instance = super(CreateFilmboxFBX, self).create( subset_name, instance_data, @@ -44,33 +45,14 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # get the created node instance_node = hou.node(instance.get("instance_node")) - # Get this node specific parms - # 1. get output path - output_path = hou.text.expandString( - "$HIP/pyblish/{}.fbx".format(subset_name)) - - # 2. get selection - selection = self.get_selection() - - # 3. get Vertex Cache Format - vcformat = pre_create_data.get("vcformat") - - # 3. get Valid Frame Range - trange = pre_create_data.get("trange") - - # parms dictionary - parms = { - "startnode" : selection, - "sopoutput": output_path, - "vcformat" : vcformat, - "trange" : trange - } + # get parms + parms = self.get_parms(subset_name, pre_create_data) # set parms instance_node.setParms(parms) # Lock any parameters in this list - to_lock = [] + to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) # Overrides HoudiniCreator.get_network_categories() @@ -100,6 +82,32 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): return attrs + [vcformat, trange] + def get_parms(self, subset_name, pre_create_data): + """Get parameters values for this specific node.""" + + # 1. get output path + output_path = hou.text.expandString( + "$HIP/pyblish/{}.fbx".format(subset_name)) + + # 2. get selection + selection = self.get_selection() + + # 3. get Vertex Cache Format + vcformat = pre_create_data.get("vcformat") + + # 4. get Valid Frame Range + trange = pre_create_data.get("trange") + + # parms dictionary + parms = { + "startnode" : selection, + "sopoutput": output_path, + "vcformat" : vcformat, + "trange" : trange + } + + return parms + def get_selection(self): """Selection Logic. From b1ac707a68335990970481885f7b3ac6bcbb87c5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 7 Aug 2023 22:11:01 +0300 Subject: [PATCH 044/175] update plugins for better demonstration --- .../houdini/plugins/create/create_fbx.py | 7 ++- .../hosts/houdini/plugins/load/load_fbx.py | 8 ++-- .../plugins/publish/collect_fbx_type.py | 22 ++++----- .../houdini/plugins/publish/extract_fbx.py | 48 +++++++++++++++---- .../publish/validate_fbx_hierarchy_path.py | 10 ++-- 5 files changed, 66 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 9e1dbea2f4..2bfdb3e729 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -21,12 +21,15 @@ import hou class CreateFilmboxFBX(plugin.HoudiniCreator): - """Filmbox FBX Driver""" + """Filmbox FBX Driver.""" + + # you should set identifier = "io.openpype.creators.houdini.filmboxfbx" label = "Filmbox FBX" family = "filmboxfbx" icon = "fa5s.cubes" + # optional to set default_variant = "Main" default_variants = ["Main", "Test"] @@ -36,7 +39,7 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # set node type instance_data.update({"node_type": "filmboxfbx"}) - # create instance (calls super create method) + # create instance (calls HoudiniCreator.create()) instance = super(CreateFilmboxFBX, self).create( subset_name, instance_data, diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 3f66c8e88f..993b57ad21 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -22,9 +22,11 @@ class FbxLoader(load.LoaderPlugin): label = "Load FBX" families = ["filmboxfbx", "fbx"] representations = ["fbx"] - order = -10 # you can use this by default. - icon = "code-fork" # you can use this by default. - color = "orange" # you can use this by default. + + # Usually you will use these value as default + order = -10 + icon = "code-fork" + color = "orange" def load(self, context, name=None, namespace=None, data=None): diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index f1665b27f0..794d8bd6e7 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -1,10 +1,11 @@ """Collector for filmboxfbx types. -A Collector can act as a preprocessor for the validation stage. +Collectors act as a pre process for the validation stage. It is used mainly to update instance.data P.S. - There are some collectors that run by default for all types. + There are some collectors that run by default + for all types. This plugin is part of publish process guide. """ @@ -14,18 +15,21 @@ import pyblish.api class CollectFilmboxfbxType(pyblish.api.InstancePlugin): """Collect data type for filmboxfbx instance.""" - # Usually you will use this value as default - order = pyblish.api.CollectorOrder hosts = ["houdini"] families = ["filmboxfbx"] label = "Collect type of filmboxfbx" + # Usually you will use this value as default + order = pyblish.api.CollectorOrder + # overrides InstancePlugin.process() def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.filmboxfbx": # noqa: E501 # such a condition can be used to differentiate between - # instances by identifier even if they have the same type. + # instances by identifier becuase sometimes instances + # may have the same family but different identifier + # e.g. bgeo and alembic pass # Update instance.data with ouptut_node @@ -36,15 +40,11 @@ class CollectFilmboxfbxType(pyblish.api.InstancePlugin): # Disclaimer : As a convntin we use collect_output_node.py # to Update instance.data with ouptut_node of different types - # however, we use this collector instead for demonstration + # however, this collector is used for demonstration def get_output_node(self, instance): - """Getting output_node Logic. - - It's moved here so that it become easier to focus on - process method. - """ + """Getting output_node Logic.""" import hou diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 6a4b541c33..102b075838 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -1,3 +1,11 @@ +"""Extract FilmBox FBX. + +Extractors are used to generate output and +update representation dictionary. + +This plugin is part of publish process guide. +""" + import os import pyblish.api @@ -10,31 +18,51 @@ import hou class ExtractRedshiftProxy(publish.Extractor): - # Usually you will use this value as default - order = pyblish.api.ExtractorOrder + 0.1 label = "Extract FilmBox FBX" families = ["filmboxfbx"] hosts = ["houdini"] + # Usually you will use this value as default + order = pyblish.api.ExtractorOrder + 0.1 + # overrides Extractor.process() def process(self, instance): + # get rop node 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("sopoutput") - staging_dir = os.path.normpath(os.path.dirname(output)) + # render rop + render_rop(ropnode) + + # get required data + file_name, staging_dir = self.get_paths_data(ropnode) + representation = self.get_representation(instance, + file_name, + staging_dir) + + # 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) + + def get_paths_data(self, ropnode): + # Get the filename from the filename parameter + output = ropnode.evalParm("sopoutput") + + staging_dir = os.path.normpath(os.path.dirname(output)) + file_name = os.path.basename(output) self.log.info("Writing FBX '%s' to '%s'" % (file_name, staging_dir)) - render_rop(ropnode) + return file_name, staging_dir - if "representations" not in instance.data: - instance.data["representations"] = [] + def get_representation(self, instance, + file_name, staging_dir): representation = { "name": "fbx", @@ -48,4 +76,4 @@ class ExtractRedshiftProxy(publish.Extractor): representation["frameStart"] = instance.data["frameStart"] representation["frameEnd"] = instance.data["frameEnd"] - instance.data["representations"].append(representation) + return representation diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index 6a2bda1bca..e98e562fe8 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- """Validate path attribute for all primitives. +Validators are used to verify the work of artists, +by running some checks which automates the approval process. + It's almost the same as 'validate_primitive_hierarchy_paths.py' however this one includes extra comments for demonstration. @@ -44,12 +47,13 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): by default. """ - # Usually you will use this value as default - order = ValidateContentsOrder + 0.1 families = ["filmboxfbx"] hosts = ["houdini"] label = "Validate FBX Hierarchy Path" + # Usually you will use this value as default + order = ValidateContentsOrder + 0.1 + # Validation can have as many actions as you want # all of these actions are defined in a seperate place # unlike the repair action @@ -69,7 +73,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): # This method was named get_invalid as a convention # it's also used by SelectInvalidAction to select - # the returned node + # the returned nodes @classmethod def get_invalid(cls, instance): From f41ad17b7bbf962baf209adcce243372ab7948bd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 7 Aug 2023 22:22:47 +0300 Subject: [PATCH 045/175] update comments --- openpype/hosts/houdini/plugins/load/load_fbx.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 993b57ad21..5294f5248d 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -30,14 +30,19 @@ class FbxLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): + # get file path file_path = self.get_file_path(context) + # get necessary data namespace, node_name = self.get_node_name(context, name, namespace) + # create load tree nodes = self.create_load_node_tree(file_path, node_name, name) self[:] = nodes + # Call containerise function which does some + # automations for you containerised_nodes = self.get_containerised_nodes( nodes, context, node_name, namespace ) @@ -96,7 +101,7 @@ class FbxLoader(load.LoaderPlugin): return namespace, node_name def create_load_node_tree(self, file_path, node_name, subset_name): - """Create Load node network. + """Create Load network. you can start building your tree at any obj level. it'll be much easier to build it in the root obj level. @@ -148,6 +153,12 @@ class FbxLoader(load.LoaderPlugin): return nodes def get_containerised_nodes(self, nodes, context, node_name, namespace): + """Call containerise function. + + It does some automations that you don't have to worry about, e.g. + 1. It moves created nodes to the AVALON_CONTAINERS subnetwork + 2. Add extra parameters + """ containerised_nodes = pipeline.containerise( node_name, namespace, From 58d2bcad88ca7ccd69a04fa6ec1e85dd693ac462 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 8 Aug 2023 12:07:57 +0200 Subject: [PATCH 046/175] Extract active view as thumbnail --- .../publish/extract_active_view_thumbnail.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py diff --git a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py new file mode 100644 index 0000000000..1bf010896a --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py @@ -0,0 +1,51 @@ +import maya.api.OpenMaya as om +import maya.api.OpenMayaUI as omui + +import pyblish.api +import tempfile + + +class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): + """Set instance thumbnail to a screengrab of current active viewport. + + This makes it so that if an instance does not have a thumbnail set yet that + it will get a thumbnail of the currently active view at the time of + publishing as a fallback. + + """ + order = pyblish.api.ExtractorOrder + 0.49 + label = "Active View Thumbnail" + families = ["workfile"] + hosts = ["maya"] + + def process(self, instance): + thumbnail = instance.data.get("thumbnailPath") + if not thumbnail: + view_thumbnail = self.get_view_thumbnail(instance) + if not view_thumbnail: + return + + self.log.debug("Setting instance thumbnail path to: {}".format( + view_thumbnail + )) + instance.data["thumbnailPath"] = view_thumbnail + + def get_view_thumbnail(self, instance): + cache_key = "__maya_view_thumbnail" + context = instance.context + + if cache_key not in context.data: + # Generate only a single thumbnail, even for multiple instances + with tempfile.NamedTemporaryFile(suffix="_thumbnail.jpg", + delete=False) as f: + path = f.name + + view = omui.M3dView.active3dView() + image = om.MImage() + view.readColorBuffer(image, True) + image.writeToFile(path, "jpg") + self.log.debug("Generated thumbnail: {}".format(path)) + + context.data[cache_key] = path + return context.data[cache_key] + From 4049d9acb148e93f11849bc2395b904c0ff13dcf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 8 Aug 2023 12:12:14 +0200 Subject: [PATCH 047/175] Cosmetics - good doggy! --- .../hosts/maya/plugins/publish/extract_active_view_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py index 1bf010896a..cb039cbf51 100644 --- a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py @@ -48,4 +48,3 @@ class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): context.data[cache_key] = path return context.data[cache_key] - From 7b74d0b91ddaf0d1d8bda72a84d9cea87ba2f122 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Aug 2023 20:21:25 +0800 Subject: [PATCH 048/175] add ornatrix alembic loader --- openpype/hosts/max/plugins/load/load_model.py | 2 +- .../max/plugins/load/load_model_ornatrix.py | 74 +++++++++++++++++++ .../hosts/max/plugins/load/load_pointcache.py | 2 +- .../plugins/load/load_pointcache_ornatrix.py | 70 ++++++++++++++++++ 4 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/max/plugins/load/load_model_ornatrix.py create mode 100644 openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index cff82a593c..c149f939a2 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -9,7 +9,7 @@ class ModelAbcLoader(load.LoaderPlugin): """Loading model with the Alembic loader.""" families = ["model"] - label = "Load Model(Alembic)" + label = "Load Model(Alembic) with Max" representations = ["abc"] order = -10 icon = "code-fork" diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py new file mode 100644 index 0000000000..d92e92f63b --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_ornatrix.py @@ -0,0 +1,74 @@ +import os +from openpype.pipeline import load, get_representation_path +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ModelAbcLoader(load.LoaderPlugin): + """Loading model with the Ornatrix Alembic loader.""" + + families = ["model"] + label = "Load Model(Alembic) with Ornatrix" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.filepath_from_context(context)) + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = True + rt.AlembicImport.CustomAttributes = True + rt.AlembicImport.UVs = True + rt.AlembicImport.VertexColors = True + rt.importFile(file_path) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + abc_container = rt.Container(name=name) + for abc in scene_object_after: + abc.Parent = abc_container + + return containerise( + name, [abc_container], context, loader=self.__class__.__name__ + ) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node_name = container["instance_node"] + instance_name, _ = os.path.splitext(node_name) + container = rt.getNodeByName(instance_name) + for children in container.Children: + rt.Delete(children) + + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = True + rt.AlembicImport.CustomAttributes = True + rt.AlembicImport.UVs = True + rt.AlembicImport.VertexColors = True + rt.importFile(path) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + for scene_object in scene_object_after: + scene_object.Parent = container + + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 290503e053..e59ad09c9f 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -14,7 +14,7 @@ class AbcLoader(load.LoaderPlugin): """Alembic loader.""" families = ["camera", "animation", "pointcache"] - label = "Load Alembic" + label = "Load Alembic with Max" representations = ["abc"] order = -10 icon = "code-fork" diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py new file mode 100644 index 0000000000..0b0932da6a --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -0,0 +1,70 @@ +import os +from openpype.pipeline import load, get_representation_path +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ModelAbcLoader(load.LoaderPlugin): + """Ornatrix Alembic loader.""" + + families = ["camera", "animation", "pointcache"] + label = "Load Model(Alembic) with Ornatrix" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.filepath_from_context(context)) + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = True + rt.AlembicImport.CustomAttributes = True + rt.importFile(file_path) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + abc_container = rt.Container(name=name) + for abc in scene_object_after: + abc.Parent = abc_container + + return containerise( + name, [abc_container], context, loader=self.__class__.__name__ + ) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node_name = container["instance_node"] + instance_name, _ = os.path.splitext(node_name) + container = rt.getNodeByName(instance_name) + for children in container.Children: + rt.Delete(children) + + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = False + rt.AlembicImport.CustomAttributes = True + rt.importFile(path) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + for scene_object in scene_object_after: + scene_object.Parent = container + + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) From 8a175b06edb6b6ca8039ac6560c19541d923bd0f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Aug 2023 20:29:20 +0800 Subject: [PATCH 049/175] rename the label --- openpype/hosts/max/plugins/load/load_model.py | 2 +- openpype/hosts/max/plugins/load/load_model_ornatrix.py | 2 +- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index c149f939a2..2f402efef8 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -9,7 +9,7 @@ class ModelAbcLoader(load.LoaderPlugin): """Loading model with the Alembic loader.""" families = ["model"] - label = "Load Model(Alembic) with Max" + label = "Load Model with Alembic" representations = ["abc"] order = -10 icon = "code-fork" diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py index d92e92f63b..8dc34278f5 100644 --- a/openpype/hosts/max/plugins/load/load_model_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_model_ornatrix.py @@ -8,7 +8,7 @@ class ModelAbcLoader(load.LoaderPlugin): """Loading model with the Ornatrix Alembic loader.""" families = ["model"] - label = "Load Model(Alembic) with Ornatrix" + label = "Load Model with Ornatrix Alembic" representations = ["abc"] order = -10 icon = "code-fork" diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 0b0932da6a..663c64bc0a 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -8,7 +8,7 @@ class ModelAbcLoader(load.LoaderPlugin): """Ornatrix Alembic loader.""" families = ["camera", "animation", "pointcache"] - label = "Load Model(Alembic) with Ornatrix" + label = "Load Alembic with Ornatrix" representations = ["abc"] order = -10 icon = "code-fork" From 938185c20835b545fa5c3b4b3e7673264509acf5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Aug 2023 20:32:39 +0800 Subject: [PATCH 050/175] rename the class name --- openpype/hosts/max/plugins/load/load_model_ornatrix.py | 2 +- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py index 8dc34278f5..424820e6e6 100644 --- a/openpype/hosts/max/plugins/load/load_model_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_model_ornatrix.py @@ -4,7 +4,7 @@ from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib -class ModelAbcLoader(load.LoaderPlugin): +class ModelOxAbcLoader(load.LoaderPlugin): """Loading model with the Ornatrix Alembic loader.""" families = ["model"] diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 663c64bc0a..0d04dbdef3 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -4,7 +4,7 @@ from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib -class ModelAbcLoader(load.LoaderPlugin): +class OxAbcLoader(load.LoaderPlugin): """Ornatrix Alembic loader.""" families = ["camera", "animation", "pointcache"] From cb48a2268c996ade66744540c2019b3eb8d06e98 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Aug 2023 00:19:52 +0800 Subject: [PATCH 051/175] add the using alembic import back to the importfile --- openpype/hosts/max/plugins/load/load_model.py | 3 +- .../max/plugins/load/load_model_ornatrix.py | 74 ------------------- .../hosts/max/plugins/load/load_pointcache.py | 3 +- .../plugins/load/load_pointcache_ornatrix.py | 70 ------------------ 4 files changed, 4 insertions(+), 146 deletions(-) delete mode 100644 openpype/hosts/max/plugins/load/load_model_ornatrix.py delete mode 100644 openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 2f402efef8..288fc58454 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -30,7 +30,8 @@ class ModelAbcLoader(load.LoaderPlugin): rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True - rt.importFile(file_path, rt.name("noPrompt")) + rt.importFile( + file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py deleted file mode 100644 index 424820e6e6..0000000000 --- a/openpype/hosts/max/plugins/load/load_model_ornatrix.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise -from openpype.hosts.max.api import lib - - -class ModelOxAbcLoader(load.LoaderPlugin): - """Loading model with the Ornatrix Alembic loader.""" - - families = ["model"] - label = "Load Model with Ornatrix Alembic" - representations = ["abc"] - order = -10 - icon = "code-fork" - color = "orange" - - def load(self, context, name=None, namespace=None, data=None): - from pymxs import runtime as rt - - file_path = os.path.normpath(self.filepath_from_context(context)) - scene_object_before = [obj for obj in rt.rootNode.Children] - rt.AlembicImport.ImportToRoot = True - rt.AlembicImport.CustomAttributes = True - rt.AlembicImport.UVs = True - rt.AlembicImport.VertexColors = True - rt.importFile(file_path) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - - abc_container = rt.Container(name=name) - for abc in scene_object_after: - abc.Parent = abc_container - - return containerise( - name, [abc_container], context, loader=self.__class__.__name__ - ) - - def update(self, container, representation): - from pymxs import runtime as rt - - path = get_representation_path(representation) - node_name = container["instance_node"] - instance_name, _ = os.path.splitext(node_name) - container = rt.getNodeByName(instance_name) - for children in container.Children: - rt.Delete(children) - - scene_object_before = [obj for obj in rt.rootNode.Children] - rt.AlembicImport.ImportToRoot = True - rt.AlembicImport.CustomAttributes = True - rt.AlembicImport.UVs = True - rt.AlembicImport.VertexColors = True - rt.importFile(path) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - - for scene_object in scene_object_after: - scene_object.Parent = container - - lib.imprint( - container["instance_node"], - {"representation": str(representation["_id"])}, - ) - - def switch(self, container, representation): - self.update(container, representation) - - def remove(self, container): - from pymxs import runtime as rt - - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index e59ad09c9f..24bcf58582 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -33,7 +33,8 @@ class AbcLoader(load.LoaderPlugin): } rt.AlembicImport.ImportToRoot = False - rt.importFile(file_path, rt.name("noPrompt")) + rt.importFile( + file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py deleted file mode 100644 index 0d04dbdef3..0000000000 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise -from openpype.hosts.max.api import lib - - -class OxAbcLoader(load.LoaderPlugin): - """Ornatrix Alembic loader.""" - - families = ["camera", "animation", "pointcache"] - label = "Load Alembic with Ornatrix" - representations = ["abc"] - order = -10 - icon = "code-fork" - color = "orange" - - def load(self, context, name=None, namespace=None, data=None): - from pymxs import runtime as rt - - file_path = os.path.normpath(self.filepath_from_context(context)) - scene_object_before = [obj for obj in rt.rootNode.Children] - rt.AlembicImport.ImportToRoot = True - rt.AlembicImport.CustomAttributes = True - rt.importFile(file_path) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - - abc_container = rt.Container(name=name) - for abc in scene_object_after: - abc.Parent = abc_container - - return containerise( - name, [abc_container], context, loader=self.__class__.__name__ - ) - - def update(self, container, representation): - from pymxs import runtime as rt - - path = get_representation_path(representation) - node_name = container["instance_node"] - instance_name, _ = os.path.splitext(node_name) - container = rt.getNodeByName(instance_name) - for children in container.Children: - rt.Delete(children) - - scene_object_before = [obj for obj in rt.rootNode.Children] - rt.AlembicImport.ImportToRoot = False - rt.AlembicImport.CustomAttributes = True - rt.importFile(path) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - - for scene_object in scene_object_after: - scene_object.Parent = container - - lib.imprint( - container["instance_node"], - {"representation": str(representation["_id"])}, - ) - - def switch(self, container, representation): - self.update(container, representation) - - def remove(self, container): - from pymxs import runtime as rt - - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) From 5cb43a141476fbba18af9a36df747d6082c31036 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Aug 2023 00:30:06 +0800 Subject: [PATCH 052/175] restore the label name --- openpype/hosts/max/plugins/load/load_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 24bcf58582..f250377cfd 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -14,7 +14,7 @@ class AbcLoader(load.LoaderPlugin): """Alembic loader.""" families = ["camera", "animation", "pointcache"] - label = "Load Alembic with Max" + label = "Load Alembic" representations = ["abc"] order = -10 icon = "code-fork" From 0a69439f2ef257e9800eda5e88d925b258f2ee25 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Aug 2023 01:00:34 +0800 Subject: [PATCH 053/175] supports loading ornatrix alembic --- .../max/plugins/load/load_model_ornatrix.py | 74 +++++++++++++++++++ .../plugins/load/load_pointcache_ornatrix.py | 70 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 openpype/hosts/max/plugins/load/load_model_ornatrix.py create mode 100644 openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py new file mode 100644 index 0000000000..92773ad567 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_ornatrix.py @@ -0,0 +1,74 @@ +import os +from openpype.pipeline import load, get_representation_path +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class OxModelAbcLoader(load.LoaderPlugin): + """Loading model with the Ornatrix Alembic loader.""" + + families = ["model"] + label = "Load Model with Ornatrix Alembic" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.filepath_from_context(context)) + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = True + rt.AlembicImport.CustomAttributes = True + rt.AlembicImport.UVs = True + rt.AlembicImport.VertexColors = True + rt.importFile(file_path, rt.name("noPrompt")) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + abc_container = rt.Container(name=name) + for abc in scene_object_after: + abc.Parent = abc_container + + return containerise( + name, [abc_container], context, loader=self.__class__.__name__ + ) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node_name = container["instance_node"] + instance_name, _ = os.path.splitext(node_name) + container = rt.getNodeByName(instance_name) + for children in container.Children: + rt.Delete(children) + + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = True + rt.AlembicImport.CustomAttributes = True + rt.AlembicImport.UVs = True + rt.AlembicImport.VertexColors = True + rt.importFile(path) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + for scene_object in scene_object_after: + scene_object.Parent = container + + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py new file mode 100644 index 0000000000..cd08e9a2ff --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -0,0 +1,70 @@ +import os +from openpype.pipeline import load, get_representation_path +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class OxAbcLoader(load.LoaderPlugin): + """Ornatrix Alembic loader.""" + + families = ["camera", "animation", "pointcache"] + label = "Load Alembic with Ornatrix" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.filepath_from_context(context)) + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = True + rt.AlembicImport.CustomAttributes = True + rt.importFile(file_path, rt.name("noPrompt")) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + abc_container = rt.Container(name=name) + for abc in scene_object_after: + abc.Parent = abc_container + + return containerise( + name, [abc_container], context, loader=self.__class__.__name__ + ) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node_name = container["instance_node"] + instance_name, _ = os.path.splitext(node_name) + container = rt.getNodeByName(instance_name) + for children in container.Children: + rt.Delete(children) + + scene_object_before = [obj for obj in rt.rootNode.Children] + rt.AlembicImport.ImportToRoot = False + rt.AlembicImport.CustomAttributes = True + rt.importFile(path) + scene_object_after = [obj for obj in rt.rootNode.Children] + for scene_object in scene_object_before: + scene_object_after = scene_object_after.remove(scene_object) + + for scene_object in scene_object_after: + scene_object.Parent = container + + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) From 17904600a00dbb64875ce06fd07d3d6d220ec8ca Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Aug 2023 14:56:23 +0800 Subject: [PATCH 054/175] add using ornatrix alembic importer into the ornatrix alembic loaders --- openpype/hosts/max/plugins/load/load_model_ornatrix.py | 4 +++- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py index 92773ad567..38c081b91c 100644 --- a/openpype/hosts/max/plugins/load/load_model_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_model_ornatrix.py @@ -23,7 +23,9 @@ class OxModelAbcLoader(load.LoaderPlugin): rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True - rt.importFile(file_path, rt.name("noPrompt")) + rt.importFile( + file_path, rt.name("noPrompt"), + using=rt.Ornatrix_Alembic_Importer) scene_object_after = [obj for obj in rt.rootNode.Children] for scene_object in scene_object_before: scene_object_after = scene_object_after.remove(scene_object) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index cd08e9a2ff..65a8273ab5 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -21,7 +21,9 @@ class OxAbcLoader(load.LoaderPlugin): scene_object_before = [obj for obj in rt.rootNode.Children] rt.AlembicImport.ImportToRoot = True rt.AlembicImport.CustomAttributes = True - rt.importFile(file_path, rt.name("noPrompt")) + rt.importFile( + file_path, rt.name("noPrompt"), + using=rt.Ornatrix_Alembic_Importer) scene_object_after = [obj for obj in rt.rootNode.Children] for scene_object in scene_object_before: scene_object_after = scene_object_after.remove(scene_object) From a3c761c0683c25ab5f4a901d06a3f955ffcd8d10 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Aug 2023 15:36:59 +0800 Subject: [PATCH 055/175] add using ornatrix alembic importer into the ornatrix alembic loaders when updating version --- openpype/hosts/max/plugins/load/load_model_ornatrix.py | 4 +++- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py index 38c081b91c..07633ec55d 100644 --- a/openpype/hosts/max/plugins/load/load_model_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_model_ornatrix.py @@ -53,7 +53,9 @@ class OxModelAbcLoader(load.LoaderPlugin): rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True - rt.importFile(path) + rt.importFile( + path, rt.name("noPrompt"), + using=rt.Ornatrix_Alembic_Importer) scene_object_after = [obj for obj in rt.rootNode.Children] for scene_object in scene_object_before: scene_object_after = scene_object_after.remove(scene_object) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 65a8273ab5..f783583ff1 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -49,7 +49,9 @@ class OxAbcLoader(load.LoaderPlugin): scene_object_before = [obj for obj in rt.rootNode.Children] rt.AlembicImport.ImportToRoot = False rt.AlembicImport.CustomAttributes = True - rt.importFile(path) + rt.importFile( + path, rt.name("noPrompt"), + using=rt.Ornatrix_Alembic_Importer) scene_object_after = [obj for obj in rt.rootNode.Children] for scene_object in scene_object_before: scene_object_after = scene_object_after.remove(scene_object) From f79f0c44da2e486887f0078c7e3d4dcfa2d4e007 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 11 Aug 2023 12:27:09 +0200 Subject: [PATCH 056/175] Add to cleanup files Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/maya/plugins/publish/extract_active_view_thumbnail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py index cb039cbf51..b4e62f8acc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py @@ -46,5 +46,6 @@ class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): image.writeToFile(path, "jpg") self.log.debug("Generated thumbnail: {}".format(path)) + context.data["cleanupFullPaths"].append(path) context.data[cache_key] = path return context.data[cache_key] From ef58284bceb6bff2a6ee965bc456600d821e80a8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 22 Aug 2023 11:30:45 +0100 Subject: [PATCH 057/175] Respect persistent dir on Deadline. --- openpype/plugins/publish/collect_rendered_files.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 4b95d8ac44..dc54e296e1 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -103,13 +103,16 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # stash render job id for later validation instance.data["render_job_id"] = data.get("job").get("_id") - + staging_dir_persistent = instance.data.get( + "stagingDir_persistent", False + ) representations = [] for repre_data in instance_data.get("representations") or []: self._fill_staging_dir(repre_data, anatomy) representations.append(repre_data) - add_repre_files_for_cleanup(instance, repre_data) + if not staging_dir_persistent: + add_repre_files_for_cleanup(instance, repre_data) instance.data["representations"] = representations @@ -124,7 +127,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): self.log.info( f"Adding audio to instance: {instance.data['audio']}") - return instance.data.get("stagingDir_persistent", False) + return staging_dir_persistent def process(self, context): self._context = context From 77d18d0b060d009a80102f00a3f76b31f488bd03 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 22 Aug 2023 23:20:33 +0300 Subject: [PATCH 058/175] make hound happy --- .../houdini/plugins/create/create_fbx.py | 32 +++++++++---------- .../hosts/houdini/plugins/load/load_fbx.py | 10 +++--- .../plugins/publish/collect_fbx_type.py | 1 - .../publish/validate_fbx_hierarchy_path.py | 1 + 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 2bfdb3e729..1ff63ab2c4 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -69,19 +69,19 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super().get_pre_create_attr_defs() vcformat = EnumDef("vcformat", - items={ - 0: "Maya Compatible (MC)", - 1: "3DS MAX Compatible (PC2)" - }, - default=0, - label="Vertex Cache Format") + items={ + 0: "Maya Compatible (MC)", + 1: "3DS MAX Compatible (PC2)" + }, + default=0, + label="Vertex Cache Format") trange = EnumDef("trange", - items={ - 0: "Render Current Frame", - 1: "Render Frame Range" - }, - default=0, - label="Valid Frame Range") + items={ + 0: "Render Current Frame", + 1: "Render Frame Range" + }, + default=0, + label="Valid Frame Range") return attrs + [vcformat, trange] @@ -103,10 +103,10 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # parms dictionary parms = { - "startnode" : selection, + "startnode": selection, "sopoutput": output_path, - "vcformat" : vcformat, - "trange" : trange + "vcformat": vcformat, + "trange": trange } return parms @@ -139,7 +139,7 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): ) # Allow object level paths to Geometry nodes (e.g. /obj/geo1) - # but do not allow other object level nodes types like cameras, etc. + # but do not allow other object level nodes types like cameras. elif isinstance(selected_node, hou.ObjNode) and \ selected_node.type().name() in ["geo"]: diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 5294f5248d..34f75e1485 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -126,19 +126,19 @@ class FbxLoader(load.LoaderPlugin): file_node.destroy() # Create a new file node - file_node = parent_node.createNode("file", node_name= node_name) - file_node.setParms({"file":file_path}) + file_node = parent_node.createNode("file", node_name=node_name) + file_node.setParms({"file": file_path}) # Create attribute delete attribdelete_name = "attribdelete_{}".format(subset_name) attribdelete = parent_node.createNode("attribdelete", - node_name= attribdelete_name) - attribdelete.setParms({"ptdel":"fbx_*"}) + node_name=attribdelete_name) + attribdelete.setParms({"ptdel": "fbx_*"}) attribdelete.setInput(0, file_node) # Create a Null node null_name = "OUT_{}".format(subset_name) - null = parent_node.createNode("null", node_name= null_name) + null = parent_node.createNode("null", node_name=null_name) null.setInput(0, attribdelete) # Ensure display flag is on the file_node input node and not on the OUT diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index 794d8bd6e7..3ee2541f72 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -42,7 +42,6 @@ class CollectFilmboxfbxType(pyblish.api.InstancePlugin): # to Update instance.data with ouptut_node of different types # however, this collector is used for demonstration - def get_output_node(self, instance): """Getting output_node Logic.""" diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index e98e562fe8..9208a16bd1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -29,6 +29,7 @@ from openpype.hosts.houdini.api.action import ( import hou + # Each validation can have a single repair action # which calls the repair method class AddDefaultPathAction(RepairAction): From 0fe908a7cc4e2130e36f52820f9064ba48621d59 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 22 Aug 2023 23:23:56 +0300 Subject: [PATCH 059/175] make hound happy again --- openpype/hosts/houdini/plugins/create/create_fbx.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index 1ff63ab2c4..a92a4a5a24 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -70,11 +70,11 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): attrs = super().get_pre_create_attr_defs() vcformat = EnumDef("vcformat", items={ - 0: "Maya Compatible (MC)", - 1: "3DS MAX Compatible (PC2)" - }, - default=0, - label="Vertex Cache Format") + 0: "Maya Compatible (MC)", + 1: "3DS MAX Compatible (PC2)" + }, + default=0, + label="Vertex Cache Format") trange = EnumDef("trange", items={ 0: "Render Current Frame", From 4bfde17a420cc3686734d049728e7eaf66b51538 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 22 Aug 2023 23:26:21 +0300 Subject: [PATCH 060/175] make hound happy --- openpype/hosts/houdini/plugins/create/create_fbx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index a92a4a5a24..e26fd660ba 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -70,8 +70,8 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): attrs = super().get_pre_create_attr_defs() vcformat = EnumDef("vcformat", items={ - 0: "Maya Compatible (MC)", - 1: "3DS MAX Compatible (PC2)" + 0: "Maya Compatible (MC)", + 1: "3DS MAX Compatible (PC2)" }, default=0, label="Vertex Cache Format") From 433160e75ae70c2a17d5460c6815588bb17c58b0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 22 Aug 2023 23:29:02 +0300 Subject: [PATCH 061/175] make hound happy again --- openpype/hosts/houdini/plugins/create/create_fbx.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index e26fd660ba..ed95daafca 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -72,16 +72,16 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): items={ 0: "Maya Compatible (MC)", 1: "3DS MAX Compatible (PC2)" - }, + }, default=0, label="Vertex Cache Format") trange = EnumDef("trange", items={ 0: "Render Current Frame", 1: "Render Frame Range" - }, - default=0, - label="Valid Frame Range") + }, + default=0, + label="Valid Frame Range") return attrs + [vcformat, trange] From 58408817999f053118a5c14cff8058c909a512cf Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 22 Aug 2023 23:30:00 +0300 Subject: [PATCH 062/175] make hound happy --- openpype/hosts/houdini/plugins/create/create_fbx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index ed95daafca..cac90f1e87 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -72,7 +72,7 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): items={ 0: "Maya Compatible (MC)", 1: "3DS MAX Compatible (PC2)" - }, + }, default=0, label="Vertex Cache Format") trange = EnumDef("trange", From ee8a2b1aa37567ccfc77fe3f31562eca0622f294 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 23 Aug 2023 19:10:40 +0300 Subject: [PATCH 063/175] update geo validation --- .../publish/validate_fbx_hierarchy_path.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index 9208a16bd1..e060756801 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -17,7 +17,10 @@ This plugin is part of publish process guide. """ import pyblish.api -from openpype.pipeline import PublishValidationError +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, @@ -37,7 +40,8 @@ class AddDefaultPathAction(RepairAction): icon = "mdi.pencil-plus-outline" -class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): +class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate all primitives build hierarchy from attribute when enabled. @@ -50,7 +54,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): families = ["filmboxfbx"] hosts = ["houdini"] - label = "Validate FBX Hierarchy Path" + label = "Validate Prims Hierarchy Path (FBX)" # Usually you will use this value as default order = ValidateContentsOrder + 0.1 @@ -61,6 +65,10 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): actions = [SelectInvalidAction, AddDefaultPathAction, SelectROPAction] + # 'OptionalPyblishPluginMixin' where logic for 'optional' is implemented. + # It requires updating project settings + optional = True + # overrides InstancePlugin.process() def process(self, instance): invalid = self.get_invalid(instance) @@ -108,19 +116,21 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): cls.log.debug("Checking for attribute: %s", path_attr) - if not hasattr(output_node, "geometry"): - # In the case someone has explicitly set an Object - # node instead of a SOP node in Geometry context - # then for now we ignore - this allows us to also - # export object transforms. - cls.log.warning("No geometry output node found," + # Get frame + frame = hou.intFrame() + trange = rop_node.parm("trange").eval() + if trange: + frame = int(hou.playbar.frameRange()[0]) + + frame = instance.data.get("frameStart", frame) + + # Get Geo at that frame + geo = output_node.geometryAtFrame(frame) + if not geo: + cls.log.warning("No geometry found," " skipping check..") return - # Check if the primitive attribute exists - frame = instance.data.get("frameStart", 0) - geo = output_node.geometryAtFrame(frame) - # If there are no primitives on the current frame then # we can't check whether the path names are correct. # So we'll just issue a warning that the check can't From 1d0c78f044a4f0d5dd6092968ff543f09d5c5987 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 23 Aug 2023 19:11:04 +0300 Subject: [PATCH 064/175] update label --- .../plugins/publish/validate_primitive_hierarchy_paths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 471fa5b6d1..930978ef16 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -26,7 +26,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): order = ValidateContentsOrder + 0.1 families = ["abc"] hosts = ["houdini"] - label = "Validate Prims Hierarchy Path" + label = "Validate Prims Hierarchy Path (ABC)" actions = [AddDefaultPathAction] def process(self, instance): From 4ea1f2b586835f6a6cad97633d5149fc614ce991 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 23 Aug 2023 19:11:47 +0300 Subject: [PATCH 065/175] include fbx validator in settings --- openpype/settings/defaults/project_settings/houdini.json | 5 +++++ .../projects_schema/schemas/schema_houdini_publish.json | 6 +++++- server_addon/houdini/server/settings/publish_plugins.py | 8 ++++++++ server_addon/houdini/server/version.py | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 9d047c28bd..2295422202 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -97,6 +97,11 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateFBXPrimitiveHierarchyPaths": { + "enabled": true, + "optional": true, + "active": true } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index aa6eaf5164..d58b36eff1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -43,8 +43,12 @@ { "key": "ValidateContainers", "label": "ValidateContainers" + }, + { + "key": "ValidateFBXPrimitiveHierarchyPaths", + "label": "Validate Path Attribute for FBX" } ] } ] -} \ No newline at end of file +} diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 7d35d7e634..44ff00c318 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -133,6 +133,9 @@ class PublishPluginsModel(BaseSettingsModel): ValidateContainers: ValidateContainersModel = Field( default_factory=ValidateContainersModel, title="Validate Latest Containers.") + ValidateFBXPrimitiveHierarchyPaths: ValidateContainersModel = Field( + default_factory=ValidateContainersModel, + title="Validate Path Attribute for FBX.") DEFAULT_HOUDINI_PUBLISH_SETTINGS = { @@ -152,5 +155,10 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "enabled": True, "optional": True, "active": True + }, + "ValidateFBXPrimitiveHierarchyPaths": { + "enabled": True, + "optional": True, + "active": True } } diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From 96893f6ab0a56db976aa14709f2fbbd734b70f88 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 23 Aug 2023 19:56:29 +0300 Subject: [PATCH 066/175] update doc string --- openpype/hosts/houdini/plugins/create/create_fbx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_fbx.py index cac90f1e87..b45aef8fdf 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_fbx.py @@ -67,6 +67,8 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # Overrides HoudiniCreator.get_pre_create_attr_defs() def get_pre_create_attr_defs(self): + """Add settings for users. """ + attrs = super().get_pre_create_attr_defs() vcformat = EnumDef("vcformat", items={ From e181eeab93db55b7804f3b6f5055369e1a30ede7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 24 Aug 2023 16:02:18 +0300 Subject: [PATCH 067/175] convert filmboxfbx to UE static mesh --- ...ate_fbx.py => create_unreal_staticmesh.py} | 25 ++++++--------- .../hosts/houdini/plugins/load/load_fbx.py | 2 +- .../plugins/publish/collect_fbx_type.py | 10 +++--- .../houdini/plugins/publish/extract_fbx.py | 4 +-- .../publish/validate_fbx_hierarchy_path.py | 2 +- .../publish/validate_sop_output_node.py | 2 +- openpype/plugins/publish/integrate.py | 3 +- .../defaults/project_settings/houdini.json | 13 ++++++++ .../schemas/schema_houdini_create.json | 31 +++++++++++++++++++ .../server/settings/publish_plugins.py | 29 +++++++++++++++++ 10 files changed, 94 insertions(+), 27 deletions(-) rename openpype/hosts/houdini/plugins/create/{create_fbx.py => create_unreal_staticmesh.py} (89%) diff --git a/openpype/hosts/houdini/plugins/create/create_fbx.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py similarity index 89% rename from openpype/hosts/houdini/plugins/create/create_fbx.py rename to openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index b45aef8fdf..4543f14934 100644 --- a/openpype/hosts/houdini/plugins/create/create_fbx.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -20,17 +20,18 @@ from openpype.lib import EnumDef import hou -class CreateFilmboxFBX(plugin.HoudiniCreator): +class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): """Filmbox FBX Driver.""" # you should set - identifier = "io.openpype.creators.houdini.filmboxfbx" - label = "Filmbox FBX" - family = "filmboxfbx" + identifier = "io.openpype.creators.houdini.unrealstaticmesh" + label = "Unreal - Static Mesh" + family = "staticMesh" icon = "fa5s.cubes" # optional to set default_variant = "Main" + # 'default_variants' will be overriden by settings. default_variants = ["Main", "Test"] # Overrides HoudiniCreator.create() @@ -40,7 +41,7 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): instance_data.update({"node_type": "filmboxfbx"}) # create instance (calls HoudiniCreator.create()) - instance = super(CreateFilmboxFBX, self).create( + instance = super(HouCreateUnrealStaticMesh, self).create( subset_name, instance_data, pre_create_data) @@ -77,15 +78,8 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): }, default=0, label="Vertex Cache Format") - trange = EnumDef("trange", - items={ - 0: "Render Current Frame", - 1: "Render Frame Range" - }, - default=0, - label="Valid Frame Range") - return attrs + [vcformat, trange] + return attrs + [vcformat] def get_parms(self, subset_name, pre_create_data): """Get parameters values for this specific node.""" @@ -100,8 +94,9 @@ class CreateFilmboxFBX(plugin.HoudiniCreator): # 3. get Vertex Cache Format vcformat = pre_create_data.get("vcformat") - # 4. get Valid Frame Range - trange = pre_create_data.get("trange") + # 4. Valid Frame Range + # It should publish the current frame. + trange = 0 # parms dictionary parms = { diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 34f75e1485..d661f84eeb 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -20,7 +20,7 @@ class FbxLoader(load.LoaderPlugin): """Load fbx files to Houdini.""" label = "Load FBX" - families = ["filmboxfbx", "fbx"] + families = ["staticMesh", "fbx"] representations = ["fbx"] # Usually you will use these value as default diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index 3ee2541f72..4c83829c67 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -13,11 +13,11 @@ import pyblish.api class CollectFilmboxfbxType(pyblish.api.InstancePlugin): - """Collect data type for filmboxfbx instance.""" + """Collect data type for fbx instance.""" hosts = ["houdini"] - families = ["filmboxfbx"] - label = "Collect type of filmboxfbx" + families = ["fbx", "staticMesh"] + label = "Collect type of fbx" # Usually you will use this value as default order = pyblish.api.CollectorOrder @@ -25,12 +25,12 @@ class CollectFilmboxfbxType(pyblish.api.InstancePlugin): # overrides InstancePlugin.process() def process(self, instance): - if instance.data["creator_identifier"] == "io.openpype.creators.houdini.filmboxfbx": # noqa: E501 + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh": # noqa: E501 # such a condition can be used to differentiate between # instances by identifier becuase sometimes instances # may have the same family but different identifier # e.g. bgeo and alembic - pass + instance.data["families"] += ["fbx"] # Update instance.data with ouptut_node out_node = self.get_output_node(instance) diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 102b075838..8e45a554c0 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -18,8 +18,8 @@ import hou class ExtractRedshiftProxy(publish.Extractor): - label = "Extract FilmBox FBX" - families = ["filmboxfbx"] + label = "Extract FBX" + families = ["fbx"] hosts = ["houdini"] # Usually you will use this value as default diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index e060756801..01cb01f497 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -52,7 +52,7 @@ class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin, by default. """ - families = ["filmboxfbx"] + families = ["fbx"] hosts = ["houdini"] label = "Validate Prims Hierarchy Path (FBX)" diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index da9752505a..2b426d96dd 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -22,7 +22,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["pointcache", "vdbcache", "filmboxfbx"] + families = ["pointcache", "vdbcache", "fbx"] hosts = ["houdini"] label = "Validate Output Node" actions = [SelectROPAction, SelectInvalidAction] diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index ee4af1a0e0..be07cffe72 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -139,8 +139,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "simpleUnrealTexture", "online", "uasset", - "blendScene", - "filmboxfbx" + "blendScene" ] default_template_name = "publish" diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 2295422202..e19e71de17 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -19,6 +19,19 @@ ], "ext": ".ass" }, + "HouCreateUnrealStaticMesh": { + "enabled": true, + "default_variants": [ + "Main" + ], + "static_mesh_prefix": "S", + "collision_prefixes": [ + "UBX", + "UCP", + "USP", + "UCX" + ] + }, "CreateAlembicCamera": { "enabled": true, "default_variants": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 799bc0e81a..3d55bd834f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -39,6 +39,37 @@ ] }, + { + "type": "dict", + "collapsible": true, + "key": "HouCreateUnrealStaticMesh", + "label": "Create Unreal - Static Mesh", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default Variants", + "object_type": "text" + }, + { + "type": "text", + "key": "static_mesh_prefix", + "label": "Static Mesh Prefix" + }, + { + "type": "list", + "key": "collision_prefixes", + "label": "Collision Mesh Prefixes", + "object_type": "text" + } + ] + }, { "type": "schema_template", "name": "template_create_plugin", diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 44ff00c318..5ddfa07bc4 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -20,11 +20,27 @@ class CreateArnoldAssModel(BaseSettingsModel): ) ext: str = Field(Title="Extension") +class HouCreateUnrealStaticMeshModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + default_variants: list[str] = Field( + default_factory=list, + title="Default Products" + ) + static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") + collision_prefixes: list[str] = Field( + default_factory=list, + title="Collision Prefixes" + ) class CreatePluginsModel(BaseSettingsModel): CreateArnoldAss: CreateArnoldAssModel = Field( default_factory=CreateArnoldAssModel, title="Create Alembic Camera") + # "-" is not compatible in the new model + HouCreateUnrealStaticMesh: HouCreateUnrealStaticMeshModel = Field( + default_factory=HouCreateUnrealStaticMeshModel, + title="Create Unreal_Static Mesh" + ) CreateAlembicCamera: CreatorModel = Field( default_factory=CreatorModel, title="Create Alembic Camera") @@ -63,6 +79,19 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "default_variants": ["Main"], "ext": ".ass" }, + "HouCreateUnrealStaticMesh": { + "enabled": True, + "default_variants": [ + "Main" + ], + "static_mesh_prefix": "S", + "collision_prefixes": [ + "UBX", + "UCP", + "USP", + "UCX" + ] + }, "CreateAlembicCamera": { "enabled": True, "default_variants": ["Main"] From b6a81bd64b74c60e2b0e8e271f7f2704b6b80bf7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 24 Aug 2023 16:06:21 +0300 Subject: [PATCH 068/175] resolve hound conversations --- .../hosts/houdini/plugins/create/create_unreal_staticmesh.py | 3 ++- server_addon/houdini/server/settings/publish_plugins.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index 4543f14934..2a284f7979 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -154,7 +154,8 @@ class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): if not selection: self.log.debug( - "Selection isn't valid. 'Export' in filmboxfbx will be empty." + "Selection isn't valid. 'Export' in " + "filmboxfbx will be empty." ) else: self.log.debug( diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 5ddfa07bc4..aaa1d0ba1d 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -20,6 +20,7 @@ class CreateArnoldAssModel(BaseSettingsModel): ) ext: str = Field(Title="Extension") + class HouCreateUnrealStaticMeshModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") default_variants: list[str] = Field( @@ -32,6 +33,7 @@ class HouCreateUnrealStaticMeshModel(BaseSettingsModel): title="Collision Prefixes" ) + class CreatePluginsModel(BaseSettingsModel): CreateArnoldAss: CreateArnoldAssModel = Field( default_factory=CreateArnoldAssModel, From e1f2a77089f406dfa7ebb8bfc22102155f1a3f1b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 25 Aug 2023 22:36:38 +0300 Subject: [PATCH 069/175] Introduce houdini unreal static mesh --- .../create/create_unreal_staticmesh.py | 36 +++-- .../houdini/plugins/publish/extract_fbx.py | 2 +- .../publish/validate_fbx_hierarchy_path.py | 14 +- .../publish/validate_mesh_is_static.py | 133 ++++++++++++++++++ .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_create.json | 2 +- .../server/settings/publish_plugins.py | 8 +- 7 files changed, 169 insertions(+), 28 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index 2a284f7979..479392f231 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- -"""Creator plugin for creating fbx. +"""Creator plugin for creating Unreal Static Meshes. -It was made to pratice publish process. +Unreal Static Meshes will be published as FBX. Filmbox by default expects an ObjNode however, we set the sop node explictly to eleminate any confusion. -This creator by default will select +This will make Filmbox to ignore any object transformations! + +get_obj_output selects the output sop with mimimum idx or the node with render flag isntead. @@ -15,13 +17,13 @@ This plugin is part of publish process guide. """ from openpype.hosts.houdini.api import plugin -from openpype.lib import EnumDef +from openpype.lib import BoolDef, EnumDef import hou -class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): - """Filmbox FBX Driver.""" +class CreateUnrealStaticMesh(plugin.HoudiniCreator): + """Unreal Static Meshes with collisions. """ # you should set identifier = "io.openpype.creators.houdini.unrealstaticmesh" @@ -41,7 +43,7 @@ class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): instance_data.update({"node_type": "filmboxfbx"}) # create instance (calls HoudiniCreator.create()) - instance = super(HouCreateUnrealStaticMesh, self).create( + instance = super(CreateUnrealStaticMesh, self).create( subset_name, instance_data, pre_create_data) @@ -78,8 +80,15 @@ class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): }, default=0, label="Vertex Cache Format") + convert_units = BoolDef("convertunits", + tooltip="When on, the FBX is converted" + "from the current Houdini " + "system units to the native " + "FBX unit of centimeters.", + default=False, + label="Convert Units") - return attrs + [vcformat] + return attrs + [vcformat, convert_units] def get_parms(self, subset_name, pre_create_data): """Get parameters values for this specific node.""" @@ -94,15 +103,18 @@ class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): # 3. get Vertex Cache Format vcformat = pre_create_data.get("vcformat") - # 4. Valid Frame Range - # It should publish the current frame. - trange = 0 + # 4. get convert_units + convertunits = pre_create_data.get("convertunits") + + # 5. get Valid Frame Range + trange = 1 # parms dictionary parms = { "startnode": selection, "sopoutput": output_path, "vcformat": vcformat, + "convertunits": convertunits, "trange": trange } @@ -166,7 +178,7 @@ class HouCreateUnrealStaticMesh(plugin.HoudiniCreator): def get_obj_output(self, obj_node): """Find output node with the smallest 'outputidx' - or return tje node with the render flag instead. + or return the node with the render flag instead. """ outputs = obj_node.subnetOutputs() diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 8e45a554c0..c0e84c00c8 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -16,7 +16,7 @@ from openpype.hosts.houdini.api.lib import render_rop import hou -class ExtractRedshiftProxy(publish.Extractor): +class ExtractFBX(publish.Extractor): label = "Extract FBX" families = ["fbx"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py index 01cb01f497..be73ccd223 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py @@ -65,8 +65,8 @@ class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin, actions = [SelectInvalidAction, AddDefaultPathAction, SelectROPAction] - # 'OptionalPyblishPluginMixin' where logic for 'optional' is implemented. - # It requires updating project settings + # 'OptionalPyblishPluginMixin' adds the functionality to + # enable/disable plugins, It requires adding new settings. optional = True # overrides InstancePlugin.process() @@ -116,13 +116,9 @@ class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin, cls.log.debug("Checking for attribute: %s", path_attr) - # Get frame - frame = hou.intFrame() - trange = rop_node.parm("trange").eval() - if trange: - frame = int(hou.playbar.frameRange()[0]) - - frame = instance.data.get("frameStart", frame) + # Use current frame if "frameStart" doesn't exist + # This only happens when ""trange" is 0 + frame = instance.data.get("frameStart", hou.intFrame()) # Get Geo at that frame geo = output_node.geometryAtFrame(frame) diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py new file mode 100644 index 0000000000..fa6442d0d4 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""Validate mesh is static. + +This plugin is part of publish process guide. +""" + +import pyblish.api +from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import ( + ValidateContentsOrder, + RepairAction, +) +from openpype.hosts.houdini.api.action import ( + SelectInvalidAction, + SelectROPAction, +) + +import hou + + +# Each validation can have a single repair action +# which calls the repair method +class FreezeTimeAction(RepairAction): + label = "Freeze Time" + icon = "ei.pause-alt" + + +class ValidateMeshIsStatic(pyblish.api.InstancePlugin): + """Validate mesh is static. + + It checks if node is time dependant. + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Validate mesh is static" + + # Usually you will use this value as default + order = ValidateContentsOrder + 0.1 + + # Validation can have as many actions as you want + # all of these actions are defined in a seperate place + # unlike the repair action + actions = [FreezeTimeAction, SelectInvalidAction, + SelectROPAction] + + # overrides InstancePlugin.process() + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes), + title=self.label + ) + + # This method was named get_invalid as a convention + # it's also used by SelectInvalidAction to select + # the returned nodes + @classmethod + def get_invalid(cls, instance): + + output_node = instance.data.get("output_node") + if output_node.isTimeDependent(): + cls.log.info("Mesh is not static!") + return [output_node] + + + + # what repair action expects to find and call + @classmethod + def repair(cls, instance): + """Adds a time shift node. + + It should kill time dependency. + """ + + rop_node = hou.node(instance.data["instance_node"]) + # I'm doing so because an artist may change output node + # before clicking the button. + output_node = rop_node.parm("startnode").evalAsNode() + + if not output_node: + cls.log.debug( + "Action isn't performed, invalid SOP Path on %s", + rop_node + ) + return + + # This check to prevent the action from running multiple times. + # git_invalid only returns [output_node] when + # path attribute is the problem + if cls.get_invalid(instance) != [output_node]: + return + + + + time_shift = output_node.parent().createNode("timeshift", + "freeze_time") + time_shift.parm("frame").deleteAllKeyframes() + + frame = instance.data.get("frameStart", hou.intFrame()) + time_shift.parm("frame").set(frame) + + cls.log.debug( + "'%s' was created. It will kill time dependency." + , time_shift + ) + + time_shift.setGenericFlag(hou.nodeFlag.DisplayComment, True) + time_shift.setComment( + 'This node was created automatically by ' + '"Freeze Time" Action' + '\nFeel free to modify or replace it.' + ) + + if output_node.type().name() in ["null", "output"]: + # Connect before + time_shift.setFirstInput(output_node.input(0)) + time_shift.moveToGoodPosition() + output_node.setFirstInput(time_shift) + output_node.moveToGoodPosition() + else: + # Connect after + time_shift.setFirstInput(output_node) + rop_node.parm("startnode").set(time_shift.path()) + time_shift.moveToGoodPosition() + + cls.log.debug( + "SOP path on '%s' updated to new output node '%s'", + rop_node, time_shift + ) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index e19e71de17..c39eb717fd 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -19,7 +19,7 @@ ], "ext": ".ass" }, - "HouCreateUnrealStaticMesh": { + "CreateUnrealStaticMesh": { "enabled": true, "default_variants": [ "Main" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 3d55bd834f..b19761df91 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -42,7 +42,7 @@ { "type": "dict", "collapsible": true, - "key": "HouCreateUnrealStaticMesh", + "key": "CreateUnrealStaticMesh", "label": "Create Unreal - Static Mesh", "checkbox_key": "enabled", "children": [ diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index aaa1d0ba1d..1e5cd7f551 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -21,7 +21,7 @@ class CreateArnoldAssModel(BaseSettingsModel): ext: str = Field(Title="Extension") -class HouCreateUnrealStaticMeshModel(BaseSettingsModel): +class CreateUnrealStaticMeshModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") default_variants: list[str] = Field( default_factory=list, @@ -39,8 +39,8 @@ class CreatePluginsModel(BaseSettingsModel): default_factory=CreateArnoldAssModel, title="Create Alembic Camera") # "-" is not compatible in the new model - HouCreateUnrealStaticMesh: HouCreateUnrealStaticMeshModel = Field( - default_factory=HouCreateUnrealStaticMeshModel, + CreateUnrealStaticMesh: CreateUnrealStaticMeshModel = Field( + default_factory=CreateUnrealStaticMeshModel, title="Create Unreal_Static Mesh" ) CreateAlembicCamera: CreatorModel = Field( @@ -81,7 +81,7 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "default_variants": ["Main"], "ext": ".ass" }, - "HouCreateUnrealStaticMesh": { + "CreateUnrealStaticMesh": { "enabled": True, "default_variants": [ "Main" From 3d658bb3f23aa9bf50f0a46a62f810b01bc06139 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 25 Aug 2023 22:39:10 +0300 Subject: [PATCH 070/175] resolve hound conversations --- .../houdini/plugins/publish/validate_mesh_is_static.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py index fa6442d0d4..ac80fda537 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -66,8 +66,6 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin): cls.log.info("Mesh is not static!") return [output_node] - - # what repair action expects to find and call @classmethod def repair(cls, instance): @@ -94,8 +92,6 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin): if cls.get_invalid(instance) != [output_node]: return - - time_shift = output_node.parent().createNode("timeshift", "freeze_time") time_shift.parm("frame").deleteAllKeyframes() @@ -104,8 +100,8 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin): time_shift.parm("frame").set(frame) cls.log.debug( - "'%s' was created. It will kill time dependency." - , time_shift + "'%s' was created. It will kill time dependency.", + time_shift ) time_shift.setGenericFlag(hou.nodeFlag.DisplayComment, True) From c610a850b9e12023b2a6e5f92769b2a971b7b2d1 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Sat, 26 Aug 2023 01:23:20 +0300 Subject: [PATCH 071/175] dynamic subset name --- .../plugins/create/create_unreal_staticmesh.py | 14 ++++++++++++++ .../settings/defaults/project_settings/global.json | 3 ++- server_addon/core/server/settings/tools.py | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index 479392f231..179c81510a 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -90,6 +90,20 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): return attrs + [vcformat, convert_units] + # Overrides BaseCreator.get_dynamic_data() + def get_dynamic_data( + self, variant, task_name, asset_doc, project_name, host_name, instance + ): + """ + The default subset name templates for Unreal include {asset} and thus + we should pass that along as dynamic data. + """ + dynamic_data = super(CreateUnrealStaticMesh, self).get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + dynamic_data["asset"] = asset_doc["name"] + return dynamic_data + def get_parms(self, subset_name, pre_create_data): """Get parameters values for this specific node.""" diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 06a595d1c5..52ac745f6d 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -429,7 +429,8 @@ "staticMesh" ], "hosts": [ - "maya" + "maya", + "houdini" ], "task_types": [], "tasks": [], diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py index 7befc795e4..5dbe6ab215 100644 --- a/server_addon/core/server/settings/tools.py +++ b/server_addon/core/server/settings/tools.py @@ -370,7 +370,8 @@ DEFAULT_TOOLS_VALUES = { "staticMesh" ], "hosts": [ - "maya" + "maya", + "houdini" ], "task_types": [], "tasks": [], From 3a43806a5b30496c44b0adb469f1b1a52355b66d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Sat, 26 Aug 2023 01:23:52 +0300 Subject: [PATCH 072/175] dynamic subset name --- server_addon/core/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/core/server/version.py b/server_addon/core/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/core/server/version.py +++ b/server_addon/core/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From 1157971186c8b364afa03a6081246b8bca41abb9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 28 Aug 2023 18:55:49 +0800 Subject: [PATCH 073/175] ornatrix is a fur modifier so no loading model for ornatrix abc --- .../max/plugins/load/load_model_ornatrix.py | 78 ------------------- 1 file changed, 78 deletions(-) delete mode 100644 openpype/hosts/max/plugins/load/load_model_ornatrix.py diff --git a/openpype/hosts/max/plugins/load/load_model_ornatrix.py b/openpype/hosts/max/plugins/load/load_model_ornatrix.py deleted file mode 100644 index 07633ec55d..0000000000 --- a/openpype/hosts/max/plugins/load/load_model_ornatrix.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise -from openpype.hosts.max.api import lib - - -class OxModelAbcLoader(load.LoaderPlugin): - """Loading model with the Ornatrix Alembic loader.""" - - families = ["model"] - label = "Load Model with Ornatrix Alembic" - representations = ["abc"] - order = -10 - icon = "code-fork" - color = "orange" - - def load(self, context, name=None, namespace=None, data=None): - from pymxs import runtime as rt - - file_path = os.path.normpath(self.filepath_from_context(context)) - scene_object_before = [obj for obj in rt.rootNode.Children] - rt.AlembicImport.ImportToRoot = True - rt.AlembicImport.CustomAttributes = True - rt.AlembicImport.UVs = True - rt.AlembicImport.VertexColors = True - rt.importFile( - file_path, rt.name("noPrompt"), - using=rt.Ornatrix_Alembic_Importer) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - - abc_container = rt.Container(name=name) - for abc in scene_object_after: - abc.Parent = abc_container - - return containerise( - name, [abc_container], context, loader=self.__class__.__name__ - ) - - def update(self, container, representation): - from pymxs import runtime as rt - - path = get_representation_path(representation) - node_name = container["instance_node"] - instance_name, _ = os.path.splitext(node_name) - container = rt.getNodeByName(instance_name) - for children in container.Children: - rt.Delete(children) - - scene_object_before = [obj for obj in rt.rootNode.Children] - rt.AlembicImport.ImportToRoot = True - rt.AlembicImport.CustomAttributes = True - rt.AlembicImport.UVs = True - rt.AlembicImport.VertexColors = True - rt.importFile( - path, rt.name("noPrompt"), - using=rt.Ornatrix_Alembic_Importer) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - - for scene_object in scene_object_after: - scene_object.Parent = container - - lib.imprint( - container["instance_node"], - {"representation": str(representation["_id"])}, - ) - - def switch(self, container, representation): - self.update(container, representation) - - def remove(self, container): - from pymxs import runtime as rt - - node = rt.GetNodeByName(container["instance_node"]) - rt.Delete(node) From 749e8911f5f1a8ce00ce8e3d024004d3fb13c832 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 28 Aug 2023 23:20:43 +0800 Subject: [PATCH 074/175] make sure ornatrix loader would raise runtime error if the user doesn't have the plugin installed --- .../plugins/load/load_pointcache_ornatrix.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index f783583ff1..1dce9aaa65 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -2,7 +2,7 @@ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib - +from pymxs import runtime as rt class OxAbcLoader(load.LoaderPlugin): """Ornatrix Alembic loader.""" @@ -15,7 +15,10 @@ class OxAbcLoader(load.LoaderPlugin): color = "orange" def load(self, context, name=None, namespace=None, data=None): - from pymxs import runtime as rt + plugin_list = get_plugins() + if "ornatrix.dlo" not in plugin_list: + raise RuntimeError("Ornatrix plugin not " + "found/installed in Max yet..") file_path = os.path.normpath(self.filepath_from_context(context)) scene_object_before = [obj for obj in rt.rootNode.Children] @@ -37,8 +40,6 @@ class OxAbcLoader(load.LoaderPlugin): ) def update(self, container, representation): - from pymxs import runtime as rt - path = get_representation_path(representation) node_name = container["instance_node"] instance_name, _ = os.path.splitext(node_name) @@ -68,7 +69,17 @@ class OxAbcLoader(load.LoaderPlugin): self.update(container, representation) def remove(self, container): - from pymxs import runtime as rt - node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) + + +def get_plugins() -> list: + """Get plugin list from 3ds max.""" + manager = rt.PluginManager + count = manager.pluginDllCount + plugin_info_list = [] + for p in range(1, count + 1): + plugin_info = manager.pluginDllName(p) + plugin_info_list.append(plugin_info) + + return plugin_info_list From d92f2bac2f0c4115500d48b3223f65eb7c052e7f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 28 Aug 2023 23:21:54 +0800 Subject: [PATCH 075/175] hound --- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 1dce9aaa65..5f7e0a05f8 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -4,6 +4,7 @@ from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from pymxs import runtime as rt + class OxAbcLoader(load.LoaderPlugin): """Ornatrix Alembic loader.""" From ad6d03cedae5c5b7bf8ed3ea8261383045f9383d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 29 Aug 2023 17:30:15 +0800 Subject: [PATCH 076/175] fix the playbacktype --- openpype/hosts/max/plugins/load/load_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index f250377cfd..3f8b241351 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -52,7 +52,7 @@ class AbcLoader(load.LoaderPlugin): for abc in rt.GetCurrentSelection(): for cam_shape in abc.Children: - cam_shape.playbackType = 2 + cam_shape.playbackType = 0 return containerise( name, [abc_container], context, loader=self.__class__.__name__ From 42d0b19b46d6c30e18b9dbaee00bba87fc7ea5fc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 29 Aug 2023 17:37:12 +0800 Subject: [PATCH 077/175] add correct plugin name --- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 5f7e0a05f8..a3c9d94f83 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -17,7 +17,7 @@ class OxAbcLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): plugin_list = get_plugins() - if "ornatrix.dlo" not in plugin_list: + if "ephere.plugins.autodesk.max.ornatrix.dlo" not in plugin_list: raise RuntimeError("Ornatrix plugin not " "found/installed in Max yet..") From 7be680190f0cf97da06e02747b4a4c8ea9ff46fc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 17:29:53 +0800 Subject: [PATCH 078/175] bug fix on the ornatrix loader --- .../plugins/load/load_pointcache_ornatrix.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index a3c9d94f83..56cac00aeb 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -22,18 +22,20 @@ class OxAbcLoader(load.LoaderPlugin): "found/installed in Max yet..") file_path = os.path.normpath(self.filepath_from_context(context)) - scene_object_before = [obj for obj in rt.rootNode.Children] rt.AlembicImport.ImportToRoot = True rt.AlembicImport.CustomAttributes = True rt.importFile( file_path, rt.name("noPrompt"), using=rt.Ornatrix_Alembic_Importer) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) + + scene_object = [] + for obj in rt.rootNode.Children: + obj_type = rt.ClassOf(obj) + if str(obj_type).startswith("Ox_"): + scene_object.append(obj) abc_container = rt.Container(name=name) - for abc in scene_object_after: + for abc in scene_object: abc.Parent = abc_container return containerise( @@ -48,18 +50,20 @@ class OxAbcLoader(load.LoaderPlugin): for children in container.Children: rt.Delete(children) - scene_object_before = [obj for obj in rt.rootNode.Children] rt.AlembicImport.ImportToRoot = False rt.AlembicImport.CustomAttributes = True rt.importFile( path, rt.name("noPrompt"), using=rt.Ornatrix_Alembic_Importer) - scene_object_after = [obj for obj in rt.rootNode.Children] - for scene_object in scene_object_before: - scene_object_after = scene_object_after.remove(scene_object) - for scene_object in scene_object_after: - scene_object.Parent = container + scene_object = [] + for obj in rt.rootNode.Children: + obj_type = rt.ClassOf(obj) + if str(obj_type).startswith("Ox_"): + scene_object.append(obj) + + for abc in scene_object: + abc.Parent = container lib.imprint( container["instance_node"], From e4819fc952a8711e013c5e473933559219014483 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 01:02:32 +0300 Subject: [PATCH 079/175] Kayla's and BigRoy's comments --- .../create/create_unreal_staticmesh.py | 4 +- .../hosts/houdini/plugins/load/load_fbx.py | 58 ++++++++----------- .../plugins/publish/collect_fbx_type.py | 32 +--------- .../plugins/publish/collect_output_node.py | 7 ++- 4 files changed, 34 insertions(+), 67 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index 179c81510a..a048965364 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -26,8 +26,8 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): """Unreal Static Meshes with collisions. """ # you should set - identifier = "io.openpype.creators.houdini.unrealstaticmesh" - label = "Unreal - Static Mesh" + identifier = "io.openpype.creators.houdini.unrealstaticmesh.fbx" + label = "Unreal - Static Mesh (FBX)" family = "staticMesh" icon = "fa5s.cubes" diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index d661f84eeb..2e4dafc2d8 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -31,7 +31,7 @@ class FbxLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): # get file path - file_path = self.get_file_path(context) + file_path = self.get_file_path(context=context) # get necessary data namespace, node_name = self.get_node_name(context, name, namespace) @@ -41,10 +41,15 @@ class FbxLoader(load.LoaderPlugin): self[:] = nodes - # Call containerise function which does some - # automations for you - containerised_nodes = self.get_containerised_nodes( - nodes, context, node_name, namespace + # Call containerise function which does some automations for you + # like moving created nodes to the AVALON_CONTAINERS subnetwork + containerised_nodes = pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", ) return containerised_nodes @@ -61,8 +66,7 @@ class FbxLoader(load.LoaderPlugin): return # Update the file path - file_path = get_representation_path(representation) - file_path = self.format_path(file_path, representation) + file_path = self.get_file_path(representation=representation) file_node.setParms({"file": file_path}) @@ -77,15 +81,18 @@ class FbxLoader(load.LoaderPlugin): def switch(self, container, representation): self.update(container, representation) - def get_file_path(self, context): + def get_file_path(self, context=None, representation=None): """Return formatted file path.""" # 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("\\", "/") + if context: + file_path = self.filepath_from_context(context) + elif representation: + file_path = get_representation_path(representation) + else: + return "" - return file_path + return file_path.replace("\\", "/") def get_node_name(self, context, name=None, namespace=None): """Define node name.""" @@ -145,27 +152,8 @@ class FbxLoader(load.LoaderPlugin): # node to optimize "debug" displaying in the viewport. file_node.setDisplayFlag(True) - # Set new position for unpack node else it gets cluttered - nodes = [parent_node, file_node, attribdelete, null] - for nr, node in enumerate(nodes): - node.setPosition([0, (0 - nr)]) + # Set new position for children nodes + parent_node.layoutChildren() - return nodes - - def get_containerised_nodes(self, nodes, context, node_name, namespace): - """Call containerise function. - - It does some automations that you don't have to worry about, e.g. - 1. It moves created nodes to the AVALON_CONTAINERS subnetwork - 2. Add extra parameters - """ - containerised_nodes = pipeline.containerise( - node_name, - namespace, - nodes, - context, - self.__class__.__name__, - suffix="", - ) - - return containerised_nodes + # Retrun all the nodes + return [parent_node, file_node, attribdelete, null] diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py index 4c83829c67..6ac40a4f50 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py @@ -16,7 +16,7 @@ class CollectFilmboxfbxType(pyblish.api.InstancePlugin): """Collect data type for fbx instance.""" hosts = ["houdini"] - families = ["fbx", "staticMesh"] + families = ["staticMesh"] label = "Collect type of fbx" # Usually you will use this value as default @@ -25,35 +25,9 @@ class CollectFilmboxfbxType(pyblish.api.InstancePlugin): # overrides InstancePlugin.process() def process(self, instance): - if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh": # noqa: E501 + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh.fbx": # noqa: E501 # such a condition can be used to differentiate between - # instances by identifier becuase sometimes instances + # instances by identifier because sometimes instances # may have the same family but different identifier # e.g. bgeo and alembic instance.data["families"] += ["fbx"] - - # Update instance.data with ouptut_node - out_node = self.get_output_node(instance) - - if out_node: - instance.data["output_node"] = out_node - - # Disclaimer : As a convntin we use collect_output_node.py - # to Update instance.data with ouptut_node of different types - # however, this collector is used for demonstration - - def get_output_node(self, instance): - """Getting output_node Logic.""" - - import hou - - # get output node - node = hou.node(instance.data["instance_node"]) - out_node = node.parm("startnode").evalAsNode() - - if not out_node: - self.log.warning("No output node collected.") - return - - self.log.debug("Output node: %s" % out_node.path()) - return out_node diff --git a/openpype/hosts/houdini/plugins/publish/collect_output_node.py b/openpype/hosts/houdini/plugins/publish/collect_output_node.py index 601ed17b39..91bd5fdb15 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/collect_output_node.py @@ -12,7 +12,8 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): "imagesequence", "usd", "usdrender", - "redshiftproxy" + "redshiftproxy", + "staticMesh" ] hosts = ["houdini"] @@ -57,6 +58,10 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): elif node_type == "Redshift_Proxy_Output": out_node = node.parm("RS_archive_sopPath").evalAsNode() + + elif node_type == "filmboxfbx": + out_node = node.parm("startnode").evalAsNode() + else: raise ValueError( "ROP node type '%s' is" " not supported." % node_type From 127c63eb94b98af297287833e81f0ec9b2a8c5e7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 01:04:11 +0300 Subject: [PATCH 080/175] hound comments --- openpype/hosts/houdini/plugins/load/load_fbx.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 2e4dafc2d8..681837e046 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -7,8 +7,6 @@ however this one includes extra comments for demonstration. This plugin is part of publish process guide. """ -import os - from openpype.pipeline import ( load, get_representation_path, From d1e82ceb7ed7e0681a9856b6376cc8831f1c2d2a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 15:00:45 +0300 Subject: [PATCH 081/175] allow publishing sop and obj nodes --- .../create/create_unreal_staticmesh.py | 70 ++++++------------- .../publish/validate_sop_output_node.py | 2 +- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index a048965364..0b5b313d9e 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -73,6 +73,13 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): """Add settings for users. """ attrs = super().get_pre_create_attr_defs() + createsubnetroot = BoolDef("createsubnetroot", + tooltip="Create an extra root for the Export node " + "when it’s a subnetwork. This causes the " + "exporting subnetwork node to be " + "represented in the FBX file.", + default=False, + label="Create Root for Subnet") vcformat = EnumDef("vcformat", items={ 0: "Maya Compatible (MC)", @@ -88,7 +95,7 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): default=False, label="Convert Units") - return attrs + [vcformat, convert_units] + return attrs + [createsubnetroot, vcformat, convert_units] # Overrides BaseCreator.get_dynamic_data() def get_dynamic_data( @@ -123,13 +130,17 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): # 5. get Valid Frame Range trange = 1 + # 6. get createsubnetroot + createsubnetroot = pre_create_data.get("createsubnetroot") + # parms dictionary parms = { "startnode": selection, "sopoutput": output_path, "vcformat": vcformat, "convertunits": convertunits, - "trange": trange + "trange": trange, + "createsubnetroot": createsubnetroot } return parms @@ -149,36 +160,23 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): if self.selected_nodes: selected_node = self.selected_nodes[0] - # Although Houdini allows ObjNode path on `startnode` for the - # the ROP node we prefer it set to the SopNode path explicitly - - # Allow sop level paths (e.g. /obj/geo1/box1) + # Accept sop level nodes (e.g. /obj/geo1/box1) if isinstance(selected_node, hou.SopNode): selection = selected_node.path() self.log.debug( "Valid SopNode selection, 'Export' in filmboxfbx" - " will be set to '%s'." - % selected_node + " will be set to '%s'.", selected_node ) - # Allow object level paths to Geometry nodes (e.g. /obj/geo1) - # but do not allow other object level nodes types like cameras. - elif isinstance(selected_node, hou.ObjNode) and \ - selected_node.type().name() in ["geo"]: + # Accept object level nodes (e.g. /obj/geo1) + elif isinstance(selected_node, hou.ObjNode): + selection = selected_node.path() + self.log.debug( + "Valid ObjNode selection, 'Export' in filmboxfbx " + "will be set to the child path '%s'.", selection + ) - # get the output node with the minimum - # 'outputidx' or the node with display flag - sop_path = self.get_obj_output(selected_node) - - if sop_path: - selection = sop_path.path() - self.log.debug( - "Valid ObjNode selection, 'Export' in filmboxfbx " - "will be set to the child path '%s'." - % sop_path - ) - - if not selection: + else: self.log.debug( "Selection isn't valid. 'Export' in " "filmboxfbx will be empty." @@ -189,25 +187,3 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): ) return selection - - def get_obj_output(self, obj_node): - """Find output node with the smallest 'outputidx' - or return the node with the render flag instead. - """ - - outputs = obj_node.subnetOutputs() - - # if obj_node is empty - if not outputs: - return - - # if obj_node has one output child whether its - # sop output node or a node with the render flag - elif len(outputs) == 1: - return outputs[0] - - # if there are more than one, then it have multiple ouput nodes - # return the one with the minimum 'outputidx' - else: - return min(outputs, - key=lambda node: node.evalParm('outputidx')) diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index 2b426d96dd..d9dee38680 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -22,7 +22,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["pointcache", "vdbcache", "fbx"] + families = ["pointcache", "vdbcache"] hosts = ["houdini"] label = "Validate Output Node" actions = [SelectROPAction, SelectInvalidAction] From e6b9a7d0381a6325100c65537dd744d0796c3b31 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 17:04:22 +0300 Subject: [PATCH 082/175] validate mesh name --- .../publish/validate_fbx_hierarchy_path.py | 235 ------------------ .../publish/validate_mesh_is_static.py | 129 ---------- .../validate_unreal_staticmesh_naming.py | 102 ++++++++ 3 files changed, 102 insertions(+), 364 deletions(-) delete mode 100644 openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py delete mode 100644 openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py create mode 100644 openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py deleted file mode 100644 index be73ccd223..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_hierarchy_path.py +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate path attribute for all primitives. - -Validators are used to verify the work of artists, -by running some checks which automates the approval process. - -It's almost the same as -'validate_primitive_hierarchy_paths.py' -however this one includes extra comments for demonstration. - -FYI, path for fbx behaves a little differently. -In maya terms: -in Filmbox FBX: it sets the name of the object -in Alembic ROP: it sets the name of the shape - -This plugin is part of publish process guide. -""" - -import pyblish.api -from openpype.pipeline import ( - PublishValidationError, - OptionalPyblishPluginMixin -) -from openpype.pipeline.publish import ( - ValidateContentsOrder, - RepairAction, -) -from openpype.hosts.houdini.api.action import ( - SelectInvalidAction, - SelectROPAction, -) - -import hou - - -# Each validation can have a single repair action -# which calls the repair method -class AddDefaultPathAction(RepairAction): - label = "Add a default path" - icon = "mdi.pencil-plus-outline" - - -class ValidateFBXPrimitiveHierarchyPaths(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Validate all primitives build hierarchy from attribute - when enabled. - - The name of the attribute must exist on the prims and have the - same name as Build Hierarchy from Attribute's `Path Attribute` - value on the FilmBox node. - This validation enables 'Build Hierarchy from Attribute' - by default. - """ - - families = ["fbx"] - hosts = ["houdini"] - label = "Validate Prims Hierarchy Path (FBX)" - - # Usually you will use this value as default - order = ValidateContentsOrder + 0.1 - - # Validation can have as many actions as you want - # all of these actions are defined in a seperate place - # unlike the repair action - actions = [SelectInvalidAction, AddDefaultPathAction, - SelectROPAction] - - # 'OptionalPyblishPluginMixin' adds the functionality to - # enable/disable plugins, It requires adding new settings. - optional = True - - # overrides InstancePlugin.process() - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - nodes = [n.path() for n in invalid] - raise PublishValidationError( - "See log for details. " - "Invalid nodes: {0}".format(nodes), - title=self.label - ) - - # This method was named get_invalid as a convention - # it's also used by SelectInvalidAction to select - # the returned nodes - @classmethod - def get_invalid(cls, instance): - - output_node = instance.data.get("output_node") - rop_node = hou.node(instance.data["instance_node"]) - - if output_node is None: - cls.log.error( - "SOP Output node in '%s' does not exist. " - "Ensure a valid SOP output path is set.", - rop_node.path() - ) - - return [rop_node] - - build_from_path = rop_node.parm("buildfrompath").eval() - if not build_from_path: - cls.log.debug( - "Filmbox FBX has 'Build from Path' disabled. " - "Enbaling it as default." - ) - rop_node.parm("buildfrompath").set(1) - - path_attr = rop_node.parm("pathattrib").eval() - if not path_attr: - cls.log.debug( - "Filmbox FBX node has no Path Attribute" - "value set, setting it to 'path' as default." - ) - rop_node.parm("pathattrib").set("path") - - cls.log.debug("Checking for attribute: %s", path_attr) - - # Use current frame if "frameStart" doesn't exist - # This only happens when ""trange" is 0 - frame = instance.data.get("frameStart", hou.intFrame()) - - # Get Geo at that frame - geo = output_node.geometryAtFrame(frame) - if not geo: - cls.log.warning("No geometry found," - " skipping check..") - return - - # If there are no primitives on the current frame then - # we can't check whether the path names are correct. - # So we'll just issue a warning that the check can't - # be done consistently and skip validation. - - if len(geo.iterPrims()) == 0: - cls.log.warning( - "No primitives found on current frame." - " Validation for primitive hierarchy" - " paths will be skipped," - " thus can't be validated." - ) - return - - # Check if there are any values for the primitives - attrib = geo.findPrimAttrib(path_attr) - if not attrib: - cls.log.info( - "Geometry Primitives are missing " - "path attribute: `%s`", path_attr - ) - return [output_node] - - # Ensure at least a single string value is present - if not attrib.strings(): - cls.log.info( - "Primitive path attribute has no " - "string values: %s", path_attr - ) - return [output_node] - - paths = geo.primStringAttribValues(path_attr) - # Ensure all primitives are set to a valid path - # Collect all invalid primitive numbers - invalid_prims = [i for i, path in enumerate(paths) if not path] - if invalid_prims: - num_prims = len(geo.iterPrims()) # faster than len(geo.prims()) - cls.log.info( - "Prims have no value for attribute `%s` " - "(%s of %s prims)", - path_attr, len(invalid_prims), num_prims - ) - return [output_node] - - # what repair action expects to find and call - @classmethod - def repair(cls, instance): - """Add a default path attribute Action. - - It is a helper action more than a repair action, - used to add a default single value for the path. - """ - - rop_node = hou.node(instance.data["instance_node"]) - # I'm doing so because an artist may change output node - # before clicking the button. - output_node = rop_node.parm("startnode").evalAsNode() - - if not output_node: - cls.log.debug( - "Action isn't performed, invalid SOP Path on %s", - rop_node - ) - return - - # This check to prevent the action from running multiple times. - # git_invalid only returns [output_node] when - # path attribute is the problem - if cls.get_invalid(instance) != [output_node]: - return - - path_attr = rop_node.parm("pathattrib").eval() - - path_node = output_node.parent().createNode("name", - "AUTO_PATH") - path_node.parm("attribname").set(path_attr) - path_node.parm("name1").set('`opname("..")`_GEO') - - cls.log.debug( - "'%s' was created. It adds '%s' with a default" - " single value", path_node, path_attr - ) - - path_node.setGenericFlag(hou.nodeFlag.DisplayComment, True) - path_node.setComment( - 'Auto path node was created automatically by ' - '"Add a default path attribute"' - '\nFeel free to modify or replace it.' - ) - - if output_node.type().name() in ["null", "output"]: - # Connect before - path_node.setFirstInput(output_node.input(0)) - path_node.moveToGoodPosition() - output_node.setFirstInput(path_node) - output_node.moveToGoodPosition() - else: - # Connect after - path_node.setFirstInput(output_node) - rop_node.parm("startnode").set(path_node.path()) - path_node.moveToGoodPosition() - - cls.log.debug( - "SOP path on '%s' updated to new output node '%s'", - rop_node, path_node - ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py deleted file mode 100644 index ac80fda537..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate mesh is static. - -This plugin is part of publish process guide. -""" - -import pyblish.api -from openpype.pipeline import PublishValidationError -from openpype.pipeline.publish import ( - ValidateContentsOrder, - RepairAction, -) -from openpype.hosts.houdini.api.action import ( - SelectInvalidAction, - SelectROPAction, -) - -import hou - - -# Each validation can have a single repair action -# which calls the repair method -class FreezeTimeAction(RepairAction): - label = "Freeze Time" - icon = "ei.pause-alt" - - -class ValidateMeshIsStatic(pyblish.api.InstancePlugin): - """Validate mesh is static. - - It checks if node is time dependant. - """ - - families = ["staticMesh"] - hosts = ["houdini"] - label = "Validate mesh is static" - - # Usually you will use this value as default - order = ValidateContentsOrder + 0.1 - - # Validation can have as many actions as you want - # all of these actions are defined in a seperate place - # unlike the repair action - actions = [FreezeTimeAction, SelectInvalidAction, - SelectROPAction] - - # overrides InstancePlugin.process() - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - nodes = [n.path() for n in invalid] - raise PublishValidationError( - "See log for details. " - "Invalid nodes: {0}".format(nodes), - title=self.label - ) - - # This method was named get_invalid as a convention - # it's also used by SelectInvalidAction to select - # the returned nodes - @classmethod - def get_invalid(cls, instance): - - output_node = instance.data.get("output_node") - if output_node.isTimeDependent(): - cls.log.info("Mesh is not static!") - return [output_node] - - # what repair action expects to find and call - @classmethod - def repair(cls, instance): - """Adds a time shift node. - - It should kill time dependency. - """ - - rop_node = hou.node(instance.data["instance_node"]) - # I'm doing so because an artist may change output node - # before clicking the button. - output_node = rop_node.parm("startnode").evalAsNode() - - if not output_node: - cls.log.debug( - "Action isn't performed, invalid SOP Path on %s", - rop_node - ) - return - - # This check to prevent the action from running multiple times. - # git_invalid only returns [output_node] when - # path attribute is the problem - if cls.get_invalid(instance) != [output_node]: - return - - time_shift = output_node.parent().createNode("timeshift", - "freeze_time") - time_shift.parm("frame").deleteAllKeyframes() - - frame = instance.data.get("frameStart", hou.intFrame()) - time_shift.parm("frame").set(frame) - - cls.log.debug( - "'%s' was created. It will kill time dependency.", - time_shift - ) - - time_shift.setGenericFlag(hou.nodeFlag.DisplayComment, True) - time_shift.setComment( - 'This node was created automatically by ' - '"Freeze Time" Action' - '\nFeel free to modify or replace it.' - ) - - if output_node.type().name() in ["null", "output"]: - # Connect before - time_shift.setFirstInput(output_node.input(0)) - time_shift.moveToGoodPosition() - output_node.setFirstInput(time_shift) - output_node.moveToGoodPosition() - else: - # Connect after - time_shift.setFirstInput(output_node) - rop_node.parm("startnode").set(time_shift.path()) - time_shift.moveToGoodPosition() - - cls.log.debug( - "SOP path on '%s' updated to new output node '%s'", - rop_node, time_shift - ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py new file mode 100644 index 0000000000..be450a0410 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ValidateContentsOrder + +from openpype.hosts.houdini.api.action import SelectInvalidAction + +import hou + + +class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate name of Unreal Static Mesh + + This validator checks if output node name has a collision prefix: + - UBX + - UCP + - USP + - UCX + + This validator also checks if subset name is correct + - {static mesh prefix}_{Asset-Name}{Variant}. + + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Unreal Static Mesh Name (FBX)" + order = ValidateContentsOrder + 0.1 + actions = [SelectInvalidAction] + + optional = True + + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = ( + project_settings["houdini"]["create"]["CreateUnrealStaticMesh"] + ) + cls.collision_prefixes = settings["collision_prefixes"] + cls.static_mesh_prefix = settings["static_mesh_prefix"] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes), + title=self.label + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + rop_node = hou.node(instance.data["instance_node"]) + output_node = instance.data.get("output_node") + cls.log.debug(cls.collision_prefixes) + + # Check nodes names + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + for child in output_node.children(): + for prefix in cls.collision_prefixes: + if child.name().startswith(prefix): + invalid.append(child) + cls.log.error( + "Invalid name: Child node '%s' in '%s' " + "has a collision prefix '%s'" + , child.name(), output_node.path(), prefix + ) + break + else: + cls.log.debug(output_node.name()) + for prefix in cls.collision_prefixes: + if output_node.name().startswith(prefix): + invalid.append(output_node) + cls.log.error( + "Invalid name: output node '%s' " + "has a collision prefix '%s'" + , output_node.name(), prefix + ) + + # Check subset name + subset_name = "{}_{}{}".format( + cls.static_mesh_prefix, + instance.data["asset"], + instance.data.get("variant", "") + ) + + if instance.data.get("subset") != subset_name: + invalid.append(rop_node) + cls.log.error( + "Invalid subset name on rop node '%s' should be '%s'." + , rop_node.path(), subset_name + ) + + return invalid From 128939068084b3b5af50db7812d1a9b8021a471c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 17:23:53 +0300 Subject: [PATCH 083/175] update settings --- .../plugins/publish/validate_unreal_staticmesh_naming.py | 6 +++++- openpype/settings/defaults/project_settings/houdini.json | 2 +- .../projects_schema/schemas/schema_houdini_publish.json | 4 ++-- server_addon/houdini/server/settings/publish_plugins.py | 6 +++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index be450a0410..24ef304185 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -14,7 +14,7 @@ import hou class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Validate name of Unreal Static Mesh + """Validate name of Unreal Static Mesh. This validator checks if output node name has a collision prefix: - UBX @@ -44,6 +44,10 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, cls.static_mesh_prefix = settings["static_mesh_prefix"] def process(self, instance): + + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index c39eb717fd..65f13fa1ab 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -111,7 +111,7 @@ "optional": true, "active": true }, - "ValidateFBXPrimitiveHierarchyPaths": { + "ValidateUnrealStaticMeshName": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index d58b36eff1..4339f86db6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -45,8 +45,8 @@ "label": "ValidateContainers" }, { - "key": "ValidateFBXPrimitiveHierarchyPaths", - "label": "Validate Path Attribute for FBX" + "key": "ValidateUnrealStaticMeshName", + "label": "Validate Unreal Static Mesh Name" } ] } diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 1e5cd7f551..335751e5f9 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -164,9 +164,9 @@ class PublishPluginsModel(BaseSettingsModel): ValidateContainers: ValidateContainersModel = Field( default_factory=ValidateContainersModel, title="Validate Latest Containers.") - ValidateFBXPrimitiveHierarchyPaths: ValidateContainersModel = Field( + ValidateUnrealStaticMeshName: ValidateContainersModel = Field( default_factory=ValidateContainersModel, - title="Validate Path Attribute for FBX.") + title="Validate Unreal Static Mesh Name.") DEFAULT_HOUDINI_PUBLISH_SETTINGS = { @@ -187,7 +187,7 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "optional": True, "active": True }, - "ValidateFBXPrimitiveHierarchyPaths": { + "ValidateUnrealStaticMeshName": { "enabled": True, "optional": True, "active": True From c73d76ef15289f868d016c689ecaa880c3144262 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 17:33:54 +0300 Subject: [PATCH 084/175] resolve houn --- .../plugins/create/create_unreal_staticmesh.py | 13 +++++++------ .../publish/validate_unreal_staticmesh_naming.py | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index 0b5b313d9e..6002f7b1d7 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -74,12 +74,13 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): attrs = super().get_pre_create_attr_defs() createsubnetroot = BoolDef("createsubnetroot", - tooltip="Create an extra root for the Export node " - "when it’s a subnetwork. This causes the " - "exporting subnetwork node to be " - "represented in the FBX file.", - default=False, - label="Create Root for Subnet") + tooltip="Create an extra root for the " + "Export node when it’s a " + "subnetwork. This causes the " + "exporting subnetwork node to be " + "represented in the FBX file.", + default=False, + label="Create Root for Subnet") vcformat = EnumDef("vcformat", items={ 0: "Maya Compatible (MC)", diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index 24ef304185..a3426d2f19 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -74,8 +74,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, invalid.append(child) cls.log.error( "Invalid name: Child node '%s' in '%s' " - "has a collision prefix '%s'" - , child.name(), output_node.path(), prefix + "has a collision prefix '%s'", + child.name(), output_node.path(), prefix ) break else: @@ -85,8 +85,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, invalid.append(output_node) cls.log.error( "Invalid name: output node '%s' " - "has a collision prefix '%s'" - , output_node.name(), prefix + "has a collision prefix '%s'", + output_node.name(), prefix ) # Check subset name @@ -99,8 +99,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, if instance.data.get("subset") != subset_name: invalid.append(rop_node) cls.log.error( - "Invalid subset name on rop node '%s' should be '%s'." - , rop_node.path(), subset_name + "Invalid subset name on rop node '%s' should be '%s'.", + rop_node.path(), subset_name ) return invalid From 1f452510d8e4fb38deaebe59eb5f4d970d2b5aa1 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 17:35:57 +0300 Subject: [PATCH 085/175] resolve hound --- .../plugins/publish/validate_unreal_staticmesh_naming.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index a3426d2f19..7820be4009 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -99,8 +99,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, if instance.data.get("subset") != subset_name: invalid.append(rop_node) cls.log.error( - "Invalid subset name on rop node '%s' should be '%s'.", - rop_node.path(), subset_name + "Invalid subset name on rop node '%s' should be '%s'.", + rop_node.path(), subset_name ) return invalid From 938dc72d9179ee8938a98f0ce326c4dfadda5657 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 11:56:10 +0300 Subject: [PATCH 086/175] revise creator and collector --- .../create/create_unreal_staticmesh.py | 36 +++---------------- .../plugins/publish/collect_fbx_type.py | 33 ----------------- .../publish/collect_staticmesh_type.py | 20 +++++++++++ .../validate_primitive_hierarchy_paths.py | 2 +- 4 files changed, 26 insertions(+), 65 deletions(-) delete mode 100644 openpype/hosts/houdini/plugins/publish/collect_fbx_type.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index 6002f7b1d7..ca5e2e8fb4 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -1,21 +1,5 @@ # -*- coding: utf-8 -*- -"""Creator plugin for creating Unreal Static Meshes. - -Unreal Static Meshes will be published as FBX. - -Filmbox by default expects an ObjNode -however, we set the sop node explictly -to eleminate any confusion. - -This will make Filmbox to ignore any object transformations! - -get_obj_output selects -the output sop with mimimum idx -or the node with render flag isntead. - -This plugin is part of publish process guide. -""" - +"""Creator for Unreal Static Meshes.""" from openpype.hosts.houdini.api import plugin from openpype.lib import BoolDef, EnumDef @@ -25,30 +9,23 @@ import hou class CreateUnrealStaticMesh(plugin.HoudiniCreator): """Unreal Static Meshes with collisions. """ - # you should set identifier = "io.openpype.creators.houdini.unrealstaticmesh.fbx" label = "Unreal - Static Mesh (FBX)" family = "staticMesh" icon = "fa5s.cubes" - # optional to set - default_variant = "Main" - # 'default_variants' will be overriden by settings. - default_variants = ["Main", "Test"] + default_variants = ["Main"] - # Overrides HoudiniCreator.create() def create(self, subset_name, instance_data, pre_create_data): - # set node type instance_data.update({"node_type": "filmboxfbx"}) - # create instance (calls HoudiniCreator.create()) instance = super(CreateUnrealStaticMesh, self).create( subset_name, instance_data, pre_create_data) - # get the created node + # get the created rop node instance_node = hou.node(instance.get("instance_node")) # get parms @@ -61,21 +38,19 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) - # Overrides HoudiniCreator.get_network_categories() def get_network_categories(self): return [ hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] - # Overrides HoudiniCreator.get_pre_create_attr_defs() def get_pre_create_attr_defs(self): """Add settings for users. """ attrs = super().get_pre_create_attr_defs() createsubnetroot = BoolDef("createsubnetroot", tooltip="Create an extra root for the " - "Export node when it’s a " + "Export node when it's a " "subnetwork. This causes the " "exporting subnetwork node to be " "represented in the FBX file.", @@ -98,7 +73,6 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): return attrs + [createsubnetroot, vcformat, convert_units] - # Overrides BaseCreator.get_dynamic_data() def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): @@ -113,7 +87,7 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): return dynamic_data def get_parms(self, subset_name, pre_create_data): - """Get parameters values for this specific node.""" + """Get parameters values. """ # 1. get output path output_path = hou.text.expandString( diff --git a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py b/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py deleted file mode 100644 index 6ac40a4f50..0000000000 --- a/openpype/hosts/houdini/plugins/publish/collect_fbx_type.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Collector for filmboxfbx types. - -Collectors act as a pre process for the validation stage. -It is used mainly to update instance.data - -P.S. - There are some collectors that run by default - for all types. - -This plugin is part of publish process guide. -""" -import pyblish.api - - -class CollectFilmboxfbxType(pyblish.api.InstancePlugin): - """Collect data type for fbx instance.""" - - hosts = ["houdini"] - families = ["staticMesh"] - label = "Collect type of fbx" - - # Usually you will use this value as default - order = pyblish.api.CollectorOrder - - # overrides InstancePlugin.process() - def process(self, instance): - - if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh.fbx": # noqa: E501 - # such a condition can be used to differentiate between - # instances by identifier because sometimes instances - # may have the same family but different identifier - # e.g. bgeo and alembic - instance.data["families"] += ["fbx"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py new file mode 100644 index 0000000000..8fb07c1c5c --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Collector for staticMesh types. """ + +import pyblish.api + + +class CollectStaticMeshType(pyblish.api.InstancePlugin): + """Collect data type for fbx instance.""" + + hosts = ["houdini"] + families = ["staticMesh"] + label = "Collect type of staticMesh" + + order = pyblish.api.CollectorOrder + + def process(self, instance): + + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh.fbx": # noqa: E501 + # Marking this instance as FBX which triggers the FBX extractor. + instance.data["families"] += ["fbx"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index 930978ef16..471fa5b6d1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -26,7 +26,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): order = ValidateContentsOrder + 0.1 families = ["abc"] hosts = ["houdini"] - label = "Validate Prims Hierarchy Path (ABC)" + label = "Validate Prims Hierarchy Path" actions = [AddDefaultPathAction] def process(self, instance): From ad62e1fd469fe122b9b7490a5d68b49db26741b2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 15:06:42 +0300 Subject: [PATCH 087/175] revise collect-validators-extract --- .../hosts/houdini/plugins/load/load_fbx.py | 21 ++---- .../publish/collect_staticmesh_type.py | 2 +- .../houdini/plugins/publish/extract_fbx.py | 13 +--- .../publish/validate_mesh_is_static.py | 70 +++++++++++++++++++ .../plugins/publish/validate_output_node.py | 55 +++++++++++++++ .../publish/validate_sop_output_node.py | 2 +- .../validate_unreal_staticmesh_naming.py | 9 ++- 7 files changed, 142 insertions(+), 30 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py create mode 100644 openpype/hosts/houdini/plugins/publish/validate_output_node.py diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 681837e046..9c7dbf578e 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -1,12 +1,5 @@ # -*- coding: utf-8 -*- -"""Fbx Loader for houdini. - -It's almost a copy of -'load_bgeo.py'and 'load_alembic.py' -however this one includes extra comments for demonstration. - -This plugin is part of publish process guide. -""" +"""Fbx Loader for houdini. """ from openpype.pipeline import ( load, get_representation_path, @@ -15,17 +8,17 @@ from openpype.hosts.houdini.api import pipeline class FbxLoader(load.LoaderPlugin): - """Load fbx files to Houdini.""" + """Load fbx files. """ label = "Load FBX" - families = ["staticMesh", "fbx"] - representations = ["fbx"] - - # Usually you will use these value as default - order = -10 icon = "code-fork" color = "orange" + order = -10 + + families = ["staticMesh", "fbx"] + representations = ["fbx"] + def load(self, context, name=None, namespace=None, data=None): # get file path diff --git a/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py index 8fb07c1c5c..263d7c1001 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py @@ -16,5 +16,5 @@ class CollectStaticMeshType(pyblish.api.InstancePlugin): def process(self, instance): if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh.fbx": # noqa: E501 - # Marking this instance as FBX which triggers the FBX extractor. + # Marking this instance as FBX triggers the FBX extractor. instance.data["families"] += ["fbx"] diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index c0e84c00c8..2a95734ece 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -1,15 +1,8 @@ -"""Extract FilmBox FBX. - -Extractors are used to generate output and -update representation dictionary. - -This plugin is part of publish process guide. -""" +# -*- coding: utf-8 -*- +"""Fbx Extractor for houdini. """ import os - import pyblish.api - from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop @@ -22,10 +15,8 @@ class ExtractFBX(publish.Extractor): families = ["fbx"] hosts = ["houdini"] - # Usually you will use this value as default order = pyblish.api.ExtractorOrder + 0.1 - # overrides Extractor.process() def process(self, instance): # get rop node diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py new file mode 100644 index 0000000000..90985b4239 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ValidateContentsOrder + +from openpype.hosts.houdini.api.action import SelectInvalidAction + +import hou + + +class ValidateMeshIsStatic(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate mesh is static. + + It checks if output node is time dependant. + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Validate Mesh is Static" + order = ValidateContentsOrder + 0.1 + actions = [SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes) + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + output_node = instance.data.get("output_node") + if output_node is None: + cls.log.debug( + "No Output Node, skipping check.." + ) + return + + + + if output_node.name().isTimeDependent(): + invalid.append(output_node) + cls.log.error( + "Output node '%s' is time dependent.", + output_node.name() + ) + + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + for child in output_node.children(): + if output_node.name().isTimeDependent(): + invalid.append(child) + cls.log.error( + "Child node '%s' in '%s' " + "his time dependent.", + child.name(), output_node.path() + ) + break + + return invalid diff --git a/openpype/hosts/houdini/plugins/publish/validate_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_output_node.py new file mode 100644 index 0000000000..99a6cda077 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_output_node.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from openpype.hosts.houdini.api.action import ( + SelectInvalidAction, + SelectROPAction, +) + +import hou + + +class ValidateOutputNode(pyblish.api.InstancePlugin): + """Validate the instance Output Node. + + This will ensure: + - The Output Node Path is set. + - The Output Node Path refers to an existing object. + """ + + order = pyblish.api.ValidatorOrder + families = ["fbx"] + hosts = ["houdini"] + label = "Validate Output Node" + actions = [SelectROPAction, SelectInvalidAction] + + def process(self, instance): + + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + "Output node(s) are incorrect", + title="Invalid output node(s)" + ) + + @classmethod + def get_invalid(cls, instance): + output_node = instance.data.get("output_node") + + if output_node is None: + rop_node = hou.node(instance.data["instance_node"]) + cls.log.error( + "Output node in '%s' does not exist. " + "Ensure a valid output path is set.", rop_node.path() + ) + + return [rop_node] + + if output_node.type().category().name() not in ["Sop", "Object"]: + cls.log.error( + "Output node %s is not a SOP or OBJ node. " + "It must point to a SOP or OBJ node, " + "instead found category type: %s" + % (output_node.path(), output_node.type().category().name()) + ) + return [output_node] diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index d9dee38680..9590e37d26 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -24,7 +24,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["pointcache", "vdbcache"] hosts = ["houdini"] - label = "Validate Output Node" + label = "Validate Output Node (SOP)" actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index 7820be4009..f1ea9b3844 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -53,8 +53,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] raise PublishValidationError( "See log for details. " - "Invalid nodes: {0}".format(nodes), - title=self.label + "Invalid nodes: {0}".format(nodes) ) @classmethod @@ -64,7 +63,11 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) output_node = instance.data.get("output_node") - cls.log.debug(cls.collision_prefixes) + if output_node is None: + cls.log.debug( + "No Output Node, skipping check.." + ) + return # Check nodes names if output_node.childTypeCategory() == hou.objNodeTypeCategory(): From 8440d65949e76dd9aca42206df528a62625ea32b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Sep 2023 20:32:16 +0800 Subject: [PATCH 088/175] update on the ornatrix laoder --- openpype/hosts/max/api/pipeline.py | 51 ------------------- .../plugins/load/load_pointcache_ornatrix.py | 46 ++++++++++++++--- 2 files changed, 38 insertions(+), 59 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index d9a66c60f5..2ce96d16e1 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -15,10 +15,8 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib -from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB from openpype.hosts.max import MAX_HOST_DIR - from pymxs import runtime as rt # noqa log = logging.getLogger("openpype.hosts.max") @@ -173,52 +171,3 @@ def containerise(name: str, nodes: list, context, if not lib.imprint(container_name, data): print(f"imprinting of {container_name} failed.") return container - - -def load_custom_attribute_data(): - """Re-loading the Openpype/AYON custom parameter built by the creator - - Returns: - attribute: re-loading the custom OP attributes set in Maxscript - """ - return rt.Execute(MS_CUSTOM_ATTRIB) - - -def import_custom_attribute_data(container: str, selections: list): - """Importing the Openpype/AYON custom parameter built by the creator - - Args: - container (str): target container which adds custom attributes - selections (list): nodes to be added into - group in custom attributes - """ - attrs = load_custom_attribute_data() - modifier = rt.EmptyModifier() - rt.addModifier(container, modifier) - container.modifiers[0].name = "OP Data" - rt.custAttributes.add(container.modifiers[0], attrs) - nodes = {} - for i in selections: - nodes = { - str(i): rt.NodeTransformMonitor(node=i), - } - # Setting the property - rt.setProperty( - container.modifiers[0].openPypeData, - "all_handles", nodes.values()) - rt.setProperty( - container.modifiers[0].openPypeData, - "sel_list", nodes.keys()) - - -def update_custom_attribute_data(container: str, selections: list): - """Updating the Openpype/AYON custom parameter built by the creator - - Args: - container (str): target container which adds custom attributes - selections (list): nodes to be added into - group in custom attributes - """ - if container.modifiers[0].name == "OP Data": - rt.deleteModifier(container, container.modifiers[0]) - import_custom_attribute_data(container, selections) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 56cac00aeb..d3b7c61ff8 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -1,6 +1,15 @@ import os from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import ( + containerise, + import_custom_attribute_data, + update_custom_attribute_data +) +from openpype.hosts.max.api.lib import ( + unique_namespace, + get_namespace, + object_transform_set +) from openpype.hosts.max.api import lib from pymxs import runtime as rt @@ -14,6 +23,7 @@ class OxAbcLoader(load.LoaderPlugin): order = -10 icon = "code-fork" color = "orange" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): plugin_list = get_plugins() @@ -34,21 +44,37 @@ class OxAbcLoader(load.LoaderPlugin): if str(obj_type).startswith("Ox_"): scene_object.append(obj) - abc_container = rt.Container(name=name) + namespace = unique_namespace( + name + "_", + suffix="_", + ) + + abc_container = rt.Container() for abc in scene_object: abc.Parent = abc_container + abc.name = f"{namespace}:{abc.name}" + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}_{self.postfix}" + abc_container.name = abc_container_name + import_custom_attribute_data( + abc_container, abc_container.Children) return containerise( - name, [abc_container], context, loader=self.__class__.__name__ + name, [abc_container], context, + namespace, loader=self.__class__.__name__ ) def update(self, container, representation): path = get_representation_path(representation) node_name = container["instance_node"] - instance_name, _ = os.path.splitext(node_name) - container = rt.getNodeByName(instance_name) - for children in container.Children: - rt.Delete(children) + namespace, name = get_namespace(node_name) + sub_node_name = f"{namespace}:{name}_{self.postfix}" + inst_container = rt.getNodeByName(sub_node_name) + rt.Select(inst_container.Children) + transform_data = object_transform_set(inst_container.Children) + for prev_obj in rt.selection: + if rt.isValidNode(prev_obj): + rt.Delete(prev_obj) rt.AlembicImport.ImportToRoot = False rt.AlembicImport.CustomAttributes = True @@ -61,9 +87,13 @@ class OxAbcLoader(load.LoaderPlugin): obj_type = rt.ClassOf(obj) if str(obj_type).startswith("Ox_"): scene_object.append(obj) - + update_custom_attribute_data( + inst_container, scene_object.Children) for abc in scene_object: abc.Parent = container + abc.name = f"{namespace}:{abc.name}" + abc.pos = transform_data[f"{abc.name}.transform"] + abc.scale = transform_data[f"{abc.name}.scale"] lib.imprint( container["instance_node"], From 1678bd56032e8975301651c4bcdfb6ae1290dce6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 19:48:15 +0300 Subject: [PATCH 089/175] update fetch output --- .../validate_unreal_staticmesh_naming.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index f1ea9b3844..3c13f081a9 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -107,3 +107,30 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, ) return invalid + + def get_outputs(self, output_node): + + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + out_list = [output_node] + for child in output_node.children(): + out_list += self.get_outputs(child) + + return out_list + + elif output_node.childTypeCategory() == hou.sopNodeTypeCategory(): + return [output_node, self.get_obj_output(output_node)] + + def get_obj_output(self, obj_node): + """Find sop output node, """ + + outputs = obj_node.subnetOutputs() + + if not outputs: + return + + elif len(outputs) == 1: + return outputs[0] + + else: + return min(outputs, + key=lambda node: node.evalParm('outputidx')) From 79aab2534ea53931980b4bb44e52713047898c65 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 21:59:04 +0300 Subject: [PATCH 090/175] update retrieving output nodes --- openpype/hosts/houdini/api/lib.py | 39 ++++++++++++ .../publish/validate_mesh_is_static.py | 27 +++----- .../validate_unreal_staticmesh_naming.py | 62 +++++-------------- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 75c7ff9fee..0f1cfe0717 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -649,3 +649,42 @@ def get_color_management_preferences(): "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } + + +def get_obj_node_output(obj_node): + """Find output node. + + get the output node with the minimum 'outputidx' + or the node with display flag. + """ + + outputs = obj_node.subnetOutputs() + if not outputs: + return + + elif len(outputs) == 1: + return outputs[0] + + else: + return min(outputs, + key=lambda node: node.evalParm('outputidx')) + + +def get_output_children(output_node, include_sops=True): + """Recursively return a list of all output nodes + contained in this node including this node. + + It works in a similar manner to output_node.allNodes(). + """ + out_list = [output_node] + + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + for child in output_node.children(): + out_list += get_output_children(child, include_sops=include_sops) + + elif include_sops and output_node.childTypeCategory() == hou.sopNodeTypeCategory(): + out = get_obj_node_output(output_node) + if out: + out_list += [out] + + return out_list diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py index 90985b4239..36c8ef6d63 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( from openpype.pipeline.publish import ValidateContentsOrder from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.hosts.houdini.api.lib import get_output_children import hou @@ -47,24 +48,14 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin, ) return + all_outputs = get_output_children(output_node) - - if output_node.name().isTimeDependent(): - invalid.append(output_node) - cls.log.error( - "Output node '%s' is time dependent.", - output_node.name() - ) - - if output_node.childTypeCategory() == hou.objNodeTypeCategory(): - for child in output_node.children(): - if output_node.name().isTimeDependent(): - invalid.append(child) - cls.log.error( - "Child node '%s' in '%s' " - "his time dependent.", - child.name(), output_node.path() - ) - break + for output in all_outputs: + if output.isTimeDependent(): + invalid.append(output) + cls.log.error( + "Output node '%s' is time dependent.", + output.path() + ) return invalid diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index 3c13f081a9..5558b43258 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( from openpype.pipeline.publish import ValidateContentsOrder from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.hosts.houdini.api.lib import get_output_children import hou @@ -69,28 +70,26 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, ) return + if not rop_node.evalParm('buildfrompath'): + # This validator doesn't support naming check if + # building hierarchy from path' is used + cls.log.info( + "Using 'Build Hierarchy from Path Attribute', skipping check.." + ) + return + # Check nodes names - if output_node.childTypeCategory() == hou.objNodeTypeCategory(): - for child in output_node.children(): - for prefix in cls.collision_prefixes: - if child.name().startswith(prefix): - invalid.append(child) - cls.log.error( - "Invalid name: Child node '%s' in '%s' " - "has a collision prefix '%s'", - child.name(), output_node.path(), prefix - ) - break - else: - cls.log.debug(output_node.name()) + all_outputs = get_output_children(output_node, include_sops=False) + for output in all_outputs: for prefix in cls.collision_prefixes: - if output_node.name().startswith(prefix): - invalid.append(output_node) + if output.name().startswith(prefix): + invalid.append(output) cls.log.error( - "Invalid name: output node '%s' " - "has a collision prefix '%s'", - output_node.name(), prefix + "Invalid node name: Node '%s' " + "includes a collision prefix '%s'", + output.path(), prefix ) + break # Check subset name subset_name = "{}_{}{}".format( @@ -107,30 +106,3 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, ) return invalid - - def get_outputs(self, output_node): - - if output_node.childTypeCategory() == hou.objNodeTypeCategory(): - out_list = [output_node] - for child in output_node.children(): - out_list += self.get_outputs(child) - - return out_list - - elif output_node.childTypeCategory() == hou.sopNodeTypeCategory(): - return [output_node, self.get_obj_output(output_node)] - - def get_obj_output(self, obj_node): - """Find sop output node, """ - - outputs = obj_node.subnetOutputs() - - if not outputs: - return - - elif len(outputs) == 1: - return outputs[0] - - else: - return min(outputs, - key=lambda node: node.evalParm('outputidx')) From 9ea11e7f2d90faf25487ebe2f8f7db4d461da018 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 22:02:23 +0300 Subject: [PATCH 091/175] resolve hound --- openpype/hosts/houdini/api/lib.py | 7 ++++--- .../houdini/plugins/publish/validate_mesh_is_static.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 0f1cfe0717..3e51912c26 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -667,7 +667,7 @@ def get_obj_node_output(obj_node): else: return min(outputs, - key=lambda node: node.evalParm('outputidx')) + key=lambda node: node.evalParm('outputidx')) def get_output_children(output_node, include_sops=True): @@ -682,8 +682,9 @@ def get_output_children(output_node, include_sops=True): for child in output_node.children(): out_list += get_output_children(child, include_sops=include_sops) - elif include_sops and output_node.childTypeCategory() == hou.sopNodeTypeCategory(): - out = get_obj_node_output(output_node) + elif include_sops and \ + output_node.childTypeCategory() == hou.sopNodeTypeCategory(): + out = get_obj_node_output(output_node) if out: out_list += [out] diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py index 36c8ef6d63..25ab362a88 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -14,7 +14,7 @@ import hou class ValidateMeshIsStatic(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): + OptionalPyblishPluginMixin): """Validate mesh is static. It checks if output node is time dependant. From 6c1385e2c94e263f9186609c272ab7c0503485d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 23:49:54 +0200 Subject: [PATCH 092/175] Inherit from correct new style creator --- .../hosts/maya/plugins/create/create_multiverse_usd_over.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py index e1534dd68c..166dbf6515 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd_over.py @@ -6,7 +6,7 @@ from openpype.lib import ( ) -class CreateMultiverseUsdOver(plugin.Creator): +class CreateMultiverseUsdOver(plugin.MayaCreator): """Create Multiverse USD Override""" identifier = "io.openpype.creators.maya.mvusdoverride" From d54111bd75843c4713cac3409c1770594ce634e2 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Tue, 5 Sep 2023 11:59:12 +0300 Subject: [PATCH 093/175] Update docstring Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/api/lib.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 3e51912c26..b108d0d881 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -654,8 +654,19 @@ def get_color_management_preferences(): def get_obj_node_output(obj_node): """Find output node. - get the output node with the minimum 'outputidx' - or the node with display flag. + If the node has any output node return the + output node with the minimum `outputidx`. + When no output is present return the node + with the display flag set. If no output node is + detected then None is returned. + + Arguments: + node (hou.Node): The node to retrieve a single + the output node for. + + Returns: + Optional[hou.Node]: The child output node. + """ outputs = obj_node.subnetOutputs() From 8ceedd7b60c58bf94cc1721f033bff89c0a2b121 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 17:56:09 +0200 Subject: [PATCH 094/175] Draft for implementing a native Maya USD creator next to the Multiverse USD creator --- openpype/hosts/maya/api/plugin.py | 35 ++++ .../maya/plugins/create/create_maya_usd.py | 143 +++++++++++++++ .../plugins/create/create_multiverse_usd.py | 4 + .../hosts/maya/plugins/load/load_maya_usd.py | 100 +++++++++++ .../maya/plugins/publish/extract_maya_usd.py | 168 ++++++++++++++++++ .../plugins/publish/extract_multiverse_usd.py | 2 +- 6 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/maya/plugins/create/create_maya_usd.py create mode 100644 openpype/hosts/maya/plugins/load/load_maya_usd.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_maya_usd.py diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3f383fafb8..058637c8b5 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -129,12 +129,34 @@ class MayaCreatorBase(object): shared_data["maya_cached_legacy_subsets"] = cache_legacy return shared_data + def get_publish_families(self): + """Return families for the instances of this creator. + + Allow a Creator to define multiple families so that a creator can + e.g. specify `usd` and `usdMaya` and another USD creator can also + specify `usd` but apply different extractors like `usdMultiverse`. + + There is no need to override this method if you only have the + primary family defined by the `family` property as that will always + be set. + + Returns: + list: families for instances of this creator + + """ + return [] + def imprint_instance_node(self, node, data): # We never store the instance_node as value on the node since # it's the node name itself data.pop("instance_node", None) + # Don't store `families` since it's up to the creator itself + # to define the initial publish families - not a stored attribute of + # `families` + data.pop("families", None) + # We store creator attributes at the root level and assume they # will not clash in names with `subset`, `task`, etc. and other # default names. This is just so these attributes in many cases @@ -186,6 +208,11 @@ class MayaCreatorBase(object): # Explicitly re-parse the node name node_data["instance_node"] = node + # If the creator plug-in specifies + families = self.get_publish_families() + if families: + node_data["families"] = families + return node_data def _default_collect_instances(self): @@ -230,6 +257,14 @@ class MayaCreator(NewCreator, MayaCreatorBase): if pre_create_data.get("use_selection"): members = cmds.ls(selection=True) + # Allow a Creator to define multiple families + publish_families = self.get_publish_families() + if publish_families: + families = instance_data.setdefault("families", []) + for family in self.get_publish_families(): + if family not in families: + families.append(family) + with lib.undo_chunk(): instance_node = cmds.sets(members, name=subset_name) instance_data["instance_node"] = instance_node diff --git a/openpype/hosts/maya/plugins/create/create_maya_usd.py b/openpype/hosts/maya/plugins/create/create_maya_usd.py new file mode 100644 index 0000000000..298dc6a24f --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_maya_usd.py @@ -0,0 +1,143 @@ +from openpype.hosts.maya.api import plugin, lib +from openpype.lib import ( + BoolDef, + NumberDef, + TextDef, + EnumDef +) + + +class CreateMayaUsd(plugin.MayaCreator): + """Create Maya USD Export""" + + identifier = "io.openpype.creators.maya.mayausd" + label = "Maya USD" + family = "usd" + icon = "cubes" + description = "Create Maya USD Export" + + def get_publish_families(self): + return ["usd", "mayaUsd"] + + def get_instance_attr_defs(self): + + defs = lib.collect_animation_defs() + defs.extend([ + EnumDef("defaultUSDFormat", + label="File format", + items={ + "usdc": "Binary", + "usda": "ASCII" + }, + default="usdc"), + BoolDef("stripNamespaces", + label="Strip Namespaces", + default=True), + BoolDef("mergeTransformAndShape", + label="Merge Transform and Shape", + default=True), + # BoolDef("writeAncestors", + # label="Write Ancestors", + # default=True), + # BoolDef("flattenParentXforms", + # label="Flatten Parent Xforms", + # default=False), + # BoolDef("writeSparseOverrides", + # label="Write Sparse Overrides", + # default=False), + # BoolDef("useMetaPrimPath", + # label="Use Meta Prim Path", + # default=False), + # TextDef("customRootPath", + # label="Custom Root Path", + # default=''), + # TextDef("customAttributes", + # label="Custom Attributes", + # tooltip="Comma-separated list of attribute names", + # default=''), + # TextDef("nodeTypesToIgnore", + # label="Node Types to Ignore", + # tooltip="Comma-separated list of node types to be ignored", + # default=''), + # BoolDef("writeMeshes", + # label="Write Meshes", + # default=True), + # BoolDef("writeCurves", + # label="Write Curves", + # default=True), + # BoolDef("writeParticles", + # label="Write Particles", + # default=True), + # BoolDef("writeCameras", + # label="Write Cameras", + # default=False), + # BoolDef("writeLights", + # label="Write Lights", + # default=False), + # BoolDef("writeJoints", + # label="Write Joints", + # default=False), + # BoolDef("writeCollections", + # label="Write Collections", + # default=False), + # BoolDef("writePositions", + # label="Write Positions", + # default=True), + # BoolDef("writeNormals", + # label="Write Normals", + # default=True), + # BoolDef("writeUVs", + # label="Write UVs", + # default=True), + # BoolDef("writeColorSets", + # label="Write Color Sets", + # default=False), + # BoolDef("writeTangents", + # label="Write Tangents", + # default=False), + # BoolDef("writeRefPositions", + # label="Write Ref Positions", + # default=True), + # BoolDef("writeBlendShapes", + # label="Write BlendShapes", + # default=False), + # BoolDef("writeDisplayColor", + # label="Write Display Color", + # default=True), + # BoolDef("writeSkinWeights", + # label="Write Skin Weights", + # default=False), + # BoolDef("writeMaterialAssignment", + # label="Write Material Assignment", + # default=False), + # BoolDef("writeHardwareShader", + # label="Write Hardware Shader", + # default=False), + # BoolDef("writeShadingNetworks", + # label="Write Shading Networks", + # default=False), + # BoolDef("writeTransformMatrix", + # label="Write Transform Matrix", + # default=True), + # BoolDef("writeUsdAttributes", + # label="Write USD Attributes", + # default=True), + # BoolDef("writeInstancesAsReferences", + # label="Write Instances as References", + # default=False), + # BoolDef("timeVaryingTopology", + # label="Time Varying Topology", + # default=False), + # TextDef("customMaterialNamespace", + # label="Custom Material Namespace", + # default=''), + # NumberDef("numTimeSamples", + # label="Num Time Samples", + # default=1), + # NumberDef("timeSamplesSpan", + # label="Time Samples Span", + # default=0.0), + # + ]) + + return defs diff --git a/openpype/hosts/maya/plugins/create/create_multiverse_usd.py b/openpype/hosts/maya/plugins/create/create_multiverse_usd.py index 0b0ad3bccb..2963d4d5b6 100644 --- a/openpype/hosts/maya/plugins/create/create_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/create/create_multiverse_usd.py @@ -14,6 +14,10 @@ class CreateMultiverseUsd(plugin.MayaCreator): label = "Multiverse USD Asset" family = "usd" icon = "cubes" + description = "Create Multiverse USD Asset" + + def get_publish_families(self): + return ["usd", "mvUsd"] def get_instance_attr_defs(self): diff --git a/openpype/hosts/maya/plugins/load/load_maya_usd.py b/openpype/hosts/maya/plugins/load/load_maya_usd.py new file mode 100644 index 0000000000..26c497768d --- /dev/null +++ b/openpype/hosts/maya/plugins/load/load_maya_usd.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +import maya.cmds as cmds + +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.pipeline.load import get_representation_path_from_context +from openpype.hosts.maya.api.lib import ( + namespaced, + unique_namespace +) +from openpype.hosts.maya.api.pipeline import containerise + + +class MayaUsdLoader(load.LoaderPlugin): + """Read USD data in a Maya USD Proxy""" + + families = ["model", "usd", "pointcache", "animation"] + representations = ["usd", "usda", "usdc", "usdz", "abc"] + + label = "Load USD to Maya Proxy" + order = -1 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, options=None): + asset = context['asset']['name'] + namespace = namespace or unique_namespace( + asset + "_", + prefix="_" if asset[0].isdigit() else "", + suffix="_", + ) + + # Make sure we can load the plugin + cmds.loadPlugin("mayaUsdPlugin", quiet=True) + + path = get_representation_path_from_context(context) + + # Create the shape + cmds.namespace(addNamespace=namespace) + with namespaced(namespace, new=False): + transform = cmds.createNode("transform", + name=name, + skipSelect=True) + proxy = cmds.createNode('mayaUsdProxyShape', + name="{}Shape".format(name), + parent=transform, + skipSelect=True) + + cmds.connectAttr("time1.outTime", "{}.time".format(proxy)) + cmds.setAttr("{}.filePath".format(proxy), path, type="string") + + nodes = [transform, proxy] + self[:] = nodes + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) + + def update(self, container, representation): + # type: (dict, dict) -> None + """Update container with specified representation.""" + node = container['objectName'] + assert cmds.objExists(node), "Missing container" + + members = cmds.sets(node, query=True) or [] + shapes = cmds.ls(members, type="mayaUsdProxyShape") + + path = get_representation_path(representation) + for shape in shapes: + cmds.setAttr("{}.filePath".format(shape), path, type="string") + + cmds.setAttr("{}.representation".format(node), + str(representation["_id"]), + type="string") + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + # type: (dict) -> None + """Remove loaded container.""" + # Delete container and its contents + if cmds.objExists(container['objectName']): + members = cmds.sets(container['objectName'], query=True) or [] + cmds.delete([container['objectName']] + members) + + # Remove the namespace, if empty + namespace = container['namespace'] + if cmds.namespace(exists=namespace): + members = cmds.namespaceInfo(namespace, listNamespace=True) + if not members: + cmds.namespace(removeNamespace=namespace) + else: + self.log.warning("Namespace not deleted because it " + "still has members: %s", namespace) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py new file mode 100644 index 0000000000..3b95037d4c --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -0,0 +1,168 @@ +import os +import six + +from maya import cmds + +import pyblish.api +from openpype.pipeline import publish +from openpype.hosts.maya.api.lib import maintained_selection + + +class ExtractMayaUsd(publish.Extractor): + """Extractor for Maya USD Asset data. + + Upon publish a .usd (or .usdz) asset file will typically be written. + """ + + label = "Extract Maya USD Asset" + hosts = ["maya"] + families = ["mayaUsd"] + + @property + def options(self): + """Overridable options for Maya USD Export + + Given in the following format + - {NAME: EXPECTED TYPE} + + If the overridden option's type does not match, + the option is not included and a warning is logged. + + """ + + # TODO: Support more `mayaUSDExport` parameters + return { + "stripNamespaces": bool, + "mergeTransformAndShape": bool, + "exportDisplayColor": bool, + "exportColorSets": bool, + "exportInstances": bool, + "exportUVs": bool, + "exportVisibility": bool, + "exportComponentTags": bool, + "exportRefsAsInstanceable": bool, + "eulerFilter": bool, + "renderableOnly": bool, + #"worldspace": bool, + } + + @property + def default_options(self): + """The default options for Maya USD Export.""" + + # TODO: Support more `mayaUSDExport` parameters + return { + "stripNamespaces": False, + "mergeTransformAndShape": False, + "exportDisplayColor": False, + "exportColorSets": True, + "exportInstances": True, + "exportUVs": True, + "exportVisibility": True, + "exportComponentTags": True, + "exportRefsAsInstanceable": False, + "eulerFilter": True, + "renderableOnly": False, + #"worldspace": False + } + + def parse_overrides(self, instance, options): + """Inspect data of instance to determine overridden options""" + + for key in instance.data: + if key not in self.options: + continue + + # Ensure the data is of correct type + value = instance.data[key] + if isinstance(value, six.text_type): + value = str(value) + if not isinstance(value, self.options[key]): + self.log.warning( + "Overridden attribute {key} was of " + "the wrong type: {invalid_type} " + "- should have been {valid_type}".format( + key=key, + invalid_type=type(value).__name__, + valid_type=self.options[key].__name__)) + continue + + options[key] = value + + return options + + def filter_members(self, members): + # Can be overridden by inherited classes + return members + + def process(self, instance): + + # Load plugin first + cmds.loadPlugin("mayaUsdPlugin", quiet=True) + + # Define output file path + staging_dir = self.staging_dir(instance) + file_name = "{0}.usd".format(instance.name) + file_path = os.path.join(staging_dir, file_name) + file_path = file_path.replace('\\', '/') + + # Parse export options + options = self.default_options + options = self.parse_overrides(instance, options) + self.log.info("Export options: {0}".format(options)) + + # Perform extraction + self.log.debug("Performing extraction ...") + + members = instance.data("setMembers") + self.log.debug('Collected objects: {}'.format(members)) + members = self.filter_members(members) + if not members: + self.log.error('No members!') + return + + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + + with maintained_selection(): + self.log.debug('Exporting USD: {} / {}'.format(file_path, members)) + cmds.mayaUSDExport(file=file_path, + frameRange=(start, end), + frameStride=instance.data.get("step", 1.0), + exportRoots=members, + **options) + + representation = { + 'name': "usd", + 'ext': "usd", + 'files': file_name, + 'stagingDir': staging_dir + } + instance.data.setdefault("representations", []).append(representation) + + self.log.debug( + "Extracted instance {} to {}".format(instance.name, file_path) + ) + + +class ExtractMayaUsdAnim(ExtractMayaUsd): + """Extractor for Maya USD Animation Sparse Cache data. + + This will extract the sparse cache data from the scene and generate a + USD file with all the animation data. + + Upon publish a .usd sparse cache will be written. + """ + label = "Extract Maya USD Animation Sparse Cache" + families = ["animation", "mayaUsd"] + match = pyblish.api.Subset + + def filter_members(self, members): + out_set = next((i for i in members if i.endswith("out_SET")), None) + + if out_set is None: + self.log.warning("Expecting out_SET") + return None + + members = cmds.ls(cmds.sets(out_set, query=True), long=True) + return members diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py index 4399eacda1..e0a1369556 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py @@ -28,7 +28,7 @@ class ExtractMultiverseUsd(publish.Extractor): label = "Extract Multiverse USD Asset" hosts = ["maya"] - families = ["usd"] + families = ["mvUsd"] scene_type = "usd" file_formats = ["usd", "usda", "usdz"] From 90417a42c38e6197583969d2549de2b3b810568d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 17:56:30 +0200 Subject: [PATCH 095/175] Allow loading USD into Arnold Standin in Maya --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index b5cc4d629b..e1bd1954fa 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -32,8 +32,8 @@ def get_current_session_fps(): class ArnoldStandinLoader(load.LoaderPlugin): """Load as Arnold standin""" - families = ["ass", "animation", "model", "proxyAbc", "pointcache"] - representations = ["ass", "abc"] + families = ["ass", "animation", "model", "proxyAbc", "pointcache", "usd"] + representations = ["ass", "abc", "usda", "usdc", "usd"] label = "Load as Arnold standin" order = -5 From 4456ac86f41e7fa529b5f07d32da6e71f601128d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 5 Sep 2023 17:56:49 +0200 Subject: [PATCH 096/175] :art: add default isort config --- setup.cfg | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 10cca3eb3f..216bae848f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,4 +28,11 @@ omit = /tests directory = ./coverage [tool:pytest] -norecursedirs = repos/* openpype/modules/ftrack/* \ No newline at end of file +norecursedirs = repos/* openpype/modules/ftrack/* + +[isort] +line_length = 79 +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +combine_as_imports = True From 34b15587c11005ca9c2d7db77f44fa18ebb37f4e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 18:00:23 +0200 Subject: [PATCH 097/175] Cosmetics --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index e1bd1954fa..2e1329f201 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -17,6 +17,7 @@ from openpype.hosts.maya.api.lib import ( ) from openpype.hosts.maya.api.pipeline import containerise + def is_sequence(files): sequence = False collections, remainder = clique.assemble(files, minimum_items=1) @@ -29,6 +30,7 @@ def get_current_session_fps(): session_fps = float(legacy_io.Session.get('AVALON_FPS', 25)) return convert_to_maya_fps(session_fps) + class ArnoldStandinLoader(load.LoaderPlugin): """Load as Arnold standin""" From 5c12d9c8621130703d063ec5aba0fef832d479ac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 18:04:21 +0200 Subject: [PATCH 098/175] Remove commented out attribute definitions --- .../maya/plugins/create/create_maya_usd.py | 106 +----------------- 1 file changed, 1 insertion(+), 105 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_maya_usd.py b/openpype/hosts/maya/plugins/create/create_maya_usd.py index 298dc6a24f..554b8169db 100644 --- a/openpype/hosts/maya/plugins/create/create_maya_usd.py +++ b/openpype/hosts/maya/plugins/create/create_maya_usd.py @@ -1,8 +1,6 @@ from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, - NumberDef, - TextDef, EnumDef ) @@ -35,109 +33,7 @@ class CreateMayaUsd(plugin.MayaCreator): default=True), BoolDef("mergeTransformAndShape", label="Merge Transform and Shape", - default=True), - # BoolDef("writeAncestors", - # label="Write Ancestors", - # default=True), - # BoolDef("flattenParentXforms", - # label="Flatten Parent Xforms", - # default=False), - # BoolDef("writeSparseOverrides", - # label="Write Sparse Overrides", - # default=False), - # BoolDef("useMetaPrimPath", - # label="Use Meta Prim Path", - # default=False), - # TextDef("customRootPath", - # label="Custom Root Path", - # default=''), - # TextDef("customAttributes", - # label="Custom Attributes", - # tooltip="Comma-separated list of attribute names", - # default=''), - # TextDef("nodeTypesToIgnore", - # label="Node Types to Ignore", - # tooltip="Comma-separated list of node types to be ignored", - # default=''), - # BoolDef("writeMeshes", - # label="Write Meshes", - # default=True), - # BoolDef("writeCurves", - # label="Write Curves", - # default=True), - # BoolDef("writeParticles", - # label="Write Particles", - # default=True), - # BoolDef("writeCameras", - # label="Write Cameras", - # default=False), - # BoolDef("writeLights", - # label="Write Lights", - # default=False), - # BoolDef("writeJoints", - # label="Write Joints", - # default=False), - # BoolDef("writeCollections", - # label="Write Collections", - # default=False), - # BoolDef("writePositions", - # label="Write Positions", - # default=True), - # BoolDef("writeNormals", - # label="Write Normals", - # default=True), - # BoolDef("writeUVs", - # label="Write UVs", - # default=True), - # BoolDef("writeColorSets", - # label="Write Color Sets", - # default=False), - # BoolDef("writeTangents", - # label="Write Tangents", - # default=False), - # BoolDef("writeRefPositions", - # label="Write Ref Positions", - # default=True), - # BoolDef("writeBlendShapes", - # label="Write BlendShapes", - # default=False), - # BoolDef("writeDisplayColor", - # label="Write Display Color", - # default=True), - # BoolDef("writeSkinWeights", - # label="Write Skin Weights", - # default=False), - # BoolDef("writeMaterialAssignment", - # label="Write Material Assignment", - # default=False), - # BoolDef("writeHardwareShader", - # label="Write Hardware Shader", - # default=False), - # BoolDef("writeShadingNetworks", - # label="Write Shading Networks", - # default=False), - # BoolDef("writeTransformMatrix", - # label="Write Transform Matrix", - # default=True), - # BoolDef("writeUsdAttributes", - # label="Write USD Attributes", - # default=True), - # BoolDef("writeInstancesAsReferences", - # label="Write Instances as References", - # default=False), - # BoolDef("timeVaryingTopology", - # label="Time Varying Topology", - # default=False), - # TextDef("customMaterialNamespace", - # label="Custom Material Namespace", - # default=''), - # NumberDef("numTimeSamples", - # label="Num Time Samples", - # default=1), - # NumberDef("timeSamplesSpan", - # label="Time Samples Span", - # default=0.0), - # + default=True) ]) return defs From d6f2ace99d87d2cb8069c02de72c6f7244a222bf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 18:04:57 +0200 Subject: [PATCH 099/175] Cosmetics --- openpype/hosts/maya/plugins/publish/extract_maya_usd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py index 3b95037d4c..70508042c0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -43,7 +43,7 @@ class ExtractMayaUsd(publish.Extractor): "exportRefsAsInstanceable": bool, "eulerFilter": bool, "renderableOnly": bool, - #"worldspace": bool, + # "worldspace": bool, } @property @@ -63,7 +63,7 @@ class ExtractMayaUsd(publish.Extractor): "exportRefsAsInstanceable": False, "eulerFilter": True, "renderableOnly": False, - #"worldspace": False + # "worldspace": False } def parse_overrides(self, instance, options): From 45f86749e165f3825ccdff4dbe8200180f4369b8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 5 Sep 2023 19:52:13 +0300 Subject: [PATCH 100/175] resolve some comments --- .../create/create_unreal_staticmesh.py | 50 +++------- .../hosts/houdini/plugins/load/load_fbx.py | 2 +- .../houdini/plugins/publish/extract_fbx.py | 45 +++------ ...ut_node.py => validate_fbx_output_node.py} | 3 +- .../publish/validate_mesh_is_static.py | 2 +- .../plugins/publish/validate_subset_name.py | 94 +++++++++++++++++++ .../validate_unreal_staticmesh_naming.py | 21 +---- .../defaults/project_settings/houdini.json | 10 ++ .../schemas/schema_houdini_publish.json | 8 ++ .../server/settings/publish_plugins.py | 16 ++++ 10 files changed, 164 insertions(+), 87 deletions(-) rename openpype/hosts/houdini/plugins/publish/{validate_output_node.py => validate_fbx_output_node.py} (93%) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_subset_name.py diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index ca5e2e8fb4..fc3783c0d1 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -28,8 +28,18 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): # get the created rop node instance_node = hou.node(instance.get("instance_node")) - # get parms - parms = self.get_parms(subset_name, pre_create_data) + # prepare parms + output_path = hou.text.expandString("$HIP/pyblish/{}.fbx".format(subset_name)) + parms = { + "startnode": self.get_selection(), + "sopoutput": output_path, + # vertex cache format + "vcformat": pre_create_data.get("vcformat"), + "convertunits": pre_create_data.get("convertunits"), + # set render range to use frame range start-end frame + "trange": 1, + "createsubnetroot": pre_create_data.get("createsubnetroot") + } # set parms instance_node.setParms(parms) @@ -47,7 +57,7 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): """Add settings for users. """ - attrs = super().get_pre_create_attr_defs() + attrs = super(CreateUnrealStaticMesh, self).get_pre_create_attr_defs() createsubnetroot = BoolDef("createsubnetroot", tooltip="Create an extra root for the " "Export node when it's a " @@ -86,40 +96,6 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): dynamic_data["asset"] = asset_doc["name"] return dynamic_data - def get_parms(self, subset_name, pre_create_data): - """Get parameters values. """ - - # 1. get output path - output_path = hou.text.expandString( - "$HIP/pyblish/{}.fbx".format(subset_name)) - - # 2. get selection - selection = self.get_selection() - - # 3. get Vertex Cache Format - vcformat = pre_create_data.get("vcformat") - - # 4. get convert_units - convertunits = pre_create_data.get("convertunits") - - # 5. get Valid Frame Range - trange = 1 - - # 6. get createsubnetroot - createsubnetroot = pre_create_data.get("createsubnetroot") - - # parms dictionary - parms = { - "startnode": selection, - "sopoutput": output_path, - "vcformat": vcformat, - "convertunits": convertunits, - "trange": trange, - "createsubnetroot": createsubnetroot - } - - return parms - def get_selection(self): """Selection Logic. diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 9c7dbf578e..7e7f0c04e5 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -146,5 +146,5 @@ class FbxLoader(load.LoaderPlugin): # Set new position for children nodes parent_node.layoutChildren() - # Retrun all the nodes + # Return all the nodes return [parent_node, file_node, attribdelete, null] diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index 2a95734ece..e8cd207818 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -21,40 +21,17 @@ class ExtractFBX(publish.Extractor): # get rop node ropnode = hou.node(instance.data.get("instance_node")) + output_node = ropnode.evalParm("sopoutput") + + # get staging_dir and file_name + staging_dir = os.path.normpath(os.path.dirname(output_node)) + file_name = os.path.basename(output_node) # render rop + self.log.debug("Writing FBX '%s' to '%s'",file_name, staging_dir) render_rop(ropnode) - # get required data - file_name, staging_dir = self.get_paths_data(ropnode) - representation = self.get_representation(instance, - file_name, - staging_dir) - - # 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) - - def get_paths_data(self, ropnode): - # Get the filename from the filename parameter - output = ropnode.evalParm("sopoutput") - - staging_dir = os.path.normpath(os.path.dirname(output)) - - file_name = os.path.basename(output) - - self.log.info("Writing FBX '%s' to '%s'" % (file_name, - staging_dir)) - - return file_name, staging_dir - - def get_representation(self, instance, - file_name, staging_dir): - + # prepare representation representation = { "name": "fbx", "ext": "fbx", @@ -67,4 +44,10 @@ class ExtractFBX(publish.Extractor): representation["frameStart"] = instance.data["frameStart"] representation["frameEnd"] = instance.data["frameEnd"] - return representation + # 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/openpype/hosts/houdini/plugins/publish/validate_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py similarity index 93% rename from openpype/hosts/houdini/plugins/publish/validate_output_node.py rename to openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py index 99a6cda077..503a3bb3c1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -9,12 +9,13 @@ from openpype.hosts.houdini.api.action import ( import hou -class ValidateOutputNode(pyblish.api.InstancePlugin): +class ValidateFBXOutputNode(pyblish.api.InstancePlugin): """Validate the instance Output Node. This will ensure: - The Output Node Path is set. - The Output Node Path refers to an existing object. + - The Output Node is a Sop or Obj node. """ order = pyblish.api.ValidatorOrder diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py index 25ab362a88..4d0904eb53 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -17,7 +17,7 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate mesh is static. - It checks if output node is time dependant. + It checks if output node is time dependent. """ families = ["staticMesh"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py new file mode 100644 index 0000000000..299729a6e8 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""Validator for correct naming of Static Meshes.""" +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ( + ValidateContentsOrder, + RepairAction, +) +from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.pipeline.create import get_subset_name + +import hou + + +class FixSubsetNameAction(RepairAction): + label = "Fix Subset Name" + + +class ValidateSubsetName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate Subset name. + + """ + + families = ["staticMesh"] + hosts = ["houdini"] + label = "Validate Subset Name" + order = ValidateContentsOrder + 0.1 + actions = [FixSubsetNameAction, SelectInvalidAction] + + optional = True + + + def process(self, instance): + + if not self.is_active(instance.data): + return + + invalid = self.get_invalid(instance) + if invalid: + nodes = [n.path() for n in invalid] + raise PublishValidationError( + "See log for details. " + "Invalid nodes: {0}".format(nodes) + ) + + @classmethod + def get_invalid(cls, instance): + + invalid = [] + + rop_node = hou.node(instance.data["instance_node"]) + + # Check subset name + subset_name = get_subset_name( + family=instance.data["family"], + variant=instance.data["variant"], + task_name=instance.data["task"], + asset_doc=instance.data["assetEntity"], + dynamic_data={"asset":instance.data["asset"]} + ) + + if instance.data.get("subset") != subset_name: + invalid.append(rop_node) + cls.log.error( + "Invalid subset name on rop node '%s' should be '%s'.", + rop_node.path(), subset_name + ) + + return invalid + + @classmethod + def repair(cls, instance): + rop_node = hou.node(instance.data["instance_node"]) + + # Check subset name + subset_name = get_subset_name( + family=instance.data["family"], + variant=instance.data["variant"], + task_name=instance.data["task"], + asset_doc=instance.data["assetEntity"], + dynamic_data={"asset":instance.data["asset"]} + ) + + instance.data["subset"] = subset_name + rop_node.parm("subset").set(subset_name) + + cls.log.debug( + "Subset name on rop node '%s' has been set to '%s'.", + rop_node.path(), subset_name + ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index 5558b43258..791db8198f 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -35,9 +35,12 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, actions = [SelectInvalidAction] optional = True + collision_prefixes = [] + static_mesh_prefix = "" @classmethod def apply_settings(cls, project_settings, system_settings): + settings = ( project_settings["houdini"]["create"]["CreateUnrealStaticMesh"] ) @@ -51,7 +54,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] + nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes) @@ -70,7 +73,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, ) return - if not rop_node.evalParm('buildfrompath'): + if rop_node.evalParm("buildfrompath"): # This validator doesn't support naming check if # building hierarchy from path' is used cls.log.info( @@ -91,18 +94,4 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, ) break - # Check subset name - subset_name = "{}_{}{}".format( - cls.static_mesh_prefix, - instance.data["asset"], - instance.data.get("variant", "") - ) - - if instance.data.get("subset") != subset_name: - invalid.append(rop_node) - cls.log.error( - "Invalid subset name on rop node '%s' should be '%s'.", - rop_node.path(), subset_name - ) - return invalid diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 65f13fa1ab..7673725831 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -111,6 +111,16 @@ "optional": true, "active": true }, + "ValidateSubsetName": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateMeshIsStatic": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateUnrealStaticMeshName": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index 4339f86db6..670b1a0bc2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -44,6 +44,14 @@ "key": "ValidateContainers", "label": "ValidateContainers" }, + { + "key": "ValidateSubsetName", + "label": "Validate Subset Name" + }, + { + "key": "ValidateMeshIsStatic", + "label": "Validate Mesh is Static" + }, { "key": "ValidateUnrealStaticMeshName", "label": "Validate Unreal Static Mesh Name" diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 335751e5f9..b3e47d6948 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -164,6 +164,12 @@ class PublishPluginsModel(BaseSettingsModel): ValidateContainers: ValidateContainersModel = Field( default_factory=ValidateContainersModel, title="Validate Latest Containers.") + ValidateSubsetName: ValidateContainersModel = Field( + default_factory=ValidateContainersModel, + title="Validate Subset Name.") + ValidateMeshIsStatic: ValidateContainersModel = Field( + default_factory=ValidateContainersModel, + title="Validate Mesh is Static.") ValidateUnrealStaticMeshName: ValidateContainersModel = Field( default_factory=ValidateContainersModel, title="Validate Unreal Static Mesh Name.") @@ -187,6 +193,16 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ValidateSubsetName": { + "enabled": True, + "optional": True, + "active": True + }, + "ValidateMeshIsStatic": { + "enabled": True, + "optional": True, + "active": True + }, "ValidateUnrealStaticMeshName": { "enabled": True, "optional": True, From ebae3cf03ef443b51296eb160a06d26e3c4ba637 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 5 Sep 2023 19:54:31 +0300 Subject: [PATCH 101/175] remove white spaces --- openpype/hosts/houdini/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index b108d0d881..7d3edbc707 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -654,19 +654,19 @@ def get_color_management_preferences(): def get_obj_node_output(obj_node): """Find output node. - If the node has any output node return the + If the node has any output node return the output node with the minimum `outputidx`. When no output is present return the node with the display flag set. If no output node is detected then None is returned. - + Arguments: node (hou.Node): The node to retrieve a single the output node for. - + Returns: Optional[hou.Node]: The child output node. - + """ outputs = obj_node.subnetOutputs() From f00d76c0330f37eff8956cfd633484e1cd607ec5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 5 Sep 2023 19:58:13 +0300 Subject: [PATCH 102/175] resolve hound --- .../hosts/houdini/plugins/create/create_unreal_staticmesh.py | 5 ++++- openpype/hosts/houdini/plugins/publish/extract_fbx.py | 4 ++-- .../hosts/houdini/plugins/publish/validate_subset_name.py | 5 ++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py index fc3783c0d1..2f92def54a 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py @@ -29,7 +29,10 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) # prepare parms - output_path = hou.text.expandString("$HIP/pyblish/{}.fbx".format(subset_name)) + output_path = hou.text.expandString( + "$HIP/pyblish/{}.fbx".format(subset_name) + ) + parms = { "startnode": self.get_selection(), "sopoutput": output_path, diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index e8cd207818..dd61e68f3b 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -28,7 +28,7 @@ class ExtractFBX(publish.Extractor): file_name = os.path.basename(output_node) # render rop - self.log.debug("Writing FBX '%s' to '%s'",file_name, staging_dir) + self.log.debug("Writing FBX '%s' to '%s'", file_name, staging_dir) render_rop(ropnode) # prepare representation @@ -36,7 +36,7 @@ class ExtractFBX(publish.Extractor): "name": "fbx", "ext": "fbx", "files": file_name, - "stagingDir": staging_dir, + "stagingDir": staging_dir } # A single frame may also be rendered without start/end frame. diff --git a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py index 299729a6e8..bb3648f361 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_subset_name.py +++ b/openpype/hosts/houdini/plugins/publish/validate_subset_name.py @@ -33,7 +33,6 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, optional = True - def process(self, instance): if not self.is_active(instance.data): @@ -60,7 +59,7 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, variant=instance.data["variant"], task_name=instance.data["task"], asset_doc=instance.data["assetEntity"], - dynamic_data={"asset":instance.data["asset"]} + dynamic_data={"asset": instance.data["asset"]} ) if instance.data.get("subset") != subset_name: @@ -82,7 +81,7 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, variant=instance.data["variant"], task_name=instance.data["task"], asset_doc=instance.data["assetEntity"], - dynamic_data={"asset":instance.data["asset"]} + dynamic_data={"asset": instance.data["asset"]} ) instance.data["subset"] = subset_name From 7f78a95559870a79f2b20c456ea4ec8a3419e30d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 22:04:38 +0200 Subject: [PATCH 103/175] Export correct file type (ascii vs binary) based on instance setting --- openpype/hosts/maya/plugins/publish/extract_maya_usd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py index 70508042c0..32730d2963 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -32,6 +32,7 @@ class ExtractMayaUsd(publish.Extractor): # TODO: Support more `mayaUSDExport` parameters return { + "defaultUSDFormat": str, "stripNamespaces": bool, "mergeTransformAndShape": bool, "exportDisplayColor": bool, @@ -52,6 +53,7 @@ class ExtractMayaUsd(publish.Extractor): # TODO: Support more `mayaUSDExport` parameters return { + "defaultUSDFormat": "usdc", "stripNamespaces": False, "mergeTransformAndShape": False, "exportDisplayColor": False, From 93e3e310295c758f236e208dc7ed130eee4a4335 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 22:05:02 +0200 Subject: [PATCH 104/175] Log message as debug --- openpype/hosts/maya/plugins/publish/extract_maya_usd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py index 32730d2963..4a5fcc3366 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -111,7 +111,7 @@ class ExtractMayaUsd(publish.Extractor): # Parse export options options = self.default_options options = self.parse_overrides(instance, options) - self.log.info("Export options: {0}".format(options)) + self.log.debug("Export options: {0}".format(options)) # Perform extraction self.log.debug("Performing extraction ...") From f4a0ab45e4a9f50d60488fb8cfc7ccaae7f016a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 23:10:32 +0200 Subject: [PATCH 105/175] Allow exporting custom attributes with `mayaUSDExport` --- .../maya/plugins/create/create_maya_usd.py | 16 ++- .../maya/plugins/publish/extract_maya_usd.py | 113 +++++++++++++++++- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_maya_usd.py b/openpype/hosts/maya/plugins/create/create_maya_usd.py index 554b8169db..f05f155dd9 100644 --- a/openpype/hosts/maya/plugins/create/create_maya_usd.py +++ b/openpype/hosts/maya/plugins/create/create_maya_usd.py @@ -1,7 +1,8 @@ from openpype.hosts.maya.api import plugin, lib from openpype.lib import ( BoolDef, - EnumDef + EnumDef, + TextDef ) @@ -33,7 +34,18 @@ class CreateMayaUsd(plugin.MayaCreator): default=True), BoolDef("mergeTransformAndShape", label="Merge Transform and Shape", - default=True) + default=True), + BoolDef("includeUserDefinedAttributes", + label="Include User Defined Attributes", + default=False), + TextDef("attr", + label="Custom Attributes", + default="", + placeholder="attr1, attr2"), + TextDef("attrPrefix", + label="Custom Attributes Prefix", + default="", + placeholder="prefix1, prefix2") ]) return defs diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py index 4a5fcc3366..dfdea6868f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -1,5 +1,7 @@ import os import six +import json +import contextlib from maya import cmds @@ -8,6 +10,88 @@ from openpype.pipeline import publish from openpype.hosts.maya.api.lib import maintained_selection +@contextlib.contextmanager +def usd_export_attributes(nodes, attrs=None, attr_prefixes=None, mapping=None): + """Define attributes for the given nodes that should be exported. + + MayaUSDExport will export custom attributes if the Maya node has a + string attribute `USD_UserExportedAttributesJson` that provides an + export mapping for the maya attributes. This context manager will try + to autogenerate such an attribute during the export to include attributes + for the export. + + """ + # todo: this might be better done with a custom export chaser + # see `chaser` argument for `mayaUSDExport` + + import maya.api.OpenMaya as om + + if not attrs and not attr_prefixes: + # context manager does nothing + yield + return + + if attrs is None: + attrs = [] + if attr_prefixes is None: + attr_prefixes = [] + if mapping is None: + mapping = {} + + usd_json_attr = "USD_UserExportedAttributesJson" + strings = attrs + ["{}*".format(prefix) for prefix in attr_prefixes] + context_state = {} + for node in set(nodes): + node_attrs = cmds.listAttr(node, st=strings) + if not node_attrs: + # Nothing to do for this node + continue + + node_attr_data = {} + for node_attr in set(node_attrs): + node_attr_data[node_attr] = mapping.get(node_attr, {}) + + if cmds.attributeQuery(usd_json_attr, node=node, exists=True): + existing_node_attr_value = cmds.getAttr( + "{}.{}".format(node, usd_json_attr) + ) + if existing_node_attr_value and existing_node_attr_value != "{}": + # Any existing attribute mappings in an existing + # `USD_UserExportedAttributesJson` attribute always take + # precedence over what this function tries to imprint + existing_node_attr_data = json.loads(existing_node_attr_value) + node_attr_data.update(existing_node_attr_data) + + context_state[node] = json.dumps(node_attr_data) + + sel = om.MSelectionList() + dg_mod = om.MDGModifier() + fn_string = om.MFnStringData() + fn_typed = om.MFnTypedAttribute() + try: + for node, value in context_state.items(): + data = fn_string.create(value) + sel.clear() + if cmds.attributeQuery(usd_json_attr, node=node, exists=True): + # Set the attribute value + sel.add("{}.{}".format(node, usd_json_attr)) + plug = sel.getPlug(0) + dg_mod.newPlugValue(plug, data) + else: + # Create attribute with the value as default value + sel.add(node) + node_obj = sel.getDependNode(0) + attr_obj = fn_typed.create(usd_json_attr, + usd_json_attr, + om.MFnData.kString, + data) + dg_mod.addAttribute(node_obj, attr_obj) + dg_mod.doIt() + yield + finally: + dg_mod.undoIt() + + class ExtractMayaUsd(publish.Extractor): """Extractor for Maya USD Asset data. @@ -126,13 +210,30 @@ class ExtractMayaUsd(publish.Extractor): start = instance.data["frameStartHandle"] end = instance.data["frameEndHandle"] + def parse_attr_str(attr_str): + result = list() + for attr in attr_str.split(","): + attr = attr.strip() + if not attr: + continue + result.append(attr) + return result + + attrs = parse_attr_str(instance.data.get("attr", "")) + attrs += instance.data.get("userDefinedAttributes", []) + attrs += ["cbId"] + attr_prefixes = parse_attr_str(instance.data.get("attrPrefix", "")) + + self.log.debug('Exporting USD: {} / {}'.format(file_path, members)) with maintained_selection(): - self.log.debug('Exporting USD: {} / {}'.format(file_path, members)) - cmds.mayaUSDExport(file=file_path, - frameRange=(start, end), - frameStride=instance.data.get("step", 1.0), - exportRoots=members, - **options) + with usd_export_attributes(instance[:], + attrs=attrs, + attr_prefixes=attr_prefixes): + cmds.mayaUSDExport(file=file_path, + frameRange=(start, end), + frameStride=instance.data.get("step", 1.0), + exportRoots=members, + **options) representation = { 'name': "usd", From 17d494c1a2cb5229b764daeb415010f97fe46ad4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 23:13:39 +0200 Subject: [PATCH 106/175] Add logic to collect user defined attributes and merge logic with pointcache and animation family + optimize the query by doing only one `cmds.listAttr` call --- .../maya/plugins/publish/collect_animation.py | 14 ------- .../plugins/publish/collect_pointcache.py | 15 ------- .../collect_user_defined_attributes.py | 39 +++++++++++++++++++ 3 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index 8f523f770b..26a0a01c8b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -58,17 +58,3 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): if instance.data.get("farm"): instance.data["families"].append("publish.farm") - # Collect user defined attributes. - if not instance.data.get("includeUserDefinedAttributes", False): - return - - user_defined_attributes = set() - for node in hierarchy: - attrs = cmds.listAttr(node, userDefined=True) or list() - shapes = cmds.listRelatives(node, shapes=True) or list() - for shape in shapes: - attrs.extend(cmds.listAttr(shape, userDefined=True) or list()) - - user_defined_attributes.update(attrs) - - instance.data["userDefinedAttributes"] = list(user_defined_attributes) diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index bb9065792f..5578a57f31 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -45,18 +45,3 @@ class CollectPointcache(pyblish.api.InstancePlugin): if proxy_set: instance.remove(proxy_set) instance.data["setMembers"].remove(proxy_set) - - # Collect user defined attributes. - if not instance.data.get("includeUserDefinedAttributes", False): - return - - user_defined_attributes = set() - for node in instance: - attrs = cmds.listAttr(node, userDefined=True) or list() - shapes = cmds.listRelatives(node, shapes=True) or list() - for shape in shapes: - attrs.extend(cmds.listAttr(shape, userDefined=True) or list()) - - user_defined_attributes.update(attrs) - - instance.data["userDefinedAttributes"] = list(user_defined_attributes) diff --git a/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py b/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py new file mode 100644 index 0000000000..4d0790ad7c --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py @@ -0,0 +1,39 @@ +from maya import cmds + +import pyblish.api + + +class CollectUserDefinedAttributes(pyblish.api.InstancePlugin): + """Collect user defined attributes for nodes in instance.""" + + order = pyblish.api.CollectorOrder + 0.4 + families = ["pointcache", "animation", "usd"] + label = "Collect User Defined Attributes" + hosts = ["maya"] + + def process(self, instance): + + # Collect user defined attributes. + if not instance.data.get("includeUserDefinedAttributes", False): + return + + if "out_hierarchy" in instance.data: + # animation family + nodes = instance.data["out_hierarchy"] + else: + nodes = instance[:] + if not nodes: + return + + shapes = cmds.listRelatives(nodes, shapes=True, fullPath=True) or [] + nodes = set(nodes).union(shapes) + + attrs = cmds.listAttr(list(nodes), userDefined=True) or [] + user_defined_attributes = list(sorted(set(attrs))) + instance.data["userDefinedAttributes"] = user_defined_attributes + + self.log.debug( + "Collected user defined attributes: {}".format( + ", ".join(user_defined_attributes) + ) + ) From 4a861a6bfcdc06026fe4146e162523a1dc33cb58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 23:29:46 +0200 Subject: [PATCH 107/175] Improve docstring --- .../maya/plugins/publish/extract_maya_usd.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py index dfdea6868f..09bbf01831 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -20,6 +20,26 @@ def usd_export_attributes(nodes, attrs=None, attr_prefixes=None, mapping=None): to autogenerate such an attribute during the export to include attributes for the export. + Arguments: + nodes (List[str]): Nodes to process. + attrs (Optional[List[str]]): Full name of attributes to include. + attr_prefixes (Optional[List[str]]): Prefixes of attributes to include. + mapping (Optional[Dict[Dict]]): A mapping per attribute name for the + conversion to a USD attribute, including renaming, defining type, + converting attribute precision, etc. This match the usual + `USD_UserExportedAttributesJson` json mapping of `mayaUSDExport`. + When no mapping provided for an attribute it will use `{}` as + value. + + Examples: + >>> with usd_export_attributes( + >>> ["pCube1"], attrs="myDoubleAttributeAsFloat", mapping={ + >>> "myDoubleAttributeAsFloat": { + >>> "usdAttrName": "my:namespace:attrib", + >>> "translateMayaDoubleToUsdSinglePrecision": True, + >>> } + >>> }) + """ # todo: this might be better done with a custom export chaser # see `chaser` argument for `mayaUSDExport` From a61f7ac7998453dad917d01e43fa134320a8e7e5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 00:02:39 +0200 Subject: [PATCH 108/175] Add support for Look Assigner to assign looks in `aiStandin` for USD files based on `cbId` attributes in the USD file. - For this to currently work the transform and shape should *not* be merged into a single Prim inside USD because otherwise the unique `cbId` between Transform and Shape node will be lost. --- .../tools/mayalookassigner/arnold_standin.py | 8 ++++ .../hosts/maya/tools/mayalookassigner/usd.py | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 openpype/hosts/maya/tools/mayalookassigner/usd.py diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 0ce2b21dcd..076b0047bb 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -10,6 +10,7 @@ from openpype.client import get_last_version_by_subset_name from openpype.hosts.maya import api from . import lib from .alembic import get_alembic_ids_cache +from .usd import is_usd_lib_supported, get_usd_ids_cache log = logging.getLogger(__name__) @@ -74,6 +75,13 @@ def get_nodes_by_id(standin): # Support alembic files directly return get_alembic_ids_cache(path) + elif ( + is_usd_lib_supported and + any(path.endswith(ext) for ext in [".usd", ".usda", ".usdc"]) + ): + # Support usd files directly + return get_usd_ids_cache(path) + json_path = None for f in os.listdir(os.path.dirname(path)): if f.endswith(".json"): diff --git a/openpype/hosts/maya/tools/mayalookassigner/usd.py b/openpype/hosts/maya/tools/mayalookassigner/usd.py new file mode 100644 index 0000000000..beecbd531a --- /dev/null +++ b/openpype/hosts/maya/tools/mayalookassigner/usd.py @@ -0,0 +1,38 @@ +from collections import defaultdict + +try: + from pxr import Usd + is_usd_lib_supported = True +except ImportError: + is_usd_lib_supported = False + + +def get_usd_ids_cache(path): + # type: (str) -> dict + """Build a id to node mapping in a USD file. + + Nodes without IDs are ignored. + + Returns: + dict: Mapping of id to nodes in the USD file. + + """ + if not is_usd_lib_supported: + raise RuntimeError("No pxr.Usd python library available.") + + stage = Usd.Stage.Open(path) + ids = {} + for prim in stage.Traverse(): + attr = prim.GetAttribute("userProperties:cbId") + if not attr.IsValid(): + continue + path = str(prim.GetPath()) + value = attr.Get() + if not value: + continue + ids[path] = value + + cache = defaultdict(list) + for path, value in ids.items(): + cache[value].append(path) + return dict(cache) From b953391f43592fedf18069093c21e33a35136871 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 00:07:39 +0200 Subject: [PATCH 109/175] Only get path if a value is found --- openpype/hosts/maya/tools/mayalookassigner/usd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/usd.py b/openpype/hosts/maya/tools/mayalookassigner/usd.py index beecbd531a..6b5cb2f0f5 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/usd.py +++ b/openpype/hosts/maya/tools/mayalookassigner/usd.py @@ -26,10 +26,10 @@ def get_usd_ids_cache(path): attr = prim.GetAttribute("userProperties:cbId") if not attr.IsValid(): continue - path = str(prim.GetPath()) value = attr.Get() if not value: continue + path = str(prim.GetPath()) ids[path] = value cache = defaultdict(list) From 11cd5a874eee0877c16c9cfde8cb8b74f1630364 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 00:13:34 +0200 Subject: [PATCH 110/175] Make sure to run after `CollectPointcache` and `CollectAnimation` --- .../maya/plugins/publish/collect_user_defined_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py b/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py index 4d0790ad7c..16fef2e168 100644 --- a/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py +++ b/openpype/hosts/maya/plugins/publish/collect_user_defined_attributes.py @@ -6,7 +6,7 @@ import pyblish.api class CollectUserDefinedAttributes(pyblish.api.InstancePlugin): """Collect user defined attributes for nodes in instance.""" - order = pyblish.api.CollectorOrder + 0.4 + order = pyblish.api.CollectorOrder + 0.45 families = ["pointcache", "animation", "usd"] label = "Collect User Defined Attributes" hosts = ["maya"] From 8f5b09af2110bf93169bc69f91989981e8f3ad99 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 15:58:09 +0800 Subject: [PATCH 111/175] adding back the missing function after resolving conflict --- openpype/hosts/max/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index c4167dfe0a..23b89a9ac2 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -15,6 +15,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib +from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB from openpype.hosts.max import MAX_HOST_DIR from pymxs import runtime as rt # noqa From 862907079c7c143854e864270878afb40449060e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 15:23:11 +0200 Subject: [PATCH 112/175] Support special creator attributes in Maya's flattened `creator_attributes` structure that are not convertable to Maya native attribute types (list, tuple, dict), like e.g. `EnumDef` with `multiselection=True` --- openpype/hosts/maya/api/plugin.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 058637c8b5..770767fc7d 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -161,8 +161,17 @@ class MayaCreatorBase(object): # will not clash in names with `subset`, `task`, etc. and other # default names. This is just so these attributes in many cases # are still editable in the maya UI by artists. - # pop to move to end of dict to sort attributes last on the node + # note: pop to move to end of dict to sort attributes last on the node creator_attributes = data.pop("creator_attributes", {}) + + # We only flatten value types which `imprint` function supports + json_creator_attributes = {} + for key, value in dict(creator_attributes).items(): + if isinstance(value, (list, tuple, dict)): + creator_attributes.pop(key) + json_creator_attributes[key] = value + + # Flatten remaining creator attributes to the node itself data.update(creator_attributes) # We know the "publish_attributes" will be complex data of @@ -172,6 +181,10 @@ class MayaCreatorBase(object): data.pop("publish_attributes", {}) ) + # Persist the non-flattened creator attributes (special value types, + # like multiselection EnumDef) + data["creator_attributes"] = json.dumps(json_creator_attributes) + # Since we flattened the data structure for creator attributes we want # to correctly detect which flattened attributes should end back in the # creator attributes when reading the data from the node, so we store @@ -192,15 +205,22 @@ class MayaCreatorBase(object): # being read as 'data' node_data.pop("cbId", None) + # Make sure we convert any creator attributes from the json string + creator_attributes = node_data.get("creator_attributes") + if creator_attributes: + node_data["creator_attributes"] = json.loads(creator_attributes) + else: + node_data["creator_attributes"] = {} + # Move the relevant attributes into "creator_attributes" that # we flattened originally - node_data["creator_attributes"] = {} creator_attribute_keys = node_data.pop("__creator_attributes_keys", "").split(",") for key in creator_attribute_keys: if key in node_data: node_data["creator_attributes"][key] = node_data.pop(key) + # Make sure we convert any publish attributes from the json string publish_attributes = node_data.get("publish_attributes") if publish_attributes: node_data["publish_attributes"] = json.loads(publish_attributes) From 48ab18f5431a42bdd185f25a7bedab5b45770ad9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 21:25:50 +0800 Subject: [PATCH 113/175] Ondrej's comment on changing the raise exception to LoaderError --- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index d3b7c61ff8..d87d6b1bfe 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -1,10 +1,12 @@ import os from openpype.pipeline import load, get_representation_path +from openpype.pipeline.load import LoaderError from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, update_custom_attribute_data ) + from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, @@ -28,8 +30,8 @@ class OxAbcLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): plugin_list = get_plugins() if "ephere.plugins.autodesk.max.ornatrix.dlo" not in plugin_list: - raise RuntimeError("Ornatrix plugin not " - "found/installed in Max yet..") + raise LoaderError("Ornatrix plugin not " + "found/installed in Max yet..") file_path = os.path.normpath(self.filepath_from_context(context)) rt.AlembicImport.ImportToRoot = True From a0f7951ea3927de61722d299fb77239abfd9e033 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 16:02:46 +0200 Subject: [PATCH 114/175] Add job context parameter to USD publisher E.g. You can now directly export with a `Arnold` job context (if it's registered) so that the USD export is Arnold supported and directly renderable with shaders/render attributes by Arnold renderer. --- .../maya/plugins/create/create_maya_usd.py | 25 ++++++++++++++++++- .../maya/plugins/publish/extract_maya_usd.py | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_maya_usd.py b/openpype/hosts/maya/plugins/create/create_maya_usd.py index f05f155dd9..38de218bfb 100644 --- a/openpype/hosts/maya/plugins/create/create_maya_usd.py +++ b/openpype/hosts/maya/plugins/create/create_maya_usd.py @@ -5,6 +5,8 @@ from openpype.lib import ( TextDef ) +from maya import cmds + class CreateMayaUsd(plugin.MayaCreator): """Create Maya USD Export""" @@ -15,11 +17,28 @@ class CreateMayaUsd(plugin.MayaCreator): icon = "cubes" description = "Create Maya USD Export" + cache = {} + def get_publish_families(self): return ["usd", "mayaUsd"] def get_instance_attr_defs(self): + if "jobContextItems" not in self.cache: + # Query once instead of per instance + job_context_items = {} + try: + cmds.loadPlugin("mayaUsdPlugin", quiet=True) + job_context_items = { + cmds.mayaUSDListJobContexts(jobContext=name): name + for name in cmds.mayaUSDListJobContexts(export=True) + } + except RuntimeError: + # Likely `mayaUsdPlugin` plug-in not available + self.log.warning("Unable to retrieve available job " + "contexts for `mayaUsdPlugin` exports") + self.cache["jobContextItems"] = job_context_items + defs = lib.collect_animation_defs() defs.extend([ EnumDef("defaultUSDFormat", @@ -45,7 +64,11 @@ class CreateMayaUsd(plugin.MayaCreator): TextDef("attrPrefix", label="Custom Attributes Prefix", default="", - placeholder="prefix1, prefix2") + placeholder="prefix1, prefix2"), + EnumDef("jobContext", + label="Job Context", + items=self.cache["jobContextItems"], + multiselection=True), ]) return defs diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py index 09bbf01831..8c32ac1e39 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_usd.py @@ -148,6 +148,7 @@ class ExtractMayaUsd(publish.Extractor): "exportRefsAsInstanceable": bool, "eulerFilter": bool, "renderableOnly": bool, + "jobContext": (list, None) # optional list # "worldspace": bool, } @@ -169,6 +170,7 @@ class ExtractMayaUsd(publish.Extractor): "exportRefsAsInstanceable": False, "eulerFilter": True, "renderableOnly": False, + "jobContext": None # "worldspace": False } From 388ae935e1e37d5771816defd4983bb1d6092bd2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 16:09:51 +0200 Subject: [PATCH 115/175] By default load proxy as *not* `Shareable` --- openpype/hosts/maya/plugins/load/load_maya_usd.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_maya_usd.py b/openpype/hosts/maya/plugins/load/load_maya_usd.py index 26c497768d..2fb1a625a5 100644 --- a/openpype/hosts/maya/plugins/load/load_maya_usd.py +++ b/openpype/hosts/maya/plugins/load/load_maya_usd.py @@ -51,6 +51,14 @@ class MayaUsdLoader(load.LoaderPlugin): cmds.connectAttr("time1.outTime", "{}.time".format(proxy)) cmds.setAttr("{}.filePath".format(proxy), path, type="string") + # By default, we force the proxy to not use a shared stage because + # when doing so Maya will quite easily allow to save into the + # loaded usd file. Since we are loading published files we want to + # avoid altering them. Unshared stages also save their edits into + # the workfile as an artist might expect it to do. + cmds.setAttr("{}.shareStage".format(proxy), False) + # cmds.setAttr("{}.shareStage".format(proxy), lock=True) + nodes = [transform, proxy] self[:] = nodes From f1267546d23040eb778b9f8ead135d6de36184d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 18:02:24 +0200 Subject: [PATCH 116/175] Avoid error if no job contexts are available --- openpype/hosts/maya/plugins/create/create_maya_usd.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_maya_usd.py b/openpype/hosts/maya/plugins/create/create_maya_usd.py index 38de218bfb..77a96dd4cb 100644 --- a/openpype/hosts/maya/plugins/create/create_maya_usd.py +++ b/openpype/hosts/maya/plugins/create/create_maya_usd.py @@ -31,12 +31,17 @@ class CreateMayaUsd(plugin.MayaCreator): cmds.loadPlugin("mayaUsdPlugin", quiet=True) job_context_items = { cmds.mayaUSDListJobContexts(jobContext=name): name - for name in cmds.mayaUSDListJobContexts(export=True) + for name in cmds.mayaUSDListJobContexts(export=True) or [] } except RuntimeError: # Likely `mayaUsdPlugin` plug-in not available self.log.warning("Unable to retrieve available job " "contexts for `mayaUsdPlugin` exports") + + if not job_context_items: + # enumdef multiselection may not be empty + job_context_items = [""] + self.cache["jobContextItems"] = job_context_items defs = lib.collect_animation_defs() From d5823ab556e016c424b7c4bdd12ddac646d745be Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 18:08:51 +0200 Subject: [PATCH 117/175] Add a few tooltips --- .../maya/plugins/create/create_maya_usd.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_maya_usd.py b/openpype/hosts/maya/plugins/create/create_maya_usd.py index 77a96dd4cb..cc9a14bd3a 100644 --- a/openpype/hosts/maya/plugins/create/create_maya_usd.py +++ b/openpype/hosts/maya/plugins/create/create_maya_usd.py @@ -55,12 +55,29 @@ class CreateMayaUsd(plugin.MayaCreator): default="usdc"), BoolDef("stripNamespaces", label="Strip Namespaces", + tooltip=( + "Remove namespaces during export. By default, " + "namespaces are exported to the USD file in the " + "following format: nameSpaceExample_pPlatonic1" + ), default=True), BoolDef("mergeTransformAndShape", label="Merge Transform and Shape", + tooltip=( + "Combine Maya transform and shape into a single USD" + "prim that has transform and geometry, for all" + " \"geometric primitives\" (gprims).\n" + "This results in smaller and faster scenes. Gprims " + "will be \"unpacked\" back into transform and shape " + "nodes when imported into Maya from USD." + ), default=True), BoolDef("includeUserDefinedAttributes", label="Include User Defined Attributes", + tooltip=( + "Whether to include all custom maya attributes found " + "on nodes as metadata (userProperties) in USD." + ), default=False), TextDef("attr", label="Custom Attributes", @@ -73,6 +90,12 @@ class CreateMayaUsd(plugin.MayaCreator): EnumDef("jobContext", label="Job Context", items=self.cache["jobContextItems"], + tooltip=( + "Specifies an additional export context to handle.\n" + "These usually contain extra schemas, primitives,\n" + "and materials that are to be exported for a " + "specific\ntask, a target renderer for example." + ), multiselection=True), ]) From d2140c36192d353d68c287af6385fac1b8fa68d7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 6 Sep 2023 21:37:20 +0300 Subject: [PATCH 118/175] match maya render mask in houdini --- openpype/hosts/houdini/api/lib.py | 53 ++++++++++++ openpype/hosts/houdini/api/pipeline.py | 2 + .../plugins/inventory/set_asset_resolution.py | 24 ++++++ .../hosts/houdini/plugins/load/load_camera.py | 81 +++++++++++-------- 4 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 75c7ff9fee..c6672cf969 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -649,3 +649,56 @@ def get_color_management_preferences(): "display": hou.Color.ocio_defaultDisplay(), "view": hou.Color.ocio_defaultView() } + + +def get_current_asset_doc(): + """Get asset document of the current asset. """ + + project_name = get_current_project_name() + asset_name = get_current_asset_name() + asset_doc = get_asset_by_name(project_name, asset_name) + + return asset_doc + + +def get_resolution_from_data(doc): + if not doc or "data" not in doc: + print("Entered document is not valid. \"{}\"".format(str(doc))) + return None + + resolution_width = doc["data"].get("resolutionWidth") + resolution_height = doc["data"].get("resolutionHeight") + + # Make sure both width and height are set + if resolution_width is None or resolution_height is None: + print("No resolution information found for \"{}\"".format(doc["name"])) + return None + + return int(resolution_width), int(resolution_height) + + +def set_camera_resolution(camera, asset_doc=None): + """Apply resolution to camera from asset document of the publish""" + + if not asset_doc: + asset_doc = get_current_asset_doc() + + resolution = get_resolution_from_data(asset_doc) + + if resolution: + print("Setting camera resolution: {} -> {}x{}".format( + camera.name(), resolution[0], resolution[1] + )) + camera.parm("resx").set(resolution[0]) + camera.parm("resy").set(resolution[1]) + + +def get_camera_from_container(container): + """Get camera from container node. """ + + cameras = container.recursiveGlob("*", + filter=hou.nodeTypeFilter.ObjCamera, + include_subnets=False) + + assert len(cameras) == 1, "Camera instance must have only one camera" + return cameras[0] diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index c9ae801af5..6aa65deb89 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -14,6 +14,7 @@ import pyblish.api from openpype.pipeline import ( register_creator_plugin_path, register_loader_plugin_path, + register_inventory_action_path, AVALON_CONTAINER_ID, ) from openpype.pipeline.load import any_outdated_containers @@ -55,6 +56,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) + register_inventory_action_path(INVENTORY_PATH) log.info("Installing callbacks ... ") # register_event_callback("init", on_init) diff --git a/openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py b/openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py new file mode 100644 index 0000000000..cff7f89288 --- /dev/null +++ b/openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py @@ -0,0 +1,24 @@ +from openpype.pipeline import InventoryAction +from openpype.hosts.houdini.api.lib import ( + get_camera_from_container, + set_camera_resolution +) + +class SetAssetResolution(InventoryAction): + + label = "Set Asset Resolution" + icon = "desktop" + color = "orange" + + @staticmethod + def is_compatible(container): + print(container) + return ( + container.get("loader") == "CameraLoader" + ) + + def process(self, containers): + for container in containers: + node = container["node"] + camera = get_camera_from_container(node) + set_camera_resolution(camera) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 7b4a04809e..cffa5ca813 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -4,6 +4,13 @@ from openpype.pipeline import ( ) from openpype.hosts.houdini.api import pipeline +from openpype.hosts.houdini.api.lib import ( + set_camera_resolution, + get_camera_from_container +) + +import hou + ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")' '.alembicGetCameraDict') @@ -25,7 +32,15 @@ def transfer_non_default_values(src, dest, ignore=None): channel expression and ignore certain Parm types. """ - import hou + + ignore_types = { + hou.parmTemplateType.Toggle, + hou.parmTemplateType.Menu, + hou.parmTemplateType.Button, + hou.parmTemplateType.FolderSet, + hou.parmTemplateType.Separator, + hou.parmTemplateType.Label, + } src.updateParmStates() @@ -62,14 +77,6 @@ def transfer_non_default_values(src, dest, ignore=None): continue # Ignore folders, separators, etc. - ignore_types = { - hou.parmTemplateType.Toggle, - hou.parmTemplateType.Menu, - hou.parmTemplateType.Button, - hou.parmTemplateType.FolderSet, - hou.parmTemplateType.Separator, - hou.parmTemplateType.Label, - } if parm.parmTemplate().type() in ignore_types: continue @@ -90,13 +97,8 @@ class CameraLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): - import os - 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("\\", "/") + file_path = self.fname.replace("\\", "/") # Get the root node obj = hou.node("/obj") @@ -106,19 +108,21 @@ class CameraLoader(load.LoaderPlugin): node_name = "{}_{}".format(namespace, name) if namespace else name # Create a archive node - container = self.create_and_connect(obj, "alembicarchive", node_name) + node = self.create_and_connect(obj, "alembicarchive", node_name) # TODO: add FPS of project / asset - container.setParms({"fileName": file_path, - "channelRef": True}) + node.setParms({"fileName": file_path, "channelRef": True}) # Apply some magic - container.parm("buildHierarchy").pressButton() - container.moveToGoodPosition() + node.parm("buildHierarchy").pressButton() + node.moveToGoodPosition() # Create an alembic xform node - nodes = [container] + nodes = [node] + camera = get_camera_from_container(node) + self._match_maya_render_mask(camera) + set_camera_resolution(camera, asset_doc=context["asset"]) self[:] = nodes return pipeline.containerise(node_name, @@ -143,14 +147,14 @@ class CameraLoader(load.LoaderPlugin): # Store the cam temporarily next to the Alembic Archive # so that we can preserve parm values the user set on it # after build hierarchy was triggered. - old_camera = self._get_camera(node) + old_camera = get_camera_from_container(node) temp_camera = old_camera.copyTo(node.parent()) # Rebuild node.parm("buildHierarchy").pressButton() # Apply values to the new camera - new_camera = self._get_camera(node) + new_camera = get_camera_from_container(node) transfer_non_default_values(temp_camera, new_camera, # The hidden uniform scale attribute @@ -158,6 +162,9 @@ class CameraLoader(load.LoaderPlugin): # "icon_scale" just skip that completely ignore={"scale"}) + self._match_maya_render_mask(new_camera) + set_camera_resolution(new_camera) + temp_camera.destroy() def remove(self, container): @@ -165,15 +172,6 @@ class CameraLoader(load.LoaderPlugin): node = container["node"] node.destroy() - def _get_camera(self, node): - import hou - cameras = node.recursiveGlob("*", - filter=hou.nodeTypeFilter.ObjCamera, - include_subnets=False) - - assert len(cameras) == 1, "Camera instance must have only one camera" - return cameras[0] - def create_and_connect(self, node, node_type, name=None): """Create a node within a node which and connect it to the input @@ -194,5 +192,20 @@ class CameraLoader(load.LoaderPlugin): new_node.moveToGoodPosition() return new_node - def switch(self, container, representation): - self.update(container, representation) + def _match_maya_render_mask(self, camera): + """Workaround to match Maya render mask in Houdini""" + + # print("Setting match maya render mask ") + parm = camera.parm("aperture") + expression = parm.expression() + expression = expression.replace("return ", "aperture = ") + expression += """ +# Match maya render mask (logic from Houdini's own FBX importer) +node = hou.pwd() +resx = node.evalParm('resx') +resy = node.evalParm('resy') +aspect = node.evalParm('aspect') +aperture *= min(1, (resx / resy * aspect) / 1.5) +return aperture +""" + parm.setExpression(expression, language=hou.exprLanguage.Python) From 32a192b1929598c82fc8d199b19ff0edb5763564 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 6 Sep 2023 21:44:46 +0300 Subject: [PATCH 119/175] change action name --- .../{set_asset_resolution.py => set_camera_resolution.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename openpype/hosts/houdini/plugins/inventory/{set_asset_resolution.py => set_camera_resolution.py} (87%) diff --git a/openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py similarity index 87% rename from openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py rename to openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py index cff7f89288..441aef8b1c 100644 --- a/openpype/hosts/houdini/plugins/inventory/set_asset_resolution.py +++ b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py @@ -4,9 +4,9 @@ from openpype.hosts.houdini.api.lib import ( set_camera_resolution ) -class SetAssetResolution(InventoryAction): +class SetCameraResolution(InventoryAction): - label = "Set Asset Resolution" + label = "Set Camera Resolution" icon = "desktop" color = "orange" From f8d03955f71b6d7fe9d87b1835cb1d62a2adf788 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 6 Sep 2023 21:59:11 +0300 Subject: [PATCH 120/175] resolve hound --- openpype/hosts/houdini/api/lib.py | 14 +++++++++----- .../plugins/inventory/set_camera_resolution.py | 1 + openpype/hosts/houdini/plugins/load/load_camera.py | 14 +++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index c6672cf969..f83519ddb8 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -661,7 +661,9 @@ def get_current_asset_doc(): return asset_doc -def get_resolution_from_data(doc): +def get_resolution_from_doc(doc): + """Get resolution from the given asset document. """ + if not doc or "data" not in doc: print("Entered document is not valid. \"{}\"".format(str(doc))) return None @@ -683,7 +685,7 @@ def set_camera_resolution(camera, asset_doc=None): if not asset_doc: asset_doc = get_current_asset_doc() - resolution = get_resolution_from_data(asset_doc) + resolution = get_resolution_from_doc(asset_doc) if resolution: print("Setting camera resolution: {} -> {}x{}".format( @@ -696,9 +698,11 @@ def set_camera_resolution(camera, asset_doc=None): def get_camera_from_container(container): """Get camera from container node. """ - cameras = container.recursiveGlob("*", - filter=hou.nodeTypeFilter.ObjCamera, - include_subnets=False) + cameras = container.recursiveGlob( + "*", + filter=hou.nodeTypeFilter.ObjCamera, + include_subnets=False + ) assert len(cameras) == 1, "Camera instance must have only one camera" return cameras[0] diff --git a/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py index 441aef8b1c..5dd94232b8 100644 --- a/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py +++ b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py @@ -4,6 +4,7 @@ from openpype.hosts.houdini.api.lib import ( set_camera_resolution ) + class SetCameraResolution(InventoryAction): label = "Set Camera Resolution" diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index cffa5ca813..53567b6f97 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -34,13 +34,13 @@ def transfer_non_default_values(src, dest, ignore=None): """ ignore_types = { - hou.parmTemplateType.Toggle, - hou.parmTemplateType.Menu, - hou.parmTemplateType.Button, - hou.parmTemplateType.FolderSet, - hou.parmTemplateType.Separator, - hou.parmTemplateType.Label, - } + hou.parmTemplateType.Toggle, + hou.parmTemplateType.Menu, + hou.parmTemplateType.Button, + hou.parmTemplateType.FolderSet, + hou.parmTemplateType.Separator, + hou.parmTemplateType.Label, + } src.updateParmStates() From 7a3aaa5408b3d4a9ff22220018506e25e55858bf Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 7 Sep 2023 10:26:09 +0300 Subject: [PATCH 121/175] replace fname --- openpype/hosts/houdini/plugins/load/load_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 53567b6f97..e16146a267 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -98,7 +98,7 @@ class CameraLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): # Format file name, Houdini only wants forward slashes - file_path = self.fname.replace("\\", "/") + file_path = self.filepath_from_context(context).replace("\\", "/") # Get the root node obj = hou.node("/obj") From 9d2fc2fca4622e8c91ba4539034a03f67b57fc40 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 7 Sep 2023 22:30:03 +0300 Subject: [PATCH 122/175] Jakub comments --- openpype/hosts/houdini/api/lib.py | 12 +----------- .../plugins/inventory/set_camera_resolution.py | 6 +++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index f83519ddb8..eff98c05f1 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -651,16 +651,6 @@ def get_color_management_preferences(): } -def get_current_asset_doc(): - """Get asset document of the current asset. """ - - project_name = get_current_project_name() - asset_name = get_current_asset_name() - asset_doc = get_asset_by_name(project_name, asset_name) - - return asset_doc - - def get_resolution_from_doc(doc): """Get resolution from the given asset document. """ @@ -683,7 +673,7 @@ def set_camera_resolution(camera, asset_doc=None): """Apply resolution to camera from asset document of the publish""" if not asset_doc: - asset_doc = get_current_asset_doc() + asset_doc = get_current_project_asset() resolution = get_resolution_from_doc(asset_doc) diff --git a/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py index 5dd94232b8..97b94e66aa 100644 --- a/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py +++ b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py @@ -3,7 +3,7 @@ from openpype.hosts.houdini.api.lib import ( get_camera_from_container, set_camera_resolution ) - +from openpype.pipeline.context_tools import get_current_project_asset class SetCameraResolution(InventoryAction): @@ -13,13 +13,13 @@ class SetCameraResolution(InventoryAction): @staticmethod def is_compatible(container): - print(container) return ( container.get("loader") == "CameraLoader" ) def process(self, containers): + asset_doc = get_current_project_asset() for container in containers: node = container["node"] camera = get_camera_from_container(node) - set_camera_resolution(camera) + set_camera_resolution(camera, asset_doc) From d38d25308dc393ced6504e86e925d8c821c92868 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 7 Sep 2023 22:31:48 +0300 Subject: [PATCH 123/175] resolve hound --- .../hosts/houdini/plugins/inventory/set_camera_resolution.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py index 97b94e66aa..18ececb019 100644 --- a/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py +++ b/openpype/hosts/houdini/plugins/inventory/set_camera_resolution.py @@ -5,6 +5,7 @@ from openpype.hosts.houdini.api.lib import ( ) from openpype.pipeline.context_tools import get_current_project_asset + class SetCameraResolution(InventoryAction): label = "Set Camera Resolution" From 572d6e3ab52d5d398a9a25fd4405cad4fecea796 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 8 Sep 2023 12:44:14 +0300 Subject: [PATCH 124/175] update ayon settings --- .../houdini/server/settings/publish_plugins.py | 12 ++++++------ server_addon/houdini/server/version.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 528e847fce..6ceff028a5 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -167,14 +167,14 @@ class PublishPluginsModel(BaseSettingsModel): ValidateContainers: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Latest Containers.") - ValidateSubsetName: ValidateContainersModel = Field( - default_factory=ValidateContainersModel, + ValidateSubsetName: BasicValidateModel = Field( + default_factory=BasicValidateModel, title="Validate Subset Name.") - ValidateMeshIsStatic: ValidateContainersModel = Field( - default_factory=ValidateContainersModel, + ValidateMeshIsStatic: BasicValidateModel = Field( + default_factory=BasicValidateModel, title="Validate Mesh is Static.") - ValidateUnrealStaticMeshName: ValidateContainersModel = Field( - default_factory=ValidateContainersModel, + ValidateUnrealStaticMeshName: BasicValidateModel = Field( + default_factory=BasicValidateModel, title="Validate Unreal Static Mesh Name.") diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 2d255f15bedc2aca61177d80cad987b7108b5a26 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 11 Sep 2023 14:05:24 +0300 Subject: [PATCH 125/175] validate empty nodes and invalid prims --- .../publish/validate_fbx_output_node.py | 109 ++++++++++++++++-- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py index 503a3bb3c1..9f6a1b8767 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -5,7 +5,8 @@ from openpype.hosts.houdini.api.action import ( SelectInvalidAction, SelectROPAction, ) - +from openpype.hosts.houdini.api.lib import get_obj_node_output +from collections import defaultdict import hou @@ -16,27 +17,38 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): - The Output Node Path is set. - The Output Node Path refers to an existing object. - The Output Node is a Sop or Obj node. + - The Output Node has geometry data. """ order = pyblish.api.ValidatorOrder families = ["fbx"] hosts = ["houdini"] - label = "Validate Output Node" + label = "Validate FBX Output Node" actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): - invalid = self.get_invalid(instance) + invalid = self.get_invalid_categorized(instance) if invalid: raise PublishValidationError( "Output node(s) are incorrect", title="Invalid output node(s)" ) - @classmethod def get_invalid(cls, instance): + out = cls.get_invalid_categorized(instance).values() + invalid = [] + for row in out: + invalid += row + return invalid + + + @classmethod + def get_invalid_categorized(cls, instance): output_node = instance.data.get("output_node") + # Check if The Output Node Path is set and + # refers to an existing object. if output_node is None: rop_node = hou.node(instance.data["instance_node"]) cls.log.error( @@ -46,11 +58,90 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): return [rop_node] - if output_node.type().category().name() not in ["Sop", "Object"]: + # Check if the Output Node is a Sop or Obj node + # also, make a dictionary of all geo obj nodes + # and their sop output node. + all_outputs = {} + # if user selects an ObjSubnet or an ObjNetwork + if output_node.childTypeCategory() == hou.objNodeTypeCategory(): + all_outputs.update({output_node : {}}) + for node in output_node.allSubChildren(): + if node.type().name() == "geo": + out = get_obj_node_output(node) + all_outputs[output_node].update({node: out}) + + # elif user selects a geometry ObjNode + elif output_node.type().name() == "geo": + out = get_obj_node_output(output_node) + all_outputs.update({output_node: out}) + + # elif user selects a SopNode + elif output_node.type().category().name() == "Sop": + # expetional case because output_node is not an obj node + all_outputs.update({output_node: output_node}) + + # Then it's wrong node type + else: cls.log.error( - "Output node %s is not a SOP or OBJ node. " - "It must point to a SOP or OBJ node, " - "instead found category type: %s" - % (output_node.path(), output_node.type().category().name()) + "Output node %s is not a SOP or OBJ Geo or OBJ SubNet node. " + "Instead found category type: %s %s" + , output_node.path(), output_node.type().category().name() + , output_node.type().name() ) return [output_node] + + # Check if geo obj node have geometry. + # return geo obj node if their sop output node + valid = {} + invalid = defaultdict(list) + cls.filter_inner_dict(all_outputs, valid, invalid) + + invalid_prim_types = ["VDB", "Volume"] + for obj_node, sop_node in valid.items(): + # Empty Geometry test + if not hasattr(sop_node, "geometry"): + invalid["empty_geometry"].append(sop_node) + cls.log.error( + "Sop node '%s' includes no geometry." + , sop_node.path() + ) + continue + + frame = instance.data.get("frameStart", 0) + geo = sop_node.geometryAtFrame(frame) + if len(geo.iterPrims()) == 0: + invalid["empty_geometry"].append(sop_node) + cls.log.error( + "Sop node '%s' includes no geometry." + , sop_node.path() + ) + continue + + # Invalid Prims test + for prim_type in invalid_prim_types: + if geo.countPrimType(prim_type) > 0: + invalid["invalid_prims"].append(sop_node) + cls.log.error( + "Sop node '%s' includes invliad prims of type '%s'." + , sop_node.path(), prim_type + ) + + if invalid: + return invalid + + @classmethod + def filter_inner_dict(cls, d: dict, valid: dict, invalid: dict): + """Parse the dictionary and filter items to valid and invalid. + + Invalid items have empty values like {}, None + Valid dictionary is a flattened dictionary that includes + the valid inner items. + """ + + for k, v in d.items(): + if not v: + invalid["empty_objs"].append(k) + elif isinstance(v, dict): + cls.filter_inner_dict(v, valid, invalid) + else: + valid.update({k:v}) From ad0c6245cdd347fed47b075e1ecb13e5dfb8c359 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 11 Sep 2023 15:52:11 +0300 Subject: [PATCH 126/175] remove unnecessary logic --- .../publish/validate_fbx_output_node.py | 89 +++++++++---------- 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py index 9f6a1b8767..d06ef593d3 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -28,23 +28,15 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): def process(self, instance): - invalid = self.get_invalid_categorized(instance) + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Output node(s) are incorrect", title="Invalid output node(s)" ) + @classmethod def get_invalid(cls, instance): - out = cls.get_invalid_categorized(instance).values() - invalid = [] - for row in out: - invalid += row - return invalid - - - @classmethod - def get_invalid_categorized(cls, instance): output_node = instance.data.get("output_node") # Check if The Output Node Path is set and @@ -58,27 +50,47 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): return [rop_node] - # Check if the Output Node is a Sop or Obj node - # also, make a dictionary of all geo obj nodes - # and their sop output node. - all_outputs = {} - # if user selects an ObjSubnet or an ObjNetwork + # Check if the Output Node is a Sop or an Obj node + # also, list all sop output nodes inside as well as + # invalid empty nodes. + all_out_sops = [] + invalid = defaultdict(list) + + # if output_node is an ObjSubnet or an ObjNetwork if output_node.childTypeCategory() == hou.objNodeTypeCategory(): - all_outputs.update({output_node : {}}) for node in output_node.allSubChildren(): if node.type().name() == "geo": out = get_obj_node_output(node) - all_outputs[output_node].update({node: out}) + if out: + all_out_sops.append(out) + else: + invalid["empty_objs"].append(node) + cls.log.error( + "Geo Obj Node '%s' is empty!" + , node.path() + ) + if not all_out_sops: + invalid["empty_objs"].append(output_node) + cls.log.error( + "Output Node '%s' is empty!" + , node.path() + ) - # elif user selects a geometry ObjNode + # elif output_node is an ObjNode elif output_node.type().name() == "geo": out = get_obj_node_output(output_node) - all_outputs.update({output_node: out}) + if out: + all_out_sops.append(out) + else: + invalid["empty_objs"].append(node) + cls.log.error( + "Output Node '%s' is empty!" + , node.path() + ) - # elif user selects a SopNode + # elif output_node is a SopNode elif output_node.type().category().name() == "Sop": - # expetional case because output_node is not an obj node - all_outputs.update({output_node: output_node}) + all_out_sops.append(output_node) # Then it's wrong node type else: @@ -90,19 +102,15 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): ) return [output_node] - # Check if geo obj node have geometry. - # return geo obj node if their sop output node - valid = {} - invalid = defaultdict(list) - cls.filter_inner_dict(all_outputs, valid, invalid) - + # Check if all output sop nodes have geometry + # and don't contain invalid prims invalid_prim_types = ["VDB", "Volume"] - for obj_node, sop_node in valid.items(): + for sop_node in all_out_sops: # Empty Geometry test if not hasattr(sop_node, "geometry"): invalid["empty_geometry"].append(sop_node) cls.log.error( - "Sop node '%s' includes no geometry." + "Sop node '%s' doesn't include any prims." , sop_node.path() ) continue @@ -112,7 +120,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): if len(geo.iterPrims()) == 0: invalid["empty_geometry"].append(sop_node) cls.log.error( - "Sop node '%s' includes no geometry." + "Sop node '%s' doesn't include any prims." , sop_node.path() ) continue @@ -127,21 +135,4 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): ) if invalid: - return invalid - - @classmethod - def filter_inner_dict(cls, d: dict, valid: dict, invalid: dict): - """Parse the dictionary and filter items to valid and invalid. - - Invalid items have empty values like {}, None - Valid dictionary is a flattened dictionary that includes - the valid inner items. - """ - - for k, v in d.items(): - if not v: - invalid["empty_objs"].append(k) - elif isinstance(v, dict): - cls.filter_inner_dict(v, valid, invalid) - else: - valid.update({k:v}) + return [output_node] From 20f4b62213530118829dbbe027d25c982f80675b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 11 Sep 2023 16:33:28 +0300 Subject: [PATCH 127/175] Remove unnecessary line --- .../hosts/houdini/plugins/publish/validate_mesh_is_static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py index 4d0904eb53..6bf94f7536 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -30,7 +30,7 @@ class ValidateMeshIsStatic(pyblish.api.InstancePlugin, invalid = self.get_invalid(instance) if invalid: - nodes = [n.path() for n in invalid if isinstance(n, hou.Node)] + nodes = [n.path() for n in invalid] raise PublishValidationError( "See log for details. " "Invalid nodes: {0}".format(nodes) From 21c174c47bd761392ff6856346bdedf5ece732d9 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 11 Sep 2023 16:34:05 +0300 Subject: [PATCH 128/175] update error message --- .../publish/validate_fbx_output_node.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py index d06ef593d3..d493092755 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -6,7 +6,6 @@ from openpype.hosts.houdini.api.action import ( SelectROPAction, ) from openpype.hosts.houdini.api.lib import get_obj_node_output -from collections import defaultdict import hou @@ -30,8 +29,10 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: + nodes = [n.path() for n in invalid] raise PublishValidationError( - "Output node(s) are incorrect", + "See log for details. " + "Invalid nodes: {0}".format(nodes), title="Invalid output node(s)" ) @@ -54,7 +55,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): # also, list all sop output nodes inside as well as # invalid empty nodes. all_out_sops = [] - invalid = defaultdict(list) + invalid = [] # if output_node is an ObjSubnet or an ObjNetwork if output_node.childTypeCategory() == hou.objNodeTypeCategory(): @@ -64,13 +65,13 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): if out: all_out_sops.append(out) else: - invalid["empty_objs"].append(node) + invalid.append(node) # empty_objs cls.log.error( "Geo Obj Node '%s' is empty!" , node.path() ) if not all_out_sops: - invalid["empty_objs"].append(output_node) + invalid.append(output_node) # empty_objs cls.log.error( "Output Node '%s' is empty!" , node.path() @@ -82,7 +83,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): if out: all_out_sops.append(out) else: - invalid["empty_objs"].append(node) + invalid.append(node) # empty_objs cls.log.error( "Output Node '%s' is empty!" , node.path() @@ -92,7 +93,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): elif output_node.type().category().name() == "Sop": all_out_sops.append(output_node) - # Then it's wrong node type + # Then it's a wrong node type else: cls.log.error( "Output node %s is not a SOP or OBJ Geo or OBJ SubNet node. " @@ -108,7 +109,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): for sop_node in all_out_sops: # Empty Geometry test if not hasattr(sop_node, "geometry"): - invalid["empty_geometry"].append(sop_node) + invalid.append(sop_node) # empty_geometry cls.log.error( "Sop node '%s' doesn't include any prims." , sop_node.path() @@ -118,7 +119,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): frame = instance.data.get("frameStart", 0) geo = sop_node.geometryAtFrame(frame) if len(geo.iterPrims()) == 0: - invalid["empty_geometry"].append(sop_node) + invalid.append(sop_node) # empty_geometry cls.log.error( "Sop node '%s' doesn't include any prims." , sop_node.path() @@ -128,11 +129,11 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): # Invalid Prims test for prim_type in invalid_prim_types: if geo.countPrimType(prim_type) > 0: - invalid["invalid_prims"].append(sop_node) + invalid.append(sop_node) # invalid_prims cls.log.error( - "Sop node '%s' includes invliad prims of type '%s'." + "Sop node '%s' includes invalid prims of type '%s'." , sop_node.path(), prim_type ) if invalid: - return [output_node] + return invalid From 9105e74b4326686510536cc9e2f8fd37f44be563 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 11 Sep 2023 16:43:26 +0300 Subject: [PATCH 129/175] resolve hound --- .../publish/validate_fbx_output_node.py | 32 +++++++++---------- .../publish/validate_mesh_is_static.py | 2 -- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py index d493092755..ea13d25122 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -67,14 +67,14 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): else: invalid.append(node) # empty_objs cls.log.error( - "Geo Obj Node '%s' is empty!" - , node.path() + "Geo Obj Node '%s' is empty!", + node.path() ) if not all_out_sops: invalid.append(output_node) # empty_objs cls.log.error( - "Output Node '%s' is empty!" - , node.path() + "Output Node '%s' is empty!", + node.path() ) # elif output_node is an ObjNode @@ -85,21 +85,21 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): else: invalid.append(node) # empty_objs cls.log.error( - "Output Node '%s' is empty!" - , node.path() + "Output Node '%s' is empty!", + node.path() ) # elif output_node is a SopNode elif output_node.type().category().name() == "Sop": - all_out_sops.append(output_node) + all_out_sops.append(output_node) # Then it's a wrong node type else: cls.log.error( "Output node %s is not a SOP or OBJ Geo or OBJ SubNet node. " - "Instead found category type: %s %s" - , output_node.path(), output_node.type().category().name() - , output_node.type().name() + "Instead found category type: %s %s", + output_node.path(), output_node.type().category().name(), + output_node.type().name() ) return [output_node] @@ -111,8 +111,8 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): if not hasattr(sop_node, "geometry"): invalid.append(sop_node) # empty_geometry cls.log.error( - "Sop node '%s' doesn't include any prims." - , sop_node.path() + "Sop node '%s' doesn't include any prims.", + sop_node.path() ) continue @@ -121,8 +121,8 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): if len(geo.iterPrims()) == 0: invalid.append(sop_node) # empty_geometry cls.log.error( - "Sop node '%s' doesn't include any prims." - , sop_node.path() + "Sop node '%s' doesn't include any prims.", + sop_node.path() ) continue @@ -131,8 +131,8 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): if geo.countPrimType(prim_type) > 0: invalid.append(sop_node) # invalid_prims cls.log.error( - "Sop node '%s' includes invalid prims of type '%s'." - , sop_node.path(), prim_type + "Sop node '%s' includes invalid prims of type '%s'.", + sop_node.path(), prim_type ) if invalid: diff --git a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py index 6bf94f7536..b499682e0b 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py +++ b/openpype/hosts/houdini/plugins/publish/validate_mesh_is_static.py @@ -10,8 +10,6 @@ from openpype.pipeline.publish import ValidateContentsOrder from openpype.hosts.houdini.api.action import SelectInvalidAction from openpype.hosts.houdini.api.lib import get_output_children -import hou - class ValidateMeshIsStatic(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): From 583cfba38c2f4ffbf0c72795ac4adb48d98715e5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 11 Sep 2023 16:48:38 +0300 Subject: [PATCH 130/175] update doc string --- .../hosts/houdini/plugins/publish/validate_fbx_output_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py index ea13d25122..894dad7d72 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_fbx_output_node.py @@ -17,6 +17,7 @@ class ValidateFBXOutputNode(pyblish.api.InstancePlugin): - The Output Node Path refers to an existing object. - The Output Node is a Sop or Obj node. - The Output Node has geometry data. + - The Output Node doesn't include invalid primitive types. """ order = pyblish.api.ValidatorOrder From 1365bec5188cfb810ee73b442e9393bf918c2ab9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 11 Sep 2023 16:19:15 +0200 Subject: [PATCH 131/175] :recycle: add check for produced file --- openpype/hosts/max/plugins/publish/extract_model_obj.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index e522b1e7a1..a5d9ad6597 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.pipeline.publish import KnownPublishError class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): @@ -27,6 +28,7 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) + self.log.info("Performing Extraction ...") with maintained_selection(): # select and export node_list = instance.data["members"] @@ -38,7 +40,10 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): using=rt.ObjExp, ) - self.log.info("Performing Extraction ...") + if not os.path.exists(filepath): + raise KnownPublishError( + "File {} wasn't produced by 3ds max, please check the logs.") + if "representations" not in instance.data: instance.data["representations"] = [] From 86629c85cab0cb59585f0fd0d6120a2c0c27f7fd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 11 Sep 2023 16:21:35 +0200 Subject: [PATCH 132/175] :recycle: make usd plugin validator optional this is temporary hack to allow testing and publishing models without USD plugin in max as the plugin validator will run always, no matter if the USD extractor is enabled or not. This will change when #5602 is implemented. --- .../max/plugins/publish/validate_usd_plugin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py index 9957e62736..36c4291925 100644 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- """Validator for USD plugin.""" -from openpype.pipeline import PublishValidationError from pyblish.api import InstancePlugin, ValidatorOrder from pymxs import runtime as rt +from openpype.pipeline import ( + OptionalPyblishPluginMixin, + PublishValidationError +) + def get_plugins() -> list: """Get plugin list from 3ds max.""" @@ -17,17 +21,25 @@ def get_plugins() -> list: return plugin_info_list -class ValidateUSDPlugin(InstancePlugin): +class ValidateUSDPlugin(OptionalPyblishPluginMixin, + InstancePlugin): """Validates if USD plugin is installed or loaded in 3ds max.""" order = ValidatorOrder - 0.01 families = ["model"] hosts = ["max"] - label = "USD Plugin" + label = "Validate USD Plugin loaded" + optional = True def process(self, instance): """Plugin entry point.""" + for sc in ValidateUSDPlugin.__subclasses__(): + self.log.info(sc) + + if not self.is_active(instance.data): + return + plugin_info = get_plugins() usd_import = "usdimport.dli" if usd_import not in plugin_info: From 2372e552d39137d0fecb3a93bf03dda1581c0361 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 13 Sep 2023 10:46:48 +0300 Subject: [PATCH 133/175] use more decriptive variable name --- openpype/hosts/houdini/plugins/publish/extract_fbx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py index dd61e68f3b..7993b3352f 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py +++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py @@ -21,11 +21,11 @@ class ExtractFBX(publish.Extractor): # get rop node ropnode = hou.node(instance.data.get("instance_node")) - output_node = ropnode.evalParm("sopoutput") + output_file = ropnode.evalParm("sopoutput") # get staging_dir and file_name - staging_dir = os.path.normpath(os.path.dirname(output_node)) - file_name = os.path.basename(output_node) + 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) From c292a11939a7cf3f980d018031e3da86335925c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Sep 2023 19:10:52 +0800 Subject: [PATCH 134/175] typo for LoadError --- openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index d87d6b1bfe..f8aadb53c0 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -1,6 +1,6 @@ import os from openpype.pipeline import load, get_representation_path -from openpype.pipeline.load import LoaderError +from openpype.pipeline.load import LoadError from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -30,8 +30,8 @@ class OxAbcLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): plugin_list = get_plugins() if "ephere.plugins.autodesk.max.ornatrix.dlo" not in plugin_list: - raise LoaderError("Ornatrix plugin not " - "found/installed in Max yet..") + raise LoadError("Ornatrix plugin not " + "found/installed in Max yet..") file_path = os.path.normpath(self.filepath_from_context(context)) rt.AlembicImport.ImportToRoot = True From 54a62348d02585ea9b186df2cabafb0f2748f6d0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 13 Sep 2023 22:16:14 +0200 Subject: [PATCH 135/175] Remove unused functions from Fusion integration --- openpype/hosts/fusion/api/__init__.py | 6 +- openpype/hosts/fusion/api/lib.py | 74 ------------------- openpype/hosts/fusion/api/pipeline.py | 43 ----------- .../tests/test_lib_restructuralization.py | 2 - 4 files changed, 3 insertions(+), 122 deletions(-) diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index dba55a98d9..aabc624016 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -3,9 +3,7 @@ from .pipeline import ( ls, imprint_container, - parse_container, - list_instances, - remove_instance + parse_container ) from .lib import ( @@ -22,6 +20,7 @@ from .menu import launch_openpype_menu __all__ = [ # pipeline + "FusionHost", "ls", "imprint_container", @@ -32,6 +31,7 @@ __all__ = [ "update_frame_range", "set_asset_framerange", "get_current_comp", + "get_bmd_library", "comp_lock_and_undo_chunk", # menu diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index d96557571b..c4a1488606 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -181,80 +181,6 @@ def validate_comp_prefs(comp=None, force_repair=False): dialog.setStyleSheet(load_stylesheet()) -def switch_item(container, - asset_name=None, - subset_name=None, - representation_name=None): - """Switch container asset, subset or representation of a container by name. - - It'll always switch to the latest version - of course a different - approach could be implemented. - - Args: - container (dict): data of the item to switch with - asset_name (str): name of the asset - subset_name (str): name of the subset - representation_name (str): name of the representation - - Returns: - dict - - """ - - if all(not x for x in [asset_name, subset_name, representation_name]): - raise ValueError("Must have at least one change provided to switch.") - - # Collect any of current asset, subset and representation if not provided - # so we can use the original name from those. - project_name = get_current_project_name() - if any(not x for x in [asset_name, subset_name, representation_name]): - repre_id = container["representation"] - representation = get_representation_by_id(project_name, repre_id) - repre_parent_docs = get_representation_parents( - project_name, representation) - if repre_parent_docs: - version, subset, asset, _ = repre_parent_docs - else: - version = subset = asset = None - - if asset_name is None: - asset_name = asset["name"] - - if subset_name is None: - subset_name = subset["name"] - - if representation_name is None: - representation_name = representation["name"] - - # Find the new one - asset = get_asset_by_name(project_name, asset_name, fields=["_id"]) - assert asset, ("Could not find asset in the database with the name " - "'%s'" % asset_name) - - subset = get_subset_by_name( - project_name, subset_name, asset["_id"], fields=["_id"] - ) - assert subset, ("Could not find subset in the database with the name " - "'%s'" % subset_name) - - version = get_last_version_by_subset_id( - project_name, subset["_id"], fields=["_id"] - ) - assert version, "Could not find a version for {}.{}".format( - asset_name, subset_name - ) - - representation = get_representation_by_name( - project_name, representation_name, version["_id"] - ) - assert representation, ("Could not find representation in the database " - "with the name '%s'" % representation_name) - - switch_container(container, representation) - - return representation - - @contextlib.contextmanager def maintained_selection(comp=None): """Reset comp selection from before the context after the context""" diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index a768a3f0f8..a886086758 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -287,49 +287,6 @@ def parse_container(tool): return container -# TODO: Function below is currently unused prototypes -def list_instances(creator_id=None): - """Return created instances in current workfile which will be published. - Returns: - (list) of dictionaries matching instances format - """ - - comp = get_current_comp() - tools = comp.GetToolList(False).values() - - instance_signature = { - "id": "pyblish.avalon.instance", - "identifier": creator_id - } - instances = [] - for tool in tools: - - data = tool.GetData('openpype') - if not isinstance(data, dict): - continue - - if data.get("id") != instance_signature["id"]: - continue - - if creator_id and data.get("identifier") != creator_id: - continue - - instances.append(tool) - - return instances - - -# TODO: Function below is currently unused prototypes -def remove_instance(instance): - """Remove instance from current workfile. - - Args: - instance (dict): instance representation from subsetmanager model - """ - # Assume instance is a Fusion tool directly - instance["tool"].Delete() - - class FusionEventThread(QtCore.QThread): """QThread which will periodically ping Fusion app for any events. The fusion.UIManager must be set up to be notified of events before they'll diff --git a/openpype/tests/test_lib_restructuralization.py b/openpype/tests/test_lib_restructuralization.py index 669706d470..a91d65f7a8 100644 --- a/openpype/tests/test_lib_restructuralization.py +++ b/openpype/tests/test_lib_restructuralization.py @@ -18,8 +18,6 @@ def test_backward_compatibility(printer): from openpype.lib import get_ffprobe_streams - from openpype.hosts.fusion.lib import switch_item - from openpype.lib import source_hash from openpype.lib import run_subprocess From 5f2756b95ee0d06b1ac2748b736db93dc47a7f46 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 13 Sep 2023 22:42:33 +0200 Subject: [PATCH 136/175] Skip view capture when Maya is in headless mode Co-authored-by: Toke Jepsen --- .../plugins/publish/extract_active_view_thumbnail.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py index b4e62f8acc..71a0ba877b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py @@ -4,6 +4,9 @@ import maya.api.OpenMayaUI as omui import pyblish.api import tempfile +from openpype.hosts.maya.lib import IS_HEADLESS + + class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): """Set instance thumbnail to a screengrab of current active viewport. @@ -19,6 +22,13 @@ class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): hosts = ["maya"] def process(self, instance): + if IS_HEADLESS: + self.log.debug( + "Skip extraction of active view thumbnail, due to being in" + "headless mode." + ) + return + thumbnail = instance.data.get("thumbnailPath") if not thumbnail: view_thumbnail = self.get_view_thumbnail(instance) From ef843e25ca35b2e0ab6b0be47f214093131e9c7c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 13 Sep 2023 23:01:58 +0200 Subject: [PATCH 137/175] Update openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py --- .../hosts/maya/plugins/publish/extract_active_view_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py index 71a0ba877b..f47dd5e084 100644 --- a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py @@ -7,7 +7,6 @@ import tempfile from openpype.hosts.maya.lib import IS_HEADLESS - class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): """Set instance thumbnail to a screengrab of current active viewport. From 5e20dd3f9cb553c053f35baf0fb321e6b46a3f43 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 09:54:32 +0100 Subject: [PATCH 138/175] Update openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py --- .../hosts/maya/plugins/publish/extract_active_view_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py index f47dd5e084..483ae6d9d3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_active_view_thumbnail.py @@ -4,7 +4,7 @@ import maya.api.OpenMayaUI as omui import pyblish.api import tempfile -from openpype.hosts.maya.lib import IS_HEADLESS +from openpype.hosts.maya.api.lib import IS_HEADLESS class ExtractActiveViewThumbnail(pyblish.api.InstancePlugin): From cdc6366662b00a3e0f413cdba8e1eb1784f10e28 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 17:42:35 +0800 Subject: [PATCH 139/175] update the OP data after the merge of #5424 --- .../plugins/load/load_pointcache_ornatrix.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index f8aadb53c0..0f32b7938f 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -50,19 +50,13 @@ class OxAbcLoader(load.LoaderPlugin): name + "_", suffix="_", ) - - abc_container = rt.Container() + abc_container =[] for abc in scene_object: - abc.Parent = abc_container abc.name = f"{namespace}:{abc.name}" - # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}_{self.postfix}" - abc_container.name = abc_container_name - import_custom_attribute_data( - abc_container, abc_container.Children) + abc_container.append(abc) return containerise( - name, [abc_container], context, + name, abc_container, context, namespace, loader=self.__class__.__name__ ) @@ -89,14 +83,17 @@ class OxAbcLoader(load.LoaderPlugin): obj_type = rt.ClassOf(obj) if str(obj_type).startswith("Ox_"): scene_object.append(obj) - update_custom_attribute_data( - inst_container, scene_object.Children) + ox_abc_objects = [] for abc in scene_object: abc.Parent = container abc.name = f"{namespace}:{abc.name}" - abc.pos = transform_data[f"{abc.name}.transform"] - abc.scale = transform_data[f"{abc.name}.scale"] - + ox_abc_objects.append(abc) + ox_transform = f"{abc.name}.transform" + if ox_transform in transform_data.keys(): + abc.pos = transform_data[ox_transform] or 0 + abc.scale = transform_data[f"{abc.name}.scale"] or 0 + update_custom_attribute_data( + inst_container, ox_abc_objects) lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, From 2bd4d295afda0e5f64434e6119d802180f4ca618 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 17:46:32 +0800 Subject: [PATCH 140/175] update the Data & hound --- .../plugins/load/load_pointcache_ornatrix.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index 0f32b7938f..cbd15ae6b8 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -3,7 +3,7 @@ from openpype.pipeline import load, get_representation_path from openpype.pipeline.load import LoadError from openpype.hosts.max.api.pipeline import ( containerise, - import_custom_attribute_data, + get_previous_loaded_object, update_custom_attribute_data ) @@ -50,7 +50,7 @@ class OxAbcLoader(load.LoaderPlugin): name + "_", suffix="_", ) - abc_container =[] + abc_container = [] for abc in scene_object: abc.name = f"{namespace}:{abc.name}" abc_container.append(abc) @@ -64,11 +64,12 @@ class OxAbcLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] namespace, name = get_namespace(node_name) - sub_node_name = f"{namespace}:{name}_{self.postfix}" - inst_container = rt.getNodeByName(sub_node_name) - rt.Select(inst_container.Children) - transform_data = object_transform_set(inst_container.Children) - for prev_obj in rt.selection: + node = rt.getNodeByName(node_name) + node_list = get_previous_loaded_object(node) + rt.Select(node_list) + selections = rt.getCurrentSelection() + transform_data = object_transform_set(selections) + for prev_obj in selections: if rt.isValidNode(prev_obj): rt.Delete(prev_obj) @@ -92,8 +93,7 @@ class OxAbcLoader(load.LoaderPlugin): if ox_transform in transform_data.keys(): abc.pos = transform_data[ox_transform] or 0 abc.scale = transform_data[f"{abc.name}.scale"] or 0 - update_custom_attribute_data( - inst_container, ox_abc_objects) + update_custom_attribute_data(node, ox_abc_objects) lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, From 963f3f42ff287742e6aa2004292dc513464bcaa8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 17:48:18 +0800 Subject: [PATCH 141/175] use the get_plugins function from max.api.lib --- .../max/plugins/load/load_pointcache_ornatrix.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py index cbd15ae6b8..96060a6a6f 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/openpype/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -10,7 +10,8 @@ from openpype.hosts.max.api.pipeline import ( from openpype.hosts.max.api.lib import ( unique_namespace, get_namespace, - object_transform_set + object_transform_set, + get_plugins ) from openpype.hosts.max.api import lib from pymxs import runtime as rt @@ -105,15 +106,3 @@ class OxAbcLoader(load.LoaderPlugin): def remove(self, container): node = rt.GetNodeByName(container["instance_node"]) rt.Delete(node) - - -def get_plugins() -> list: - """Get plugin list from 3ds max.""" - manager = rt.PluginManager - count = manager.pluginDllCount - plugin_info_list = [] - for p in range(1, count + 1): - plugin_info = manager.pluginDllName(p) - plugin_info_list.append(plugin_info) - - return plugin_info_list From 254a1859632b9b5ce59fb58c8cdb47a754ae746d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 14 Sep 2023 22:35:04 +0300 Subject: [PATCH 142/175] set PATH environment in deadline jobs --- .../repository/custom/plugins/GlobalJobPreLoad.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 97875215ae..79cd3968fb 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -385,6 +385,11 @@ def inject_openpype_environment(deadlinePlugin): for key, value in contents.items(): deadlinePlugin.SetProcessEnvironmentVariable(key, value) + if "PATH" in contents: + PATH = contents["PATH"] + print(f">>> Set 'PATH' Environment to: {PATH}") + os.environ["PATH"] = PATH + script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") if script_url: script_url = script_url.format(**contents).replace("\\", "/") @@ -509,6 +514,11 @@ def inject_ayon_environment(deadlinePlugin): for key, value in contents.items(): deadlinePlugin.SetProcessEnvironmentVariable(key, value) + if "PATH" in contents: + PATH = contents["PATH"] + print(f">>> Set 'PATH' Environment to: {PATH}") + os.environ["PATH"] = PATH + script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") if script_url: script_url = script_url.format(**contents).replace("\\", "/") From f7cc7f1908948fc0aa1526987b8cc12a20dd4736 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 14 Sep 2023 23:17:52 +0300 Subject: [PATCH 143/175] BigRoy's comment --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 79cd3968fb..24bfd983f3 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -387,7 +387,7 @@ def inject_openpype_environment(deadlinePlugin): if "PATH" in contents: PATH = contents["PATH"] - print(f">>> Set 'PATH' Environment to: {PATH}") + print(f">>> Setting 'PATH' Environment to: {PATH}") os.environ["PATH"] = PATH script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") @@ -516,7 +516,7 @@ def inject_ayon_environment(deadlinePlugin): if "PATH" in contents: PATH = contents["PATH"] - print(f">>> Set 'PATH' Environment to: {PATH}") + print(f">>> Setting 'PATH' Environment to: {PATH}") os.environ["PATH"] = PATH script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") From 1ec11da6f7c25550edd030461845b88681c1b4a6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 14 Sep 2023 23:23:31 +0300 Subject: [PATCH 144/175] add a developer note --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 24bfd983f3..044a953083 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -386,6 +386,8 @@ def inject_openpype_environment(deadlinePlugin): deadlinePlugin.SetProcessEnvironmentVariable(key, value) if "PATH" in contents: + # Set os.environ[PATH] so studio settings' path entries + # can be used to define search path for executables. PATH = contents["PATH"] print(f">>> Setting 'PATH' Environment to: {PATH}") os.environ["PATH"] = PATH @@ -515,6 +517,8 @@ def inject_ayon_environment(deadlinePlugin): deadlinePlugin.SetProcessEnvironmentVariable(key, value) if "PATH" in contents: + # Set os.environ[PATH] so studio settings' path entries + # can be used to define search path for executables. PATH = contents["PATH"] print(f">>> Setting 'PATH' Environment to: {PATH}") os.environ["PATH"] = PATH From 0c09a6772ce3c0f3c9ce47547e937e9dc254d6fe Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 15 Sep 2023 09:50:13 +0300 Subject: [PATCH 145/175] BigRoy's comment --- .../repository/custom/plugins/GlobalJobPreLoad.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 044a953083..e9b81369ca 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -388,9 +388,8 @@ def inject_openpype_environment(deadlinePlugin): if "PATH" in contents: # Set os.environ[PATH] so studio settings' path entries # can be used to define search path for executables. - PATH = contents["PATH"] - print(f">>> Setting 'PATH' Environment to: {PATH}") - os.environ["PATH"] = PATH + print(f">>> Setting 'PATH' Environment to: {contents['PATH']}") + os.environ["PATH"] = contents["PATH"] script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") if script_url: @@ -519,9 +518,8 @@ def inject_ayon_environment(deadlinePlugin): if "PATH" in contents: # Set os.environ[PATH] so studio settings' path entries # can be used to define search path for executables. - PATH = contents["PATH"] - print(f">>> Setting 'PATH' Environment to: {PATH}") - os.environ["PATH"] = PATH + print(f">>> Setting 'PATH' Environment to: {contents['PATH']}") + os.environ["PATH"] = contents["PATH"] script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") if script_url: From 73e928efc99dbcd90b84d09cd1de0397ed3f633b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Sep 2023 10:12:58 +0200 Subject: [PATCH 146/175] Fix - _id key used instead of id (#5626) Just 'id' is not returned because value in fields. --- openpype/client/server/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 39322627bb..3ee62a3172 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -422,7 +422,7 @@ def get_last_version_by_subset_name( if not subset: return None return get_last_version_by_subset_id( - project_name, subset["id"], fields=fields + project_name, subset["_id"], fields=fields ) From 36a8976c580d822c816992c752181b48a884202d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:55:07 +0200 Subject: [PATCH 147/175] AYON: Mark deprecated settings in Maya (#5627) * Mark color management preferences as deprecated * add migration hint --- server_addon/maya/server/settings/imageio.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/server_addon/maya/server/settings/imageio.py b/server_addon/maya/server/settings/imageio.py index 7512bfe253..946a14c866 100644 --- a/server_addon/maya/server/settings/imageio.py +++ b/server_addon/maya/server/settings/imageio.py @@ -39,8 +39,10 @@ class ImageIOFileRulesModel(BaseSettingsModel): class ColorManagementPreferenceV2Model(BaseSettingsModel): - """Color Management Preference v2 (Maya 2022+).""" - _layout = "expanded" + """Color Management Preference v2 (Maya 2022+). + + Please migrate all to 'imageio/workfile' and enable it. + """ enabled: bool = Field(True, title="Use Color Management Preference v2") @@ -51,7 +53,6 @@ class ColorManagementPreferenceV2Model(BaseSettingsModel): class ColorManagementPreferenceModel(BaseSettingsModel): """Color Management Preference (legacy).""" - _layout = "expanded" renderSpace: str = Field(title="Rendering Space") viewTransform: str = Field(title="Viewer Transform ") @@ -89,11 +90,11 @@ class ImageIOSettings(BaseSettingsModel): # Deprecated colorManagementPreference_v2: ColorManagementPreferenceV2Model = Field( default_factory=ColorManagementPreferenceV2Model, - title="Color Management Preference v2 (Maya 2022+)" + title="DEPRECATED: Color Management Preference v2 (Maya 2022+)" ) colorManagementPreference: ColorManagementPreferenceModel = Field( default_factory=ColorManagementPreferenceModel, - title="Color Management Preference (legacy)" + title="DEPRECATED: Color Management Preference (legacy)" ) From 411f4bacd16d3d3c5c4ffb29d69f88f383b094dc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 18 Sep 2023 15:03:45 +0100 Subject: [PATCH 148/175] Support new publisher for colorsets validation. --- .../maya/plugins/publish/validate_color_sets.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_color_sets.py b/openpype/hosts/maya/plugins/publish/validate_color_sets.py index 766124cd9e..173fee4179 100644 --- a/openpype/hosts/maya/plugins/publish/validate_color_sets.py +++ b/openpype/hosts/maya/plugins/publish/validate_color_sets.py @@ -3,9 +3,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( - RepairAction, ValidateMeshOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError, + RepairAction ) @@ -22,8 +23,9 @@ class ValidateColorSets(pyblish.api.Validator, hosts = ['maya'] families = ['model'] label = 'Mesh ColorSets' - actions = [openpype.hosts.maya.api.action.SelectInvalidAction, - RepairAction] + actions = [ + openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction + ] optional = True @staticmethod @@ -48,8 +50,9 @@ class ValidateColorSets(pyblish.api.Validator, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with " - "Color Sets: {0}".format(invalid)) + raise PublishValidationError( + message="Meshes found with Color Sets: {0}".format(invalid) + ) @classmethod def repair(cls, instance): From 9f040265e7cd965df20137d9da12396fa8338310 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 18 Sep 2023 16:07:54 +0100 Subject: [PATCH 149/175] Remove context prompt. --- openpype/hosts/maya/api/pipeline.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 60495ac652..3647ec0b6b 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -659,17 +659,6 @@ def on_task_changed(): lib.set_context_settings() lib.update_content_on_context_change() - msg = " project: {}\n asset: {}\n task:{}".format( - get_current_project_name(), - get_current_asset_name(), - get_current_task_name() - ) - - lib.show_message( - "Context was changed", - ("Context was changed to:\n{}".format(msg)), - ) - def before_workfile_open(): if handle_workfile_locks(): From 6c70df2a2e482cc1ad21d9f62b05a30e4282865a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 18 Sep 2023 21:02:58 +0300 Subject: [PATCH 150/175] remove unnecessary logic --- .../hosts/houdini/plugins/load/load_fbx.py | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 7e7f0c04e5..cac22d62d4 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -21,8 +21,9 @@ class FbxLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): - # get file path - file_path = self.get_file_path(context=context) + # get file path from context + file_path = self.filepath_from_context(context) + file_path = file_path.replace("\\", "/") # get necessary data namespace, node_name = self.get_node_name(context, name, namespace) @@ -56,8 +57,9 @@ class FbxLoader(load.LoaderPlugin): self.log.error("Could not find node of type `file`") return - # Update the file path - file_path = self.get_file_path(representation=representation) + # Update the file path from representation + file_path = get_representation_path(representation) + file_path = file_path.replace("\\", "/") file_node.setParms({"file": file_path}) @@ -72,19 +74,6 @@ class FbxLoader(load.LoaderPlugin): def switch(self, container, representation): self.update(container, representation) - def get_file_path(self, context=None, representation=None): - """Return formatted file path.""" - - # Format file name, Houdini only wants forward slashes - if context: - file_path = self.filepath_from_context(context) - elif representation: - file_path = get_representation_path(representation) - else: - return "" - - return file_path.replace("\\", "/") - def get_node_name(self, context, name=None, namespace=None): """Define node name.""" From b8997c5ae49841ed3dfc82c30d394909a3b88722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:23:45 +0200 Subject: [PATCH 151/175] create symlinks for ssl libs (#5633) build was missing `libssl.1.1.so` and `libcrypto.1.1.so` symlinks needed by the executable itself, because python is now explicitly build with OpenSSL 1.1.1 --- Dockerfile.centos7 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index 9217140f20..ab1d3f8253 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -109,6 +109,8 @@ RUN source $HOME/.bashrc \ RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.9/lib \ && cp /usr/lib64/openssl11/libssl* ./build/exe.linux-x86_64-3.9/lib \ && cp /usr/lib64/openssl11/libcrypto* ./build/exe.linux-x86_64-3.9/lib \ + && ln -sr ./build/exe.linux-x86_64-3.9/lib/libssl.so ./build/exe.linux-x86_64-3.9/lib/libssl.1.1.so \ + && ln -sr ./build/exe.linux-x86_64-3.9/lib/libcrypto.so ./build/exe.linux-x86_64-3.9/lib/libcrypto.1.1.so \ && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.9/lib \ && cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.9/vendor/python/PySide2/Qt/lib From a8fa44c3fda18d0799fc171cb6461ba80672a36d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:52:51 +0200 Subject: [PATCH 152/175] AYON: Ignore separated modules (#5619) * ayon mode explicitly ignores addons that have own repository * change warning message of missing addon directory to debug * Better log message --- openpype/modules/base.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 84e213288c..a3c21718b9 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -59,6 +59,14 @@ IGNORED_DEFAULT_FILENAMES = ( "example_addons", "default_modules", ) +# Modules that won't be loaded in AYON mode from "./openpype/modules" +# - the same modules are ignored in "./server_addon/create_ayon_addons.py" +IGNORED_FILENAMES_IN_AYON = { + "ftrack", + "shotgrid", + "sync_server", + "slack", +} # Inherit from `object` for Python 2 hosts @@ -392,9 +400,9 @@ def _load_ayon_addons(openpype_modules, modules_key, log): folder_name = "{}_{}".format(addon_name, addon_version) addon_dir = os.path.join(addons_dir, folder_name) if not os.path.exists(addon_dir): - log.warning(( - "Directory for addon {} {} does not exists. Path \"{}\"" - ).format(addon_name, addon_version, addon_dir)) + log.debug(( + "No localized client code found for addon {} {}." + ).format(addon_name, addon_version)) continue sys.path.insert(0, addon_dir) @@ -483,6 +491,10 @@ def _load_modules(): is_in_current_dir = dirpath == current_dir is_in_host_dir = dirpath == hosts_dir + ignored_current_dir_filenames = set(IGNORED_DEFAULT_FILENAMES) + if AYON_SERVER_ENABLED: + ignored_current_dir_filenames |= IGNORED_FILENAMES_IN_AYON + for filename in os.listdir(dirpath): # Ignore filenames if filename in IGNORED_FILENAMES: @@ -490,7 +502,7 @@ def _load_modules(): if ( is_in_current_dir - and filename in IGNORED_DEFAULT_FILENAMES + and filename in ignored_current_dir_filenames ): continue From bed1e35d20dfc949a8b8c3864bbb526644fa37df Mon Sep 17 00:00:00 2001 From: 64qam Date: Tue, 19 Sep 2023 16:54:48 +0200 Subject: [PATCH 153/175] Default create a desktop icon is checked (#5636) --- inno_setup.iss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 418bedbd4d..d9a41d22ee 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -36,7 +36,7 @@ WizardStyle=modern Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" [InstallDelete] ; clean everything in previous installation folder @@ -53,4 +53,3 @@ Name: "{autodesktop}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe" [Run] Filename: "{app}\openpype_gui.exe"; Description: "{cm:LaunchProgram,OpenPype}"; Flags: nowait postinstall skipifsilent - From 50af3321ae9a9e0118f6b25eb7845ce6f9ab1762 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:18:46 +0200 Subject: [PATCH 154/175] AYON: Avoid creation of duplicated links (#5593) * create set of output links before creation * find all existing links before creating them * Added small comment --- .../publish/integrate_inputlinks_ayon.py | 67 ++++++++++++++++--- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/integrate_inputlinks_ayon.py b/openpype/plugins/publish/integrate_inputlinks_ayon.py index 180524cd08..28684aa889 100644 --- a/openpype/plugins/publish/integrate_inputlinks_ayon.py +++ b/openpype/plugins/publish/integrate_inputlinks_ayon.py @@ -1,7 +1,11 @@ import collections import pyblish.api -from ayon_api import create_link, make_sure_link_type_exists +from ayon_api import ( + create_link, + make_sure_link_type_exists, + get_versions_links, +) from openpype import AYON_SERVER_ENABLED @@ -124,6 +128,33 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): version_entity["_id"], ) + def _get_existing_links(self, project_name, link_type, entity_ids): + """Find all existing links for given version ids. + + Args: + project_name (str): Name of project. + link_type (str): Type of link. + entity_ids (set[str]): Set of version ids. + + Returns: + dict[str, set[str]]: Existing links by version id. + """ + + output = collections.defaultdict(set) + if not entity_ids: + return output + + existing_in_links = get_versions_links( + project_name, entity_ids, [link_type], "output" + ) + + for entity_id, links in existing_in_links.items(): + if not links: + continue + for link in links: + output[entity_id].add(link["entityId"]) + return output + def create_links_on_server(self, context, new_links): """Create new links on server. @@ -144,16 +175,32 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): # Create link themselves for link_type, items in new_links.items(): + mapping = collections.defaultdict(set) + # Make sure there are no duplicates of src > dst ids for item in items: - input_id, output_id = item - create_link( - project_name, - link_type, - input_id, - "version", - output_id, - "version" - ) + _input_id, _output_id = item + mapping[_input_id].add(_output_id) + + existing_links_by_in_id = self._get_existing_links( + project_name, link_type, set(mapping.keys()) + ) + + for input_id, output_ids in mapping.items(): + existing_links = existing_links_by_in_id[input_id] + for output_id in output_ids: + # Skip creation of link if already exists + # NOTE: AYON server does not support + # to have same links + if output_id in existing_links: + continue + create_link( + project_name, + link_type, + input_id, + "version", + output_id, + "version" + ) if not AYON_SERVER_ENABLED: From 85951d58f64920b33e9c6902569d890dcc1690f2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 19 Sep 2023 15:31:39 +0000 Subject: [PATCH 155/175] [Automated] Release --- CHANGELOG.md | 251 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 253 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7620869b..935e7dbc4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,257 @@ # Changelog +## [3.16.7](https://github.com/ynput/OpenPype/tree/3.16.7) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.6...3.16.7) + +### **🆕 New features** + + +
+Maya: Extract active view as thumbnail when no thumbnail set #5426 + +This sets the Maya instance's thumbnail to the current active view if no thumbnail was set yet. + + +___ + +
+ + +
+Maya: Implement USD publish and load using native `mayaUsdPlugin` #5573 + +Implement Creator and Loaders for extraction and loading of USD files using Maya's own `mayaUsdPlugin`.Also adds support to load a `usd` file into an Arnold Standin (`aiStandin`) and assigning looks to it. + + +___ + +
+ + +
+AYON: Ignore separated modules #5619 + +Do not load already separated modules from default directory. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: Reduce amount of code for Collect Looks #5253 + +- Refactor `get_file_node_files` because popping from `paths` by index should have been done in reversed order anyway. It's now changed to not need popping at all. +- Removed unused `RENDERER_NODE_TYPES` and if-branch which collected `node_attrs` list which was unused + collected members which was also done outside of the if branch and thus generated no extra data. +- Collected all materials from look set attributes at once instead of multiple queries +- Collected all file nodes in history from a single query instead of per type +- Restructured assignment of `instance.data["resources"]` to be more readable +- Cached `PXR_NODES` only ones (Note: plugin load is checked on discovery of the collect look plugin) instead of querying plugin load and its nodes per file node per attribute +- Removed some debug logs or combined some messages + + +___ + +
+ + +
+AYON: Mark deprecated settings in Maya #5627 + +Added deprecated info to docstrings of maya colormanagement settings.Resolves: https://github.com/ynput/OpenPype/issues/5556 + + +___ + +
+ + +
+Max: switching versions of maxScene maintain parentage/links with the loaders #5424 + +When using scene inventory to manage or update the version of the loading objects, the linked modifiers or parentage of the objects would be kept.Meanwhile, loaded objects from all loaders no longer parented to the container with OP Data. + + +___ + +
+ + +
+3ds max: small tweaks to obj extractor and model publishing flow #5605 + +There migh be situation where OBJ Extractor passes without failure, but no obj file is produced. This is adding simple check directly into the extractor to catch it earlier then in the integration phase. Also switched `Validate USD Plugin` to optional, because it was always run no matter if the Extract USD was enabled or not, hindering testing (and publishing). + + +___ + +
+ + +
+TVPaint: Plugin can be reopened #5610 + +TVPaint plugin can be reopened. + + +___ + +
+ + +
+Maya: Remove context prompt #5632 + +More of a plea than a PR, but could we please remove the context prompt in Maya when switching tasks? + + +___ + +
+ + +
+General: Create a desktop icon is checked #5636 + +In OP Installer `Create a desktop icon` is checked by default. +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Extract look is not AYON compatible - OP-5375 #5341 + +The textures that would use hardlinking are going through texture processors. Currently all texture processors are hardcoded to copy texture instead of respecting the settings of forcing to copy.The texture processors were last modified 4 months ago, so effectively all clients that are on any pipeline updated in the last 4 months wont be utilizing hardlinking at all, since the hardcoded texture processors will copy texture no matter the OS.This opts for completely disabling the hardlinking feature, while we figure out what to do about it. + + +___ + +
+ + +
+Maya: Multiverse USD Override inherit from correct new style creator #5566 + +Fix Creator for Multiverse USD Override by inheriting from correct new style creator class type + + +___ + +
+ + +
+Max: Bug Fix Alembic Loaders with Ornatrix #5434 + +Bugfix the alembic loader with both ornatrix alembic and max alembic supportsAdd the ornatrix alembic loaders for loading the alembic with Ornatrix-related modifiers. + + +___ + +
+ + +
+AYON: Avoid creation of duplicated links #5593 + +Handle cases when an existing link should be recreated and do not create the same link multitple times during single publishing. + + +___ + +
+ + +
+Extract Review: Multilayer specification for ffmpeg #5613 + +Extract review is specifying layer name when exr is multilayer. + + +___ + +
+ + +
+Fussion: added support for Fusion 17 #5614 + +Fusion 17 still uses Python 3.6 which causes issues with some our delivered libraries. Vendorized necessary set for Python 3.6 + + +___ + +
+ + +
+Publisher: Fix screenshot widget #5615 + +Use correct super method name.EDITED:Removed fade animation which is not triggered at some cases, e.g. in Nuke the animation does not start. I do expect that is caused by `exec_` on the dialog, which blocks event processing to the animation, even when I've added the window as parent it still didn't trigger registered callback.Modified how the "empty" space is not filled by using paths instead of clear mode on painter. Added render hints to add antialiasing. + + +___ + +
+ + +
+Photoshop: auto_images without alpha will not fail #5620 + +ExtractReview caused issue on `auto_image` instance without alpha channel, this fixes it. + + +___ + +
+ + +
+Fix - _id key used instead of id in get_last_version_by_subset_name #5626 + +Just 'id' is not returned because value in fields. Caused KeyError. + + +___ + +
+ + +
+Bugfix: create symlinks for ssl libs on Centos 7 #5633 + +Docker build was missing `libssl.1.1.so` and `libcrypto.1.1.so` symlinks needed by the executable itself, because Python is now explicitly built with OpenSSL 1.1.1 + + +___ + +
+ +### **📃 Documentation** + + +
+Documentation/local settings #5102 + +I completed the "Working with local settings" page. I updated the screenshot, wrote an explanation for each empty category, and if available, linked the more detailed pages already existing. I also added the "Environments" category. + + +___ + +
+ + + + ## [3.16.6](https://github.com/ynput/OpenPype/tree/3.16.6) diff --git a/openpype/version.py b/openpype/version.py index bbed1b0ef3..c593f0f71f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.7-nightly.2" +__version__ = "3.16.7" diff --git a/pyproject.toml b/pyproject.toml index f859e1aff4..b394a895e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.6" # OpenPype +version = "3.16.7" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 16641e750045f8639a7138b83580ba089abfc492 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Sep 2023 15:32:44 +0000 Subject: [PATCH 156/175] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cf3cb8ba1a..9159d3fe4a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.7 - 3.16.7-nightly.2 - 3.16.7-nightly.1 - 3.16.6 @@ -134,7 +135,6 @@ body: - 3.14.10-nightly.2 - 3.14.10-nightly.1 - 3.14.9 - - 3.14.9-nightly.5 validations: required: true - type: dropdown From 982c72224cdbf3b92ac954d42e03769a79bbd6f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:21:05 +0200 Subject: [PATCH 157/175] Chore: Remove schema from OpenPype root (#5355) * removed schema from openpype root * removed schema from include files --- schema/application-1.0.json | 68 ---------------- schema/asset-1.0.json | 35 -------- schema/asset-2.0.json | 55 ------------- schema/asset-3.0.json | 55 ------------- schema/config-1.0.json | 85 -------------------- schema/config-1.1.json | 87 -------------------- schema/config-2.0.json | 87 -------------------- schema/container-1.0.json | 100 ----------------------- schema/container-2.0.json | 59 -------------- schema/hero_version-1.0.json | 44 ---------- schema/inventory-1.0.json | 10 --- schema/inventory-1.1.json | 10 --- schema/project-2.0.json | 86 -------------------- schema/project-2.1.json | 86 -------------------- schema/project-3.0.json | 59 -------------- schema/representation-1.0.json | 28 ------- schema/representation-2.0.json | 78 ------------------ schema/session-1.0.json | 143 --------------------------------- schema/session-2.0.json | 134 ------------------------------ schema/session-3.0.json | 81 ------------------- schema/shaders-1.0.json | 32 -------- schema/subset-1.0.json | 35 -------- schema/subset-2.0.json | 51 ------------ schema/subset-3.0.json | 62 -------------- schema/thumbnail-1.0.json | 42 ---------- schema/version-1.0.json | 50 ------------ schema/version-2.0.json | 92 --------------------- schema/version-3.0.json | 84 ------------------- schema/workfile-1.0.json | 52 ------------ setup.py | 1 - 30 files changed, 1891 deletions(-) delete mode 100644 schema/application-1.0.json delete mode 100644 schema/asset-1.0.json delete mode 100644 schema/asset-2.0.json delete mode 100644 schema/asset-3.0.json delete mode 100644 schema/config-1.0.json delete mode 100644 schema/config-1.1.json delete mode 100644 schema/config-2.0.json delete mode 100644 schema/container-1.0.json delete mode 100644 schema/container-2.0.json delete mode 100644 schema/hero_version-1.0.json delete mode 100644 schema/inventory-1.0.json delete mode 100644 schema/inventory-1.1.json delete mode 100644 schema/project-2.0.json delete mode 100644 schema/project-2.1.json delete mode 100644 schema/project-3.0.json delete mode 100644 schema/representation-1.0.json delete mode 100644 schema/representation-2.0.json delete mode 100644 schema/session-1.0.json delete mode 100644 schema/session-2.0.json delete mode 100644 schema/session-3.0.json delete mode 100644 schema/shaders-1.0.json delete mode 100644 schema/subset-1.0.json delete mode 100644 schema/subset-2.0.json delete mode 100644 schema/subset-3.0.json delete mode 100644 schema/thumbnail-1.0.json delete mode 100644 schema/version-1.0.json delete mode 100644 schema/version-2.0.json delete mode 100644 schema/version-3.0.json delete mode 100644 schema/workfile-1.0.json diff --git a/schema/application-1.0.json b/schema/application-1.0.json deleted file mode 100644 index 953abee569..0000000000 --- a/schema/application-1.0.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:application-1.0", - "description": "An application definition.", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "label", - "application_dir", - "executable" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "label": { - "description": "Nice name of application.", - "type": "string" - }, - "application_dir": { - "description": "Name of directory used for application resources.", - "type": "string" - }, - "executable": { - "description": "Name of callable executable, this is called to launch the application", - "type": "string" - }, - "description": { - "description": "Description of application.", - "type": "string" - }, - "environment": { - "description": "Key/value pairs for environment variables related to this application. Supports lists for paths, such as PYTHONPATH.", - "type": "object", - "items": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - } - }, - "default_dirs": { - "type": "array", - "items": { - "type": "string" - } - }, - "copy": { - "type": "object", - "patternProperties": { - "^.*$": { - "anyOf": [ - {"type": "string"}, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - } -} diff --git a/schema/asset-1.0.json b/schema/asset-1.0.json deleted file mode 100644 index ab104c002a..0000000000 --- a/schema/asset-1.0.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:asset-1.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "name", - "subsets" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "name": { - "description": "Name of directory", - "type": "string" - }, - "subsets": { - "type": "array", - "items": { - "$ref": "subset.json" - } - } - }, - - "definitions": {} -} diff --git a/schema/asset-2.0.json b/schema/asset-2.0.json deleted file mode 100644 index b894d79792..0000000000 --- a/schema/asset-2.0.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:asset-2.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "silo", - "data" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:asset-2.0"], - "example": "openpype:asset-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["asset"], - "example": "asset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of asset", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "Bruce" - }, - "silo": { - "description": "Group or container of asset", - "type": "string", - "example": "assets" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": {"key": "value"} - } - }, - - "definitions": {} -} diff --git a/schema/asset-3.0.json b/schema/asset-3.0.json deleted file mode 100644 index 948704d2a1..0000000000 --- a/schema/asset-3.0.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:asset-3.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:asset-3.0"], - "example": "openpype:asset-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["asset"], - "example": "asset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of asset", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "Bruce" - }, - "silo": { - "description": "Group or container of asset", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "assets" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": {"key": "value"} - } - }, - - "definitions": {} -} diff --git a/schema/config-1.0.json b/schema/config-1.0.json deleted file mode 100644 index 49398a57cd..0000000000 --- a/schema/config-1.0.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.0", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": false, - "required": [ - "tasks", - "apps" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "template": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^.*$": { - "type": "string" - } - } - }, - "tasks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "apps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "families": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "label": {"type": "string"}, - "hideFilter": {"type": "boolean"} - }, - "required": ["name"] - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "color": {"type": "string"}, - "order": {"type": ["integer", "number"]} - }, - "required": ["name"] - } - }, - "copy": { - "type": "object" - } - } -} diff --git a/schema/config-1.1.json b/schema/config-1.1.json deleted file mode 100644 index 6e15514aaf..0000000000 --- a/schema/config-1.1.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.1", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": false, - "required": [ - "tasks", - "apps" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "template": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^.*$": { - "type": "string" - } - } - }, - "tasks": { - "type": "object", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": [ - "short_name" - ] - } - }, - "apps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "families": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "label": {"type": "string"}, - "hideFilter": {"type": "boolean"} - }, - "required": ["name"] - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "color": {"type": "string"}, - "order": {"type": ["integer", "number"]} - }, - "required": ["name"] - } - }, - "copy": { - "type": "object" - } - } -} diff --git a/schema/config-2.0.json b/schema/config-2.0.json deleted file mode 100644 index 54b226711a..0000000000 --- a/schema/config-2.0.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-2.0", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": false, - "required": [ - "tasks", - "apps" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "templates": { - "type": "object" - }, - "roots": { - "type": "object" - }, - "imageio": { - "type": "object" - }, - "tasks": { - "type": "object", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": [ - "short_name" - ] - } - }, - "apps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "families": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "label": {"type": "string"}, - "hideFilter": {"type": "boolean"} - }, - "required": ["name"] - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "color": {"type": "string"}, - "order": {"type": ["integer", "number"]} - }, - "required": ["name"] - } - }, - "copy": { - "type": "object" - } - } -} diff --git a/schema/container-1.0.json b/schema/container-1.0.json deleted file mode 100644 index 012e8499e6..0000000000 --- a/schema/container-1.0.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:container-1.0", - "description": "A loaded asset", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "id", - "objectName", - "name", - "author", - "loader", - "families", - "time", - "subset", - "asset", - "representation", - "version", - "silo", - "path", - "source" - ], - "properties": { - "id": { - "description": "Identifier for finding object in host", - "type": "string", - "enum": ["pyblish.mindbender.container"], - "example": "pyblish.mindbender.container" - }, - "objectName": { - "description": "Name of internal object, such as the objectSet in Maya.", - "type": "string", - "example": "Bruce_:rigDefault_CON" - }, - "name": { - "description": "Full name of application object", - "type": "string", - "example": "modelDefault" - }, - "author": { - "description": "Name of the author of the published version", - "type": "string", - "example": "Marcus Ottosson" - }, - "loader": { - "description": "Name of loader plug-in used to produce this container", - "type": "string", - "example": "ModelLoader" - }, - "families": { - "description": "Families associated with the this subset", - "type": "string", - "example": "mindbender.model" - }, - "time": { - "description": "File-system safe, formatted time", - "type": "string", - "example": "20170329T131545Z" - }, - "subset": { - "description": "Name of source subset", - "type": "string", - "example": "modelDefault" - }, - "asset": { - "description": "Name of source asset", - "type": "string" , - "example": "Bruce" - }, - "representation": { - "description": "Name of source representation", - "type": "string" , - "example": ".ma" - }, - "version": { - "description": "Version number", - "type": "number", - "example": 12 - }, - "silo": { - "description": "Silo of parent asset", - "type": "string", - "example": "assets" - }, - "path": { - "description": "Absolute path on disk", - "type": "string", - "example": "{root}/assets/Bruce/publish/rigDefault/v002" - }, - "source": { - "description": "Absolute path to file from which this version was published", - "type": "string", - "example": "{root}/assets/Bruce/work/rigging/maya/scenes/rig_v001.ma" - } - } -} diff --git a/schema/container-2.0.json b/schema/container-2.0.json deleted file mode 100644 index 1673ee5d1d..0000000000 --- a/schema/container-2.0.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:container-2.0", - "description": "A loaded asset", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "id", - "objectName", - "name", - "namespace", - "loader", - "representation" - ], - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:container-2.0"], - "example": "openpype:container-2.0" - }, - "id": { - "description": "Identifier for finding object in host", - "type": "string", - "enum": ["pyblish.avalon.container"], - "example": "pyblish.avalon.container" - }, - "objectName": { - "description": "Name of internal object, such as the objectSet in Maya.", - "type": "string", - "example": "Bruce_:rigDefault_CON" - }, - "loader": { - "description": "Name of loader plug-in used to produce this container", - "type": "string", - "example": "ModelLoader" - }, - "name": { - "description": "Internal object name of container in application", - "type": "string", - "example": "modelDefault_01" - }, - "namespace": { - "description": "Internal namespace of container in application", - "type": "string", - "example": "Bruce_" - }, - "representation": { - "description": "Unique id of representation in database", - "type": "string", - "example": "59523f355f8c1b5f6c5e8348" - } - } -} diff --git a/schema/hero_version-1.0.json b/schema/hero_version-1.0.json deleted file mode 100644 index b720dc2887..0000000000 --- a/schema/hero_version-1.0.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:hero_version-1.0", - "description": "Hero version of asset", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "version_id", - "schema", - "type", - "parent" - ], - - "properties": { - "_id": { - "description": "Document's id (database will create it's if not entered)", - "example": "ObjectId(592c33475f8c1b064c4d1696)" - }, - "version_id": { - "description": "The version ID from which it was created", - "example": "ObjectId(592c33475f8c1b064c4d1695)" - }, - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:hero_version-1.0"], - "example": "openpype:hero_version-1.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["hero_version"], - "example": "hero_version" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "ObjectId(592c33475f8c1b064c4d1697)" - } - } -} diff --git a/schema/inventory-1.0.json b/schema/inventory-1.0.json deleted file mode 100644 index 2fe78794ab..0000000000 --- a/schema/inventory-1.0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.0", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": true -} diff --git a/schema/inventory-1.1.json b/schema/inventory-1.1.json deleted file mode 100644 index b61a76b32a..0000000000 --- a/schema/inventory-1.1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.1", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": true -} diff --git a/schema/project-2.0.json b/schema/project-2.0.json deleted file mode 100644 index 0ed5a55599..0000000000 --- a/schema/project-2.0.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:project-2.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data", - "config" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:project-2.0"], - "example": "openpype:project-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["project"], - "example": "project" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "hulk" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "fps": 24, - "width": 1920, - "height": 1080 - } - }, - "config": { - "type": "object", - "description": "Document metadata", - "example": { - "schema": "openpype:config-1.0", - "apps": [ - { - "name": "maya2016", - "label": "Autodesk Maya 2016" - }, - { - "name": "nuke10", - "label": "The Foundry Nuke 10.0" - } - ], - "tasks": [ - {"name": "model"}, - {"name": "render"}, - {"name": "animate"}, - {"name": "rig"}, - {"name": "lookdev"}, - {"name": "layout"} - ], - "template": { - "work": - "{root}/{project}/{silo}/{asset}/work/{task}/{app}", - "publish": - "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}" - } - }, - "$ref": "config-1.0.json" - } - }, - - "definitions": {} -} diff --git a/schema/project-2.1.json b/schema/project-2.1.json deleted file mode 100644 index 9413c9f691..0000000000 --- a/schema/project-2.1.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:project-2.1", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data", - "config" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:project-2.1"], - "example": "openpype:project-2.1" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["project"], - "example": "project" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "hulk" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "fps": 24, - "width": 1920, - "height": 1080 - } - }, - "config": { - "type": "object", - "description": "Document metadata", - "example": { - "schema": "openpype:config-1.1", - "apps": [ - { - "name": "maya2016", - "label": "Autodesk Maya 2016" - }, - { - "name": "nuke10", - "label": "The Foundry Nuke 10.0" - } - ], - "tasks": { - "Model": {"short_name": "mdl"}, - "Render": {"short_name": "rnd"}, - "Animate": {"short_name": "anim"}, - "Rig": {"short_name": "rig"}, - "Lookdev": {"short_name": "look"}, - "Layout": {"short_name": "lay"} - }, - "template": { - "work": - "{root}/{project}/{silo}/{asset}/work/{task}/{app}", - "publish": - "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}" - } - }, - "$ref": "config-1.1.json" - } - }, - - "definitions": {} -} diff --git a/schema/project-3.0.json b/schema/project-3.0.json deleted file mode 100644 index be23e10c93..0000000000 --- a/schema/project-3.0.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:project-3.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data", - "config" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:project-3.0"], - "example": "openpype:project-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["project"], - "example": "project" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "hulk" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "fps": 24, - "width": 1920, - "height": 1080 - } - }, - "config": { - "type": "object", - "description": "Document metadata", - "$ref": "config-2.0.json" - } - }, - - "definitions": {} -} diff --git a/schema/representation-1.0.json b/schema/representation-1.0.json deleted file mode 100644 index 347c585f52..0000000000 --- a/schema/representation-1.0.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:representation-1.0", - "description": "The inverse of an instance", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "format", - "path" - ], - - "properties": { - "schema": {"type": "string"}, - "format": { - "description": "File extension, including '.'", - "type": "string" - }, - "path": { - "description": "Unformatted path to version.", - "type": "string" - } - } -} diff --git a/schema/representation-2.0.json b/schema/representation-2.0.json deleted file mode 100644 index f47c16a10a..0000000000 --- a/schema/representation-2.0.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:representation-2.0", - "description": "The inverse of an instance", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:representation-2.0"], - "example": "openpype:representation-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["representation"], - "example": "representation" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of representation", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "abc" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "label": "Alembic" - } - }, - "dependencies": { - "description": "Other representation that this representation depends on", - "type": "array", - "items": {"type": "string"}, - "example": [ - "592d547a5f8c1b388093c145" - ] - }, - "context": { - "description": "Summary of the context to which this representation belong.", - "type": "object", - "properties": { - "project": {"type": "object"}, - "asset": {"type": "string"}, - "silo": {"type": ["string", "null"]}, - "subset": {"type": "string"}, - "version": {"type": "number"}, - "representation": {"type": "string"} - }, - "example": { - "project": "hulk", - "asset": "Bruce", - "silo": "assets", - "subset": "rigDefault", - "version": 12, - "representation": "ma" - } - } - } -} diff --git a/schema/session-1.0.json b/schema/session-1.0.json deleted file mode 100644 index 5ced0a6f08..0000000000 --- a/schema/session-1.0.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-1.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECTS", - "AVALON_PROJECT", - "AVALON_ASSET", - "AVALON_SILO", - "AVALON_CONFIG" - ], - - "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^\\w*$", - "example": "Bruce" - }, - "AVALON_SILO": { - "description": "Name of asset group or container", - "type": "string", - "pattern": "^\\w*$", - "example": "assets" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_CONFIG": { - "description": "Name of Avalon configuration", - "type": "string", - "pattern": "^\\w*$", - "example": "polly" - }, - "AVALON_APP": { - "description": "Name of application", - "type": "string", - "pattern": "^\\w*$", - "example": "maya2016" - }, - "AVALON_MONGO": { - "description": "Address to the asset database", - "type": "string", - "pattern": "^mongodb://[\\w/@:.]*$", - "example": "mongodb://localhost:27017", - "default": "mongodb://localhost:27017" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "Mindbender", - "default": "Avalon" - }, - "AVALON_SENTRY": { - "description": "Address to Sentry", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", - "default": null - }, - "AVALON_DEADLINE": { - "description": "Address to Deadline", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "http://192.168.99.101", - "default": null - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - }, - "AVALON_UPLOAD": { - "description": "Boolean of whether to upload published material to central asset repository", - "type": "string", - "default": null, - "example": "True" - }, - "AVALON_USERNAME": { - "description": "Generic username", - "type": "string", - "pattern": "^\\w*$", - "default": "avalon", - "example": "myself" - }, - "AVALON_PASSWORD": { - "description": "Generic password", - "type": "string", - "pattern": "^\\w*$", - "default": "secret", - "example": "abc123" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" - }, - "AVALON_DEBUG": { - "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", - "type": "string", - "default": null, - "example": "True" - } - } -} diff --git a/schema/session-2.0.json b/schema/session-2.0.json deleted file mode 100644 index 0a4d51beb2..0000000000 --- a/schema/session-2.0.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-2.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECT", - "AVALON_ASSET", - "AVALON_CONFIG" - ], - - "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^\\w*$", - "example": "Bruce" - }, - "AVALON_SILO": { - "description": "Name of asset group or container", - "type": "string", - "pattern": "^\\w*$", - "example": "assets" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_CONFIG": { - "description": "Name of Avalon configuration", - "type": "string", - "pattern": "^\\w*$", - "example": "polly" - }, - "AVALON_APP": { - "description": "Name of application", - "type": "string", - "pattern": "^\\w*$", - "example": "maya2016" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "Mindbender", - "default": "Avalon" - }, - "AVALON_SENTRY": { - "description": "Address to Sentry", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", - "default": null - }, - "AVALON_DEADLINE": { - "description": "Address to Deadline", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "http://192.168.99.101", - "default": null - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - }, - "AVALON_UPLOAD": { - "description": "Boolean of whether to upload published material to central asset repository", - "type": "string", - "default": null, - "example": "True" - }, - "AVALON_USERNAME": { - "description": "Generic username", - "type": "string", - "pattern": "^\\w*$", - "default": "avalon", - "example": "myself" - }, - "AVALON_PASSWORD": { - "description": "Generic password", - "type": "string", - "pattern": "^\\w*$", - "default": "secret", - "example": "abc123" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" - }, - "AVALON_DEBUG": { - "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", - "type": "string", - "default": null, - "example": "True" - } - } -} diff --git a/schema/session-3.0.json b/schema/session-3.0.json deleted file mode 100644 index 9f785939e4..0000000000 --- a/schema/session-3.0.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-3.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECT", - "AVALON_ASSET" - ], - - "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^\\w*$", - "example": "Bruce" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_APP": { - "description": "Name of host", - "type": "string", - "pattern": "^\\w*$", - "example": "maya2016" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "Mindbender", - "default": "Avalon" - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" - } - } -} diff --git a/schema/shaders-1.0.json b/schema/shaders-1.0.json deleted file mode 100644 index 7102ba1861..0000000000 --- a/schema/shaders-1.0.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:shaders-1.0", - "description": "Relationships between shaders and Avalon IDs", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "shader" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "shader": { - "description": "Name of directory", - "type": "array", - "items": { - "type": "str", - "description": "Avalon ID and optional face indexes, e.g. 'f9520572-ac1d-11e6-b39e-3085a99791c9.f[5002:5185]'" - } - } - }, - - "definitions": {} -} diff --git a/schema/subset-1.0.json b/schema/subset-1.0.json deleted file mode 100644 index a299a6d341..0000000000 --- a/schema/subset-1.0.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:subset-1.0", - "description": "A container of instances", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "name", - "versions" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "name": { - "description": "Name of directory", - "type": "string" - }, - "versions": { - "type": "array", - "items": { - "$ref": "version.json" - } - } - }, - - "definitions": {} -} diff --git a/schema/subset-2.0.json b/schema/subset-2.0.json deleted file mode 100644 index db256ec7fb..0000000000 --- a/schema/subset-2.0.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:subset-2.0", - "description": "A container of instances", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:subset-2.0"], - "example": "openpype:subset-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["subset"], - "example": "subset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "shot01" - }, - "data": { - "type": "object", - "description": "Document metadata", - "example": { - "frameStart": 1000, - "frameEnd": 1201 - } - } - } -} diff --git a/schema/subset-3.0.json b/schema/subset-3.0.json deleted file mode 100644 index 1a0db53c04..0000000000 --- a/schema/subset-3.0.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:subset-3.0", - "description": "A container of instances", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:subset-3.0"], - "example": "openpype:subset-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["subset"], - "example": "subset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "shot01" - }, - "data": { - "description": "Document metadata", - "type": "object", - "required": ["families"], - "properties": { - "families": { - "type": "array", - "items": {"type": "string"}, - "description": "One or more families associated with this subset" - } - }, - "example": { - "families" : [ - "avalon.camera" - ], - "frameStart": 1000, - "frameEnd": 1201 - } - } - } -} diff --git a/schema/thumbnail-1.0.json b/schema/thumbnail-1.0.json deleted file mode 100644 index 5bdf78a4b1..0000000000 --- a/schema/thumbnail-1.0.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:thumbnail-1.0", - "description": "Entity with thumbnail data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:thumbnail-1.0"], - "example": "openpype:thumbnail-1.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["thumbnail"], - "example": "thumbnail" - }, - "data": { - "description": "Thumbnail data", - "type": "object", - "example": { - "binary_data": "Binary({byte data of image})", - "template": "{thumbnail_root}/{project[name]}/{_id}{ext}}", - "template_data": { - "ext": ".jpg" - } - } - } - } -} diff --git a/schema/version-1.0.json b/schema/version-1.0.json deleted file mode 100644 index daa1997721..0000000000 --- a/schema/version-1.0.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:version-1.0", - "description": "An individual version", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "version", - "path", - "time", - "author", - "source", - "representations" - ], - - "properties": { - "schema": {"type": "string"}, - "representations": { - "type": "array", - "items": { - "$ref": "representation.json" - } - }, - "time": { - "description": "ISO formatted, file-system compatible time", - "type": "string" - }, - "author": { - "description": "User logged on to the machine at time of publish", - "type": "string" - }, - "version": { - "description": "Number of this version", - "type": "number" - }, - "path": { - "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", - "type": "string" - }, - "source": { - "description": "Original file from which this version was made.", - "type": "string" - } - } -} diff --git a/schema/version-2.0.json b/schema/version-2.0.json deleted file mode 100644 index 099e9be70a..0000000000 --- a/schema/version-2.0.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:version-2.0", - "description": "An individual version", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:version-2.0"], - "example": "openpype:version-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["version"], - "example": "version" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Number of version", - "type": "number", - "example": 12 - }, - "locations": { - "description": "Where on the planet this version can be found.", - "type": "array", - "items": {"type": "string"}, - "example": ["data.avalon.com"] - }, - "data": { - "description": "Document metadata", - "type": "object", - "required": ["families", "author", "source", "time"], - "properties": { - "time": { - "description": "ISO formatted, file-system compatible time", - "type": "string" - }, - "timeFormat": { - "description": "ISO format of time", - "type": "string" - }, - "author": { - "description": "User logged on to the machine at time of publish", - "type": "string" - }, - "version": { - "description": "Number of this version", - "type": "number" - }, - "path": { - "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", - "type": "string" - }, - "source": { - "description": "Original file from which this version was made.", - "type": "string" - }, - "families": { - "type": "array", - "items": {"type": "string"}, - "description": "One or more families associated with this version" - } - }, - "example": { - "source" : "{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma", - "author" : "marcus", - "families" : [ - "avalon.model" - ], - "time" : "20170510T090203Z" - } - } - } -} diff --git a/schema/version-3.0.json b/schema/version-3.0.json deleted file mode 100644 index 3e07fc4499..0000000000 --- a/schema/version-3.0.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:version-3.0", - "description": "An individual version", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:version-3.0"], - "example": "openpype:version-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["version"], - "example": "version" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Number of version", - "type": "number", - "example": 12 - }, - "locations": { - "description": "Where on the planet this version can be found.", - "type": "array", - "items": {"type": "string"}, - "example": ["data.avalon.com"] - }, - "data": { - "description": "Document metadata", - "type": "object", - "required": ["author", "source", "time"], - "properties": { - "time": { - "description": "ISO formatted, file-system compatible time", - "type": "string" - }, - "timeFormat": { - "description": "ISO format of time", - "type": "string" - }, - "author": { - "description": "User logged on to the machine at time of publish", - "type": "string" - }, - "version": { - "description": "Number of this version", - "type": "number" - }, - "path": { - "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", - "type": "string" - }, - "source": { - "description": "Original file from which this version was made.", - "type": "string" - } - }, - "example": { - "source" : "{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma", - "author" : "marcus", - "time" : "20170510T090203Z" - } - } - } -} diff --git a/schema/workfile-1.0.json b/schema/workfile-1.0.json deleted file mode 100644 index 5f9600ef20..0000000000 --- a/schema/workfile-1.0.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:workfile-1.0", - "description": "Workfile additional information.", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "filename", - "task_name", - "parent" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:workfile-1.0"], - "example": "openpype:workfile-1.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["workfile"], - "example": "workfile" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "filename": { - "description": "Workfile's filename", - "type": "string", - "example": "kuba_each_case_Alpaca_01_animation_v001.ma" - }, - "task_name": { - "description": "Task name", - "type": "string", - "example": "animation" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": {"key": "value"} - } - } -} diff --git a/setup.py b/setup.py index 4b6f286730..6179de1d34 100644 --- a/setup.py +++ b/setup.py @@ -124,7 +124,6 @@ bin_includes = [ include_files = [ "igniter", "openpype", - "schema", "LICENSE", "README.md" ] From 975d09d66868d76dad1eb356539389700eb35c99 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 30 Aug 2023 14:29:16 +0200 Subject: [PATCH 158/175] lower urllib3 to support older OpenSSL than 1.1.1 --- poetry.lock | 15 +++++++-------- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5621d39988..5e88f5b93c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3372,21 +3372,20 @@ files = [ [[package]] name = "urllib3" -version = "2.0.2" +version = "1.26.16" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-2.0.2-py3-none-any.whl", hash = "sha256:d055c2f9d38dc53c808f6fdc8eab7360b6fdbbde02340ed25cfbcd817c62469e"}, - {file = "urllib3-2.0.2.tar.gz", hash = "sha256:61717a1095d7e155cdb737ac7bb2f4324a858a1e2e6466f6d03ff630ca68d3cc"}, + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" diff --git a/pyproject.toml b/pyproject.toml index b394a895e3..e2d35f3c6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ QtPy = "^2.3.0" qtawesome = "0.7.3" speedcopy = "^2.1" six = "^1.15" +urllib3 = "1.26.16" semver = "^2.13.0" # for version resolution wsrpc_aiohttp = "^3.1.1" # websocket server pywin32 = { version = "301", markers = "sys_platform == 'win32'" } From b1796d6603c3ff74aaf9798e14f9b4e08e0c9c3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Sep 2023 11:19:33 +0200 Subject: [PATCH 159/175] do not add schema to zip files --- igniter/bootstrap_repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 408764e1a8..e7b440f812 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -589,7 +589,7 @@ class BootstrapRepos: self.registry = OpenPypeSettingsRegistry() self.zip_filter = [".pyc", "__pycache__"] self.openpype_filter = [ - "openpype", "schema", "LICENSE" + "openpype", "LICENSE" ] # dummy progress reporter From b4aa3b15dfbac86834fdf6dad796672634802363 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:31:44 +0200 Subject: [PATCH 160/175] do not override scale factor rounding policy if has defined value through env variable --- igniter/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index 16ffb940f6..085a825860 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -34,7 +34,11 @@ def _get_qt_app(): if attr is not None: QtWidgets.QApplication.setAttribute(attr) - if hasattr(QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy"): + policy = os.getenv("QT_SCALE_FACTOR_ROUNDING_POLICY") + if ( + hasattr(QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy") + and not policy + ): QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough ) From 3e9528b5e06e9de103f0b71f1ab1d063635083e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 23:41:32 +0000 Subject: [PATCH 161/175] Bump certifi from 2023.5.7 to 2023.7.22 Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 178 ++-------------------------------------------------- 1 file changed, 6 insertions(+), 172 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5e88f5b93c..2b9fab4bc4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "acre" version = "1.0.0" description = "Lightweight cross-platform environment management Python package that makes it trivial to launch applications in their own configurable working environment." -category = "main" optional = false python-versions = ">=2.7" files = [] @@ -20,7 +19,6 @@ resolved_reference = "126f7a188cfe36718f707f42ebbc597e86aa86c3" name = "aiohttp" version = "3.8.4" description = "Async http client/server framework (asyncio)" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -129,7 +127,6 @@ speedups = ["Brotli", "aiodns", "cchardet"] name = "aiohttp-json-rpc" version = "0.13.3" description = "Implementation JSON-RPC 2.0 server and client using aiohttp on top of websockets transport" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -144,7 +141,6 @@ aiohttp = ">=3,<4" name = "aiohttp-middlewares" version = "2.2.0" description = "Collection of useful middlewares for aiohttp applications." -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -161,7 +157,6 @@ yarl = ">=1.5.1,<2.0.0" name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -176,7 +171,6 @@ frozenlist = ">=1.1.0" name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -188,7 +182,6 @@ files = [ name = "ansicon" version = "1.89.0" description = "Python wrapper for loading Jason Hood's ANSICON" -category = "main" optional = false python-versions = "*" files = [ @@ -200,7 +193,6 @@ files = [ name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [] @@ -216,7 +208,6 @@ resolved_reference = "8734277956c1df3b85385e6b308e954910533884" name = "arrow" version = "0.17.0" description = "Better dates & times for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -231,7 +222,6 @@ python-dateutil = ">=2.7.0" name = "astroid" version = "2.15.5" description = "An abstract syntax tree for Python with inference support." -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -248,7 +238,6 @@ wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -260,7 +249,6 @@ files = [ name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -271,7 +259,6 @@ files = [ name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -290,7 +277,6 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "autopep8" version = "2.0.2" description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -306,7 +292,6 @@ tomli = {version = "*", markers = "python_version < \"3.11\""} name = "babel" version = "2.12.1" description = "Internationalization utilities" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -318,7 +303,6 @@ files = [ name = "bcrypt" version = "4.0.1" description = "Modern password hashing for your software and your servers" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -353,7 +337,6 @@ typecheck = ["mypy"] name = "bidict" version = "0.22.1" description = "The bidirectional mapping library for Python." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -370,7 +353,6 @@ test = ["hypothesis", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "py name = "blessed" version = "1.20.0" description = "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." -category = "main" optional = false python-versions = ">=2.7" files = [ @@ -387,7 +369,6 @@ wcwidth = ">=0.1.4" name = "cachetools" version = "5.3.1" description = "Extensible memoizing collections and decorators" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -397,21 +378,19 @@ files = [ [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -488,7 +467,6 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.6.1" files = [ @@ -500,7 +478,6 @@ files = [ name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -585,7 +562,6 @@ files = [ name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -600,7 +576,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "clique" version = "1.6.1" description = "Manage collections with common numerical component" -category = "main" optional = false python-versions = ">=2.7, <4.0" files = [ @@ -617,7 +592,6 @@ test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -629,7 +603,6 @@ files = [ name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" -category = "dev" optional = false python-versions = "*" files = [ @@ -644,7 +617,6 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] name = "coolname" version = "2.2.0" description = "Random name and slug generator" -category = "main" optional = false python-versions = "*" files = [ @@ -656,7 +628,6 @@ files = [ name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -732,7 +703,6 @@ toml = ["tomli"] name = "cryptography" version = "39.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -776,7 +746,6 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0 name = "cx-freeze" version = "6.12.0" description = "Create standalone executables from Python scripts" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -834,7 +803,6 @@ test = ["nose (==1.3.7)", "pygments (>=2.11.2)", "pytest (>=7.0.1)", "pytest-cov name = "cx-logging" version = "3.1.0" description = "Python and C interfaces for logging" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -865,7 +833,6 @@ files = [ name = "dill" version = "0.3.6" description = "serialize all of python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -880,7 +847,6 @@ graph = ["objgraph (>=1.7.2)"] name = "distlib" version = "0.3.6" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -892,7 +858,6 @@ files = [ name = "dnspython" version = "2.3.0" description = "DNS toolkit" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -913,7 +878,6 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -925,7 +889,6 @@ files = [ name = "dropbox" version = "11.36.0" description = "Official Dropbox API Client" -category = "main" optional = false python-versions = "*" files = [ @@ -943,7 +906,6 @@ stone = ">=2" name = "enlighten" version = "1.11.2" description = "Enlighten Progress Bar" -category = "main" optional = false python-versions = "*" files = [ @@ -959,7 +921,6 @@ prefixed = ">=0.3.2" name = "evdev" version = "1.6.1" description = "Bindings to the Linux input handling subsystem" -category = "main" optional = false python-versions = "*" files = [ @@ -970,7 +931,6 @@ files = [ name = "filelock" version = "3.12.0" description = "A platform independent file lock." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -986,7 +946,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p name = "flake8" version = "6.0.0" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -1003,7 +962,6 @@ pyflakes = ">=3.0.0,<3.1.0" name = "frozenlist" version = "1.3.3" description = "A list-like structure which implements collections.abc.MutableSequence" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1087,7 +1045,6 @@ files = [ name = "ftrack-python-api" version = "2.5.0" description = "Python API for ftrack." -category = "main" optional = false python-versions = ">=2.7.9, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1110,7 +1067,6 @@ websocket-client = ">=0.40.0,<1" name = "future" version = "0.18.3" description = "Clean single-source support for Python 3 and 2" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1121,7 +1077,6 @@ files = [ name = "gazu" version = "0.9.3" description = "Gazu is a client for Zou, the API to store the data of your CG production." -category = "main" optional = false python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*, != 3.4.*, != 3.5.*, != 3.6.1, != 3.6.2" files = [ @@ -1141,7 +1096,6 @@ test = ["pytest", "pytest-cov", "requests-mock"] name = "gitdb" version = "4.0.10" description = "Git Object Database" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1156,7 +1110,6 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.31" description = "GitPython is a Python library used to interact with Git repositories" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1171,7 +1124,6 @@ gitdb = ">=4.0.1,<5" name = "google-api-core" version = "2.11.0" description = "Google API client core library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1194,7 +1146,6 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] name = "google-api-python-client" version = "1.12.11" description = "Google API Client Library for Python" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" files = [ @@ -1214,7 +1165,6 @@ uritemplate = ">=3.0.0,<4dev" name = "google-auth" version = "2.17.3" description = "Google Authentication Library" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" files = [ @@ -1239,7 +1189,6 @@ requests = ["requests (>=2.20.0,<3.0.0dev)"] name = "google-auth-httplib2" version = "0.1.0" description = "Google Authentication Library: httplib2 transport" -category = "main" optional = false python-versions = "*" files = [ @@ -1256,7 +1205,6 @@ six = "*" name = "googleapis-common-protos" version = "1.59.0" description = "Common protobufs used in Google APIs" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1274,7 +1222,6 @@ grpc = ["grpcio (>=1.44.0,<2.0.0dev)"] name = "httplib2" version = "0.22.0" description = "A comprehensive HTTP client library." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1289,7 +1236,6 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 name = "identify" version = "2.5.24" description = "File identification library for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1304,7 +1250,6 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1316,7 +1261,6 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1328,7 +1272,6 @@ files = [ name = "importlib-metadata" version = "6.6.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1348,7 +1291,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1360,7 +1302,6 @@ files = [ name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -1378,7 +1319,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jedi" version = "0.13.3" description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1396,7 +1336,6 @@ testing = ["colorama", "docopt", "pytest (>=3.1.0)"] name = "jeepney" version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1412,7 +1351,6 @@ trio = ["async_generator", "trio"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1430,7 +1368,6 @@ i18n = ["Babel (>=2.7)"] name = "jinxed" version = "1.2.0" description = "Jinxed Terminal Library" -category = "main" optional = false python-versions = "*" files = [ @@ -1445,7 +1382,6 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""} name = "jsonschema" version = "2.6.0" description = "An implementation of JSON Schema validation for Python" -category = "main" optional = false python-versions = "*" files = [ @@ -1460,7 +1396,6 @@ format = ["rfc3987", "strict-rfc3339", "webcolors"] name = "keyring" version = "22.4.0" description = "Store and access your passwords safely." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1482,7 +1417,6 @@ testing = ["pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2. name = "lazy-object-proxy" version = "1.9.0" description = "A fast and thorough lazy object proxy." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1528,7 +1462,6 @@ files = [ name = "lief" version = "0.13.1" description = "Library to instrument executable formats" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1561,7 +1494,6 @@ files = [ name = "linkify-it-py" version = "2.0.2" description = "Links recognition library with FULL unicode support." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1582,7 +1514,6 @@ test = ["coverage", "pytest", "pytest-cov"] name = "log4mongo" version = "1.7.0" description = "mongo database handler for python logging" -category = "main" optional = false python-versions = "*" files = [ @@ -1596,7 +1527,6 @@ pymongo = "*" name = "m2r2" version = "0.3.3.post2" description = "Markdown and reStructuredText in a single file." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1612,7 +1542,6 @@ mistune = "0.8.4" name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1637,7 +1566,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1716,7 +1644,6 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1728,7 +1655,6 @@ files = [ name = "mdit-py-plugins" version = "0.3.5" description = "Collection of plugins for markdown-it-py" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1748,7 +1674,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1760,7 +1685,6 @@ files = [ name = "mistune" version = "0.8.4" description = "The fastest markdown parser in pure Python" -category = "dev" optional = false python-versions = "*" files = [ @@ -1772,7 +1696,6 @@ files = [ name = "multidict" version = "6.0.4" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1856,7 +1779,6 @@ files = [ name = "myst-parser" version = "0.18.1" description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1883,7 +1805,6 @@ testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1898,7 +1819,6 @@ setuptools = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1910,7 +1830,6 @@ files = [ name = "paramiko" version = "3.2.0" description = "SSH2 protocol library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1932,7 +1851,6 @@ invoke = ["invoke (>=2.0)"] name = "parso" version = "0.8.3" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1948,7 +1866,6 @@ testing = ["docopt", "pytest (<6.0.0)"] name = "patchelf" version = "0.17.2.1" description = "A small utility to modify the dynamic linker and RPATH of ELF executables." -category = "dev" optional = false python-versions = "*" files = [ @@ -1968,7 +1885,6 @@ test = ["importlib-metadata", "pytest"] name = "pathlib2" version = "2.3.7.post1" description = "Object-oriented filesystem paths" -category = "main" optional = false python-versions = "*" files = [ @@ -1983,7 +1899,6 @@ six = "*" name = "pillow" version = "9.5.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2063,7 +1978,6 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa name = "platformdirs" version = "3.5.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2079,7 +1993,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2095,7 +2008,6 @@ testing = ["pytest", "pytest-benchmark"] name = "ply" version = "3.11" description = "Python Lex & Yacc" -category = "main" optional = false python-versions = "*" files = [ @@ -2107,7 +2019,6 @@ files = [ name = "pockets" version = "0.9.1" description = "A collection of helpful Python tools!" -category = "dev" optional = false python-versions = "*" files = [ @@ -2122,7 +2033,6 @@ six = ">=1.5.2" name = "pre-commit" version = "3.3.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2141,7 +2051,6 @@ virtualenv = ">=20.10.0" name = "prefixed" version = "0.7.0" description = "Prefixed alternative numeric library" -category = "main" optional = false python-versions = "*" files = [ @@ -2153,7 +2062,6 @@ files = [ name = "protobuf" version = "4.23.2" description = "" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2176,7 +2084,6 @@ files = [ name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2188,7 +2095,6 @@ files = [ name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2200,7 +2106,6 @@ files = [ name = "pyasn1-modules" version = "0.3.0" description = "A collection of ASN.1-based protocols modules" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -2215,7 +2120,6 @@ pyasn1 = ">=0.4.6,<0.6.0" name = "pyblish-base" version = "1.8.11" description = "Plug-in driven automation framework for content" -category = "main" optional = false python-versions = "*" files = [ @@ -2227,7 +2131,6 @@ files = [ name = "pycodestyle" version = "2.10.0" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2239,7 +2142,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2251,7 +2153,6 @@ files = [ name = "pydocstyle" version = "6.3.0" description = "Python docstring style checker" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2269,7 +2170,6 @@ toml = ["tomli (>=1.2.3)"] name = "pyflakes" version = "3.0.1" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2281,7 +2181,6 @@ files = [ name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2296,7 +2195,6 @@ plugins = ["importlib-metadata"] name = "pylint" version = "2.17.4" description = "python code static checker" -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -2323,7 +2221,6 @@ testutils = ["gitpython (>3)"] name = "pymongo" version = "3.13.0" description = "Python driver for MongoDB " -category = "main" optional = false python-versions = "*" files = [ @@ -2452,7 +2349,6 @@ zstd = ["zstandard"] name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2479,7 +2375,6 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] name = "pynput" version = "1.7.6" description = "Monitor and control user input devices" -category = "main" optional = false python-versions = "*" files = [ @@ -2498,7 +2393,6 @@ six = "*" name = "pyobjc-core" version = "9.1.1" description = "Python<->ObjC Interoperability Module" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2515,7 +2409,6 @@ files = [ name = "pyobjc-framework-applicationservices" version = "9.1.1" description = "Wrappers for the framework ApplicationServices on macOS" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2537,7 +2430,6 @@ pyobjc-framework-Quartz = ">=9.1.1" name = "pyobjc-framework-cocoa" version = "9.1.1" description = "Wrappers for the Cocoa frameworks on macOS" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2557,7 +2449,6 @@ pyobjc-core = ">=9.1.1" name = "pyobjc-framework-quartz" version = "9.1.1" description = "Wrappers for the Quartz frameworks on macOS" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2578,7 +2469,6 @@ pyobjc-framework-Cocoa = ">=9.1.1" name = "pyparsing" version = "2.4.7" description = "Python parsing module" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2590,7 +2480,6 @@ files = [ name = "pysftp" version = "0.2.9" description = "A friendly face on SFTP" -category = "main" optional = false python-versions = "*" files = [ @@ -2604,7 +2493,6 @@ paramiko = ">=1.17" name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2629,7 +2517,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2648,7 +2535,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-print" version = "0.3.1" description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2666,7 +2552,6 @@ test = ["coverage (>=5)"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2681,7 +2566,6 @@ six = ">=1.5" name = "python-engineio" version = "4.4.1" description = "Engine.IO server and client for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2697,7 +2581,6 @@ client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] name = "python-socketio" version = "5.8.0" description = "Socket.IO server and client for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2719,7 +2602,6 @@ client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] name = "python-xlib" version = "0.33" description = "Python X Library" -category = "main" optional = false python-versions = "*" files = [ @@ -2734,7 +2616,6 @@ six = ">=1.10.0" name = "python3-xlib" version = "0.15" description = "Python3 X Library" -category = "main" optional = false python-versions = "*" files = [ @@ -2745,7 +2626,6 @@ files = [ name = "pywin32" version = "301" description = "Python for Window Extensions" -category = "main" optional = false python-versions = "*" files = [ @@ -2765,7 +2645,6 @@ files = [ name = "pywin32-ctypes" version = "0.2.0" description = "" -category = "main" optional = false python-versions = "*" files = [ @@ -2777,7 +2656,6 @@ files = [ name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2827,7 +2705,6 @@ files = [ name = "qt-py" version = "1.3.8" description = "Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5." -category = "main" optional = false python-versions = "*" files = [ @@ -2842,7 +2719,6 @@ types-PySide2 = "*" name = "qtawesome" version = "0.7.3" description = "FontAwesome icons in PyQt and PySide applications" -category = "main" optional = false python-versions = "*" files = [ @@ -2858,7 +2734,6 @@ six = "*" name = "qtpy" version = "2.3.1" description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2876,7 +2751,6 @@ test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] name = "recommonmark" version = "0.7.1" description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." -category = "dev" optional = false python-versions = "*" files = [ @@ -2893,7 +2767,6 @@ sphinx = ">=1.3.1" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2915,7 +2788,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "revitron-sphinx-theme" version = "0.7.2" description = "Revitron theme for Sphinx" -category = "dev" optional = false python-versions = "*" files = [] @@ -2937,7 +2809,6 @@ resolved_reference = "c0779c66365d9d258d93575ebaff7db9d3aee282" name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -2952,7 +2823,6 @@ pyasn1 = ">=0.1.3" name = "secretstorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2968,7 +2838,6 @@ jeepney = ">=0.6" name = "semver" version = "2.13.0" description = "Python helper for Semantic Versioning (http://semver.org/)" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2980,7 +2849,6 @@ files = [ name = "setuptools" version = "65.7.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2997,7 +2865,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "shotgun-api3" version = "3.3.3" description = "Shotgun Python API" -category = "main" optional = false python-versions = "*" files = [] @@ -3013,7 +2880,6 @@ resolved_reference = "b9f066c0edbea6e0733242e18f32f75489064840" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3025,7 +2891,6 @@ files = [ name = "slack-sdk" version = "3.21.3" description = "The Slack API Platform SDK for Python" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -3041,7 +2906,6 @@ testing = ["Flask (>=1,<2)", "Flask-Sockets (>=0.2,<1)", "Jinja2 (==3.0.3)", "We name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3053,7 +2917,6 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" optional = false python-versions = "*" files = [ @@ -3065,7 +2928,6 @@ files = [ name = "speedcopy" version = "2.1.4" description = "Replacement or alternative for python copyfile() utilizing server side copy on network shares for faster copying." -category = "main" optional = false python-versions = "*" files = [ @@ -3077,7 +2939,6 @@ files = [ name = "sphinx" version = "5.3.0" description = "Python documentation generator" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -3113,7 +2974,6 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] name = "sphinx-autoapi" version = "2.1.0" description = "Sphinx API documentation generator" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3137,7 +2997,6 @@ go = ["sphinxcontrib-golangdomain"] name = "sphinxcontrib-applehelp" version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3153,7 +3012,6 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3169,7 +3027,6 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3185,7 +3042,6 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3200,7 +3056,6 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-napoleon" version = "0.7" description = "Sphinx \"napoleon\" extension." -category = "dev" optional = false python-versions = "*" files = [ @@ -3216,7 +3071,6 @@ six = ">=1.5.2" name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3232,7 +3086,6 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -3248,7 +3101,6 @@ test = ["pytest"] name = "stone" version = "3.3.1" description = "Stone is an interface description language (IDL) for APIs." -category = "main" optional = false python-versions = "*" files = [ @@ -3265,7 +3117,6 @@ six = ">=1.12.0" name = "termcolor" version = "1.1.0" description = "ANSII Color formatting for output in terminal." -category = "main" optional = false python-versions = "*" files = [ @@ -3276,7 +3127,6 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3288,7 +3138,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3300,7 +3149,6 @@ files = [ name = "tomlkit" version = "0.11.8" description = "Style preserving TOML library" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3312,7 +3160,6 @@ files = [ name = "types-pyside2" version = "5.15.2.1.5" description = "The most accurate stubs for PySide2" -category = "main" optional = false python-versions = "*" files = [ @@ -3323,7 +3170,6 @@ files = [ name = "typing-extensions" version = "4.6.2" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3335,7 +3181,6 @@ files = [ name = "uc-micro-py" version = "1.0.2" description = "Micro subset of unicode data files for linkify-it-py projects." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3350,7 +3195,6 @@ test = ["coverage", "pytest", "pytest-cov"] name = "unidecode" version = "1.2.0" description = "ASCII transliterations of Unicode text" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3362,7 +3206,6 @@ files = [ name = "uritemplate" version = "3.0.1" description = "URI templates" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3374,7 +3217,6 @@ files = [ name = "urllib3" version = "1.26.16" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -3391,7 +3233,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "virtualenv" version = "20.23.0" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3412,7 +3253,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess name = "wcwidth" version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" -category = "main" optional = false python-versions = "*" files = [ @@ -3424,7 +3264,6 @@ files = [ name = "websocket-client" version = "0.59.0" description = "WebSocket client for Python with low level API options" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3439,7 +3278,6 @@ six = "*" name = "wheel" version = "0.40.0" description = "A built-package format for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3454,7 +3292,6 @@ test = ["pytest (>=6.0.0)"] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -3539,7 +3376,6 @@ files = [ name = "wsrpc-aiohttp" version = "3.2.0" description = "WSRPC is the RPC over WebSocket for aiohttp" -category = "main" optional = false python-versions = ">3.5.*, <4" files = [ @@ -3560,7 +3396,6 @@ ujson = ["ujson"] name = "yarl" version = "1.9.2" description = "Yet another URL library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3648,7 +3483,6 @@ multidict = ">=4.0" name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3665,5 +3499,5 @@ docs = [] [metadata] lock-version = "2.0" -python-versions = ">=3.9.1,<3.10" -content-hash = "d2b8da22dcd11e0b03f19b9b79e51f205156c5ce75e41cc0225392e9afd8803b" +python-versions = "~3.9" +content-hash = "bc3e256094db6e33894840bb6a5adda4473d3736b852433ad8d5bd478c7e0c1c" From 817ef16840467b55c0832d74696d5ba29ad20929 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:04:08 +0200 Subject: [PATCH 162/175] lower version of click to 7.1.2 to support older python versions (#5629) --- poetry.lock | 11 ++++------- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2b9fab4bc4..d074a0c3d9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -560,18 +560,15 @@ files = [ [[package]] name = "click" -version = "8.1.3" +version = "7.1.2" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "clique" version = "1.6.1" diff --git a/pyproject.toml b/pyproject.toml index e2d35f3c6d..68d31cf4ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master blessed = "^1.17" # openpype terminal formatting coolname = "*" clique = "1.6.*" -Click = "^8" +Click = "7.1.2" dnspython = "^2.1.0" ftrack-python-api = "^2.3.3" arrow = "^0.17" From 87d8ff66bbb90dd81a93266ba16cf56273d93613 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 19 Sep 2023 15:47:43 +0000 Subject: [PATCH 163/175] [Automated] Release --- CHANGELOG.md | 122 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 935e7dbc4c..9001e9fec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,128 @@ # Changelog +## [3.16.8](https://github.com/ynput/OpenPype/tree/3.16.8) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.7...3.16.8) + +### **🚀 Enhancements** + + +
+Chore: Remove schema from OpenPype root #5355 + +Remove unused schema directory in root of repository which was moved inside openpype/pipeline/schema. + + +___ + +
+ + +
+Igniter: Allow custom Qt scale factor rounding policy #5554 + +Do not force `PassThrough` rounding policy if different policy is defined via env variable. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Chore: Lower urllib3 to support older OpenSSL #5538 + +Lowered `urllib3` to `1.26.16` to support older OpenSSL. + + +___ + +
+ + +
+Chore: Do not try to add schema to zip files #5557 + +Do not add `schema` folder to zip file. This fixes issue cause by https://github.com/ynput/OpenPype/pull/5355 . + + +___ + +
+ + +
+Chore: Lower click dependency version #5629 + +Lower click version to support older versions of python. + + +___ + +
+ +### **Merged pull requests** + + +
+Bump certifi from 2023.5.7 to 2023.7.22 #5351 + +Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22. +
+Commits + +
+
+ + +[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2023.5.7&new-version=2023.7.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) + +You can trigger a rebase of this PR by commenting `@dependabot rebase`. + +[//]: # (dependabot-automerge-start) +[//]: # (dependabot-automerge-end) + +--- + +
+Dependabot commands and options +
+ +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it +- `@dependabot merge` will merge this PR after your CI passes on it +- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it +- `@dependabot cancel merge` will cancel a previously requested merge and block automerging +- `@dependabot reopen` will reopen this PR if it is closed +- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually +- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) +You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts). + +
+ +> **Note** +> Automatic rebases have been disabled on this pull request as it has been open for over 30 days. + +___ + +
+ + + + ## [3.16.7](https://github.com/ynput/OpenPype/tree/3.16.7) diff --git a/openpype/version.py b/openpype/version.py index c593f0f71f..0578bb8780 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.7" +__version__ = "3.16.8" diff --git a/pyproject.toml b/pyproject.toml index b394a895e3..c5d5e87f49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.7" # OpenPype +version = "3.16.8" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 5bf4b826ac960103f121f094506100e993507bef Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 19 Sep 2023 17:56:06 +0200 Subject: [PATCH 164/175] update release --- CHANGELOG.md | 4 ++-- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9001e9fec5..4bcf66a210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Changelog -## [3.16.8](https://github.com/ynput/OpenPype/tree/3.16.8) +## [3.17.0](https://github.com/ynput/OpenPype/tree/3.17.0) -[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.7...3.16.8) +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.7...3.17.0) ### **🚀 Enhancements** diff --git a/openpype/version.py b/openpype/version.py index 0578bb8780..281e756967 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.8" +__version__ = "3.17.0" diff --git a/pyproject.toml b/pyproject.toml index 233cf73db8..d0b1ecf589 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.8" # OpenPype +version = "3.17.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 77600e624c31697f515ac37d3d0834253d0951bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Sep 2023 15:48:46 +0000 Subject: [PATCH 165/175] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9159d3fe4a..be515983ca 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.8 - 3.16.7 - 3.16.7-nightly.2 - 3.16.7-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.10-nightly.3 - 3.14.10-nightly.2 - 3.14.10-nightly.1 - - 3.14.9 validations: required: true - type: dropdown From dc5c730cba6119342600dcbe6a5a13d6d1c4b569 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 19 Sep 2023 16:01:55 +0000 Subject: [PATCH 166/175] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index be515983ca..0762eb2f20 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,7 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: - - 3.16.8 + - 3.17.0 - 3.16.7 - 3.16.7-nightly.2 - 3.16.7-nightly.1 From 7195479080639489b65a2715b3fba74414dc8815 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 20 Sep 2023 03:24:46 +0000 Subject: [PATCH 167/175] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 281e756967..483b70436a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.0" +__version__ = "3.17.1-nightly.1" From 41845651b2aaf701b3a7d27c0091a23948c3c2b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:38:35 +0200 Subject: [PATCH 168/175] Update setup.cfg Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 216bae848f..ead9b25164 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ omit = /tests directory = ./coverage [tool:pytest] -norecursedirs = repos/* openpype/modules/ftrack/* +norecursedirs = openpype/modules/ftrack/* [isort] line_length = 79 From d91f54f2f74edac0a729513639f40ed2558e5ff5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 20 Sep 2023 19:18:33 +0300 Subject: [PATCH 169/175] rename files and make validator optional --- ...e_unreal_staticmesh.py => create_staticmesh.py} | 14 +++++++------- .../plugins/publish/collect_staticmesh_type.py | 2 +- .../publish/validate_unreal_staticmesh_naming.py | 2 +- .../settings/defaults/project_settings/global.json | 3 +-- .../defaults/project_settings/houdini.json | 4 ++-- .../schemas/schema_houdini_create.json | 4 ++-- server_addon/core/server/settings/tools.py | 3 +-- .../houdini/server/settings/publish_plugins.py | 12 ++++++------ 8 files changed, 21 insertions(+), 23 deletions(-) rename openpype/hosts/houdini/plugins/create/{create_unreal_staticmesh.py => create_staticmesh.py} (91%) diff --git a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py similarity index 91% rename from openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py rename to openpype/hosts/houdini/plugins/create/create_staticmesh.py index 2f92def54a..ea0b36f03f 100644 --- a/openpype/hosts/houdini/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py @@ -6,11 +6,11 @@ from openpype.lib import BoolDef, EnumDef import hou -class CreateUnrealStaticMesh(plugin.HoudiniCreator): - """Unreal Static Meshes with collisions. """ +class CreateStaticMesh(plugin.HoudiniCreator): + """Static Meshes as FBX. """ - identifier = "io.openpype.creators.houdini.unrealstaticmesh.fbx" - label = "Unreal - Static Mesh (FBX)" + identifier = "io.openpype.creators.houdini.staticmesh.fbx" + label = "Static Mesh (FBX)" family = "staticMesh" icon = "fa5s.cubes" @@ -20,7 +20,7 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): instance_data.update({"node_type": "filmboxfbx"}) - instance = super(CreateUnrealStaticMesh, self).create( + instance = super(CreateStaticMesh, self).create( subset_name, instance_data, pre_create_data) @@ -60,7 +60,7 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): """Add settings for users. """ - attrs = super(CreateUnrealStaticMesh, self).get_pre_create_attr_defs() + attrs = super(CreateStaticMesh, self).get_pre_create_attr_defs() createsubnetroot = BoolDef("createsubnetroot", tooltip="Create an extra root for the " "Export node when it's a " @@ -93,7 +93,7 @@ class CreateUnrealStaticMesh(plugin.HoudiniCreator): The default subset name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ - dynamic_data = super(CreateUnrealStaticMesh, self).get_dynamic_data( + dynamic_data = super(CreateStaticMesh, self).get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, instance ) dynamic_data["asset"] = asset_doc["name"] diff --git a/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py index 263d7c1001..db9efec7a1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py +++ b/openpype/hosts/houdini/plugins/publish/collect_staticmesh_type.py @@ -15,6 +15,6 @@ class CollectStaticMeshType(pyblish.api.InstancePlugin): def process(self, instance): - if instance.data["creator_identifier"] == "io.openpype.creators.houdini.unrealstaticmesh.fbx": # noqa: E501 + if instance.data["creator_identifier"] == "io.openpype.creators.houdini.staticmesh.fbx": # noqa: E501 # Marking this instance as FBX triggers the FBX extractor. instance.data["families"] += ["fbx"] diff --git a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index 791db8198f..ae3c7e5602 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -42,7 +42,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, def apply_settings(cls, project_settings, system_settings): settings = ( - project_settings["houdini"]["create"]["CreateUnrealStaticMesh"] + project_settings["houdini"]["create"]["CreateStaticMesh"] ) cls.collision_prefixes = settings["collision_prefixes"] cls.static_mesh_prefix = settings["static_mesh_prefix"] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 52ac745f6d..06a595d1c5 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -429,8 +429,7 @@ "staticMesh" ], "hosts": [ - "maya", - "houdini" + "maya" ], "task_types": [], "tasks": [], diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 6964db0013..5392fc34dd 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -19,7 +19,7 @@ ], "ext": ".ass" }, - "CreateUnrealStaticMesh": { + "CreateStaticMesh": { "enabled": true, "default_variants": [ "Main" @@ -127,7 +127,7 @@ "active": true }, "ValidateUnrealStaticMeshName": { - "enabled": true, + "enabled": false, "optional": true, "active": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index b19761df91..cd8c260124 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -42,8 +42,8 @@ { "type": "dict", "collapsible": true, - "key": "CreateUnrealStaticMesh", - "label": "Create Unreal - Static Mesh", + "key": "CreateStaticMesh", + "label": "Create Static Mesh", "checkbox_key": "enabled", "children": [ { diff --git a/server_addon/core/server/settings/tools.py b/server_addon/core/server/settings/tools.py index 5dbe6ab215..7befc795e4 100644 --- a/server_addon/core/server/settings/tools.py +++ b/server_addon/core/server/settings/tools.py @@ -370,8 +370,7 @@ DEFAULT_TOOLS_VALUES = { "staticMesh" ], "hosts": [ - "maya", - "houdini" + "maya" ], "task_types": [], "tasks": [], diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 6ceff028a5..58240b0205 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -21,7 +21,7 @@ class CreateArnoldAssModel(BaseSettingsModel): ext: str = Field(Title="Extension") -class CreateUnrealStaticMeshModel(BaseSettingsModel): +class CreateStaticMeshModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") default_variants: list[str] = Field( default_factory=list, @@ -39,9 +39,9 @@ class CreatePluginsModel(BaseSettingsModel): default_factory=CreateArnoldAssModel, title="Create Alembic Camera") # "-" is not compatible in the new model - CreateUnrealStaticMesh: CreateUnrealStaticMeshModel = Field( - default_factory=CreateUnrealStaticMeshModel, - title="Create Unreal_Static Mesh" + CreateStaticMesh: CreateStaticMeshModel = Field( + default_factory=CreateStaticMeshModel, + title="Create Static Mesh" ) CreateAlembicCamera: CreatorModel = Field( default_factory=CreatorModel, @@ -81,7 +81,7 @@ DEFAULT_HOUDINI_CREATE_SETTINGS = { "default_variants": ["Main"], "ext": ".ass" }, - "CreateUnrealStaticMesh": { + "CreateStaticMesh": { "enabled": True, "default_variants": [ "Main" @@ -212,7 +212,7 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "active": True }, "ValidateUnrealStaticMeshName": { - "enabled": True, + "enabled": False, "optional": True, "active": True } From 23e9f5e504c482bc13268d16190378142b5ca936 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 21 Sep 2023 10:26:33 +0200 Subject: [PATCH 170/175] TVPaint: Fix review family extraction (#5637) * mark review family representation for review * implemented 'get_publish_instance_families' in publish lib * use 'get_publish_instance_families' in tvpaint extract sequence --- .../plugins/publish/extract_sequence.py | 8 +++++-- openpype/pipeline/publish/__init__.py | 2 ++ openpype/pipeline/publish/lib.py | 24 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index a13a91de46..fd568b2826 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -6,7 +6,10 @@ from PIL import Image import pyblish.api -from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish import ( + KnownPublishError, + get_publish_instance_families, +) from openpype.hosts.tvpaint.api.lib import ( execute_george, execute_george_through_file, @@ -140,8 +143,9 @@ class ExtractSequence(pyblish.api.Extractor): ) # Fill tags and new families from project settings + instance_families = get_publish_instance_families(instance) tags = [] - if "review" in instance.data["families"]: + if "review" in instance_families: tags.append("review") # Sequence of one frame diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 0c57915c05..3a82d6f565 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -40,6 +40,7 @@ from .lib import ( apply_plugin_settings_automatically, get_plugin_settings, get_publish_instance_label, + get_publish_instance_families, ) from .abstract_expected_files import ExpectedFiles @@ -87,6 +88,7 @@ __all__ = ( "apply_plugin_settings_automatically", "get_plugin_settings", "get_publish_instance_label", + "get_publish_instance_families", "ExpectedFiles", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 1ae6ea43b2..4d9443f635 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1002,3 +1002,27 @@ def get_publish_instance_label(instance): or instance.data.get("name") or str(instance) ) + + +def get_publish_instance_families(instance): + """Get all families of the instance. + + Look for families under 'family' and 'families' keys in instance data. + Value of 'family' is used as first family and then all other families + in random order. + + Args: + pyblish.api.Instance: Instance to get families from. + + Returns: + list[str]: List of families. + """ + + family = instance.data.get("family") + families = set(instance.data.get("families") or []) + output = [] + if family: + output.append(family) + families.discard(family) + output.extend(families) + return output From 3cf203e46580d88c842bd931177ebdb159690f89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Sep 2023 10:48:31 +0200 Subject: [PATCH 171/175] AYON settings: Extract OIIO transcode settings (#5639) * added name to ExtractOIIOTranscode output definition * convert outputs of 'ExtractOIIOTranscode' to 'dict' --- openpype/settings/ayon_settings.py | 23 ++++++++++++++++++- .../core/server/settings/publish_plugins.py | 7 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 9a4f0607e0..3be8ac8ae5 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1102,7 +1102,7 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): "studio_name", "studio_code", ): - ayon_core.pop(key) + ayon_core.pop(key, None) # Publish conversion ayon_publish = ayon_core["publish"] @@ -1140,6 +1140,27 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): profile["outputs"] = new_outputs + # ExtractOIIOTranscode plugin + extract_oiio_transcode = ayon_publish["ExtractOIIOTranscode"] + extract_oiio_transcode_profiles = extract_oiio_transcode["profiles"] + for profile in extract_oiio_transcode_profiles: + new_outputs = {} + name_counter = {} + for output in profile["outputs"]: + if "name" in output: + name = output.pop("name") + else: + # Backwards compatibility for setting without 'name' in model + name = output["extension"] + if name in new_outputs: + name_counter[name] += 1 + name = "{}_{}".format(name, name_counter[name]) + else: + name_counter[name] = 0 + + new_outputs[name] = output + profile["outputs"] = new_outputs + # Extract Burnin plugin extract_burnin = ayon_publish["ExtractBurnin"] extract_burnin_options = extract_burnin["options"] diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py index c012312579..69a759465e 100644 --- a/server_addon/core/server/settings/publish_plugins.py +++ b/server_addon/core/server/settings/publish_plugins.py @@ -116,6 +116,8 @@ class OIIOToolArgumentsModel(BaseSettingsModel): class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Name") extension: str = Field("", title="Extension") transcoding_type: str = Field( "colorspace", @@ -164,6 +166,11 @@ class ExtractOIIOTranscodeProfileModel(BaseSettingsModel): title="Output Definitions", ) + @validator("outputs") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + class ExtractOIIOTranscodeModel(BaseSettingsModel): enabled: bool = Field(True) From 87ed2f960daa97d4d94b73403c5076519bf6b20c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:47:39 +0200 Subject: [PATCH 172/175] Launcher tool: Refactor launcher tool (for AYON) (#5612) * added helper classes to utils * implemented base of ayon utils * initial commit for launcher tool * use image for extender * actions are shown and can be triggered * fix actions on finished refresh * refresh automatically * fix re-refreshing of projects model * added page slide animation * updated abstrack classes * change how icon is prepared * fix actions sorting * show messages like in launcher tool * do not clear items on refresh * stop refresh timer only on close event * use Ynput/AYON for local settings json * register default actions in launcher action module * change register naming * move 'SquareButton' to utils widgets * removed duplicated method * removed unused variable * removed unused import * don't use lambda * swap default name for 'OpenPypeSettingsRegistry' * Change support version --- openpype/lib/local_settings.py | 14 +- openpype/modules/launcher_action.py | 73 ++- openpype/pipeline/actions.py | 8 +- openpype/tools/ayon_launcher/abstract.py | 297 ++++++++++ openpype/tools/ayon_launcher/control.py | 149 ++++++ .../tools/ayon_launcher/models/__init__.py | 8 + .../tools/ayon_launcher/models/actions.py | 505 ++++++++++++++++++ .../tools/ayon_launcher/models/selection.py | 72 +++ openpype/tools/ayon_launcher/ui/__init__.py | 6 + .../tools/ayon_launcher/ui/actions_widget.py | 453 ++++++++++++++++ .../tools/ayon_launcher/ui/hierarchy_page.py | 102 ++++ .../tools/ayon_launcher/ui/projects_widget.py | 135 +++++ .../ayon_launcher/ui/resources/__init__.py | 7 + .../ayon_launcher/ui/resources/options.png | Bin 0 -> 1772 bytes openpype/tools/ayon_launcher/ui/window.py | 295 ++++++++++ openpype/tools/ayon_utils/models/__init__.py | 29 + openpype/tools/ayon_utils/models/cache.py | 196 +++++++ openpype/tools/ayon_utils/models/hierarchy.py | 340 ++++++++++++ openpype/tools/ayon_utils/models/projects.py | 145 +++++ openpype/tools/ayon_utils/widgets/__init__.py | 37 ++ .../ayon_utils/widgets/folders_widget.py | 364 +++++++++++++ .../ayon_utils/widgets/projects_widget.py | 325 +++++++++++ .../tools/ayon_utils/widgets/tasks_widget.py | 436 +++++++++++++++ openpype/tools/ayon_utils/widgets/utils.py | 98 ++++ openpype/tools/launcher/actions.py | 44 +- openpype/tools/utils/__init__.py | 9 + openpype/tools/utils/widgets.py | 79 ++- 27 files changed, 4158 insertions(+), 68 deletions(-) create mode 100644 openpype/tools/ayon_launcher/abstract.py create mode 100644 openpype/tools/ayon_launcher/control.py create mode 100644 openpype/tools/ayon_launcher/models/__init__.py create mode 100644 openpype/tools/ayon_launcher/models/actions.py create mode 100644 openpype/tools/ayon_launcher/models/selection.py create mode 100644 openpype/tools/ayon_launcher/ui/__init__.py create mode 100644 openpype/tools/ayon_launcher/ui/actions_widget.py create mode 100644 openpype/tools/ayon_launcher/ui/hierarchy_page.py create mode 100644 openpype/tools/ayon_launcher/ui/projects_widget.py create mode 100644 openpype/tools/ayon_launcher/ui/resources/__init__.py create mode 100644 openpype/tools/ayon_launcher/ui/resources/options.png create mode 100644 openpype/tools/ayon_launcher/ui/window.py create mode 100644 openpype/tools/ayon_utils/models/__init__.py create mode 100644 openpype/tools/ayon_utils/models/cache.py create mode 100644 openpype/tools/ayon_utils/models/hierarchy.py create mode 100644 openpype/tools/ayon_utils/models/projects.py create mode 100644 openpype/tools/ayon_utils/widgets/__init__.py create mode 100644 openpype/tools/ayon_utils/widgets/folders_widget.py create mode 100644 openpype/tools/ayon_utils/widgets/projects_widget.py create mode 100644 openpype/tools/ayon_utils/widgets/tasks_widget.py create mode 100644 openpype/tools/ayon_utils/widgets/utils.py diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 3fb35a7e7b..dae6e074af 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -494,10 +494,18 @@ class OpenPypeSettingsRegistry(JSONSettingRegistry): """ def __init__(self, name=None): - self.vendor = "pypeclub" - self.product = "openpype" + if AYON_SERVER_ENABLED: + vendor = "Ynput" + product = "AYON" + default_name = "AYON_settings" + else: + vendor = "pypeclub" + product = "openpype" + default_name = "openpype_settings" + self.vendor = vendor + self.product = product if not name: - name = "openpype_settings" + name = default_name path = appdirs.user_data_dir(self.product, self.vendor) super(OpenPypeSettingsRegistry, self).__init__(name, path) diff --git a/openpype/modules/launcher_action.py b/openpype/modules/launcher_action.py index c4331b6094..5e14f25f76 100644 --- a/openpype/modules/launcher_action.py +++ b/openpype/modules/launcher_action.py @@ -1,3 +1,6 @@ +import os + +from openpype import PLUGINS_DIR, AYON_SERVER_ENABLED from openpype.modules import ( OpenPypeModule, ITrayAction, @@ -13,36 +16,66 @@ class LauncherAction(OpenPypeModule, ITrayAction): self.enabled = True # Tray attributes - self.window = None + self._window = None def tray_init(self): - self.create_window() + self._create_window() - self.add_doubleclick_callback(self.show_launcher) + self.add_doubleclick_callback(self._show_launcher) def tray_start(self): return def connect_with_modules(self, enabled_modules): # Register actions - if self.tray_initialized: - from openpype.tools.launcher import actions - actions.register_config_actions() - actions_paths = self.manager.collect_plugin_paths()["actions"] - actions.register_actions_from_paths(actions_paths) - actions.register_environment_actions() - - def create_window(self): - if self.window: + if not self.tray_initialized: return - from openpype.tools.launcher import LauncherWindow - self.window = LauncherWindow() + + from openpype.pipeline.actions import register_launcher_action_path + + actions_dir = os.path.join(PLUGINS_DIR, "actions") + if os.path.exists(actions_dir): + register_launcher_action_path(actions_dir) + + actions_paths = self.manager.collect_plugin_paths()["actions"] + for path in actions_paths: + if path and os.path.exists(path): + register_launcher_action_path(actions_dir) + + paths_str = os.environ.get("AVALON_ACTIONS") or "" + if paths_str: + self.log.warning( + "WARNING: 'AVALON_ACTIONS' is deprecated. Support of this" + " environment variable will be removed in future versions." + " Please consider using 'OpenPypeModule' to define custom" + " action paths. Planned version to drop the support" + " is 3.17.2 or 3.18.0 ." + ) + + for path in paths_str.split(os.pathsep): + if path and os.path.exists(path): + register_launcher_action_path(path) def on_action_trigger(self): - self.show_launcher() + """Implementation for ITrayAction interface. - def show_launcher(self): - if self.window: - self.window.show() - self.window.raise_() - self.window.activateWindow() + Show launcher tool on action trigger. + """ + + self._show_launcher() + + def _create_window(self): + if self._window: + return + if AYON_SERVER_ENABLED: + from openpype.tools.ayon_launcher.ui import LauncherWindow + else: + from openpype.tools.launcher import LauncherWindow + self._window = LauncherWindow() + + def _show_launcher(self): + if self._window is None: + return + self._window.show() + self._window.raise_() + self._window.activateWindow() diff --git a/openpype/pipeline/actions.py b/openpype/pipeline/actions.py index b488fe3e1f..feb1bd05d2 100644 --- a/openpype/pipeline/actions.py +++ b/openpype/pipeline/actions.py @@ -20,7 +20,13 @@ class LauncherAction(object): log.propagate = True def is_compatible(self, session): - """Return whether the class is compatible with the Session.""" + """Return whether the class is compatible with the Session. + + Args: + session (dict[str, Union[str, None]]): Session data with + AVALON_PROJECT, AVALON_ASSET and AVALON_TASK. + """ + return True def process(self, session, **kwargs): diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py new file mode 100644 index 0000000000..00502fe930 --- /dev/null +++ b/openpype/tools/ayon_launcher/abstract.py @@ -0,0 +1,297 @@ +from abc import ABCMeta, abstractmethod + +import six + + +@six.add_metaclass(ABCMeta) +class AbstractLauncherCommon(object): + @abstractmethod + def register_event_callback(self, topic, callback): + """Register event callback. + + Listen for events with given topic. + + Args: + topic (str): Name of topic. + callback (Callable): Callback that will be called when event + is triggered. + """ + + pass + + +class AbstractLauncherBackend(AbstractLauncherCommon): + @abstractmethod + def emit_event(self, topic, data=None, source=None): + """Emit event. + + Args: + topic (str): Event topic used for callbacks filtering. + data (Optional[dict[str, Any]]): Event data. + source (Optional[str]): Event source. + """ + + pass + + @abstractmethod + def get_project_settings(self, project_name): + """Project settings for current project. + + Args: + project_name (Union[str, None]): Project name. + + Returns: + dict[str, Any]: Project settings. + """ + + pass + + @abstractmethod + def get_project_entity(self, project_name): + """Get project entity by name. + + Args: + project_name (str): Project name. + + Returns: + dict[str, Any]: Project entity data. + """ + + pass + + @abstractmethod + def get_folder_entity(self, project_name, folder_id): + """Get folder entity by id. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + + Returns: + dict[str, Any]: Folder entity data. + """ + + pass + + @abstractmethod + def get_task_entity(self, project_name, task_id): + """Get task entity by id. + + Args: + project_name (str): Project name. + task_id (str): Task id. + + Returns: + dict[str, Any]: Task entity data. + """ + + pass + + +class AbstractLauncherFrontEnd(AbstractLauncherCommon): + # Entity items for UI + @abstractmethod + def get_project_items(self, sender=None): + """Project items for all projects. + + This function may trigger events 'projects.refresh.started' and + 'projects.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of project items in UI elements. + + Args: + sender (str): Who requested folder items. + + Returns: + list[ProjectItem]: Minimum possible information needed + for visualisation of folder hierarchy. + """ + + pass + + @abstractmethod + def get_folder_items(self, project_name, sender=None): + """Folder items to visualize project hierarchy. + + This function may trigger events 'folders.refresh.started' and + 'folders.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of folder items in UI elements. + + Args: + project_name (str): Project name. + sender (str): Who requested folder items. + + Returns: + list[FolderItem]: Minimum possible information needed + for visualisation of folder hierarchy. + """ + + pass + + @abstractmethod + def get_task_items(self, project_name, folder_id, sender=None): + """Task items. + + This function may trigger events 'tasks.refresh.started' and + 'tasks.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of task items in UI elements. + + Args: + project_name (str): Project name. + folder_id (str): Folder ID for which are tasks requested. + sender (str): Who requested folder items. + + Returns: + list[TaskItem]: Minimum possible information needed + for visualisation of tasks. + """ + + pass + + @abstractmethod + def get_selected_project_name(self): + """Selected project name. + + Returns: + Union[str, None]: Selected project name. + """ + + pass + + @abstractmethod + def get_selected_folder_id(self): + """Selected folder id. + + Returns: + Union[str, None]: Selected folder id. + """ + + pass + + @abstractmethod + def get_selected_task_id(self): + """Selected task id. + + Returns: + Union[str, None]: Selected task id. + """ + + pass + + @abstractmethod + def get_selected_task_name(self): + """Selected task name. + + Returns: + Union[str, None]: Selected task name. + """ + + pass + + @abstractmethod + def get_selected_context(self): + """Get whole selected context. + + Example: + { + "project_name": self.get_selected_project_name(), + "folder_id": self.get_selected_folder_id(), + "task_id": self.get_selected_task_id(), + "task_name": self.get_selected_task_name(), + } + + Returns: + dict[str, Union[str, None]]: Selected context. + """ + + pass + + @abstractmethod + def set_selected_project(self, project_name): + """Change selected folder. + + Args: + project_name (Union[str, None]): Project nameor None if no project + is selected. + """ + + pass + + @abstractmethod + def set_selected_folder(self, folder_id): + """Change selected folder. + + Args: + folder_id (Union[str, None]): Folder id or None if no folder + is selected. + """ + + pass + + @abstractmethod + def set_selected_task(self, task_id, task_name): + """Change selected task. + + Args: + task_id (Union[str, None]): Task id or None if no task + is selected. + task_name (Union[str, None]): Task name or None if no task + is selected. + """ + + pass + + # Actions + @abstractmethod + def get_action_items(self, project_name, folder_id, task_id): + """Get action items for given context. + + Args: + project_name (Union[str, None]): Project name. + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[ActionItem]: List of action items that should be shown + for given context. + """ + + pass + + @abstractmethod + def trigger_action(self, project_name, folder_id, task_id, action_id): + """Trigger action on given context. + + Args: + project_name (Union[str, None]): Project name. + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + action_id (str): Action identifier. + """ + + pass + + @abstractmethod + def set_application_force_not_open_workfile( + self, project_name, folder_id, task_id, action_id, enabled + ): + """This is application action related to force not open last workfile. + + Args: + project_name (Union[str, None]): Project name. + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + action_id (str): Action identifier. + enabled (bool): New value of force not open workfile. + """ + + pass + + @abstractmethod + def refresh(self): + """Refresh everything, models, ui etc. + + Triggers 'controller.refresh.started' event at the beginning and + 'controller.refresh.finished' at the end. + """ + + pass diff --git a/openpype/tools/ayon_launcher/control.py b/openpype/tools/ayon_launcher/control.py new file mode 100644 index 0000000000..09e07893c3 --- /dev/null +++ b/openpype/tools/ayon_launcher/control.py @@ -0,0 +1,149 @@ +from openpype.lib import Logger +from openpype.lib.events import QueuedEventSystem +from openpype.settings import get_project_settings +from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel + +from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend +from .models import LauncherSelectionModel, ActionsModel + + +class BaseLauncherController( + AbstractLauncherFrontEnd, AbstractLauncherBackend +): + def __init__(self): + self._project_settings = {} + self._event_system = None + self._log = None + + self._selection_model = LauncherSelectionModel(self) + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + self._actions_model = ActionsModel(self) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + @property + def event_system(self): + """Inner event system for workfiles tool controller. + + Is used for communication with UI. Event system is created on demand. + + Returns: + QueuedEventSystem: Event system which can trigger callbacks + for topics. + """ + + if self._event_system is None: + self._event_system = QueuedEventSystem() + return self._event_system + + # --------------------------------- + # Implementation of abstract methods + # --------------------------------- + # Events system + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self.event_system.emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self.event_system.add_callback(topic, callback) + + # Entity items for UI + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender) + + # Project settings for applications actions + def get_project_settings(self, project_name): + if project_name in self._project_settings: + return self._project_settings[project_name] + settings = get_project_settings(project_name) + self._project_settings[project_name] = settings + return settings + + # Entity for backend + def get_project_entity(self, project_name): + return self._projects_model.get_project_entity(project_name) + + def get_folder_entity(self, project_name, folder_id): + return self._hierarchy_model.get_folder_entity( + project_name, folder_id) + + def get_task_entity(self, project_name, task_id): + return self._hierarchy_model.get_task_entity(project_name, task_id) + + # Selection methods + def get_selected_project_name(self): + return self._selection_model.get_selected_project_name() + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + def get_selected_folder_id(self): + return self._selection_model.get_selected_folder_id() + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def get_selected_task_name(self): + return self._selection_model.get_selected_task_name() + + def set_selected_task(self, task_id, task_name): + self._selection_model.set_selected_task(task_id, task_name) + + def get_selected_context(self): + return { + "project_name": self.get_selected_project_name(), + "folder_id": self.get_selected_folder_id(), + "task_id": self.get_selected_task_id(), + "task_name": self.get_selected_task_name(), + } + + # Actions + def get_action_items(self, project_name, folder_id, task_id): + return self._actions_model.get_action_items( + project_name, folder_id, task_id) + + def set_application_force_not_open_workfile( + self, project_name, folder_id, task_id, action_id, enabled + ): + self._actions_model.set_application_force_not_open_workfile( + project_name, folder_id, task_id, action_id, enabled + ) + + def trigger_action(self, project_name, folder_id, task_id, identifier): + self._actions_model.trigger_action( + project_name, folder_id, task_id, identifier) + + # General methods + def refresh(self): + self._emit_event("controller.refresh.started") + + self._project_settings = {} + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._actions_model.refresh() + self._projects_model.refresh() + + self._emit_event("controller.refresh.finished") + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") diff --git a/openpype/tools/ayon_launcher/models/__init__.py b/openpype/tools/ayon_launcher/models/__init__.py new file mode 100644 index 0000000000..1bc60c85f0 --- /dev/null +++ b/openpype/tools/ayon_launcher/models/__init__.py @@ -0,0 +1,8 @@ +from .actions import ActionsModel +from .selection import LauncherSelectionModel + + +__all__ = ( + "ActionsModel", + "LauncherSelectionModel", +) diff --git a/openpype/tools/ayon_launcher/models/actions.py b/openpype/tools/ayon_launcher/models/actions.py new file mode 100644 index 0000000000..24fea44db2 --- /dev/null +++ b/openpype/tools/ayon_launcher/models/actions.py @@ -0,0 +1,505 @@ +import os + +from openpype import resources +from openpype.lib import Logger, OpenPypeSettingsRegistry +from openpype.pipeline.actions import ( + discover_launcher_actions, + LauncherAction, +) + + +# class Action: +# def __init__(self, label, icon=None, identifier=None): +# self._label = label +# self._icon = icon +# self._callbacks = [] +# self._identifier = identifier or uuid.uuid4().hex +# self._checked = True +# self._checkable = False +# +# def set_checked(self, checked): +# self._checked = checked +# +# def set_checkable(self, checkable): +# self._checkable = checkable +# +# def set_label(self, label): +# self._label = label +# +# def add_callback(self, callback): +# self._callbacks = callback +# +# +# class Menu: +# def __init__(self, label, icon=None): +# self.label = label +# self.icon = icon +# self._actions = [] +# +# def add_action(self, action): +# self._actions.append(action) + + +class ApplicationAction(LauncherAction): + """Action to launch an application. + + Application action based on 'ApplicationManager' system. + + Handling of applications in launcher is not ideal and should be completely + redone from scratch. This is just a temporary solution to keep backwards + compatibility with OpenPype launcher. + + Todos: + Move handling of errors to frontend. + """ + + # Application object + application = None + # Action attributes + name = None + label = None + label_variant = None + group = None + icon = None + color = None + order = 0 + data = {} + project_settings = {} + project_entities = {} + + _log = None + required_session_keys = ( + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK" + ) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def is_compatible(self, session): + for key in self.required_session_keys: + if not session.get(key): + return False + + project_name = session["AVALON_PROJECT"] + project_entity = self.project_entities[project_name] + apps = project_entity["attrib"].get("applications") + if not apps or self.application.full_name not in apps: + return False + + project_settings = self.project_settings[project_name] + only_available = project_settings["applications"]["only_available"] + if only_available and not self.application.find_executable(): + return False + return True + + def _show_message_box(self, title, message, details=None): + from qtpy import QtWidgets, QtGui + from openpype import style + + dialog = QtWidgets.QMessageBox() + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + dialog.setWindowIcon(icon) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle(title) + dialog.setText(message) + if details: + dialog.setDetailedText(details) + dialog.exec_() + + def process(self, session, **kwargs): + """Process the full Application action""" + + from openpype.lib import ( + ApplictionExecutableNotFound, + ApplicationLaunchFailed, + ) + + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session["AVALON_TASK"] + try: + self.application.launch( + project_name=project_name, + asset_name=asset_name, + task_name=task_name, + **self.data + ) + + except ApplictionExecutableNotFound as exc: + details = exc.details + msg = exc.msg + log_msg = str(msg) + if details: + log_msg += "\n" + details + self.log.warning(log_msg) + self._show_message_box( + "Application executable not found", msg, details + ) + + except ApplicationLaunchFailed as exc: + msg = str(exc) + self.log.warning(msg, exc_info=True) + self._show_message_box("Application launch failed", msg) + + +class ActionItem: + """Item representing single action to trigger. + + Todos: + Get rid of application specific logic. + + Args: + identifier (str): Unique identifier of action item. + label (str): Action label. + variant_label (Union[str, None]): Variant label, full label is + concatenated with space. Actions are grouped under single + action if it has same 'label' and have set 'variant_label'. + icon (dict[str, str]): Icon definition. + order (int): Action ordering. + is_application (bool): Is action application action. + force_not_open_workfile (bool): Force not open workfile. Application + related. + full_label (Optional[str]): Full label, if not set it is generated + from 'label' and 'variant_label'. + """ + + def __init__( + self, + identifier, + label, + variant_label, + icon, + order, + is_application, + force_not_open_workfile, + full_label=None + ): + self.identifier = identifier + self.label = label + self.variant_label = variant_label + self.icon = icon + self.order = order + self.is_application = is_application + self.force_not_open_workfile = force_not_open_workfile + self._full_label = full_label + + def copy(self): + return self.from_data(self.to_data()) + + @property + def full_label(self): + if self._full_label is None: + if self.variant_label: + self._full_label = " ".join([self.label, self.variant_label]) + else: + self._full_label = self.label + return self._full_label + + def to_data(self): + return { + "identifier": self.identifier, + "label": self.label, + "variant_label": self.variant_label, + "icon": self.icon, + "order": self.order, + "is_application": self.is_application, + "force_not_open_workfile": self.force_not_open_workfile, + "full_label": self._full_label, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + +def get_action_icon(action): + """Get action icon info. + + Args: + action (LacunherAction): Action instance. + + Returns: + dict[str, str]: Icon info. + """ + + icon = action.icon + if not icon: + return { + "type": "awesome-font", + "name": "fa.cube", + "color": "white" + } + + if isinstance(icon, dict): + return icon + + icon_path = resources.get_resource(icon) + if not os.path.exists(icon_path): + try: + icon_path = icon.format(resources.RESOURCES_DIR) + except Exception: + pass + + if os.path.exists(icon_path): + return { + "type": "path", + "path": icon_path, + } + + return { + "type": "awesome-font", + "name": icon, + "color": action.color or "white" + } + + +class ActionsModel: + """Actions model. + + Args: + controller (AbstractLauncherBackend): Controller instance. + """ + + _not_open_workfile_reg_key = "force_not_open_workfile" + + def __init__(self, controller): + self._controller = controller + + self._log = None + + self._discovered_actions = None + self._actions = None + self._action_items = {} + + self._launcher_tool_reg = OpenPypeSettingsRegistry("launcher_tool") + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def refresh(self): + self._discovered_actions = None + self._actions = None + self._action_items = {} + + self._controller.emit_event("actions.refresh.started") + self._get_action_objects() + self._controller.emit_event("actions.refresh.finished") + + def get_action_items(self, project_name, folder_id, task_id): + """Get actions for project. + + Args: + project_name (Union[str, None]): Project name. + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[ActionItem]: List of actions. + """ + + not_open_workfile_actions = self._get_no_last_workfile_for_context( + project_name, folder_id, task_id) + session = self._prepare_session(project_name, folder_id, task_id) + output = [] + action_items = self._get_action_items(project_name) + for identifier, action in self._get_action_objects().items(): + if not action.is_compatible(session): + continue + + action_item = action_items[identifier] + # Handling of 'force_not_open_workfile' for applications + if action_item.is_application: + action_item = action_item.copy() + action_item.force_not_open_workfile = ( + not_open_workfile_actions.get(identifier, False) + ) + + output.append(action_item) + return output + + def set_application_force_not_open_workfile( + self, project_name, folder_id, task_id, action_id, enabled + ): + no_workfile_reg_data = self._get_no_last_workfile_reg_data() + project_data = no_workfile_reg_data.setdefault(project_name, {}) + folder_data = project_data.setdefault(folder_id, {}) + task_data = folder_data.setdefault(task_id, {}) + task_data[action_id] = enabled + self._launcher_tool_reg.set_item( + self._not_open_workfile_reg_key, no_workfile_reg_data + ) + + def trigger_action(self, project_name, folder_id, task_id, identifier): + session = self._prepare_session(project_name, folder_id, task_id) + failed = False + error_message = None + action_label = identifier + action_items = self._get_action_items(project_name) + try: + action = self._actions[identifier] + action_item = action_items[identifier] + action_label = action_item.full_label + self._controller.emit_event( + "action.trigger.started", + { + "identifier": identifier, + "full_label": action_label, + } + ) + if isinstance(action, ApplicationAction): + per_action = self._get_no_last_workfile_for_context( + project_name, folder_id, task_id + ) + force_not_open_workfile = per_action.get(identifier, False) + action.data["start_last_workfile"] = force_not_open_workfile + action.process(session) + except Exception as exc: + self.log.warning("Action trigger failed.", exc_info=True) + failed = True + error_message = str(exc) + + self._controller.emit_event( + "action.trigger.finished", + { + "identifier": identifier, + "failed": failed, + "error_message": error_message, + "full_label": action_label, + } + ) + + def _get_no_last_workfile_reg_data(self): + try: + no_workfile_reg_data = self._launcher_tool_reg.get_item( + self._not_open_workfile_reg_key) + except ValueError: + no_workfile_reg_data = {} + self._launcher_tool_reg.set_item( + self._not_open_workfile_reg_key, no_workfile_reg_data) + return no_workfile_reg_data + + def _get_no_last_workfile_for_context( + self, project_name, folder_id, task_id + ): + not_open_workfile_reg_data = self._get_no_last_workfile_reg_data() + return ( + not_open_workfile_reg_data + .get(project_name, {}) + .get(folder_id, {}) + .get(task_id, {}) + ) + + def _prepare_session(self, project_name, folder_id, task_id): + folder_name = None + if folder_id: + folder = self._controller.get_folder_entity( + project_name, folder_id) + if folder: + folder_name = folder["name"] + + task_name = None + if task_id: + task = self._controller.get_task_entity(project_name, task_id) + if task: + task_name = task["name"] + + return { + "AVALON_PROJECT": project_name, + "AVALON_ASSET": folder_name, + "AVALON_TASK": task_name, + } + + def _get_discovered_action_classes(self): + if self._discovered_actions is None: + self._discovered_actions = ( + discover_launcher_actions() + + self._get_applications_action_classes() + ) + return self._discovered_actions + + def _get_action_objects(self): + if self._actions is None: + actions = {} + for cls in self._get_discovered_action_classes(): + obj = cls() + identifier = getattr(obj, "identifier", None) + if identifier is None: + identifier = cls.__name__ + actions[identifier] = obj + self._actions = actions + return self._actions + + def _get_action_items(self, project_name): + action_items = self._action_items.get(project_name) + if action_items is not None: + return action_items + + project_entity = None + if project_name: + project_entity = self._controller.get_project_entity(project_name) + project_settings = self._controller.get_project_settings(project_name) + + action_items = {} + for identifier, action in self._get_action_objects().items(): + is_application = isinstance(action, ApplicationAction) + if is_application: + action.project_entities[project_name] = project_entity + action.project_settings[project_name] = project_settings + label = action.label or identifier + variant_label = getattr(action, "label_variant", None) + icon = get_action_icon(action) + item = ActionItem( + identifier, + label, + variant_label, + icon, + action.order, + is_application, + False + ) + action_items[identifier] = item + self._action_items[project_name] = action_items + return action_items + + def _get_applications_action_classes(self): + from openpype.lib.applications import ( + CUSTOM_LAUNCH_APP_GROUPS, + ApplicationManager, + ) + + actions = [] + + manager = ApplicationManager() + for full_name, application in manager.applications.items(): + if ( + application.group.name in CUSTOM_LAUNCH_APP_GROUPS + or not application.enabled + ): + continue + + action = type( + "app_{}".format(full_name), + (ApplicationAction,), + { + "identifier": "application.{}".format(full_name), + "application": application, + "name": application.name, + "label": application.group.label, + "label_variant": application.label, + "group": None, + "icon": application.icon, + "color": getattr(application, "color", None), + "order": getattr(application, "order", None) or 0, + "data": {} + } + ) + actions.append(action) + return actions diff --git a/openpype/tools/ayon_launcher/models/selection.py b/openpype/tools/ayon_launcher/models/selection.py new file mode 100644 index 0000000000..b156d2084c --- /dev/null +++ b/openpype/tools/ayon_launcher/models/selection.py @@ -0,0 +1,72 @@ +class LauncherSelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folder.changed" + - "selection.task.changed" + """ + + event_source = "launcher.selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_id = None + self._task_name = None + self._task_id = None + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + if project_name == self._project_name: + return + + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": project_name}, + self.event_source + ) + + def get_selected_folder_id(self): + return self._folder_id + + def set_selected_folder(self, folder_id): + if folder_id == self._folder_id: + return + + self._folder_id = folder_id + self._controller.emit_event( + "selection.folder.changed", + { + "project_name": self._project_name, + "folder_id": folder_id, + }, + self.event_source + ) + + def get_selected_task_name(self): + return self._task_name + + def get_selected_task_id(self): + return self._task_id + + def set_selected_task(self, task_id, task_name): + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": task_name, + "task_id": task_id, + }, + self.event_source + ) diff --git a/openpype/tools/ayon_launcher/ui/__init__.py b/openpype/tools/ayon_launcher/ui/__init__.py new file mode 100644 index 0000000000..da30c84656 --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/__init__.py @@ -0,0 +1,6 @@ +from .window import LauncherWindow + + +__all__ = ( + "LauncherWindow", +) diff --git a/openpype/tools/ayon_launcher/ui/actions_widget.py b/openpype/tools/ayon_launcher/ui/actions_widget.py new file mode 100644 index 0000000000..d04f8f8d24 --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/actions_widget.py @@ -0,0 +1,453 @@ +import time +import collections + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.tools.flickcharm import FlickCharm +from openpype.tools.ayon_utils.widgets import get_qt_icon + +from .resources import get_options_image_path + +ANIMATION_LEN = 7 + +ACTION_ID_ROLE = QtCore.Qt.UserRole + 1 +ACTION_IS_APPLICATION_ROLE = QtCore.Qt.UserRole + 2 +ACTION_IS_GROUP_ROLE = QtCore.Qt.UserRole + 3 +ACTION_SORT_ROLE = QtCore.Qt.UserRole + 4 +ANIMATION_START_ROLE = QtCore.Qt.UserRole + 5 +ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 6 +FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 7 + + +class ActionsQtModel(QtGui.QStandardItemModel): + """Qt model for actions. + + Args: + controller (AbstractLauncherFrontEnd): Controller instance. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(ActionsQtModel, self).__init__() + + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh_finished, + ) + controller.register_event_callback( + "selection.project.changed", + self._on_selection_project_changed, + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_selection_folder_changed, + ) + controller.register_event_callback( + "selection.task.changed", + self._on_selection_task_changed, + ) + + self._controller = controller + + self._items_by_id = {} + self._groups_by_id = {} + + self._selected_project_name = None + self._selected_folder_id = None + self._selected_task_id = None + + def get_selected_project_name(self): + return self._selected_project_name + + def get_selected_folder_id(self): + return self._selected_folder_id + + def get_selected_task_id(self): + return self._selected_task_id + + def get_group_items(self, action_id): + return self._groups_by_id[action_id] + + def get_item_by_id(self, action_id): + return self._items_by_id.get(action_id) + + def _clear_items(self): + self._items_by_id = {} + self._groups_by_id = {} + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + + def refresh(self): + items = self._controller.get_action_items( + self._selected_project_name, + self._selected_folder_id, + self._selected_task_id, + ) + if not items: + self._clear_items() + self.refreshed.emit() + return + + root_item = self.invisibleRootItem() + + all_action_items_info = [] + items_by_label = collections.defaultdict(list) + for item in items: + if not item.variant_label: + all_action_items_info.append((item, False)) + else: + items_by_label[item.label].append(item) + + groups_by_id = {} + for action_items in items_by_label.values(): + first_item = next(iter(action_items)) + all_action_items_info.append((first_item, len(action_items) > 1)) + groups_by_id[first_item.identifier] = action_items + + new_items = [] + items_by_id = {} + for action_item_info in all_action_items_info: + action_item, is_group = action_item_info + icon = get_qt_icon(action_item.icon) + if is_group: + label = action_item.label + else: + label = action_item.full_label + + item = self._items_by_id.get(action_item.identifier) + if item is None: + item = QtGui.QStandardItem() + item.setData(action_item.identifier, ACTION_ID_ROLE) + new_items.append(item) + + item.setFlags(QtCore.Qt.ItemIsEnabled) + item.setData(label, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(is_group, ACTION_IS_GROUP_ROLE) + item.setData(action_item.order, ACTION_SORT_ROLE) + item.setData( + action_item.is_application, ACTION_IS_APPLICATION_ROLE) + item.setData( + action_item.force_not_open_workfile, + FORCE_NOT_OPEN_WORKFILE_ROLE) + items_by_id[action_item.identifier] = item + + if new_items: + root_item.appendRows(new_items) + + to_remove = set(self._items_by_id.keys()) - set(items_by_id.keys()) + for identifier in to_remove: + item = self._items_by_id.pop(identifier) + root_item.removeRow(item.row()) + + self._groups_by_id = groups_by_id + self._items_by_id = items_by_id + self.refreshed.emit() + + def _on_controller_refresh_finished(self): + context = self._controller.get_selected_context() + self._selected_project_name = context["project_name"] + self._selected_folder_id = context["folder_id"] + self._selected_task_id = context["task_id"] + self.refresh() + + def _on_selection_project_changed(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_id = None + self._selected_task_id = None + self.refresh() + + def _on_selection_folder_changed(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = None + self.refresh() + + def _on_selection_task_changed(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = event["task_id"] + self.refresh() + + +class ActionDelegate(QtWidgets.QStyledItemDelegate): + _cached_extender = {} + + def __init__(self, *args, **kwargs): + super(ActionDelegate, self).__init__(*args, **kwargs) + self._anim_start_color = QtGui.QColor(178, 255, 246) + self._anim_end_color = QtGui.QColor(5, 44, 50) + + def _draw_animation(self, painter, option, index): + grid_size = option.widget.gridSize() + x_offset = int( + (grid_size.width() / 2) + - (option.rect.width() / 2) + ) + item_x = option.rect.x() - x_offset + rect_offset = grid_size.width() / 20 + size = grid_size.width() - (rect_offset * 2) + anim_rect = QtCore.QRect( + item_x + rect_offset, + option.rect.y() + rect_offset, + size, + size + ) + + painter.save() + + painter.setBrush(QtCore.Qt.transparent) + + gradient = QtGui.QConicalGradient() + gradient.setCenter(QtCore.QPointF(anim_rect.center())) + gradient.setColorAt(0, self._anim_start_color) + gradient.setColorAt(1, self._anim_end_color) + + time_diff = time.time() - index.data(ANIMATION_START_ROLE) + + # Repeat 4 times + part_anim = 2.5 + part_time = time_diff % part_anim + offset = (part_time / part_anim) * 360 + angle = (offset + 90) % 360 + + gradient.setAngle(-angle) + + pen = QtGui.QPen(QtGui.QBrush(gradient), rect_offset) + pen.setCapStyle(QtCore.Qt.RoundCap) + painter.setPen(pen) + painter.drawArc( + anim_rect, + -16 * (angle + 10), + -16 * offset + ) + + painter.restore() + + @classmethod + def _get_extender_pixmap(cls, size): + pix = cls._cached_extender.get(size) + if pix is not None: + return pix + pix = QtGui.QPixmap(get_options_image_path()).scaled( + size, size, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + cls._cached_extender[size] = pix + return pix + + def paint(self, painter, option, index): + painter.setRenderHints( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + + if index.data(ANIMATION_STATE_ROLE): + self._draw_animation(painter, option, index) + + super(ActionDelegate, self).paint(painter, option, index) + + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + rect = QtCore.QRectF( + option.rect.x(), option.rect.height(), 5, 5) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(QtGui.QColor(200, 0, 0)) + painter.drawEllipse(rect) + + if not index.data(ACTION_IS_GROUP_ROLE): + return + + grid_size = option.widget.gridSize() + x_offset = int( + (grid_size.width() / 2) + - (option.rect.width() / 2) + ) + item_x = option.rect.x() - x_offset + + tenth_size = int(grid_size.width() / 10) + extender_size = int(tenth_size * 2.4) + + extender_x = item_x + tenth_size + extender_y = option.rect.y() + tenth_size + + pix = self._get_extender_pixmap(extender_size) + painter.drawPixmap(extender_x, extender_y, pix) + + +class ActionsWidget(QtWidgets.QWidget): + def __init__(self, controller, parent): + super(ActionsWidget, self).__init__(parent) + + self._controller = controller + + view = QtWidgets.QListView(self) + view.setProperty("mode", "icon") + view.setObjectName("IconView") + view.setViewMode(QtWidgets.QListView.IconMode) + view.setResizeMode(QtWidgets.QListView.Adjust) + view.setSelectionMode(QtWidgets.QListView.NoSelection) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + view.setWrapping(True) + view.setGridSize(QtCore.QSize(70, 75)) + view.setIconSize(QtCore.QSize(30, 30)) + view.setSpacing(0) + view.setWordWrap(True) + + # Make view flickable + flick = FlickCharm(parent=view) + flick.activateOn(view) + + model = ActionsQtModel(controller) + + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + proxy_model.setSortRole(ACTION_SORT_ROLE) + + proxy_model.setSourceModel(model) + view.setModel(proxy_model) + + delegate = ActionDelegate(self) + view.setItemDelegate(delegate) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(view) + + animation_timer = QtCore.QTimer() + animation_timer.setInterval(40) + animation_timer.timeout.connect(self._on_animation) + + view.clicked.connect(self._on_clicked) + view.customContextMenuRequested.connect(self._on_context_menu) + model.refreshed.connect(self._on_model_refresh) + + self._animated_items = set() + self._animation_timer = animation_timer + + self._context_menu = None + + self._flick = flick + self._view = view + self._model = model + self._proxy_model = proxy_model + + self._set_row_height(1) + + def _set_row_height(self, rows): + self.setMinimumHeight(rows * 75) + + def _on_model_refresh(self): + self._proxy_model.sort(0) + + def _on_animation(self): + time_now = time.time() + for action_id in tuple(self._animated_items): + item = self._model.get_item_by_id(action_id) + if item is None: + self._animated_items.discard(action_id) + continue + + start_time = item.data(ANIMATION_START_ROLE) + if start_time is None or (time_now - start_time) > ANIMATION_LEN: + item.setData(0, ANIMATION_STATE_ROLE) + self._animated_items.discard(action_id) + + if not self._animated_items: + self._animation_timer.stop() + + self.update() + + def _start_animation(self, index): + # Offset refresh timout + model_index = self._proxy_model.mapToSource(index) + if not model_index.isValid(): + return + action_id = model_index.data(ACTION_ID_ROLE) + self._model.setData(model_index, time.time(), ANIMATION_START_ROLE) + self._model.setData(model_index, 1, ANIMATION_STATE_ROLE) + self._animated_items.add(action_id) + self._animation_timer.start() + + def _on_context_menu(self, point): + """Creates menu to force skip opening last workfile.""" + index = self._view.indexAt(point) + if not index.isValid(): + return + + if not index.data(ACTION_IS_APPLICATION_ROLE): + return + + menu = QtWidgets.QMenu(self._view) + checkbox = QtWidgets.QCheckBox( + "Skip opening last workfile.", menu) + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + checkbox.setChecked(True) + + action_id = index.data(ACTION_ID_ROLE) + checkbox.stateChanged.connect( + lambda: self._on_checkbox_changed( + action_id, checkbox.isChecked() + ) + ) + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + menu.addAction(action) + + self._context_menu = menu + global_point = self.mapToGlobal(point) + menu.exec_(global_point) + self._context_menu = None + + def _on_checkbox_changed(self, action_id, is_checked): + if self._context_menu is not None: + self._context_menu.close() + + project_name = self._model.get_selected_project_name() + folder_id = self._model.get_selected_folder_id() + task_id = self._model.get_selected_task_id() + self._controller.set_application_force_not_open_workfile( + project_name, folder_id, task_id, action_id, is_checked) + self._model.refresh() + + def _on_clicked(self, index): + if not index or not index.isValid(): + return + + is_group = index.data(ACTION_IS_GROUP_ROLE) + action_id = index.data(ACTION_ID_ROLE) + + project_name = self._model.get_selected_project_name() + folder_id = self._model.get_selected_folder_id() + task_id = self._model.get_selected_task_id() + + if not is_group: + self._controller.trigger_action( + project_name, folder_id, task_id, action_id + ) + self._start_animation(index) + return + + action_items = self._model.get_group_items(action_id) + + menu = QtWidgets.QMenu(self) + actions_mapping = {} + + for action_item in action_items: + menu_action = QtWidgets.QAction(action_item.full_label) + menu.addAction(menu_action) + actions_mapping[menu_action] = action_item + + result = menu.exec_(QtGui.QCursor.pos()) + if not result: + return + + action_item = actions_mapping[result] + + self._controller.trigger_action( + project_name, folder_id, task_id, action_item.identifier + ) + self._start_animation(index) diff --git a/openpype/tools/ayon_launcher/ui/hierarchy_page.py b/openpype/tools/ayon_launcher/ui/hierarchy_page.py new file mode 100644 index 0000000000..5047cdc692 --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/hierarchy_page.py @@ -0,0 +1,102 @@ +import qtawesome +from qtpy import QtWidgets, QtCore + +from openpype.tools.utils import ( + PlaceholderLineEdit, + SquareButton, + RefreshButton, +) +from openpype.tools.ayon_utils.widgets import ( + ProjectsCombobox, + FoldersWidget, + TasksWidget, +) + + +class HierarchyPage(QtWidgets.QWidget): + def __init__(self, controller, parent): + super(HierarchyPage, self).__init__(parent) + + # Header + header_widget = QtWidgets.QWidget(self) + + btn_back_icon = qtawesome.icon("fa.angle-left", color="white") + btn_back = SquareButton(header_widget) + btn_back.setIcon(btn_back_icon) + + projects_combobox = ProjectsCombobox(controller, header_widget) + + refresh_btn = RefreshButton(header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(btn_back, 0) + header_layout.addWidget(projects_combobox, 1) + header_layout.addWidget(refresh_btn, 0) + + # Body - Folders + Tasks selection + content_body = QtWidgets.QSplitter(self) + content_body.setContentsMargins(0, 0, 0, 0) + content_body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + content_body.setOrientation(QtCore.Qt.Horizontal) + + # - Folders widget with filter + folders_wrapper = QtWidgets.QWidget(content_body) + + folders_filter_text = PlaceholderLineEdit(folders_wrapper) + folders_filter_text.setPlaceholderText("Filter folders...") + + folders_widget = FoldersWidget(controller, folders_wrapper) + + folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper) + folders_wrapper_layout.setContentsMargins(0, 0, 0, 0) + folders_wrapper_layout.addWidget(folders_filter_text, 0) + folders_wrapper_layout.addWidget(folders_widget, 1) + + # - Tasks widget + tasks_widget = TasksWidget(controller, content_body) + + content_body.addWidget(folders_wrapper) + content_body.addWidget(tasks_widget) + content_body.setStretchFactor(0, 100) + content_body.setStretchFactor(1, 65) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(header_widget, 0) + main_layout.addWidget(content_body, 1) + + btn_back.clicked.connect(self._on_back_clicked) + refresh_btn.clicked.connect(self._on_refreh_clicked) + folders_filter_text.textChanged.connect(self._on_filter_text_changed) + + self._is_visible = False + self._controller = controller + + self._btn_back = btn_back + self._projects_combobox = projects_combobox + self._folders_widget = folders_widget + self._tasks_widget = tasks_widget + + # Post init + projects_combobox.set_listen_to_selection_change(self._is_visible) + + def set_page_visible(self, visible, project_name=None): + if self._is_visible == visible: + return + self._is_visible = visible + self._projects_combobox.set_listen_to_selection_change(visible) + if visible and project_name: + self._projects_combobox.set_selection(project_name) + + def _on_back_clicked(self): + self._controller.set_selected_project(None) + + def _on_refreh_clicked(self): + self._controller.refresh() + + def _on_filter_text_changed(self, text): + self._folders_widget.set_name_filer(text) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py new file mode 100644 index 0000000000..baa399d0ed --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -0,0 +1,135 @@ +from qtpy import QtWidgets, QtCore + +from openpype.tools.flickcharm import FlickCharm +from openpype.tools.utils import PlaceholderLineEdit, RefreshButton +from openpype.tools.ayon_utils.widgets import ( + ProjectsModel, + ProjectSortFilterProxy, +) +from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER + + +class ProjectIconView(QtWidgets.QListView): + """Styled ListView that allows to toggle between icon and list mode. + + Toggling between the two modes is done by Right Mouse Click. + """ + + IconMode = 0 + ListMode = 1 + + def __init__(self, parent=None, mode=ListMode): + super(ProjectIconView, self).__init__(parent=parent) + + # Workaround for scrolling being super slow or fast when + # toggling between the two visual modes + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.setObjectName("IconView") + + self._mode = None + self.set_mode(mode) + + def set_mode(self, mode): + if mode == self._mode: + return + + self._mode = mode + + if mode == self.IconMode: + self.setViewMode(QtWidgets.QListView.IconMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(True) + self.setWordWrap(True) + self.setGridSize(QtCore.QSize(151, 90)) + self.setIconSize(QtCore.QSize(50, 50)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.setProperty("mode", "icon") + self.style().polish(self) + + self.verticalScrollBar().setSingleStep(30) + + elif self.ListMode: + self.setProperty("mode", "list") + self.style().polish(self) + + self.setViewMode(QtWidgets.QListView.ListMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(False) + self.setWordWrap(False) + self.setIconSize(QtCore.QSize(20, 20)) + self.setGridSize(QtCore.QSize(100, 25)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.verticalScrollBar().setSingleStep(34) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.set_mode(int(not self._mode)) + return super(ProjectIconView, self).mousePressEvent(event) + + +class ProjectsWidget(QtWidgets.QWidget): + """Projects Page""" + def __init__(self, controller, parent=None): + super(ProjectsWidget, self).__init__(parent=parent) + + header_widget = QtWidgets.QWidget(self) + + projects_filter_text = PlaceholderLineEdit(header_widget) + projects_filter_text.setPlaceholderText("Filter projects...") + + refresh_btn = RefreshButton(header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(projects_filter_text, 1) + header_layout.addWidget(refresh_btn, 0) + + projects_view = ProjectIconView(parent=self) + projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) + flick = FlickCharm(parent=self) + flick.activateOn(projects_view) + projects_model = ProjectsModel(controller) + projects_proxy_model = ProjectSortFilterProxy() + projects_proxy_model.setSourceModel(projects_model) + + projects_view.setModel(projects_proxy_model) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(header_widget, 0) + main_layout.addWidget(projects_view, 1) + + projects_view.clicked.connect(self._on_view_clicked) + projects_filter_text.textChanged.connect( + self._on_project_filter_change) + refresh_btn.clicked.connect(self._on_refresh_clicked) + + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh_finished + ) + + self._controller = controller + + self._projects_view = projects_view + self._projects_model = projects_model + self._projects_proxy_model = projects_proxy_model + + def _on_view_clicked(self, index): + if index.isValid(): + project_name = index.data(QtCore.Qt.DisplayRole) + self._controller.set_selected_project(project_name) + + def _on_project_filter_change(self, text): + self._projects_proxy_model.setFilterFixedString(text) + + def _on_refresh_clicked(self): + self._controller.refresh() + + def _on_projects_refresh_finished(self, event): + if event["sender"] != PROJECTS_MODEL_SENDER: + self._projects_model.refresh() diff --git a/openpype/tools/ayon_launcher/ui/resources/__init__.py b/openpype/tools/ayon_launcher/ui/resources/__init__.py new file mode 100644 index 0000000000..27c59af2ba --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/resources/__init__.py @@ -0,0 +1,7 @@ +import os + +RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def get_options_image_path(): + return os.path.join(RESOURCES_DIR, "options.png") diff --git a/openpype/tools/ayon_launcher/ui/resources/options.png b/openpype/tools/ayon_launcher/ui/resources/options.png new file mode 100644 index 0000000000000000000000000000000000000000..a9617d0d1914634835f388c01479c672a8c8ffd7 GIT binary patch literal 1772 zcmb7^`B%~j7skJcd*X`HFm9u1j=6?t%;Iwb7j zwT|b(sB<3ljDb1wl4oJgfJ(AW&7Bpv&K*RI{SGm)zYu0jaC8|wa}(8OdA=SqaOrGq zQl&JypY*{sIl^1ofVAgks@SbOwLH%}dUorXyCcm^p8M0qpzTd|xz-DUnY^dOKTPeH z@AJ-CoGxRX%>y<5sp+n7Xk3?Ib+6{^?Cg%=d&rthvwyAl5BkShe>;4{WaeF*=5}tm ztt=TDXzceIC%r2%j9B($%Mh%fdW+^DZJMF}7T%OTA(YA8((QYh7f_5?|N5vQJC}2i zC0r;RJ>ErHqPC^l%s{3;^f+m96LRwULUx{B(Jk=u8K5KQK(F2Y*hE<*nGwM+6L<1iX zlK}t@mfr$Is`1VO02RGJA5=K0d~t%GY(ju`e_V2|K}V)SS?KCKM{H&_P7Y3Crp5mD z(`Z|5OJ5s)n{5lU+FfOFe`n~y1vEYAMcpmGYIrBiNw-)XnP|0HU<^ZOl*}9xr(R_s z-ktQ)Rp*UwVBUH#819d^g7kQOs&9HGq+ox_gXKk_@&4ncL<1u#E}<#6%&D0{)?q>@ z?Mq-Gs6s7mESn~{WhJteFd6)`mZ1U=UOLUm;q`|VcDE-YaVZ;Q~Te%Sofmu0UYVg!&X;RuJAt9igF;-A4LhWK?gT_dOlF*OOzD^7K zM0(b%SCz|j<0nL|yHR9|tdKubbevo`S#A{YvDG=|<+k;omrnFfC=_onNlmF}*P%aQ zz^g7hN^R`Pd;|)6L$)FXQese2SGyOAY{6{}Y=kB?Kv#n$PhZdf2ky zC~`-vdo>j`0wm``3CPJbVh^l!T^XN!WuMskOu14otdtzM{FZ(Ri1&Fi@A@7tB8dK~ zde}ZsK7UHkRttC-w4~8JO&(yw0aNwZM}4Ljqg`wPwrbtjQMQjA+>o+2e`K*~z`}^8 z$vMbbLM0Q2PC(c15PkyL_|q$ttu02DN;O3I7xQtiX;sVJ+ymk-oAE{EL?t$|+05dC zI@|D&m@;_t3WNN^wQ{8^-@~D`hWrV+gGfW9Ct4Myk1m@@3?wv$+$1JoCUMX~gcLy% zh~XxkM&oi1d}qIrrDlMz02j145nuS1%F=x$|b>QE}Uw_gWZT>QLRG z(V=0dJffRQo$`Clt5srtbzj?s=cCa_lk?tP8IX-f>!Ws5L@ck1<(p=DzydJQUDGyaDo3`;n#I_7#p1q zhXiA8A329}vHO`;fO84CeMF#Wsx?to(^2@VC=8r2mai3L)W}`t7f{yg}pV5{4$m~-H5NUJkTQN6t*Ssmw zEK7QvnhYK8CMbrSija`c4|B|61&Y%ub36cVDVz$Q7|_Ra`j1sDOF{i9-4hK;j#?7m zg4b(STtTXty_=b>KL8LX)j~j)sZmySrC>jPN|p?KUt2o6^)Rm87RP5K{aekI3Hu$u zbFte3N7JA8*R$kRE2H|HiFjp)Tf?0gH~)_%?0Q*P#>> cache = NestedCacheItem(levels=2) + >>> cache["a"]["b"].is_valid + False + >>> cache["a"]["b"].get_data() + None + >>> cache["a"]["b"] = 1 + >>> cache["a"]["b"].is_valid + True + >>> cache["a"]["b"].get_data() + 1 + >>> cache.reset() + >>> cache["a"]["b"].is_valid + False + + Args: + levels (int): Number of nested levels where read cache is stored. + default_factory (Optional[callable]): Function that returns default + value used on init and on reset. + lifetime (Optional[int]): Lifetime of the cache data in seconds. + _init_info (Optional[InitInfo]): Private argument. Init info for + nested cache where created from parent item. + """ + + def __init__( + self, levels=1, default_factory=None, lifetime=None, _init_info=None + ): + if levels < 1: + raise ValueError("Nested levels must be greater than 0") + self._data_by_key = {} + if _init_info is None: + _init_info = InitInfo(default_factory, lifetime) + self._init_info = _init_info + self._levels = levels + + def __getitem__(self, key): + """Get cached data. + + Args: + key (str): Key of the cache item. + + Returns: + Union[NestedCacheItem, CacheItem]: Cache item. + """ + + cache = self._data_by_key.get(key) + if cache is None: + if self._levels > 1: + cache = NestedCacheItem( + levels=self._levels - 1, + _init_info=self._init_info + ) + else: + cache = CacheItem( + self._init_info.default_factory, + self._init_info.lifetime + ) + self._data_by_key[key] = cache + return cache + + def __setitem__(self, key, value): + """Update cached data. + + Args: + key (str): Key of the cache item. + value (Any): Any data that are cached. + """ + + if self._levels > 1: + raise AttributeError(( + "{} does not support '__setitem__'. Lower nested level by {}" + ).format(self.__class__.__name__, self._levels - 1)) + cache = self[key] + cache.update_data(value) + + def get(self, key): + """Get cached data. + + Args: + key (str): Key of the cache item. + + Returns: + Union[NestedCacheItem, CacheItem]: Cache item. + """ + + return self[key] + + def reset(self): + """Reset cache.""" + + self._data_by_key = {} + + def set_lifetime(self, lifetime): + """Change lifetime of all children cache items. + + Args: + lifetime (int): Lifetime of the cache data in seconds. + """ + + self._init_info.lifetime = lifetime + for cache in self._data_by_key.values(): + cache.set_lifetime(lifetime) + + @property + def is_valid(self): + """Raise reasonable error when called on wront level. + + Raises: + AttributeError: If called on nested cache item. + """ + + raise AttributeError(( + "{} does not support 'is_valid'. Lower nested level by '{}'" + ).format(self.__class__.__name__, self._levels)) diff --git a/openpype/tools/ayon_utils/models/hierarchy.py b/openpype/tools/ayon_utils/models/hierarchy.py new file mode 100644 index 0000000000..8e01c557c5 --- /dev/null +++ b/openpype/tools/ayon_utils/models/hierarchy.py @@ -0,0 +1,340 @@ +import collections +import contextlib +from abc import ABCMeta, abstractmethod + +import ayon_api +import six + +from openpype.style import get_default_entity_icon_color + +from .cache import NestedCacheItem + +HIERARCHY_MODEL_SENDER = "hierarchy.model" + + +@six.add_metaclass(ABCMeta) +class AbstractHierarchyController: + @abstractmethod + def emit_event(self, topic, data, source): + pass + + +class FolderItem: + """Item representing folder entity on a server. + + Folder can be a child of another folder or a project. + + Args: + entity_id (str): Folder id. + parent_id (Union[str, None]): Parent folder id. If 'None' then project + is parent. + name (str): Name of folder. + label (str): Folder label. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, entity_id, parent_id, name, label, icon + ): + self.entity_id = entity_id + self.parent_id = parent_id + self.name = name + if not icon: + icon = { + "type": "awesome-font", + "name": "fa.folder", + "color": get_default_entity_icon_color() + } + self.icon = icon + self.label = label or name + + def to_data(self): + """Converts folder item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "entity_id": self.entity_id, + "parent_id": self.parent_id, + "name": self.name, + "label": self.label, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data): + """Re-creates folder item from data. + + Args: + data (dict[str, Any]): Folder item data. + + Returns: + FolderItem: Folder item. + """ + + return cls(**data) + + +class TaskItem: + """Task item representing task entity on a server. + + Task is child of a folder. + + Task item has label that is used for display in UI. The label is by + default using task name and type. + + Args: + task_id (str): Task id. + name (str): Name of task. + task_type (str): Type of task. + parent_id (str): Parent folder id. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, task_id, name, task_type, parent_id, icon + ): + self.task_id = task_id + self.name = name + self.task_type = task_type + self.parent_id = parent_id + if icon is None: + icon = { + "type": "awesome-font", + "name": "fa.male", + "color": get_default_entity_icon_color() + } + self.icon = icon + + self._label = None + + @property + def id(self): + """Alias for task_id. + + Returns: + str: Task id. + """ + + return self.task_id + + @property + def label(self): + """Label of task item for UI. + + Returns: + str: Label of task item. + """ + + if self._label is None: + self._label = "{} ({})".format(self.name, self.task_type) + return self._label + + def to_data(self): + """Converts task item to data. + + Returns: + dict[str, Any]: Task item data. + """ + + return { + "task_id": self.task_id, + "name": self.name, + "parent_id": self.parent_id, + "task_type": self.task_type, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data): + """Re-create task item from data. + + Args: + data (dict[str, Any]): Task item data. + + Returns: + TaskItem: Task item. + """ + + return cls(**data) + + +def _get_task_items_from_tasks(tasks): + """ + + Returns: + TaskItem: Task item. + """ + + output = [] + for task in tasks: + folder_id = task["folderId"] + output.append(TaskItem( + task["id"], + task["name"], + task["type"], + folder_id, + None + )) + return output + + +def _get_folder_item_from_hierarchy_item(item): + return FolderItem( + item["id"], + item["parentId"], + item["name"], + item["label"], + None + ) + + +class HierarchyModel(object): + """Model for project hierarchy items. + + Hierarchy items are folders and tasks. Folders can have as parent another + folder or project. Tasks can have as parent only folder. + """ + + def __init__(self, controller): + self._folders_items = NestedCacheItem(levels=1, default_factory=dict) + self._folders_by_id = NestedCacheItem(levels=2, default_factory=dict) + + self._task_items = NestedCacheItem(levels=2, default_factory=dict) + self._tasks_by_id = NestedCacheItem(levels=2, default_factory=dict) + + self._folders_refreshing = set() + self._tasks_refreshing = set() + self._controller = controller + + def reset(self): + self._folders_items.reset() + self._folders_by_id.reset() + + self._task_items.reset() + self._tasks_by_id.reset() + + def refresh_project(self, project_name): + self._refresh_folders_cache(project_name) + + def get_folder_items(self, project_name, sender): + if not self._folders_items[project_name].is_valid: + self._refresh_folders_cache(project_name, sender) + return self._folders_items[project_name].get_data() + + def get_task_items(self, project_name, folder_id, sender): + if not project_name or not folder_id: + return [] + + task_cache = self._task_items[project_name][folder_id] + if not task_cache.is_valid: + self._refresh_tasks_cache(project_name, folder_id, sender) + return task_cache.get_data() + + def get_folder_entity(self, project_name, folder_id): + cache = self._folders_by_id[project_name][folder_id] + if not cache.is_valid: + entity = None + if folder_id: + entity = ayon_api.get_folder_by_id(project_name, folder_id) + cache.update_data(entity) + return cache.get_data() + + def get_task_entity(self, project_name, task_id): + cache = self._tasks_by_id[project_name][task_id] + if not cache.is_valid: + entity = None + if task_id: + entity = ayon_api.get_task_by_id(project_name, task_id) + cache.update_data(entity) + return cache.get_data() + + @contextlib.contextmanager + def _folder_refresh_event_manager(self, project_name, sender): + self._folders_refreshing.add(project_name) + self._controller.emit_event( + "folders.refresh.started", + {"project_name": project_name, "sender": sender}, + HIERARCHY_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "folders.refresh.finished", + {"project_name": project_name, "sender": sender}, + HIERARCHY_MODEL_SENDER + ) + self._folders_refreshing.remove(project_name) + + @contextlib.contextmanager + def _task_refresh_event_manager( + self, project_name, folder_id, sender + ): + self._tasks_refreshing.add(folder_id) + self._controller.emit_event( + "tasks.refresh.started", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + HIERARCHY_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "tasks.refresh.finished", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + HIERARCHY_MODEL_SENDER + ) + self._tasks_refreshing.discard(folder_id) + + def _refresh_folders_cache(self, project_name, sender=None): + if project_name in self._folders_refreshing: + return + + with self._folder_refresh_event_manager(project_name, sender): + folder_items = self._query_folders(project_name) + self._folders_items[project_name].update_data(folder_items) + + def _query_folders(self, project_name): + hierarchy = ayon_api.get_folders_hierarchy(project_name) + + folder_items = {} + hierachy_queue = collections.deque(hierarchy["hierarchy"]) + while hierachy_queue: + item = hierachy_queue.popleft() + folder_item = _get_folder_item_from_hierarchy_item(item) + folder_items[folder_item.entity_id] = folder_item + hierachy_queue.extend(item["children"] or []) + return folder_items + + def _refresh_tasks_cache(self, project_name, folder_id, sender=None): + if folder_id in self._tasks_refreshing: + return + + with self._task_refresh_event_manager( + project_name, folder_id, sender + ): + task_items = self._query_tasks(project_name, folder_id) + self._task_items[project_name][folder_id] = task_items + + def _query_tasks(self, project_name, folder_id): + tasks = list(ayon_api.get_tasks( + project_name, + folder_ids=[folder_id], + fields={"id", "name", "label", "folderId", "type"} + )) + return _get_task_items_from_tasks(tasks) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py new file mode 100644 index 0000000000..ae3eeecea4 --- /dev/null +++ b/openpype/tools/ayon_utils/models/projects.py @@ -0,0 +1,145 @@ +import contextlib +from abc import ABCMeta, abstractmethod + +import ayon_api +import six + +from openpype.style import get_default_entity_icon_color + +from .cache import CacheItem + +PROJECTS_MODEL_SENDER = "projects.model" + + +@six.add_metaclass(ABCMeta) +class AbstractHierarchyController: + @abstractmethod + def emit_event(self, topic, data, source): + pass + + +class ProjectItem: + """Item representing folder entity on a server. + + Folder can be a child of another folder or a project. + + Args: + name (str): Project name. + active (Union[str, None]): Parent folder id. If 'None' then project + is parent. + """ + + def __init__(self, name, active, icon=None): + self.name = name + self.active = active + if icon is None: + icon = { + "type": "awesome-font", + "name": "fa.map", + "color": get_default_entity_icon_color(), + } + self.icon = icon + + def to_data(self): + """Converts folder item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "name": self.name, + "active": self.active, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data): + """Re-creates folder item from data. + + Args: + data (dict[str, Any]): Folder item data. + + Returns: + FolderItem: Folder item. + """ + + return cls(**data) + + +def _get_project_items_from_entitiy(projects): + """ + + Args: + projects (list[dict[str, Any]]): List of projects. + + Returns: + ProjectItem: Project item. + """ + + return [ + ProjectItem(project["name"], project["active"]) + for project in projects + ] + + +class ProjectsModel(object): + def __init__(self, controller): + self._projects_cache = CacheItem(default_factory=dict) + self._project_items_by_name = {} + self._projects_by_name = {} + + self._is_refreshing = False + self._controller = controller + + def reset(self): + self._projects_cache.reset() + self._project_items_by_name = {} + self._projects_by_name = {} + + def refresh(self): + self._refresh_projects_cache() + + def get_project_items(self, sender): + if not self._projects_cache.is_valid: + self._refresh_projects_cache(sender) + return self._projects_cache.get_data() + + def get_project_entity(self, project_name): + if project_name not in self._projects_by_name: + entity = None + if project_name: + entity = ayon_api.get_project(project_name) + self._projects_by_name[project_name] = entity + return self._projects_by_name[project_name] + + @contextlib.contextmanager + def _project_refresh_event_manager(self, sender): + self._is_refreshing = True + self._controller.emit_event( + "projects.refresh.started", + {"sender": sender}, + PROJECTS_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "projects.refresh.finished", + {"sender": sender}, + PROJECTS_MODEL_SENDER + ) + self._is_refreshing = False + + def _refresh_projects_cache(self, sender=None): + if self._is_refreshing: + return + + with self._project_refresh_event_manager(sender): + project_items = self._query_projects() + self._projects_cache.update_data(project_items) + + def _query_projects(self): + projects = ayon_api.get_projects(fields=["name", "active"]) + return _get_project_items_from_entitiy(projects) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py new file mode 100644 index 0000000000..59aef98faf --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -0,0 +1,37 @@ +from .projects_widget import ( + # ProjectsWidget, + ProjectsCombobox, + ProjectsModel, + ProjectSortFilterProxy, +) + +from .folders_widget import ( + FoldersWidget, + FoldersModel, +) + +from .tasks_widget import ( + TasksWidget, + TasksModel, +) +from .utils import ( + get_qt_icon, + RefreshThread, +) + + +__all__ = ( + # "ProjectsWidget", + "ProjectsCombobox", + "ProjectsModel", + "ProjectSortFilterProxy", + + "FoldersWidget", + "FoldersModel", + + "TasksWidget", + "TasksModel", + + "get_qt_icon", + "RefreshThread", +) diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py new file mode 100644 index 0000000000..3fab64f657 --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -0,0 +1,364 @@ +import collections + +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, +) + +from .utils import RefreshThread, get_qt_icon + +SENDER_NAME = "qt_folders_model" +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +ITEM_NAME_ROLE = QtCore.Qt.UserRole + 2 + + +class FoldersModel(QtGui.QStandardItemModel): + """Folders model which cares about refresh of folders. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(FoldersModel, self).__init__() + + self._controller = controller + self._items_by_id = {} + self._parent_id_by_id = {} + + self._refresh_threads = {} + self._current_refresh_thread = None + self._last_project_name = None + + self._has_content = False + self._is_refreshing = False + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: True if model is refreshing. + """ + return self._is_refreshing + + @property + def has_content(self): + """Has at least one folder. + + Returns: + bool: True if model has at least one folder. + """ + + return self._has_content + + def clear(self): + self._items_by_id = {} + self._parent_id_by_id = {} + self._has_content = False + super(FoldersModel, self).clear() + + def get_index_by_id(self, item_id): + """Get index by folder id. + + Returns: + QtCore.QModelIndex: Index of the folder. Can be invalid if folder + is not available. + """ + item = self._items_by_id.get(item_id) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def set_project_name(self, project_name): + """Refresh folders items. + + Refresh start thread because it can cause that controller can + start query from database if folders are not cached. + """ + + if not project_name: + self._last_project_name = project_name + self._current_refresh_thread = None + self._fill_items({}) + return + + self._is_refreshing = True + + if self._last_project_name != project_name: + self.clear() + self._last_project_name = project_name + + thread = self._refresh_threads.get(project_name) + if thread is not None: + self._current_refresh_thread = thread + return + + thread = RefreshThread( + project_name, + self._controller.get_folder_items, + project_name, + SENDER_NAME + ) + self._current_refresh_thread = thread + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Folders are stored by id. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_items(thread.get_result()) + + def _fill_items(self, folder_items_by_id): + if not folder_items_by_id: + if folder_items_by_id is not None: + self.clear() + self._is_refreshing = False + self.refreshed.emit() + return + + self._has_content = True + + folder_ids = set(folder_items_by_id) + ids_to_remove = set(self._items_by_id) - folder_ids + + folder_items_by_parent = collections.defaultdict(dict) + for folder_item in folder_items_by_id.values(): + ( + folder_items_by_parent + [folder_item.parent_id] + [folder_item.entity_id] + ) = folder_item + + hierarchy_queue = collections.deque() + hierarchy_queue.append((self.invisibleRootItem(), None)) + + # Keep pointers to removed items until the refresh finishes + # - some children of the items could be moved and reused elsewhere + removed_items = [] + while hierarchy_queue: + item = hierarchy_queue.popleft() + parent_item, parent_id = item + folder_items = folder_items_by_parent[parent_id] + + items_by_id = {} + folder_ids_to_add = set(folder_items) + for row_idx in reversed(range(parent_item.rowCount())): + child_item = parent_item.child(row_idx) + child_id = child_item.data(ITEM_ID_ROLE) + if child_id in ids_to_remove: + removed_items.append(parent_item.takeRow(row_idx)) + else: + items_by_id[child_id] = child_item + + new_items = [] + for item_id in folder_ids_to_add: + folder_item = folder_items[item_id] + item = items_by_id.get(item_id) + if item is None: + is_new = True + item = QtGui.QStandardItem() + item.setEditable(False) + else: + is_new = self._parent_id_by_id[item_id] != parent_id + + icon = get_qt_icon(folder_item.icon) + item.setData(item_id, ITEM_ID_ROLE) + item.setData(folder_item.name, ITEM_NAME_ROLE) + item.setData(folder_item.label, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + if is_new: + new_items.append(item) + self._items_by_id[item_id] = item + self._parent_id_by_id[item_id] = parent_id + + hierarchy_queue.append((item, item_id)) + + if new_items: + parent_item.appendRows(new_items) + + for item_id in ids_to_remove: + self._items_by_id.pop(item_id) + self._parent_id_by_id.pop(item_id) + + self._is_refreshing = False + self.refreshed.emit() + + +class FoldersWidget(QtWidgets.QWidget): + """Folders widget. + + Widget that handles folders view, model and selection. + + Expected selection handling is disabled by default. If enabled, the + widget will handle the expected in predefined way. Widget is listening + to event 'expected_selection_changed' with expected event data below, + the same data must be available when called method + 'get_expected_selection_data' on controller. + + { + "folder": { + "current": bool, # Folder is what should be set now + "folder_id": Union[str, None], # Folder id that should be selected + }, + ... + } + + Selection is confirmed by calling method 'expected_folder_selected' on + controller. + + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + handle_expected_selection (bool): If True, the widget will handle + the expected selection. Defaults to False. + """ + + def __init__(self, controller, parent, handle_expected_selection=False): + super(FoldersWidget, self).__init__(parent) + + folders_view = DeselectableTreeView(self) + folders_view.setHeaderHidden(True) + + folders_model = FoldersModel(controller) + folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model.setSourceModel(folders_model) + + folders_view.setModel(folders_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(folders_view, 1) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_change, + ) + controller.register_event_callback( + "folders.refresh.finished", + self._on_folders_refresh_finished + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = folders_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + folders_model.refreshed.connect(self._on_model_refresh) + + self._controller = controller + self._folders_view = folders_view + self._folders_model = folders_model + self._folders_proxy_model = folders_proxy_model + + self._handle_expected_selection = handle_expected_selection + self._expected_selection = None + + def set_name_filer(self, name): + """Set filter of folder name. + + Args: + name (str): The string filter. + """ + + self._folders_proxy_model.setFilterFixedString(name) + + def _on_project_selection_change(self, event): + project_name = event["project_name"] + self._set_project_name(project_name) + + def _set_project_name(self, project_name): + self._folders_model.set_project_name(project_name) + + def _clear(self): + self._folders_model.clear() + + def _on_folders_refresh_finished(self, event): + if event["sender"] != SENDER_NAME: + self._set_project_name(event["project_name"]) + + def _on_controller_refresh(self): + self._update_expected_selection() + + def _on_model_refresh(self): + if self._expected_selection: + self._set_expected_selection() + self._folders_proxy_model.sort(0) + + def _get_selected_item_id(self): + selection_model = self._folders_view.selectionModel() + for index in selection_model.selectedIndexes(): + item_id = index.data(ITEM_ID_ROLE) + if item_id is not None: + return item_id + return None + + def _on_selection_change(self): + item_id = self._get_selected_item_id() + self._controller.set_selected_folder(item_id) + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + folder_data = expected_data.get("folder") + if not folder_data or not folder_data["current"]: + return + + folder_id = folder_data["id"] + self._expected_selection = folder_id + if not self._folders_model.is_refreshing: + self._set_expected_selection() + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return + + folder_id = self._expected_selection + self._expected_selection = None + if ( + folder_id is not None + and folder_id != self._get_selected_item_id() + ): + index = self._folders_model.get_index_by_id(folder_id) + if index.isValid(): + proxy_index = self._folders_proxy_model.mapFromSource(index) + self._folders_view.setCurrentIndex(proxy_index) + self._controller.expected_folder_selected(folder_id) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py new file mode 100644 index 0000000000..818d574910 --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -0,0 +1,325 @@ +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER +from .utils import RefreshThread, get_qt_icon + +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 +PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 + + +class ProjectsModel(QtGui.QStandardItemModel): + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(ProjectsModel, self).__init__() + self._controller = controller + + self._project_items = {} + + self._empty_item = None + self._empty_item_added = False + + self._is_refreshing = False + self._refresh_thread = None + + @property + def is_refreshing(self): + return self._is_refreshing + + def refresh(self): + self._refresh() + + def has_content(self): + return len(self._project_items) > 0 + + def _add_empty_item(self): + item = self._get_empty_item() + if not self._empty_item_added: + root_item = self.invisibleRootItem() + root_item.appendRow(item) + self._empty_item_added = True + + def _remove_empty_item(self): + if not self._empty_item_added: + return + + root_item = self.invisibleRootItem() + item = self._get_empty_item() + root_item.takeRow(item.row()) + self._empty_item_added = False + + def _get_empty_item(self): + if self._empty_item is None: + item = QtGui.QStandardItem("< No projects >") + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_item = item + return self._empty_item + + def _refresh(self): + if self._is_refreshing: + return + self._is_refreshing = True + refresh_thread = RefreshThread( + "projects", self._query_project_items + ) + refresh_thread.refresh_finished.connect(self._refresh_finished) + refresh_thread.start() + self._refresh_thread = refresh_thread + + def _query_project_items(self): + return self._controller.get_project_items() + + def _refresh_finished(self): + # TODO check if failed + result = self._refresh_thread.get_result() + self._refresh_thread = None + + self._fill_items(result) + + self._is_refreshing = False + self.refreshed.emit() + + def _fill_items(self, project_items): + items_to_remove = set(self._project_items.keys()) + new_items = [] + for project_item in project_items: + project_name = project_item.name + items_to_remove.discard(project_name) + item = self._project_items.get(project_name) + if item is None: + item = QtGui.QStandardItem() + new_items.append(item) + icon = get_qt_icon(project_item.icon) + item.setData(project_name, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE) + self._project_items[project_name] = item + + root_item = self.invisibleRootItem() + if new_items: + root_item.appendRows(new_items) + + for project_name in items_to_remove: + item = self._project_items.pop(project_name) + root_item.removeRow(item.row()) + + if self.has_content(): + self._remove_empty_item() + else: + self._add_empty_item() + + +class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) + self._filter_inactive = True + # Disable case sensitivity + self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + def lessThan(self, left_index, right_index): + if left_index.data(PROJECT_NAME_ROLE) is None: + return True + + if right_index.data(PROJECT_NAME_ROLE) is None: + return False + + left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) + right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) + if right_is_active == left_is_active: + return super(ProjectSortFilterProxy, self).lessThan( + left_index, right_index + ) + + if left_is_active: + return True + return False + + def filterAcceptsRow(self, source_row, source_parent): + index = self.sourceModel().index(source_row, 0, source_parent) + string_pattern = self.filterRegularExpression().pattern() + if ( + self._filter_inactive + and not index.data(PROJECT_IS_ACTIVE_ROLE) + ): + return False + + if string_pattern: + project_name = index.data(PROJECT_IS_ACTIVE_ROLE) + if project_name is not None: + return string_pattern.lower() in project_name.lower() + + return super(ProjectSortFilterProxy, self).filterAcceptsRow( + source_row, source_parent + ) + + def _custom_index_filter(self, index): + return bool(index.data(PROJECT_IS_ACTIVE_ROLE)) + + def is_active_filter_enabled(self): + return self._filter_inactive + + def set_active_filter_enabled(self, value): + if self._filter_inactive == value: + return + self._filter_inactive = value + self.invalidateFilter() + + +class ProjectsCombobox(QtWidgets.QWidget): + def __init__(self, controller, parent, handle_expected_selection=False): + super(ProjectsCombobox, self).__init__(parent) + + projects_combobox = QtWidgets.QComboBox(self) + combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) + projects_combobox.setItemDelegate(combobox_delegate) + projects_model = ProjectsModel(controller) + projects_proxy_model = ProjectSortFilterProxy() + projects_proxy_model.setSourceModel(projects_model) + projects_combobox.setModel(projects_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(projects_combobox, 1) + + projects_model.refreshed.connect(self._on_model_refresh) + + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh_finished + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + projects_combobox.currentIndexChanged.connect( + self._on_current_index_changed + ) + + self._controller = controller + self._listen_selection_change = True + + self._handle_expected_selection = handle_expected_selection + self._expected_selection = None + + self._projects_combobox = projects_combobox + self._projects_model = projects_model + self._projects_proxy_model = projects_proxy_model + self._combobox_delegate = combobox_delegate + + def refresh(self): + self._projects_model.refresh() + + def set_selection(self, project_name): + """Set selection to a given project. + + Selection change is ignored if project is not found. + + Args: + project_name (str): Name of project. + + Returns: + bool: True if selection was changed, False otherwise. NOTE: + Selection may not be changed if project is not found, or if + project is already selected. + """ + + idx = self._projects_combobox.findData( + project_name, PROJECT_NAME_ROLE) + if idx < 0: + return False + if idx != self._projects_combobox.currentIndex(): + self._projects_combobox.setCurrentIndex(idx) + return True + return False + + def set_listen_to_selection_change(self, listen): + """Disable listening to changes of the selection. + + Because combobox is triggering selection change when it's model + is refreshed, it's necessary to disable listening to selection for + some cases, e.g. when is on a different page of UI and should be just + refreshed. + + Args: + listen (bool): Enable or disable listening to selection changes. + """ + + self._listen_selection_change = listen + + def get_current_project_name(self): + """Name of selected project. + + Returns: + Union[str, None]: Name of selected project, or None if no project + """ + + idx = self._projects_combobox.currentIndex() + if idx < 0: + return None + return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE) + + def _on_current_index_changed(self, idx): + if not self._listen_selection_change: + return + project_name = self._projects_combobox.itemData( + idx, PROJECT_NAME_ROLE) + self._controller.set_selected_project(project_name) + + def _on_model_refresh(self): + self._projects_proxy_model.sort(0) + if self._expected_selection: + self._set_expected_selection() + + def _on_projects_refresh_finished(self, event): + if event["sender"] != PROJECTS_MODEL_SENDER: + self._projects_model.refresh() + + def _on_controller_refresh(self): + self._update_expected_selection() + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return + project_name = self._expected_selection + if project_name is not None: + if project_name != self.get_current_project_name(): + self.set_selection(project_name) + else: + # Fake project change + self._on_current_index_changed( + self._projects_combobox.currentIndex() + ) + + self._controller.expected_project_selected(project_name) + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + project_data = expected_data.get("project") + if ( + not project_data + or not project_data["current"] + or project_data["selected"] + ): + return + self._expected_selection = project_data["name"] + if not self._projects_model.is_refreshing: + self._set_expected_selection() + + +class ProjectsWidget(QtWidgets.QWidget): + # TODO implement + pass diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py new file mode 100644 index 0000000000..66ebd0b777 --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -0,0 +1,436 @@ +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.style import get_disabled_entity_icon_color +from openpype.tools.utils import DeselectableTreeView + +from .utils import RefreshThread, get_qt_icon + +SENDER_NAME = "qt_tasks_model" +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +PARENT_ID_ROLE = QtCore.Qt.UserRole + 2 +ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 + + +class TasksModel(QtGui.QStandardItemModel): + """Tasks model which cares about refresh of tasks by folder id. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(TasksModel, self).__init__() + + self._controller = controller + + self._items_by_name = {} + self._has_content = False + self._is_refreshing = False + + self._invalid_selection_item_used = False + self._invalid_selection_item = None + self._empty_tasks_item_used = False + self._empty_tasks_item = None + + self._last_project_name = None + self._last_folder_id = None + + self._refresh_threads = {} + self._current_refresh_thread = None + + # Initial state + self._add_invalid_selection_item() + + def clear(self): + self._items_by_name = {} + self._has_content = False + self._remove_invalid_items() + super(TasksModel, self).clear() + + def refresh(self, project_name, folder_id): + """Refresh tasks for folder. + + Args: + project_name (Union[str]): Name of project. + folder_id (Union[str, None]): Folder id. + """ + + self._refresh(project_name, folder_id) + + def get_index_by_name(self, task_name): + """Find item by name and return its index. + + Returns: + QtCore.QModelIndex: Index of item. Is invalid if task is not + found by name. + """ + + item = self._items_by_name.get(task_name) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def get_last_project_name(self): + """Get last refreshed project name. + + Returns: + Union[str, None]: Project name. + """ + + return self._last_project_name + + def get_last_folder_id(self): + """Get last refreshed folder id. + + Returns: + Union[str, None]: Folder id. + """ + + return self._last_folder_id + + def set_selected_project(self, project_name): + self._selected_project_name = project_name + + def _get_invalid_selection_item(self): + if self._invalid_selection_item is None: + item = QtGui.QStandardItem("Select a folder") + item.setFlags(QtCore.Qt.NoItemFlags) + icon = get_qt_icon({ + "type": "awesome-font", + "name": "fa.times", + "color": get_disabled_entity_icon_color(), + }) + item.setData(icon, QtCore.Qt.DecorationRole) + self._invalid_selection_item = item + return self._invalid_selection_item + + def _get_empty_task_item(self): + if self._empty_tasks_item is None: + item = QtGui.QStandardItem("No task") + icon = get_qt_icon({ + "type": "awesome-font", + "name": "fa.exclamation-circle", + "color": get_disabled_entity_icon_color(), + }) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_tasks_item = item + return self._empty_tasks_item + + def _add_invalid_item(self, item): + self.clear() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _remove_invalid_item(self, item): + root_item = self.invisibleRootItem() + root_item.takeRow(item.row()) + + def _remove_invalid_items(self): + self._remove_invalid_selection_item() + self._remove_empty_task_item() + + def _add_invalid_selection_item(self): + if not self._invalid_selection_item_used: + self._add_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = True + + def _remove_invalid_selection_item(self): + if self._invalid_selection_item: + self._remove_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = False + + def _add_empty_task_item(self): + if not self._empty_tasks_item_used: + self._add_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = True + + def _remove_empty_task_item(self): + if self._empty_tasks_item_used: + self._remove_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = False + + def _refresh(self, project_name, folder_id): + self._is_refreshing = True + self._last_project_name = project_name + self._last_folder_id = folder_id + if not folder_id: + self._add_invalid_selection_item() + self._current_refresh_thread = None + self._is_refreshing = False + self.refreshed.emit() + return + + thread = self._refresh_threads.get(folder_id) + if thread is not None: + self._current_refresh_thread = thread + return + thread = RefreshThread( + folder_id, + self._controller.get_task_items, + project_name, + folder_id + ) + self._current_refresh_thread = thread + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + task_items = thread.get_result() + # Task items are refreshed + if task_items is None: + return + + # No tasks are available on folder + if not task_items: + self._add_empty_task_item() + return + self._remove_invalid_items() + + new_items = [] + new_names = set() + for task_item in task_items: + name = task_item.name + new_names.add(name) + item = self._items_by_name.get(name) + if item is None: + item = QtGui.QStandardItem() + item.setEditable(False) + new_items.append(item) + self._items_by_name[name] = item + + # TODO cache locally + icon = get_qt_icon(task_item.icon) + item.setData(task_item.label, QtCore.Qt.DisplayRole) + item.setData(name, ITEM_NAME_ROLE) + item.setData(task_item.id, ITEM_ID_ROLE) + item.setData(task_item.parent_id, PARENT_ID_ROLE) + item.setData(icon, QtCore.Qt.DecorationRole) + + root_item = self.invisibleRootItem() + + for name in set(self._items_by_name) - new_names: + item = self._items_by_name.pop(name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) + + self._has_content = root_item.rowCount() > 0 + self._is_refreshing = False + self.refreshed.emit() + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: Model is refreshing + """ + + return self._is_refreshing + + @property + def has_content(self): + """Model has content. + + Returns: + bools: Have at least one task. + """ + + return self._has_content + + def headerData(self, section, orientation, role): + # Show nice labels in the header + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section == 0: + return "Tasks" + + return super(TasksModel, self).headerData( + section, orientation, role + ) + + +class TasksWidget(QtWidgets.QWidget): + """Tasks widget. + + Widget that handles tasks view, model and selection. + + Args: + controller (AbstractWorkfilesFrontend): Workfiles controller. + parent (QtWidgets.QWidget): Parent widget. + handle_expected_selection (Optional[bool]): Handle expected selection. + """ + + def __init__(self, controller, parent, handle_expected_selection=False): + super(TasksWidget, self).__init__(parent) + + tasks_view = DeselectableTreeView(self) + tasks_view.setIndentation(0) + + tasks_model = TasksModel(controller) + tasks_proxy_model = QtCore.QSortFilterProxyModel() + tasks_proxy_model.setSourceModel(tasks_model) + + tasks_view.setModel(tasks_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(tasks_view, 1) + + controller.register_event_callback( + "tasks.refresh.finished", + self._on_tasks_refresh_finished + ) + controller.register_event_callback( + "selection.folder.changed", + self._folder_selection_changed + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = tasks_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + tasks_model.refreshed.connect(self._on_tasks_model_refresh) + + self._controller = controller + self._tasks_view = tasks_view + self._tasks_model = tasks_model + self._tasks_proxy_model = tasks_proxy_model + + self._selected_folder_id = None + + self._handle_expected_selection = handle_expected_selection + self._expected_selection_data = None + + def _clear(self): + self._tasks_model.clear() + + def _on_tasks_refresh_finished(self, event): + """Tasks were refreshed in controller. + + Ignore if refresh was triggered by tasks model, or refreshed folder is + not the same as currently selected folder. + + Args: + event (Event): Event object. + """ + + # Refresh only if current folder id is the same + if ( + event["sender"] == SENDER_NAME + or event["folder_id"] != self._selected_folder_id + ): + return + self._tasks_model.refresh( + event["project_name"], self._selected_folder_id + ) + + def _folder_selection_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._tasks_model.refresh( + event["project_name"], self._selected_folder_id + ) + + def _on_tasks_model_refresh(self): + if not self._set_expected_selection(): + self._on_selection_change() + self._tasks_proxy_model.sort(0) + + def _get_selected_item_ids(self): + selection_model = self._tasks_view.selectionModel() + for index in selection_model.selectedIndexes(): + task_id = index.data(ITEM_ID_ROLE) + task_name = index.data(ITEM_NAME_ROLE) + parent_id = index.data(PARENT_ID_ROLE) + if task_name is not None: + return parent_id, task_id, task_name + return self._selected_folder_id, None, None + + def _on_selection_change(self): + # Don't trigger task change during refresh + # - a task was deselected if that happens + # - can cause crash triggered during tasks refreshing + if self._tasks_model.is_refreshing: + return + + parent_id, task_id, task_name = self._get_selected_item_ids() + self._controller.set_selected_task(task_id, task_name) + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return False + + if self._expected_selection_data is None: + return False + folder_id = self._expected_selection_data["folder_id"] + task_name = self._expected_selection_data["task_name"] + self._expected_selection_data = None + model_folder_id = self._tasks_model.get_last_folder_id() + if folder_id != model_folder_id: + return False + if task_name is not None: + index = self._tasks_model.get_index_by_name(task_name) + if index.isValid(): + proxy_index = self._tasks_proxy_model.mapFromSource(index) + self._tasks_view.setCurrentIndex(proxy_index) + self._controller.expected_task_selected(folder_id, task_name) + return True + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + folder_data = expected_data.get("folder") + task_data = expected_data.get("task") + if ( + not folder_data + or not task_data + or not task_data["current"] + ): + return + folder_id = folder_data["id"] + self._expected_selection_data = { + "task_name": task_data["name"], + "folder_id": folder_id, + } + model_folder_id = self._tasks_model.get_last_folder_id() + if folder_id != model_folder_id or self._tasks_model.is_refreshing: + return + self._set_expected_selection() diff --git a/openpype/tools/ayon_utils/widgets/utils.py b/openpype/tools/ayon_utils/widgets/utils.py new file mode 100644 index 0000000000..8bc3b1ea9b --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/utils.py @@ -0,0 +1,98 @@ +import os +from functools import partial + +from qtpy import QtCore, QtGui + +from openpype.tools.utils.lib import get_qta_icon_by_name_and_color + + +class RefreshThread(QtCore.QThread): + refresh_finished = QtCore.Signal(str) + + def __init__(self, thread_id, func, *args, **kwargs): + super(RefreshThread, self).__init__() + self._id = thread_id + self._callback = partial(func, *args, **kwargs) + self._exception = None + self._result = None + + @property + def id(self): + return self._id + + @property + def failed(self): + return self._exception is not None + + def run(self): + try: + self._result = self._callback() + except Exception as exc: + self._exception = exc + self.refresh_finished.emit(self.id) + + def get_result(self): + return self._result + + +class _IconsCache: + """Cache for icons.""" + + _cache = {} + _default = None + + @classmethod + def _get_cache_key(cls, icon_def): + parts = [] + icon_type = icon_def["type"] + if icon_type == "path": + parts = [icon_type, icon_def["path"]] + + elif icon_type == "awesome-font": + parts = [icon_type, icon_def["name"], icon_def["color"]] + return "|".join(parts) + + @classmethod + def get_icon(cls, icon_def): + icon_type = icon_def["type"] + cache_key = cls._get_cache_key(icon_def) + cache = cls._cache.get(cache_key) + if cache is not None: + return cache + + icon = None + if icon_type == "path": + path = icon_def["path"] + if os.path.exists(path): + icon = QtGui.QIcon(path) + + elif icon_type == "awesome-font": + icon_name = icon_def["name"] + icon_color = icon_def["color"] + icon = get_qta_icon_by_name_and_color(icon_name, icon_color) + if icon is None: + icon = get_qta_icon_by_name_and_color( + "fa.{}".format(icon_name), icon_color) + if icon is None: + icon = cls.get_default() + cls._cache[cache_key] = icon + return icon + + @classmethod + def get_default(cls): + pix = QtGui.QPixmap(1, 1) + pix.fill(QtCore.Qt.transparent) + return QtGui.QIcon(pix) + + +def get_qt_icon(icon_def): + """Returns icon from cache or creates new one. + + Args: + icon_def (dict[str, Any]): Icon definition. + + Returns: + QtGui.QIcon: Icon. + """ + + return _IconsCache.get_icon(icon_def) diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index 61660ee9b7..285b5d04ca 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -1,8 +1,5 @@ -import os - from qtpy import QtWidgets, QtGui -from openpype import PLUGINS_DIR from openpype import style from openpype import resources from openpype.lib import ( @@ -10,46 +7,7 @@ from openpype.lib import ( ApplictionExecutableNotFound, ApplicationLaunchFailed ) -from openpype.pipeline import ( - LauncherAction, - register_launcher_action_path, -) - - -def register_actions_from_paths(paths): - if not paths: - return - - for path in paths: - if not path: - continue - - if path.startswith("."): - print(( - "BUG: Relative paths are not allowed for security reasons. {}" - ).format(path)) - continue - - if not os.path.exists(path): - print("Path was not found: {}".format(path)) - continue - - register_launcher_action_path(path) - - -def register_config_actions(): - """Register actions from the configuration for Launcher""" - - actions_dir = os.path.join(PLUGINS_DIR, "actions") - if os.path.exists(actions_dir): - register_actions_from_paths([actions_dir]) - - -def register_environment_actions(): - """Register actions from AVALON_ACTIONS for Launcher.""" - - paths_str = os.environ.get("AVALON_ACTIONS") or "" - register_actions_from_paths(paths_str.split(os.pathsep)) +from openpype.pipeline import LauncherAction # TODO move to 'openpype.pipeline.actions' diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index d343353112..018088e916 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -15,6 +15,10 @@ from .widgets import ( IconButton, PixmapButton, SeparatorWidget, + VerticalExpandButton, + SquareButton, + RefreshButton, + GoToCurrentButton, ) from .views import DeselectableTreeView from .error_dialog import ErrorMessageBox @@ -60,6 +64,11 @@ __all__ = ( "PixmapButton", "SeparatorWidget", + "VerticalExpandButton", + "SquareButton", + "RefreshButton", + "GoToCurrentButton", + "DeselectableTreeView", "ErrorMessageBox", diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index a70437cc65..9223afecaa 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -6,10 +6,13 @@ import qtawesome from openpype.style import ( get_objected_colors, - get_style_image_path + get_style_image_path, + get_default_tools_icon_color, ) from openpype.lib.attribute_definitions import AbstractAttrDef +from .lib import get_qta_icon_by_name_and_color + log = logging.getLogger(__name__) @@ -777,3 +780,77 @@ class SeparatorWidget(QtWidgets.QFrame): self._orientation = orientation self._set_size(self._size) + + +def get_refresh_icon(): + return get_qta_icon_by_name_and_color( + "fa.refresh", get_default_tools_icon_color() + ) + + +def get_go_to_current_icon(): + return get_qta_icon_by_name_and_color( + "fa.arrow-down", get_default_tools_icon_color() + ) + + +class VerticalExpandButton(QtWidgets.QPushButton): + """Button which is expanding vertically. + + By default, button is a little bit smaller than other widgets like + QLineEdit. This button is expanding vertically to match size of + other widgets, next to it. + """ + + def __init__(self, parent=None): + super(VerticalExpandButton, self).__init__(parent) + + sp = self.sizePolicy() + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + self.setSizePolicy(sp) + + +class SquareButton(QtWidgets.QPushButton): + """Make button square shape. + + Change width to match height on resize. + """ + + def __init__(self, *args, **kwargs): + super(SquareButton, self).__init__(*args, **kwargs) + + sp = self.sizePolicy() + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) + self.setSizePolicy(sp) + self._ideal_width = None + + def showEvent(self, event): + super(SquareButton, self).showEvent(event) + self._ideal_width = self.height() + self.updateGeometry() + + def resizeEvent(self, event): + super(SquareButton, self).resizeEvent(event) + self._ideal_width = self.height() + self.updateGeometry() + + def sizeHint(self): + sh = super(SquareButton, self).sizeHint() + ideal_width = self._ideal_width + if ideal_width is None: + ideal_width = sh.height() + sh.setWidth(ideal_width) + return sh + + +class RefreshButton(VerticalExpandButton): + def __init__(self, parent=None): + super(RefreshButton, self).__init__(parent) + self.setIcon(get_refresh_icon()) + + +class GoToCurrentButton(VerticalExpandButton): + def __init__(self, parent=None): + super(GoToCurrentButton, self).__init__(parent) + self.setIcon(get_go_to_current_icon()) From bfb5868417f1fbf127b65bdf8cb214585a2312b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:18:57 +0200 Subject: [PATCH 173/175] AYON: Fix task type short name conversion (#5641) * fix task type short name conversion * workfiles tool can query project entity * use project entity to fill task template data --- openpype/client/server/conversion_utils.py | 2 ++ openpype/tools/ayon_workfiles/abstract.py | 10 ++++++++++ openpype/tools/ayon_workfiles/control.py | 3 +++ .../tools/ayon_workfiles/models/hierarchy.py | 11 ++++++++++ .../tools/ayon_workfiles/models/workfiles.py | 20 ++++++++++++++----- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index f67a1ef9c4..8c18cb1c13 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -235,6 +235,8 @@ def convert_v4_project_to_v3(project): new_task_types = {} for task_type in task_types: name = task_type.pop("name") + # Change 'shortName' to 'short_name' + task_type["short_name"] = task_type.pop("shortName", None) new_task_types[name] = task_type config["tasks"] = new_task_types diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py index e30a2c2499..f511181837 100644 --- a/openpype/tools/ayon_workfiles/abstract.py +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -442,6 +442,16 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon): pass + @abstractmethod + def get_project_entity(self): + """Get current project entity. + + Returns: + dict[str, Any]: Project entity data. + """ + + pass + @abstractmethod def get_folder_entity(self, folder_id): """Get folder entity by id. diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index fc8819bff3..1153a3c01f 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -193,6 +193,9 @@ class BaseWorkfileController( self._project_anatomy = Anatomy(self.get_current_project_name()) return self._project_anatomy + def get_project_entity(self): + return self._entities_model.get_project_entity() + def get_folder_entity(self, folder_id): return self._entities_model.get_folder_entity(folder_id) diff --git a/openpype/tools/ayon_workfiles/models/hierarchy.py b/openpype/tools/ayon_workfiles/models/hierarchy.py index 948c0b8a17..a1d51525da 100644 --- a/openpype/tools/ayon_workfiles/models/hierarchy.py +++ b/openpype/tools/ayon_workfiles/models/hierarchy.py @@ -77,8 +77,11 @@ class EntitiesModel(object): event_source = "entities.model" def __init__(self, controller): + project_cache = CacheItem() + project_cache.set_invalid({}) folders_cache = CacheItem() folders_cache.set_invalid({}) + self._project_cache = project_cache self._folders_cache = folders_cache self._tasks_cache = {} @@ -90,6 +93,7 @@ class EntitiesModel(object): self._controller = controller def reset(self): + self._project_cache.set_invalid({}) self._folders_cache.set_invalid({}) self._tasks_cache = {} @@ -99,6 +103,13 @@ class EntitiesModel(object): def refresh(self): self._refresh_folders_cache() + def get_project_entity(self): + if not self._project_cache.is_valid: + project_name = self._controller.get_current_project_name() + project_entity = ayon_api.get_project(project_name) + self._project_cache.update_data(project_entity) + return self._project_cache.get_data() + def get_folder_items(self, sender): if not self._folders_cache.is_valid: self._refresh_folders_cache(sender) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py index eb82f62de3..316d8b2a16 100644 --- a/openpype/tools/ayon_workfiles/models/workfiles.py +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -43,13 +43,21 @@ def get_folder_template_data(folder): } -def get_task_template_data(task): +def get_task_template_data(project_entity, task): if not task: return {} + short_name = None + task_type_name = task["taskType"] + for task_type_info in project_entity["config"]["taskTypes"]: + if task_type_info["name"] == task_type_name: + short_name = task_type_info["shortName"] + break + return { "task": { "name": task["name"], - "type": task["taskType"] + "type": task_type_name, + "short": short_name, } } @@ -145,12 +153,13 @@ class WorkareaModel: self._fill_data_by_folder_id[folder_id] = fill_data return copy.deepcopy(fill_data) - def _get_task_data(self, folder_id, task_id): + def _get_task_data(self, project_entity, folder_id, task_id): task_data = self._task_data_by_folder_id.setdefault(folder_id, {}) if task_id not in task_data: task = self._controller.get_task_entity(task_id) if task: - task_data[task_id] = get_task_template_data(task) + task_data[task_id] = get_task_template_data( + project_entity, task) return copy.deepcopy(task_data[task_id]) def _prepare_fill_data(self, folder_id, task_id): @@ -159,7 +168,8 @@ class WorkareaModel: base_data = self._get_base_data() folder_data = self._get_folder_data(folder_id) - task_data = self._get_task_data(folder_id, task_id) + project_entity = self._controller.get_project_entity() + task_data = self._get_task_data(project_entity, folder_id, task_id) base_data.update(folder_data) base_data.update(task_data) From e27930c42064f9a6903bcedc0998c8e8b6e2eacb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 23 Sep 2023 03:24:16 +0000 Subject: [PATCH 174/175] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 483b70436a..d1ebde3d04 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.1-nightly.1" +__version__ = "3.17.1-nightly.2" From 60d75300114f9ebba17f3882a81627080ef92ac8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 23 Sep 2023 03:24:52 +0000 Subject: [PATCH 175/175] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0762eb2f20..87d904fc84 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,8 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.1-nightly.2 + - 3.17.1-nightly.1 - 3.17.0 - 3.16.7 - 3.16.7-nightly.2 @@ -133,8 +135,6 @@ body: - 3.14.10-nightly.5 - 3.14.10-nightly.4 - 3.14.10-nightly.3 - - 3.14.10-nightly.2 - - 3.14.10-nightly.1 validations: required: true - type: dropdown