diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 3c126048da..e377773007 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -35,6 +35,14 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
+ - 3.17.6-nightly.2
+ - 3.17.6-nightly.1
+ - 3.17.5
+ - 3.17.5-nightly.3
+ - 3.17.5-nightly.2
+ - 3.17.5-nightly.1
+ - 3.17.4
+ - 3.17.4-nightly.2
- 3.17.4-nightly.1
- 3.17.3
- 3.17.3-nightly.2
@@ -127,14 +135,6 @@ body:
- 3.15.2-nightly.1
- 3.15.1
- 3.15.1-nightly.6
- - 3.15.1-nightly.5
- - 3.15.1-nightly.4
- - 3.15.1-nightly.3
- - 3.15.1-nightly.2
- - 3.15.1-nightly.1
- - 3.15.0
- - 3.15.0-nightly.1
- - 3.14.11-nightly.4
validations:
required: true
- type: dropdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58428ab4d3..b3daf581ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,731 @@
# Changelog
+## [3.17.5](https://github.com/ynput/OpenPype/tree/3.17.5)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.4...3.17.5)
+
+### **🆕 New features**
+
+
+
+Fusion: Add USD loader #4896
+
+Add an OpenPype managed USD loader (`uLoader`) for Fusion.
+
+
+___
+
+
+
+
+
+Fusion: Resolution validator #5325
+
+Added a resolution validator.The code is from my old PR (https://github.com/ynput/OpenPype/pull/4921) that I closed because the PR also contained a frame range validator that no longer is needed.
+
+
+___
+
+
+
+
+
+Context Selection tool: Refactor Context tool (for AYON) #5766
+
+Context selection tool has AYON variant.
+
+
+___
+
+
+
+
+
+AYON: Use AYON username for user in template data #5842
+
+Use ayon username for template data in AYON mode.
+
+
+___
+
+
+
+
+
+Testing: app_group flag #5869
+
+`app_group` command flag. This is for changing which flavour of the host to launch. In the case of Maya, you can launch Maya and MayaPy, but it can be used for the Nuke family as well.Split from #5644
+
+
+___
+
+
+
+### **🚀 Enhancements**
+
+
+
+Enhancement: Fusion fix saver creation + minor Blender/Fusion logging tweaks #5558
+
+- Blender change logs to `debug` level in preparation for new publisher artist facing reports (note that it currently still uses the old publisher)
+- Fusion: Create Saver fix redeclaration of default_variants
+- Fusion: Fix saver being created in incorrect state without saving directly after create
+- Fusion: Allow reset frame range on render family
+- Fusion: Tweak logging level for artist-facing report
+
+
+___
+
+
+
+
+
+Resolve: load clip to timeline at set time #5665
+
+It is possible to load clip to correct place on timeline.
+
+
+___
+
+
+
+
+
+Nuke: Optional Deadline workfile dependency. #5732
+
+Adds option to add the workfile as dependency for the Deadline job.Think it used to have something like this, but it disappeared. Usecase is for remote workflow where the Nuke script needs to be synced before the job can start.
+
+
+___
+
+
+
+
+
+Enhancement/houdini rearrange ayon houdini settings files #5748
+
+Rearranging Houdini Settings to be more readable, easier to edit, update settings (include all families/product types)This PR is mainly for Ayon Settings to have more organized files. For Openpype, I'll make sure that each Houdini setting in Ayon has an equivalent in Openpype.
+- [x] update Ayon settings, fix typos and remove deprecated settings.
+- [x] Sync with Openpype
+- [x] Test in Openpype
+- [x] Test in Ayon
+
+
+___
+
+
+
+
+
+Chore: updating create ayon addon script #5822
+
+Adding developers environment options.
+
+
+___
+
+
+
+
+
+Max: Implement Validator for Properties/Attributes Value Check #5824
+
+Add optional validator which can check if the property attributes are valid in Max
+
+
+___
+
+
+
+
+
+Nuke: Remove unused 'get_render_path' function #5826
+
+Remove unused function `get_render_path` from nuke integration.
+
+
+___
+
+
+
+
+
+Chore: Limit current context template data function #5845
+
+Current implementation of `get_current_context_template_data` does return the same values as base template data function `get_template_data`.
+
+
+___
+
+
+
+
+
+Max: Make sure Collect Render not ignoring instance asset #5847
+
+- Make sure Collect Render is not always using asset from context.
+- Make sure Scene version being collected
+- Clean up unnecessary uses of code in the collector.
+
+
+___
+
+
+
+
+
+Ftrack: Events are not processed if project is not available in OpenPype #5853
+
+Events that happened on project which is not in OpenPype is not processed.
+
+
+___
+
+
+
+
+
+Nuke: Add Nuke 11.0 as default setting #5855
+
+Found I needed Nuke 11.0 in the default settings to help with unit testing.
+
+
+___
+
+
+
+
+
+TVPaint: Code cleanup #5857
+
+Removed unused import. Use `AYON` label in ayon mode. Removed unused data in publish context `"previous_context"`.
+
+
+___
+
+
+
+
+
+AYON settings: Use correct label for follow workfile version #5874
+
+Follow workfile version label was marked as Collect Anatomy Instance Data label.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Nuke: Fix workfile template builder so representations get loaded next to each other #5061
+
+Refactor when the cleanup of the placeholder happens for the cases where multiple representations are loaded by a single placeholder.The existing code didn't take into account the case where a template placeholder can load multiple representations so it was trying to do the cleanup of the placeholder node and the re-arrangement of the imported nodes too early. I assume this was designed only for the cases where a single representation can load multiple nodes.
+
+
+___
+
+
+
+
+
+Nuke: Dont update node name on update #5704
+
+When updating `Image` containers the code is trying to set the name of the node. This results in a warning message from Nuke shown below;Suggesting to not change the node name when updating.
+
+
+___
+
+
+
+
+
+UIDefLabel can be unique #5827
+
+`UILabelDef` have implemented comparison and uniqueness.
+
+
+___
+
+
+
+
+
+AYON: Skip kitsu module when creating ayon addons #5828
+
+Create AYON packages is skipping kitsu module in creation of modules/addons and kitsu module is not loaded from modules on start. The addon already has it's repository https://github.com/ynput/ayon-kitsu.
+
+
+___
+
+
+
+
+
+Bugfix: Collect Rendered Files only collecting first instance #5832
+
+Collect all instances from the metadata file - don't return on first instance iteration.
+
+
+___
+
+
+
+
+
+Houdini: set frame range for the created composite ROP #5833
+
+Quick bug fix for created composite ROP, set its frame range to the frame range of the playbar.
+
+
+___
+
+
+
+
+
+Fix registering launcher actions from OpenPypeModules #5843
+
+Fix typo `actions_dir` -> `path` to fix register launcher actions fromm OpenPypeModule
+
+
+___
+
+
+
+
+
+Bugfix in houdini shelves manager and beautify settings #5844
+
+This PR fixes the problem in this PR https://github.com/ynput/OpenPype/issues/5457 by using the right function to load a pre-made houdini `.shelf` fileAlso, it beautifies houdini shelves settings to provide better guidance for users which helps with other issue https://github.com/ynput/OpenPype/issues/5458 , Rather adding default shelf and set names, I'll educate users how to use the tool correctly.Users now are able to select between the two options.| OpenPype | Ayon || -- | -- || | |
+
+
+___
+
+
+
+
+
+Blender: Fix missing Grease Pencils in review #5848
+
+Fix Grease Pencil missing in review when isolating objects.
+
+
+___
+
+
+
+
+
+Blender: Fix Render Settings in Ayon #5849
+
+Fix Render Settings in Ayon for Blender.
+
+
+___
+
+
+
+
+
+Bugfix: houdini tab menu working as expected #5850
+
+This PR:Tab menu name changes to Ayon when using ayon get_network_categories is checked in all creator plugins. | Product | Network Category | | -- | -- | | Alembic camera | rop, obj | | Arnold Ass | rop | | Arnold ROP | rop | | Bgeo | rop, sop | | composite sequence | cop2, rop | | hda | obj | | Karma ROP | rop | | Mantra ROP | rop | | ABC | rop, sop | | RS proxy | rop, sop| | RS ROP | rop | | Review | rop | | Static mesh | rop, obj, sop | | USD | lop, rop | | USD Render | rop | | VDB | rop, obj, sop | | V Ray | rop |
+
+
+___
+
+
+
+
+
+Bigfix: Houdini skip frame_range_validator if node has no 'trange' parameter #5851
+
+I faced a bug when publishing HDA instance as it has no `trange` parameter. As this PR title says : skip frame_range_validator if node has no 'trange' parameter
+
+
+___
+
+
+
+
+
+Bugfix: houdini image sequence loading and missing frames #5852
+
+I made this PR in to fix issues mentioned here https://github.com/ynput/OpenPype/pull/5833#issuecomment-1789207727in short:
+- image load doesn't work
+- publisher only publish one frame
+
+
+___
+
+
+
+
+
+Nuke: loaders' containers updating as nodes #5854
+
+Nuke loaded containers are updating correctly even they have been duplicating of originally loaded nodes. This had previously been removed duplicated nodes.
+
+
+___
+
+
+
+
+
+deadline: settings are not blocking extension input #5864
+
+Settings are not blocking user input.
+
+
+___
+
+
+
+
+
+Blender: Fix loading of blend layouts #5866
+
+Fix a problem with loading blend layouts.
+
+
+___
+
+
+
+
+
+AYON: Launcher refresh issues #5867
+
+Fixed refresh of projects issue in launcher tool. And renamed Qt models to contain `Qt` in their name (it was really hard to find out where were used). It is not possible to click on disabled item in launcher's projects view.
+
+
+___
+
+
+
+
+
+Fix the Wrong key words for tycache workfile template settings in AYON #5870
+
+Fix the wrong key words for the tycache workfile template settings in AYON(i.e. Instead of families, product_types should be used)
+
+
+___
+
+
+
+
+
+AYON tools: Handle empty icon definition #5876
+
+Ignore if passed icon definition is `None`.
+
+
+___
+
+
+
+### **🔀 Refactored code**
+
+
+
+Houdini: Remove on instance toggled callback #5860
+
+Remove on instance toggled callback which isn't relevant to the new publisher
+
+
+___
+
+
+
+
+
+Chore: Remove unused `instanceToggled` callbacks #5862
+
+The `instanceToggled` callbacks should be irrelevant for new publisher.
+
+
+___
+
+
+
+
+
+
+## [3.17.4](https://github.com/ynput/OpenPype/tree/3.17.4)
+
+
+[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.3...3.17.4)
+
+### **🆕 New features**
+
+
+
+Add Support for Husk-AYON Integration #5816
+
+This draft pull request introduces support for integrating Husk with AYON within the OpenPype repository.
+
+
+___
+
+
+
+
+
+Push to project tool: Prepare push to project tool for AYON #5770
+
+Cloned Push to project tool for AYON and modified it.
+
+
+___
+
+
+
+### **🚀 Enhancements**
+
+
+
+Max: tycache family support #5624
+
+Tycache family supports for Tyflow Plugin in Max
+
+
+___
+
+
+
+
+
+Unreal: Changed behaviour for updating assets #5670
+
+Changed how assets are updated in Unreal.
+
+
+___
+
+
+
+
+
+Unreal: Improved error reporting for Sequence Frame Validator #5730
+
+Improved error reporting for Sequence Frame Validator.
+
+
+___
+
+
+
+
+
+Max: Setting tweaks on Review Family #5744
+
+- Bug fix of not being able to publish the preferred visual style when creating preview animation
+- Exposes the parameters after creating instance
+- Add the Quality settings and viewport texture settings for preview animation
+- add use selection for create review
+
+
+___
+
+
+
+
+
+Max: Add families with frame range extractions back to the frame range validator #5757
+
+In 3dsMax, there are some instances which exports the files in frame range but not being added to the optional frame range validator. In this PR, these instances would have the optional frame range validators to allow users to check if frame range aligns with the context data from DB.The following families have been added to have optional frame range validator:
+- maxrender
+- review
+- camera
+- redshift proxy
+- pointcache
+- point cloud(tyFlow PRT)
+
+
+___
+
+
+
+
+
+TimersManager: Use available data to get context info #5804
+
+Get context information from pyblish context data instead of using `legacy_io`.
+
+
+___
+
+
+
+
+
+Chore: Removed unused variable from `AbstractCollectRender` #5805
+
+Removed unused `_asset` variable from `RenderInstance`.
+
+
+___
+
+
+
+### **🐛 Bug fixes**
+
+
+
+Bugfix/houdini: wrong frame calculation with handles #5698
+
+This PR make collect plugins to consider `handleStart` and `handleEnd` when collecting frame range it affects three parts:
+- get frame range in collect plugins
+- expected file in render plugins
+- submit houdini job deadline plugin
+
+
+___
+
+
+
+
+
+Nuke: ayon server settings improvements #5746
+
+Nuke settings were not aligned with OpenPype settings. Also labels needed to be improved.
+
+
+___
+
+
+
+
+
+Blender: Fix pointcache family and fix alembic extractor #5747
+
+Fixed `pointcache` family and fixed behaviour of the alembic extractor.
+
+
+___
+
+
+
+
+
+AYON: Remove 'shotgun_api3' from dependencies #5803
+
+Removed `shotgun_api3` dependency from openpype dependencies for AYON launcher. The dependency is already defined in shotgrid addon and change of version causes clashes.
+
+
+___
+
+
+
+
+
+Chore: Fix typo in filename #5807
+
+Move content of `contants.py` into `constants.py`.
+
+
+___
+
+
+
+
+
+Chore: Create context respects instance changes #5809
+
+Fix issue with unrespected change propagation in `CreateContext`. All successfully saved instances are marked as saved so they have no changes. Origin data of an instance are explicitly not handled directly by the object but by the attribute wrappers.
+
+
+___
+
+
+
+
+
+Blender: Fix tools handling in AYON mode #5811
+
+Skip logic in `before_window_show` in blender when in AYON mode. Most of the stuff called there happes on show automatically.
+
+
+___
+
+
+
+
+
+Blender: Include Grease Pencil in review and thumbnails #5812
+
+Include Grease Pencil in review and thumbnails.
+
+
+___
+
+
+
+
+
+Workfiles tool AYON: Fix double click of workfile #5813
+
+Fix double click on workfiles in workfiles tool to open the file.
+
+
+___
+
+
+
+
+
+Webpublisher: removal of usage of no_of_frames in error message #5819
+
+If it throws exception, `no_of_frames` value wont be available, so it doesn't make sense to log it.
+
+
+___
+
+
+
+
+
+Attribute Defs: Hide multivalue widget in Number by default #5821
+
+Fixed default look of `NumberAttrWidget` by hiding its multiselection widget.
+
+
+___
+
+
+
+### **Merged pull requests**
+
+
+
+Corrected a typo in Readme.md (Top -> To) #5800
+
+
+___
+
+
+
+
+
+Photoshop: Removed redundant copy of extension.zxp #5802
+
+`extension.zxp` shouldn't be inside of extension folder.
+
+
+___
+
+
+
+
+
+
## [3.17.3](https://github.com/ynput/OpenPype/tree/3.17.3)
diff --git a/openpype/cli.py b/openpype/cli.py
index 7422f32f13..f0fe550a1f 100644
--- a/openpype/cli.py
+++ b/openpype/cli.py
@@ -282,6 +282,9 @@ def run(script):
"--app_variant",
help="Provide specific app variant for test, empty for latest",
default=None)
+@click.option("--app_group",
+ help="Provide specific app group for test, empty for default",
+ default=None)
@click.option("-t",
"--timeout",
help="Provide specific timeout value for test case",
@@ -294,11 +297,11 @@ def run(script):
help="MongoDB for testing.",
default=None)
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
- timeout, setup_only, mongo_url):
+ timeout, setup_only, mongo_url, app_group):
"""Run all automatic tests after proper initialization via start.py"""
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
persist, app_variant, timeout, setup_only,
- mongo_url)
+ mongo_url, app_group)
@main.command(help="DEPRECATED - run sync server")
diff --git a/openpype/client/__init__.py b/openpype/client/__init__.py
index 7831afd8ad..fe6dc97877 100644
--- a/openpype/client/__init__.py
+++ b/openpype/client/__init__.py
@@ -1,6 +1,7 @@
from .mongo import (
OpenPypeMongoConnection,
)
+from .server.utils import get_ayon_server_api_connection
from .entities import (
get_projects,
@@ -59,6 +60,8 @@ from .operations import (
__all__ = (
"OpenPypeMongoConnection",
+ "get_ayon_server_api_connection",
+
"get_projects",
"get_project",
"get_whole_project",
diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py
index 16223d3d91..b41727a797 100644
--- a/openpype/client/server/entities.py
+++ b/openpype/client/server/entities.py
@@ -1,9 +1,8 @@
import collections
-from ayon_api import get_server_api_connection
-
from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA
+from .utils import get_ayon_server_api_connection
from .openpype_comp import get_folders_with_tasks
from .conversion_utils import (
project_fields_v3_to_v4,
@@ -37,7 +36,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
elif inactive:
active = False
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = project_fields_v3_to_v4(fields, con)
for project in con.get_projects(active, library, fields=fields):
yield convert_v4_project_to_v3(project)
@@ -45,7 +44,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
def get_project(project_name, active=True, inactive=False, fields=None):
# Skip if both are disabled
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = project_fields_v3_to_v4(fields, con)
return convert_v4_project_to_v3(
con.get_project(project_name, fields=fields)
@@ -66,7 +65,7 @@ def _get_subsets(
fields=None
):
# Convert fields and add minimum required fields
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = subset_fields_v3_to_v4(fields, con)
if fields is not None:
for key in (
@@ -102,7 +101,7 @@ def _get_versions(
active=None,
fields=None
):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = version_fields_v3_to_v4(fields, con)
@@ -198,7 +197,7 @@ def get_assets(
if archived:
active = None
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = folder_fields_v3_to_v4(fields, con)
kwargs = dict(
folder_ids=asset_ids,
@@ -236,7 +235,7 @@ def get_archived_assets(
def get_asset_ids_with_subsets(project_name, asset_ids=None):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.get_folder_ids_with_products(project_name, asset_ids)
@@ -282,7 +281,7 @@ def get_subsets(
def get_subset_families(project_name, subset_ids=None):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.get_product_type_names(project_name, subset_ids)
@@ -430,7 +429,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
if not version_id:
return []
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
version_links = con.get_version_links(
project_name, version_id, link_direction="out")
@@ -446,7 +445,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
def version_is_latest(project_name, version_id):
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.version_is_latest(project_name, version_id)
@@ -501,7 +500,7 @@ def get_representations(
else:
active = None
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
fields = representation_fields_v3_to_v4(fields, con)
if fields and active is not None:
fields.add("active")
@@ -535,7 +534,7 @@ def get_representations_parents(project_name, representations):
repre["_id"]
for repre in representations
}
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
parents_by_repre_id = con.get_representations_parents(project_name,
repre_ids)
folder_ids = set()
@@ -677,7 +676,7 @@ def get_workfile_info(
if not asset_id or not task_name or not filename:
return None
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
task = con.get_task_by_name(
project_name, asset_id, task_name, fields=["id", "name", "folderId"]
)
diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py
index d8395aabe7..368dcdcb9d 100644
--- a/openpype/client/server/entity_links.py
+++ b/openpype/client/server/entity_links.py
@@ -1,6 +1,4 @@
-import ayon_api
-from ayon_api import get_folder_links, get_versions_links
-
+from .utils import get_ayon_server_api_connection
from .entities import get_assets, get_representation_by_id
@@ -28,7 +26,8 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None):
if not asset_id:
asset_id = asset_doc["_id"]
- links = get_folder_links(project_name, asset_id, link_direction="in")
+ con = get_ayon_server_api_connection()
+ links = con.get_folder_links(project_name, asset_id, link_direction="in")
return [
link["entityId"]
for link in links
@@ -115,6 +114,7 @@ def get_linked_representation_id(
if link_type:
link_types = [link_type]
+ con = get_ayon_server_api_connection()
# Store already found version ids to avoid recursion, and also to store
# output -> Don't forget to remove 'version_id' at the end!!!
linked_version_ids = {version_id}
@@ -124,7 +124,7 @@ def get_linked_representation_id(
if not versions_to_check:
break
- links = get_versions_links(
+ links = con.get_versions_links(
project_name,
versions_to_check,
link_types=link_types,
@@ -145,8 +145,8 @@ def get_linked_representation_id(
linked_version_ids.remove(version_id)
if not linked_version_ids:
return []
-
- representations = ayon_api.get_representations(
+ con = get_ayon_server_api_connection()
+ representations = con.get_representations(
project_name,
version_ids=linked_version_ids,
fields=["id"])
diff --git a/openpype/client/server/operations.py b/openpype/client/server/operations.py
index 5b38405c34..eddc1eaf60 100644
--- a/openpype/client/server/operations.py
+++ b/openpype/client/server/operations.py
@@ -5,7 +5,6 @@ import uuid
import datetime
from bson.objectid import ObjectId
-from ayon_api import get_server_api_connection
from openpype.client.operations_base import (
REMOVED_VALUE,
@@ -41,7 +40,7 @@ from .conversion_utils import (
convert_update_representation_to_v4,
convert_update_workfile_info_to_v4,
)
-from .utils import create_entity_id
+from .utils import create_entity_id, get_ayon_server_api_connection
def _create_or_convert_to_id(entity_id=None):
@@ -680,7 +679,7 @@ class OperationsSession(BaseOperationsSession):
def __init__(self, con=None, *args, **kwargs):
super(OperationsSession, self).__init__(*args, **kwargs)
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
self._con = con
self._project_cache = {}
self._nested_operations = collections.defaultdict(list)
@@ -858,7 +857,7 @@ def create_project(
"""
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.create_project(
project_name,
@@ -870,12 +869,12 @@ def create_project(
def delete_project(project_name, con=None):
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.delete_project(project_name)
def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None):
if con is None:
- con = get_server_api_connection()
+ con = get_ayon_server_api_connection()
return con.create_thumbnail(project_name, src_filepath, thumbnail_id)
diff --git a/openpype/client/server/utils.py b/openpype/client/server/utils.py
index ed128cfad9..a9dcf539bd 100644
--- a/openpype/client/server/utils.py
+++ b/openpype/client/server/utils.py
@@ -1,8 +1,33 @@
+import os
import uuid
+import ayon_api
+
from openpype.client.operations_base import REMOVED_VALUE
+class _GlobalCache:
+ initialized = False
+
+
+def get_ayon_server_api_connection():
+ if _GlobalCache.initialized:
+ con = ayon_api.get_server_api_connection()
+ else:
+ from openpype.lib.local_settings import get_local_site_id
+
+ _GlobalCache.initialized = True
+ site_id = get_local_site_id()
+ version = os.getenv("AYON_VERSION")
+ if ayon_api.is_connection_created():
+ con = ayon_api.get_server_api_connection()
+ con.set_site_id(site_id)
+ con.set_client_version(version)
+ else:
+ con = ayon_api.create_connection(site_id, version)
+ return con
+
+
def create_entity_id():
return uuid.uuid1().hex
diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py
index 8fc7a70dd8..e059f7c272 100644
--- a/openpype/hosts/aftereffects/api/pipeline.py
+++ b/openpype/hosts/aftereffects/api/pipeline.py
@@ -74,11 +74,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
- log.info(PUBLISH_PATH)
-
- pyblish.api.register_callback(
- "instanceToggled", on_pyblish_instance_toggled
- )
register_event_callback("application.launched", application_launch)
@@ -186,11 +181,6 @@ def application_launch():
check_inventory()
-def on_pyblish_instance_toggled(instance, old_value, new_value):
- """Toggle layer visibility on instance toggles."""
- instance[0].Visible = new_value
-
-
def ls():
"""Yields containers from active AfterEffects document.
diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py
index 76bef09ceb..5d8c93dc49 100644
--- a/openpype/hosts/blender/api/lib.py
+++ b/openpype/hosts/blender/api/lib.py
@@ -266,9 +266,57 @@ def read(node: bpy.types.bpy_struct_meta_idprop):
return data
-def get_selection() -> List[bpy.types.Object]:
- """Return the selected objects from the current scene."""
- return [obj for obj in bpy.context.scene.objects if obj.select_get()]
+def get_selected_collections():
+ """
+ Returns a list of the currently selected collections in the outliner.
+
+ Raises:
+ RuntimeError: If the outliner cannot be found in the main Blender
+ window.
+
+ Returns:
+ list: A list of `bpy.types.Collection` objects that are currently
+ selected in the outliner.
+ """
+ try:
+ area = next(
+ area for area in bpy.context.window.screen.areas
+ if area.type == 'OUTLINER')
+ region = next(
+ region for region in area.regions
+ if region.type == 'WINDOW')
+ except StopIteration as e:
+ raise RuntimeError("Could not find outliner. An outliner space "
+ "must be in the main Blender window.") from e
+
+ with bpy.context.temp_override(
+ window=bpy.context.window,
+ area=area,
+ region=region,
+ screen=bpy.context.window.screen
+ ):
+ ids = bpy.context.selected_ids
+
+ return [id for id in ids if isinstance(id, bpy.types.Collection)]
+
+
+def get_selection(include_collections: bool = False) -> List[bpy.types.Object]:
+ """
+ Returns a list of selected objects in the current Blender scene.
+
+ Args:
+ include_collections (bool, optional): Whether to include selected
+ collections in the result. Defaults to False.
+
+ Returns:
+ List[bpy.types.Object]: A list of selected objects.
+ """
+ selection = [obj for obj in bpy.context.scene.objects if obj.select_get()]
+
+ if include_collections:
+ selection.extend(get_selected_collections())
+
+ return selection
@contextlib.contextmanager
diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py
index 1114d136ce..e126511fd6 100644
--- a/openpype/hosts/blender/api/plugin.py
+++ b/openpype/hosts/blender/api/plugin.py
@@ -44,9 +44,16 @@ def get_unique_number(
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
return "01"
- asset_groups = avalon_container.all_objects
-
- container_names = [c.name for c in asset_groups if c.type == 'EMPTY']
+ # Check the names of both object and collection containers
+ obj_asset_groups = avalon_container.objects
+ obj_group_names = {
+ c.name for c in obj_asset_groups
+ if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)}
+ coll_asset_groups = avalon_container.children
+ coll_group_names = {
+ c.name for c in coll_asset_groups
+ if c.get(AVALON_PROPERTY)}
+ container_names = obj_group_names.union(coll_group_names)
count = 1
name = f"{asset}_{count:0>2}_{subset}"
while name in container_names:
diff --git a/openpype/hosts/blender/plugins/create/create_blendScene.py b/openpype/hosts/blender/plugins/create/create_blendScene.py
index 0773c4dae3..1dc8f44a63 100644
--- a/openpype/hosts/blender/plugins/create/create_blendScene.py
+++ b/openpype/hosts/blender/plugins/create/create_blendScene.py
@@ -18,6 +18,8 @@ class CreateBlendScene(plugin.Creator):
family = "blendScene"
icon = "cubes"
+ maintain_selection = False
+
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
):
@@ -39,10 +41,10 @@ class CreateBlendScene(plugin.Creator):
# Create instance object
asset = instance_data.get("asset")
name = plugin.asset_name(asset, subset_name)
- asset_group = bpy.data.objects.new(name=name, object_data=None)
- asset_group.empty_display_type = 'SINGLE_ARROW'
- instances.objects.link(asset_group)
+ # Create the new asset group as collection
+ asset_group = bpy.data.collections.new(name=name)
+ instances.children.link(asset_group)
asset_group[AVALON_PROPERTY] = instance_node = {
"name": asset_group.name
}
@@ -50,15 +52,13 @@ class CreateBlendScene(plugin.Creator):
self.set_instance_data(subset_name, instance_data, instance_node)
lib.imprint(asset_group, instance_data)
- # Add selected objects to instance
if (self.options or {}).get("useSelection"):
- bpy.context.view_layer.objects.active = asset_group
- selected = lib.get_selection()
- for obj in selected:
- if obj.parent in selected:
- obj.select_set(False)
- continue
- selected.append(asset_group)
- bpy.ops.object.parent_set(keep_transform=True)
+ selection = lib.get_selection(include_collections=True)
+
+ for data in selection:
+ if isinstance(data, bpy.types.Collection):
+ asset_group.children.link(data)
+ elif isinstance(data, bpy.types.Object):
+ asset_group.objects.link(data)
return asset_group
diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py
index 25d6568889..f7bbc630de 100644
--- a/openpype/hosts/blender/plugins/load/load_blend.py
+++ b/openpype/hosts/blender/plugins/load/load_blend.py
@@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import (
class BlendLoader(plugin.AssetLoader):
"""Load assets from a .blend file."""
- families = ["model", "rig", "layout", "camera", "blendScene"]
+ families = ["model", "rig", "layout", "camera"]
representations = ["blend"]
label = "Append Blend"
@@ -32,7 +32,7 @@ class BlendLoader(plugin.AssetLoader):
empties = [obj for obj in objects if obj.type == 'EMPTY']
for empty in empties:
- if empty.get(AVALON_PROPERTY):
+ if empty.get(AVALON_PROPERTY) and empty.parent is None:
return empty
return None
@@ -90,6 +90,7 @@ class BlendLoader(plugin.AssetLoader):
members.append(data)
container = self._get_asset_container(data_to.objects)
+ print(container)
assert container, "No asset group found"
container.name = group_name
@@ -100,8 +101,11 @@ class BlendLoader(plugin.AssetLoader):
# Link all the container children to the collection
for obj in container.children_recursive:
+ print(obj)
bpy.context.scene.collection.objects.link(obj)
+ print("")
+
# Remove the library from the blend file
library = bpy.data.libraries.get(bpy.path.basename(libpath))
bpy.data.libraries.remove(library)
diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py
new file mode 100644
index 0000000000..2c955af9e8
--- /dev/null
+++ b/openpype/hosts/blender/plugins/load/load_blendscene.py
@@ -0,0 +1,221 @@
+from typing import Dict, List, Optional
+from pathlib import Path
+
+import bpy
+
+from openpype.pipeline import (
+ get_representation_path,
+ AVALON_CONTAINER_ID,
+)
+from openpype.hosts.blender.api import plugin
+from openpype.hosts.blender.api.lib import imprint
+from openpype.hosts.blender.api.pipeline import (
+ AVALON_CONTAINERS,
+ AVALON_PROPERTY,
+)
+
+
+class BlendSceneLoader(plugin.AssetLoader):
+ """Load assets from a .blend file."""
+
+ families = ["blendScene"]
+ representations = ["blend"]
+
+ label = "Append Blend"
+ icon = "code-fork"
+ color = "orange"
+
+ @staticmethod
+ def _get_asset_container(collections):
+ for coll in collections:
+ parents = [c for c in collections if c.user_of_id(coll)]
+ if coll.get(AVALON_PROPERTY) and not parents:
+ return coll
+
+ return None
+
+ def _process_data(self, libpath, group_name, family):
+ # Append all the data from the .blend file
+ with bpy.data.libraries.load(
+ libpath, link=False, relative=False
+ ) as (data_from, data_to):
+ for attr in dir(data_to):
+ setattr(data_to, attr, getattr(data_from, attr))
+
+ members = []
+
+ # Rename the object to add the asset name
+ for attr in dir(data_to):
+ for data in getattr(data_to, attr):
+ data.name = f"{group_name}:{data.name}"
+ members.append(data)
+
+ container = self._get_asset_container(
+ data_to.collections)
+ assert container, "No asset group found"
+
+ container.name = group_name
+
+ # Link the group to the scene
+ bpy.context.scene.collection.children.link(container)
+
+ # Remove the library from the blend file
+ library = bpy.data.libraries.get(bpy.path.basename(libpath))
+ bpy.data.libraries.remove(library)
+
+ return container, members
+
+ def process_asset(
+ self, context: dict, name: str, namespace: Optional[str] = None,
+ options: Optional[Dict] = None
+ ) -> Optional[List]:
+ """
+ Arguments:
+ name: Use pre-defined name
+ namespace: Use pre-defined namespace
+ context: Full parenthood of representation to load
+ options: Additional settings dictionary
+ """
+ libpath = self.filepath_from_context(context)
+ asset = context["asset"]["name"]
+ subset = context["subset"]["name"]
+
+ try:
+ family = context["representation"]["context"]["family"]
+ except ValueError:
+ family = "model"
+
+ asset_name = plugin.asset_name(asset, subset)
+ unique_number = plugin.get_unique_number(asset, subset)
+ group_name = plugin.asset_name(asset, subset, unique_number)
+ namespace = namespace or f"{asset}_{unique_number}"
+
+ avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
+ if not avalon_container:
+ avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
+ bpy.context.scene.collection.children.link(avalon_container)
+
+ container, members = self._process_data(libpath, group_name, family)
+
+ avalon_container.children.link(container)
+
+ data = {
+ "schema": "openpype:container-2.0",
+ "id": AVALON_CONTAINER_ID,
+ "name": name,
+ "namespace": namespace or '',
+ "loader": str(self.__class__.__name__),
+ "representation": str(context["representation"]["_id"]),
+ "libpath": libpath,
+ "asset_name": asset_name,
+ "parent": str(context["representation"]["parent"]),
+ "family": context["representation"]["context"]["family"],
+ "objectName": group_name,
+ "members": members,
+ }
+
+ container[AVALON_PROPERTY] = data
+
+ objects = [
+ obj for obj in bpy.data.objects
+ if obj.name.startswith(f"{group_name}:")
+ ]
+
+ self[:] = objects
+ return objects
+
+ def exec_update(self, container: Dict, representation: Dict):
+ """
+ Update the loaded asset.
+ """
+ group_name = container["objectName"]
+ asset_group = bpy.data.collections.get(group_name)
+ libpath = Path(get_representation_path(representation)).as_posix()
+
+ assert asset_group, (
+ f"The asset is not loaded: {container['objectName']}"
+ )
+
+ # Get the parents of the members of the asset group, so we can
+ # re-link them after the update.
+ # Also gets the transform for each object to reapply after the update.
+ collection_parents = {}
+ member_transforms = {}
+ members = asset_group.get(AVALON_PROPERTY).get("members", [])
+ loaded_collections = {c for c in bpy.data.collections if c in members}
+ loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))
+ for member in members:
+ if isinstance(member, bpy.types.Object):
+ member_parents = set(member.users_collection)
+ member_transforms[member.name] = member.matrix_basis.copy()
+ elif isinstance(member, bpy.types.Collection):
+ member_parents = {
+ c for c in bpy.data.collections if c.user_of_id(member)}
+ else:
+ continue
+
+ member_parents = member_parents.difference(loaded_collections)
+ if member_parents:
+ collection_parents[member.name] = list(member_parents)
+
+ old_data = dict(asset_group.get(AVALON_PROPERTY))
+
+ self.exec_remove(container)
+
+ family = container["family"]
+ asset_group, members = self._process_data(libpath, group_name, family)
+
+ for member in members:
+ if member.name in collection_parents:
+ for parent in collection_parents[member.name]:
+ if isinstance(member, bpy.types.Object):
+ parent.objects.link(member)
+ elif isinstance(member, bpy.types.Collection):
+ parent.children.link(member)
+ if member.name in member_transforms and isinstance(
+ member, bpy.types.Object
+ ):
+ member.matrix_basis = member_transforms[member.name]
+
+ avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
+ avalon_container.children.link(asset_group)
+
+ # Restore the old data, but reset members, as they don't exist anymore
+ # This avoids a crash, because the memory addresses of those members
+ # are not valid anymore
+ old_data["members"] = []
+ asset_group[AVALON_PROPERTY] = old_data
+
+ new_data = {
+ "libpath": libpath,
+ "representation": str(representation["_id"]),
+ "parent": str(representation["parent"]),
+ "members": members,
+ }
+
+ imprint(asset_group, new_data)
+
+ def exec_remove(self, container: Dict) -> bool:
+ """
+ Remove an existing container from a Blender scene.
+ """
+ group_name = container["objectName"]
+ asset_group = bpy.data.collections.get(group_name)
+
+ members = set(asset_group.get(AVALON_PROPERTY).get("members", []))
+
+ if members:
+ for attr_name in dir(bpy.data):
+ attr = getattr(bpy.data, attr_name)
+ if not isinstance(attr, bpy.types.bpy_prop_collection):
+ continue
+
+ # ensure to make a list copy because we
+ # we remove members as we iterate
+ for data in list(attr):
+ if data not in members or data == asset_group:
+ continue
+
+ attr.remove(data)
+
+ bpy.data.collections.remove(asset_group)
diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py
index 66a3d7b5e8..4889c66be3 100644
--- a/openpype/hosts/blender/plugins/publish/collect_review.py
+++ b/openpype/hosts/blender/plugins/publish/collect_review.py
@@ -35,11 +35,12 @@ class CollectReview(pyblish.api.InstancePlugin):
focal_length = cameras[0].data.lens
- # get isolate objects list from meshes instance members .
+ # get isolate objects list from meshes instance members.
+ types = {"MESH", "GPENCIL"}
isolate_objects = [
obj
for obj in instance
- if isinstance(obj, bpy.types.Object) and obj.type == "MESH"
+ if isinstance(obj, bpy.types.Object) and obj.type in types
]
if not instance.data.get("remove"):
diff --git a/openpype/hosts/blender/plugins/publish/extract_blend.py b/openpype/hosts/blender/plugins/publish/extract_blend.py
index 884169f7f1..991cd040de 100644
--- a/openpype/hosts/blender/plugins/publish/extract_blend.py
+++ b/openpype/hosts/blender/plugins/publish/extract_blend.py
@@ -31,19 +31,27 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin):
data_blocks = set()
- for obj in instance:
- data_blocks.add(obj)
+ for data in instance:
+ data_blocks.add(data)
# Pack used images in the blend files.
- if obj.type == 'MESH':
- for material_slot in obj.material_slots:
- mat = material_slot.material
- if mat and mat.use_nodes:
- tree = mat.node_tree
- if tree.type == 'SHADER':
- for node in tree.nodes:
- if node.bl_idname == 'ShaderNodeTexImage':
- if node.image:
- node.image.pack()
+ if not (
+ isinstance(data, bpy.types.Object) and data.type == 'MESH'
+ ):
+ continue
+ for material_slot in data.material_slots:
+ mat = material_slot.material
+ if not (mat and mat.use_nodes):
+ continue
+ tree = mat.node_tree
+ if tree.type != 'SHADER':
+ continue
+ for node in tree.nodes:
+ if node.bl_idname != 'ShaderNodeTexImage':
+ continue
+ # Check if image is not packed already
+ # and pack it if not.
+ if node.image and node.image.packed_file is None:
+ node.image.pack()
bpy.data.libraries.write(filepath, data_blocks)
diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py
index 696cf85089..fe005c6593 100644
--- a/openpype/hosts/blender/plugins/publish/extract_playblast.py
+++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py
@@ -33,14 +33,14 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
fps = bpy.context.scene.render.fps
instance.data["fps"] = fps
- self.log.info(f"fps: {fps}")
+ self.log.debug(f"fps: {fps}")
# If start and end frames cannot be determined,
# get them from Blender timeline.
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
end = instance.data.get("frameEnd", bpy.context.scene.frame_end)
- self.log.info(f"start: {start}, end: {end}")
+ self.log.debug(f"start: {start}, end: {end}")
assert end > start, "Invalid time range !"
# get cameras
@@ -99,7 +99,7 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
frame_collection = collections[0]
- self.log.debug(f"We found collection of interest {frame_collection}")
+ self.log.debug(f"Found collection of interest {frame_collection}")
instance.data.setdefault("representations", [])
diff --git a/openpype/hosts/blender/plugins/publish/validate_instance_empty.py b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py
new file mode 100644
index 0000000000..3ebc6515d3
--- /dev/null
+++ b/openpype/hosts/blender/plugins/publish/validate_instance_empty.py
@@ -0,0 +1,23 @@
+import bpy
+
+import pyblish.api
+
+
+class ValidateInstanceEmpty(pyblish.api.InstancePlugin):
+ """Validator to verify that the instance is not empty"""
+
+ order = pyblish.api.ValidatorOrder - 0.01
+ hosts = ["blender"]
+ families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"]
+ label = "Validate Instance is not Empty"
+ optional = False
+
+ def process(self, instance):
+ asset_group = instance.data["instance_group"]
+
+ if isinstance(asset_group, bpy.types.Collection):
+ if not (asset_group.objects or asset_group.children):
+ raise RuntimeError(f"Instance {instance.name} is empty.")
+ elif isinstance(asset_group, bpy.types.Object):
+ if not asset_group.children:
+ raise RuntimeError(f"Instance {instance.name} is empty.")
diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py
index c4a1488606..85f9c54a73 100644
--- a/openpype/hosts/fusion/api/lib.py
+++ b/openpype/hosts/fusion/api/lib.py
@@ -280,7 +280,11 @@ def get_current_comp():
@contextlib.contextmanager
-def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"):
+def comp_lock_and_undo_chunk(
+ comp,
+ undo_queue_name="Script CMD",
+ keep_undo=True,
+):
"""Lock comp and open an undo chunk during the context"""
try:
comp.Lock()
@@ -288,4 +292,4 @@ def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"):
yield
finally:
comp.Unlock()
- comp.EndUndo()
+ comp.EndUndo(keep_undo)
diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py
index 4564880b50..2dc48f4b60 100644
--- a/openpype/hosts/fusion/plugins/create/create_saver.py
+++ b/openpype/hosts/fusion/plugins/create/create_saver.py
@@ -69,8 +69,6 @@ class CreateSaver(NewCreator):
# TODO Is this needed?
saver[file_format]["SaveAlpha"] = 1
- self._imprint(saver, instance_data)
-
# Register the CreatedInstance
instance = CreatedInstance(
family=self.family,
@@ -78,6 +76,8 @@ class CreateSaver(NewCreator):
data=instance_data,
creator=self,
)
+ data = instance.data_to_store()
+ self._imprint(saver, data)
# Insert the transient data
instance.transient_data["tool"] = saver
diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py
index f83ab433ee..94ba361b50 100644
--- a/openpype/hosts/fusion/plugins/load/actions.py
+++ b/openpype/hosts/fusion/plugins/load/actions.py
@@ -11,6 +11,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin):
families = ["animation",
"camera",
"imagesequence",
+ "render",
"yeticache",
"pointcache",
"render"]
@@ -46,6 +47,7 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin):
families = ["animation",
"camera",
"imagesequence",
+ "render",
"yeticache",
"pointcache",
"render"]
diff --git a/openpype/hosts/fusion/plugins/load/load_usd.py b/openpype/hosts/fusion/plugins/load/load_usd.py
new file mode 100644
index 0000000000..4f1813a646
--- /dev/null
+++ b/openpype/hosts/fusion/plugins/load/load_usd.py
@@ -0,0 +1,87 @@
+from openpype.pipeline import (
+ load,
+ get_representation_path,
+)
+from openpype.hosts.fusion.api import (
+ imprint_container,
+ get_current_comp,
+ comp_lock_and_undo_chunk
+)
+from openpype.hosts.fusion.api.lib import get_fusion_module
+
+
+class FusionLoadUSD(load.LoaderPlugin):
+ """Load USD into Fusion
+
+ Support for USD was added since Fusion 18.5
+ """
+
+ families = ["*"]
+ representations = ["*"]
+ extensions = {"usd", "usda", "usdz"}
+
+ label = "Load USD"
+ order = -10
+ icon = "code-fork"
+ color = "orange"
+
+ tool_type = "uLoader"
+
+ @classmethod
+ def apply_settings(cls, project_settings, system_settings):
+ super(FusionLoadUSD, cls).apply_settings(project_settings,
+ system_settings)
+ if cls.enabled:
+ # Enable only in Fusion 18.5+
+ fusion = get_fusion_module()
+ version = fusion.GetVersion()
+ major = version[1]
+ minor = version[2]
+ is_usd_supported = (major, minor) >= (18, 5)
+ cls.enabled = is_usd_supported
+
+ def load(self, context, name, namespace, data):
+ # Fallback to asset name when namespace is None
+ if namespace is None:
+ namespace = context['asset']['name']
+
+ # Create the Loader with the filename path set
+ comp = get_current_comp()
+ with comp_lock_and_undo_chunk(comp, "Create tool"):
+
+ path = self.fname
+
+ args = (-32768, -32768)
+ tool = comp.AddTool(self.tool_type, *args)
+ tool["Filename"] = path
+
+ imprint_container(tool,
+ name=name,
+ namespace=namespace,
+ context=context,
+ loader=self.__class__.__name__)
+
+ def switch(self, container, representation):
+ self.update(container, representation)
+
+ def update(self, container, representation):
+
+ tool = container["_tool"]
+ assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
+ comp = tool.Comp()
+
+ path = get_representation_path(representation)
+
+ with comp_lock_and_undo_chunk(comp, "Update tool"):
+ tool["Filename"] = path
+
+ # Update the imprinted representation
+ tool.SetData("avalon.representation", str(representation["_id"]))
+
+ def remove(self, container):
+ tool = container["_tool"]
+ assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
+ comp = tool.Comp()
+
+ with comp_lock_and_undo_chunk(comp, "Remove tool"):
+ tool.Delete()
diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py
new file mode 100644
index 0000000000..efa7295d11
--- /dev/null
+++ b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py
@@ -0,0 +1,105 @@
+import pyblish.api
+from openpype.pipeline import (
+ PublishValidationError,
+ OptionalPyblishPluginMixin,
+)
+
+from openpype.hosts.fusion.api.action import SelectInvalidAction
+from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
+
+
+def get_tool_resolution(tool, frame):
+ """Return the 2D input resolution to a Fusion tool
+
+ If the current tool hasn't been rendered its input resolution
+ hasn't been saved. To combat this, add an expression in
+ the comments field to read the resolution
+
+ Args
+ tool (Fusion Tool): The tool to query input resolution
+ frame (int): The frame to query the resolution on.
+
+ Returns:
+ tuple: width, height as 2-tuple of integers
+
+ """
+ comp = tool.Composition
+
+ # False undo removes the undo-stack from the undo list
+ with comp_lock_and_undo_chunk(comp, "Read resolution", False):
+ # Save old comment
+ old_comment = ""
+ has_expression = False
+ if tool["Comments"][frame] != "":
+ if tool["Comments"].GetExpression() is not None:
+ has_expression = True
+ old_comment = tool["Comments"].GetExpression()
+ tool["Comments"].SetExpression(None)
+ else:
+ old_comment = tool["Comments"][frame]
+ tool["Comments"][frame] = ""
+
+ # Get input width
+ tool["Comments"].SetExpression("self.Input.OriginalWidth")
+ width = int(tool["Comments"][frame])
+
+ # Get input height
+ tool["Comments"].SetExpression("self.Input.OriginalHeight")
+ height = int(tool["Comments"][frame])
+
+ # Reset old comment
+ tool["Comments"].SetExpression(None)
+ if has_expression:
+ tool["Comments"].SetExpression(old_comment)
+ else:
+ tool["Comments"][frame] = old_comment
+
+ return width, height
+
+
+class ValidateSaverResolution(
+ pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
+):
+ """Validate that the saver input resolution matches the asset resolution"""
+
+ order = pyblish.api.ValidatorOrder
+ label = "Validate Asset Resolution"
+ families = ["render"]
+ hosts = ["fusion"]
+ optional = True
+ actions = [SelectInvalidAction]
+
+ def process(self, instance):
+ if not self.is_active(instance.data):
+ return
+
+ resolution = self.get_resolution(instance)
+ expected_resolution = self.get_expected_resolution(instance)
+ if resolution != expected_resolution:
+ raise PublishValidationError(
+ "The input's resolution does not match "
+ "the asset's resolution {}x{}.\n\n"
+ "The input's resolution is {}x{}.".format(
+ expected_resolution[0], expected_resolution[1],
+ resolution[0], resolution[1]
+ )
+ )
+
+ @classmethod
+ def get_invalid(cls, instance):
+ resolution = cls.get_resolution(instance)
+ expected_resolution = cls.get_expected_resolution(instance)
+ if resolution != expected_resolution:
+ saver = instance.data["tool"]
+ return [saver]
+
+ @classmethod
+ def get_resolution(cls, instance):
+ saver = instance.data["tool"]
+ first_frame = instance.data["frameStartHandle"]
+ return get_tool_resolution(saver, frame=first_frame)
+
+ @classmethod
+ def get_expected_resolution(cls, instance):
+ data = instance.data["assetEntity"]["data"]
+ return data["resolutionWidth"], data["resolutionHeight"]
diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py
index 1f9fef7417..14662dc419 100644
--- a/openpype/hosts/houdini/api/creator_node_shelves.py
+++ b/openpype/hosts/houdini/api/creator_node_shelves.py
@@ -173,6 +173,7 @@ def install():
os.remove(filepath)
icon = get_openpype_icon_filepath()
+ tab_menu_label = os.environ.get("AVALON_LABEL") or "AYON"
# Create context only to get creator plugins, so we don't reset and only
# populate what we need to retrieve the list of creator plugins
@@ -197,14 +198,14 @@ def install():
if not network_categories:
continue
- key = "openpype_create.{}".format(identifier)
+ key = "ayon_create.{}".format(identifier)
log.debug(f"Registering {key}")
script = CREATE_SCRIPT.format(identifier=identifier)
data = {
"script": script,
"language": hou.scriptLanguage.Python,
"icon": icon,
- "help": "Create OpenPype publish instance for {}".format(
+ "help": "Create Ayon publish instance for {}".format(
creator.label
),
"help_url": None,
@@ -213,7 +214,7 @@ def install():
"cop_viewer_categories": [],
"network_op_type": None,
"viewer_op_type": None,
- "locations": ["OpenPype"]
+ "locations": [tab_menu_label]
}
label = "Create {}".format(creator.label)
tool = hou.shelves.tool(key)
diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py
index 3031e2d2bd..8b058e605d 100644
--- a/openpype/hosts/houdini/api/lib.py
+++ b/openpype/hosts/houdini/api/lib.py
@@ -11,20 +11,21 @@ import json
import six
from openpype.lib import StringTemplate
-from openpype.client import get_asset_by_name
+from openpype.client import get_project, get_asset_by_name
from openpype.settings import get_current_project_settings
from openpype.pipeline import (
+ Anatomy,
get_current_project_name,
get_current_asset_name,
- registered_host
-)
-from openpype.pipeline.context_tools import (
- get_current_context_template_data,
- get_current_project_asset
+ registered_host,
+ get_current_context,
+ get_current_host_name,
)
+from openpype.pipeline.create import CreateContext
+from openpype.pipeline.template_data import get_template_data
+from openpype.pipeline.context_tools import get_current_project_asset
from openpype.widgets import popup
from openpype.tools.utils.host_tools import get_tool_by_name
-from openpype.pipeline.create import CreateContext
import hou
@@ -568,29 +569,56 @@ def get_template_from_value(key, value):
return parm
-def get_frame_data(node):
- """Get the frame data: start frame, end frame and steps.
+def get_frame_data(node, log=None):
+ """Get the frame data: `frameStartHandle`, `frameEndHandle`
+ and `byFrameStep`.
+
+ This function uses Houdini node's `trange`, `t1, `t2` and `t3`
+ parameters as the source of truth for the full inclusive frame
+ range to render, as such these are considered as the frame
+ range including the handles.
+
+ The non-inclusive frame start and frame end without handles
+ can be computed by subtracting the handles from the inclusive
+ frame range.
Args:
- node(hou.Node)
+ node (hou.Node): ROP node to retrieve frame range from,
+ the frame range is assumed to be the frame range
+ *including* the start and end handles.
Returns:
- dict: frame data for star, end and steps.
+ dict: frame data for `frameStartHandle`, `frameEndHandle`
+ and `byFrameStep`.
"""
+
+ if log is None:
+ log = self.log
+
data = {}
if node.parm("trange") is None:
-
+ log.debug(
+ "Node has no 'trange' parameter: {}".format(node.path())
+ )
return data
if node.evalParm("trange") == 0:
- self.log.debug("trange is 0")
- return data
+ data["frameStartHandle"] = hou.intFrame()
+ data["frameEndHandle"] = hou.intFrame()
+ data["byFrameStep"] = 1.0
- data["frameStart"] = node.evalParm("f1")
- data["frameEnd"] = node.evalParm("f2")
- data["steps"] = node.evalParm("f3")
+ log.info(
+ "Node '{}' has 'Render current frame' set.\n"
+ "Asset Handles are ignored.\n"
+ "frameStart and frameEnd are set to the "
+ "current frame.".format(node.path())
+ )
+ else:
+ data["frameStartHandle"] = int(node.evalParm("f1"))
+ data["frameEndHandle"] = int(node.evalParm("f2"))
+ data["byFrameStep"] = node.evalParm("f3")
return data
@@ -769,6 +797,45 @@ def get_camera_from_container(container):
return cameras[0]
+def get_current_context_template_data_with_asset_data():
+ """
+ TODOs:
+ Support both 'assetData' and 'folderData' in future.
+ """
+
+ context = get_current_context()
+ project_name = context["project_name"]
+ asset_name = context["asset_name"]
+ task_name = context["task_name"]
+ host_name = get_current_host_name()
+
+ anatomy = Anatomy(project_name)
+ project_doc = get_project(project_name)
+ asset_doc = get_asset_by_name(project_name, asset_name)
+
+ # get context specific vars
+ asset_data = asset_doc["data"]
+
+ # compute `frameStartHandle` and `frameEndHandle`
+ frame_start = asset_data.get("frameStart")
+ frame_end = asset_data.get("frameEnd")
+ handle_start = asset_data.get("handleStart")
+ handle_end = asset_data.get("handleEnd")
+ if frame_start is not None and handle_start is not None:
+ asset_data["frameStartHandle"] = frame_start - handle_start
+
+ if frame_end is not None and handle_end is not None:
+ asset_data["frameEndHandle"] = frame_end + handle_end
+
+ template_data = get_template_data(
+ project_doc, asset_doc, task_name, host_name
+ )
+ template_data["root"] = anatomy.roots
+ template_data["assetData"] = asset_data
+
+ return template_data
+
+
def get_context_var_changes():
"""get context var changes."""
@@ -788,7 +855,7 @@ def get_context_var_changes():
return houdini_vars_to_update
# Get Template data
- template_data = get_current_context_template_data()
+ template_data = get_current_context_template_data_with_asset_data()
# Set Houdini Vars
for item in houdini_vars:
@@ -943,7 +1010,7 @@ def self_publish():
def add_self_publish_button(node):
"""Adds a self publish button to the rop node."""
- label = os.environ.get("AVALON_LABEL") or "OpenPype"
+ label = os.environ.get("AVALON_LABEL") or "AYON"
button_parm = hou.ButtonParmTemplate(
"ayon_self_publish",
diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py
index f8db45c56b..11135e20b2 100644
--- a/openpype/hosts/houdini/api/pipeline.py
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -3,7 +3,6 @@
import os
import sys
import logging
-import contextlib
import hou # noqa
@@ -66,10 +65,6 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
register_event_callback("open", on_open)
register_event_callback("new", on_new)
- pyblish.api.register_callback(
- "instanceToggled", on_pyblish_instance_toggled
- )
-
self._has_been_setup = True
# add houdini vendor packages
hou_pythonpath = os.path.join(HOUDINI_HOST_DIR, "vendor")
@@ -406,54 +401,3 @@ def _set_context_settings():
lib.reset_framerange()
lib.update_houdini_vars_context()
-
-
-def on_pyblish_instance_toggled(instance, new_value, old_value):
- """Toggle saver tool passthrough states on instance toggles."""
- @contextlib.contextmanager
- def main_take(no_update=True):
- """Enter root take during context"""
- original_take = hou.takes.currentTake()
- original_update_mode = hou.updateModeSetting()
- root = hou.takes.rootTake()
- has_changed = False
- try:
- if original_take != root:
- has_changed = True
- if no_update:
- hou.setUpdateMode(hou.updateMode.Manual)
- hou.takes.setCurrentTake(root)
- yield
- finally:
- if has_changed:
- if no_update:
- hou.setUpdateMode(original_update_mode)
- hou.takes.setCurrentTake(original_take)
-
- if not instance.data.get("_allowToggleBypass", True):
- return
-
- nodes = instance[:]
- if not nodes:
- return
-
- # Assume instance node is first node
- instance_node = nodes[0]
-
- if not hasattr(instance_node, "isBypassed"):
- # Likely not a node that can actually be bypassed
- log.debug("Can't bypass node: %s", instance_node.path())
- return
-
- if instance_node.isBypassed() != (not old_value):
- print("%s old bypass state didn't match old instance state, "
- "updating anyway.." % instance_node.path())
-
- try:
- # Go into the main take, because when in another take changing
- # the bypass state of a note cannot be done due to it being locked
- # by default.
- with main_take(no_update=True):
- instance_node.bypass(not new_value)
- except hou.PermissionError as exc:
- log.warning("%s - %s", instance_node.path(), exc)
diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py
index 4b5ebd4202..5093a90988 100644
--- a/openpype/hosts/houdini/api/shelves.py
+++ b/openpype/hosts/houdini/api/shelves.py
@@ -7,10 +7,11 @@ from openpype.settings import get_project_settings
from openpype.pipeline import get_current_project_name
from openpype.lib import StringTemplate
-from openpype.pipeline.context_tools import get_current_context_template_data
import hou
+from .lib import get_current_context_template_data_with_asset_data
+
log = logging.getLogger("openpype.hosts.houdini.shelves")
@@ -23,29 +24,33 @@ def generate_shelves():
# load configuration of houdini shelves
project_name = get_current_project_name()
project_settings = get_project_settings(project_name)
- shelves_set_config = project_settings["houdini"]["shelves"]
+ shelves_configs = project_settings["houdini"]["shelves"]
- if not shelves_set_config:
+ if not shelves_configs:
log.debug("No custom shelves found in project settings.")
return
# Get Template data
- template_data = get_current_context_template_data()
+ template_data = get_current_context_template_data_with_asset_data()
+
+ for config in shelves_configs:
+ selected_option = config["options"]
+ shelf_set_config = config[selected_option]
- for shelf_set_config in shelves_set_config:
shelf_set_filepath = shelf_set_config.get('shelf_set_source_path')
- shelf_set_os_filepath = shelf_set_filepath[current_os]
- if shelf_set_os_filepath:
- shelf_set_os_filepath = get_path_using_template_data(
- shelf_set_os_filepath, template_data
- )
- if not os.path.isfile(shelf_set_os_filepath):
- log.error("Shelf path doesn't exist - "
- "{}".format(shelf_set_os_filepath))
- continue
+ if shelf_set_filepath:
+ shelf_set_os_filepath = shelf_set_filepath[current_os]
+ if shelf_set_os_filepath:
+ shelf_set_os_filepath = get_path_using_template_data(
+ shelf_set_os_filepath, template_data
+ )
+ if not os.path.isfile(shelf_set_os_filepath):
+ log.error("Shelf path doesn't exist - "
+ "{}".format(shelf_set_os_filepath))
+ continue
- hou.shelves.newShelfSet(file_path=shelf_set_os_filepath)
- continue
+ hou.shelves.loadFile(shelf_set_os_filepath)
+ continue
shelf_set_name = shelf_set_config.get('shelf_set_name')
if not shelf_set_name:
diff --git a/openpype/hosts/houdini/plugins/create/create_bgeo.py b/openpype/hosts/houdini/plugins/create/create_bgeo.py
index a3f31e7e94..0f629cf9c9 100644
--- a/openpype/hosts/houdini/plugins/create/create_bgeo.py
+++ b/openpype/hosts/houdini/plugins/create/create_bgeo.py
@@ -3,6 +3,7 @@
from openpype.hosts.houdini.api import plugin
from openpype.pipeline import CreatedInstance, CreatorError
from openpype.lib import EnumDef
+import hou
class CreateBGEO(plugin.HoudiniCreator):
@@ -13,7 +14,6 @@ class CreateBGEO(plugin.HoudiniCreator):
icon = "gears"
def create(self, subset_name, instance_data, pre_create_data):
- import hou
instance_data.pop("active", None)
@@ -90,3 +90,9 @@ class CreateBGEO(plugin.HoudiniCreator):
return attrs + [
EnumDef("bgeo_type", bgeo_enum, label="BGEO Options"),
]
+
+ def get_network_categories(self):
+ return [
+ hou.ropNodeTypeCategory(),
+ hou.sopNodeTypeCategory()
+ ]
diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py
index 9d4f7969bb..52ea6fa054 100644
--- a/openpype/hosts/houdini/plugins/create/create_composite.py
+++ b/openpype/hosts/houdini/plugins/create/create_composite.py
@@ -45,6 +45,11 @@ class CreateCompositeSequence(plugin.HoudiniCreator):
instance_node.setParms(parms)
+ # Manually set f1 & f2 to $FSTART and $FEND respectively
+ # to match other Houdini nodes default.
+ instance_node.parm("f1").setExpression("$FSTART")
+ instance_node.parm("f2").setExpression("$FEND")
+
# Lock any parameters in this list
to_lock = ["prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)
diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py
index c4093bfbc6..ac075d2072 100644
--- a/openpype/hosts/houdini/plugins/create/create_hda.py
+++ b/openpype/hosts/houdini/plugins/create/create_hda.py
@@ -5,6 +5,7 @@ from openpype.client import (
get_subsets,
)
from openpype.hosts.houdini.api import plugin
+import hou
class CreateHDA(plugin.HoudiniCreator):
@@ -35,7 +36,6 @@ class CreateHDA(plugin.HoudiniCreator):
def create_instance_node(
self, node_name, parent, node_type="geometry"):
- import hou
parent_node = hou.node("/obj")
if self.selected_nodes:
@@ -81,3 +81,8 @@ class CreateHDA(plugin.HoudiniCreator):
pre_create_data) # type: plugin.CreatedInstance
return instance
+
+ def get_network_categories(self):
+ return [
+ hou.objNodeTypeCategory()
+ ]
diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py
index b814dd9d57..3a4ab7008b 100644
--- a/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py
+++ b/openpype/hosts/houdini/plugins/create/create_redshift_proxy.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating Redshift proxies."""
from openpype.hosts.houdini.api import plugin
-from openpype.pipeline import CreatedInstance
+import hou
class CreateRedshiftProxy(plugin.HoudiniCreator):
@@ -12,7 +12,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
icon = "magic"
def create(self, subset_name, instance_data, pre_create_data):
- import hou # noqa
+
# Remove the active, we are checking the bypass flag of the nodes
instance_data.pop("active", None)
@@ -28,7 +28,7 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
instance = super(CreateRedshiftProxy, self).create(
subset_name,
instance_data,
- pre_create_data) # type: CreatedInstance
+ pre_create_data)
instance_node = hou.node(instance.get("instance_node"))
@@ -44,3 +44,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator):
# Lock some Avalon attributes
to_lock = ["family", "id", "prim_to_detail_pattern"]
self.lock_parameters(instance_node, to_lock)
+
+ def get_network_categories(self):
+ return [
+ hou.ropNodeTypeCategory(),
+ hou.sopNodeTypeCategory()
+ ]
diff --git a/openpype/hosts/houdini/plugins/create/create_staticmesh.py b/openpype/hosts/houdini/plugins/create/create_staticmesh.py
index ea0b36f03f..d0985198bd 100644
--- a/openpype/hosts/houdini/plugins/create/create_staticmesh.py
+++ b/openpype/hosts/houdini/plugins/create/create_staticmesh.py
@@ -54,6 +54,7 @@ class CreateStaticMesh(plugin.HoudiniCreator):
def get_network_categories(self):
return [
hou.ropNodeTypeCategory(),
+ hou.objNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py
index 9c96e48e3a..69418f9575 100644
--- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py
+++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py
@@ -40,6 +40,7 @@ class CreateVDBCache(plugin.HoudiniCreator):
def get_network_categories(self):
return [
hou.ropNodeTypeCategory(),
+ hou.objNodeTypeCategory(),
hou.sopNodeTypeCategory()
]
diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py
index 663a93e48b..cff2b74e52 100644
--- a/openpype/hosts/houdini/plugins/load/load_image.py
+++ b/openpype/hosts/houdini/plugins/load/load_image.py
@@ -119,7 +119,8 @@ class ImageLoader(load.LoaderPlugin):
if not parent.children():
parent.destroy()
- def _get_file_sequence(self, root):
+ def _get_file_sequence(self, file_path):
+ root = os.path.dirname(file_path)
files = sorted(os.listdir(root))
first_fname = files[0]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py
index 43b8428c60..d95f763826 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py
@@ -20,7 +20,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Arnold ROP Render Products"
- order = pyblish.api.CollectorOrder + 0.4
+ # This specific order value is used so that
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["arnold_rop"]
@@ -126,8 +128,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
+
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
diff --git a/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py
new file mode 100644
index 0000000000..67a281639d
--- /dev/null
+++ b/openpype/hosts/houdini/plugins/publish/collect_asset_handles.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+"""Collector plugin for frames data on ROP instances."""
+import hou # noqa
+import pyblish.api
+from openpype.lib import BoolDef
+from openpype.pipeline import OpenPypePyblishPluginMixin
+
+
+class CollectAssetHandles(pyblish.api.InstancePlugin,
+ OpenPypePyblishPluginMixin):
+ """Apply asset handles.
+
+ If instance does not have:
+ - frameStart
+ - frameEnd
+ - handleStart
+ - handleEnd
+ But it does have:
+ - frameStartHandle
+ - frameEndHandle
+
+ Then we will retrieve the asset's handles to compute
+ the exclusive frame range and actual handle ranges.
+ """
+
+ hosts = ["houdini"]
+
+ # This specific order value is used so that
+ # this plugin runs after CollectAnatomyInstanceData
+ order = pyblish.api.CollectorOrder + 0.499
+
+ label = "Collect Asset Handles"
+ use_asset_handles = True
+
+ def process(self, instance):
+ # Only process instances without already existing handles data
+ # but that do have frameStartHandle and frameEndHandle defined
+ # like the data collected from CollectRopFrameRange
+ if "frameStartHandle" not in instance.data:
+ return
+ if "frameEndHandle" not in instance.data:
+ return
+
+ has_existing_data = {
+ "handleStart",
+ "handleEnd",
+ "frameStart",
+ "frameEnd"
+ }.issubset(instance.data)
+ if has_existing_data:
+ return
+
+ attr_values = self.get_attr_values_from_data(instance.data)
+ if attr_values.get("use_handles", self.use_asset_handles):
+ asset_data = instance.data["assetEntity"]["data"]
+ handle_start = asset_data.get("handleStart", 0)
+ handle_end = asset_data.get("handleEnd", 0)
+ else:
+ handle_start = 0
+ handle_end = 0
+
+ frame_start = instance.data["frameStartHandle"] + handle_start
+ frame_end = instance.data["frameEndHandle"] - handle_end
+
+ instance.data.update({
+ "handleStart": handle_start,
+ "handleEnd": handle_end,
+ "frameStart": frame_start,
+ "frameEnd": frame_end
+ })
+
+ # Log debug message about the collected frame range
+ if attr_values.get("use_handles", self.use_asset_handles):
+ self.log.debug(
+ "Full Frame range with Handles "
+ "[{frame_start_handle} - {frame_end_handle}]"
+ .format(
+ frame_start_handle=instance.data["frameStartHandle"],
+ frame_end_handle=instance.data["frameEndHandle"]
+ )
+ )
+ else:
+ self.log.debug(
+ "Use handles is deactivated for this instance, "
+ "start and end handles are set to 0."
+ )
+
+ # Log collected frame range to the user
+ message = "Frame range [{frame_start} - {frame_end}]".format(
+ frame_start=frame_start,
+ frame_end=frame_end
+ )
+ if handle_start or handle_end:
+ message += " with handles [{handle_start}]-[{handle_end}]".format(
+ handle_start=handle_start,
+ handle_end=handle_end
+ )
+ self.log.info(message)
+
+ if instance.data.get("byFrameStep", 1.0) != 1.0:
+ self.log.info(
+ "Frame steps {}".format(instance.data["byFrameStep"]))
+
+ # Add frame range to label if the instance has a frame range.
+ label = instance.data.get("label", instance.data["name"])
+ instance.data["label"] = (
+ "{label} [{frame_start_handle} - {frame_end_handle}]"
+ .format(
+ label=label,
+ frame_start_handle=instance.data["frameStartHandle"],
+ frame_end_handle=instance.data["frameEndHandle"]
+ )
+ )
+
+ @classmethod
+ def get_attribute_defs(cls):
+ return [
+ BoolDef("use_handles",
+ tooltip="Disable this if you want the publisher to"
+ " ignore start and end handles specified in the"
+ " asset data for this publish instance",
+ default=cls.use_asset_handles,
+ label="Use asset handles")
+ ]
diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py
index 01df809d4c..cdef642174 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_frames.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py
@@ -11,7 +11,9 @@ from openpype.hosts.houdini.api import lib
class CollectFrames(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
- order = pyblish.api.CollectorOrder + 0.01
+ # This specific order value is used so that
+ # this plugin runs after CollectRopFrameRange
+ order = pyblish.api.CollectorOrder + 0.1
label = "Collect Frames"
families = ["vdbcache", "imagesequence", "ass",
"redshiftproxy", "review", "bgeo"]
@@ -20,8 +22,8 @@ class CollectFrames(pyblish.api.InstancePlugin):
ropnode = hou.node(instance.data["instance_node"])
- start_frame = instance.data.get("frameStart", None)
- end_frame = instance.data.get("frameEnd", None)
+ start_frame = instance.data.get("frameStartHandle", None)
+ end_frame = instance.data.get("frameEndHandle", None)
output_parm = lib.get_output_parameter(ropnode)
if start_frame is not None:
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py
deleted file mode 100644
index 584343cd64..0000000000
--- a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import hou
-
-import pyblish.api
-
-
-class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin):
- """Collect time range frame data for the instance node."""
-
- order = pyblish.api.CollectorOrder + 0.001
- label = "Instance Node Frame Range"
- hosts = ["houdini"]
-
- def process(self, instance):
-
- node_path = instance.data.get("instance_node")
- node = hou.node(node_path) if node_path else None
- if not node_path or not node:
- self.log.debug("No instance node found for instance: "
- "{}".format(instance))
- return
-
- frame_data = self.get_frame_data(node)
- if not frame_data:
- return
-
- self.log.info("Collected time data: {}".format(frame_data))
- instance.data.update(frame_data)
-
- def get_frame_data(self, node):
- """Get the frame data: start frame, end frame and steps
- Args:
- node(hou.Node)
-
- Returns:
- dict
-
- """
-
- data = {}
-
- if node.parm("trange") is None:
- self.log.debug("Node has no 'trange' parameter: "
- "{}".format(node.path()))
- return data
-
- if node.evalParm("trange") == 0:
- # Ignore 'render current frame'
- self.log.debug("Node '{}' has 'Render current frame' set. "
- "Time range data ignored.".format(node.path()))
- return data
-
- data["frameStart"] = node.evalParm("f1")
- data["frameEnd"] = node.evalParm("f2")
- data["byFrameStep"] = node.evalParm("f3")
-
- return data
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py
index 3772c9e705..52966fb3c2 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py
@@ -91,27 +91,3 @@ class CollectInstances(pyblish.api.ContextPlugin):
context[:] = sorted(context, key=sort_by_family)
return context
-
- def get_frame_data(self, node):
- """Get the frame data: start frame, end frame and steps
- Args:
- node(hou.Node)
-
- Returns:
- dict
-
- """
-
- data = {}
-
- if node.parm("trange") is None:
- return data
-
- if node.evalParm("trange") == 0:
- return data
-
- data["frameStart"] = node.evalParm("f1")
- data["frameEnd"] = node.evalParm("f2")
- data["byFrameStep"] = node.evalParm("f3")
-
- return data
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
index 0600730d00..d154cdc7c0 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
@@ -122,10 +122,6 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin):
instance.data.update(save_data)
instance.data["usdLayer"] = layer
- # Don't allow the Pyblish `instanceToggled` we have installed
- # to set this node to bypass.
- instance.data["_allowToggleBypass"] = False
-
instances.append(instance)
# Store the collected ROP node dependencies
diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py
index eabb1128d8..dac350a6ef 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py
@@ -24,7 +24,9 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Karma ROP Render Products"
- order = pyblish.api.CollectorOrder + 0.4
+ # This specific order value is used so that
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["karma_rop"]
@@ -95,8 +97,9 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
+
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py
index c4460f5350..a3e7927807 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py
@@ -24,7 +24,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Mantra ROP Render Products"
- order = pyblish.api.CollectorOrder + 0.4
+ # This specific order value is used so that
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["mantra_rop"]
@@ -118,8 +120,9 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
+
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py
index dbb15ab88f..0acddab011 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py
@@ -24,7 +24,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "Redshift ROP Render Products"
- order = pyblish.api.CollectorOrder + 0.4
+ # This specific order value is used so that
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["redshift_rop"]
@@ -132,8 +134,9 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
+
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py
index 3efb75e66c..9671945b9a 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py
@@ -6,6 +6,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
"""Collect Review Data."""
label = "Collect Review Data"
+ # This specific order value is used so that
+ # this plugin runs after CollectRopFrameRange
order = pyblish.api.CollectorOrder + 0.1
hosts = ["houdini"]
families = ["review"]
@@ -41,8 +43,8 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin):
return
if focal_length_parm.isTimeDependent():
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"] + 1
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"] + 1
focal_length = [
focal_length_parm.evalAsFloatAtFrame(t)
for t in range(int(start), int(end))
diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py
index 2a6be6b9f1..1e6bc3b16e 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py
@@ -8,6 +8,7 @@ from openpype.hosts.houdini.api import lib
class CollectRopFrameRange(pyblish.api.InstancePlugin):
"""Collect all frames which would be saved from the ROP nodes"""
+ hosts = ["houdini"]
order = pyblish.api.CollectorOrder
label = "Collect RopNode Frame Range"
@@ -16,26 +17,22 @@ class CollectRopFrameRange(pyblish.api.InstancePlugin):
node_path = instance.data.get("instance_node")
if node_path is None:
# Instance without instance node like a workfile instance
+ self.log.debug(
+ "No instance node found for instance: {}".format(instance)
+ )
return
ropnode = hou.node(node_path)
- frame_data = lib.get_frame_data(ropnode)
+ frame_data = lib.get_frame_data(
+ ropnode, self.log
+ )
- if "frameStart" in frame_data and "frameEnd" in frame_data:
+ if not frame_data:
+ return
- # Log artist friendly message about the collected frame range
- message = (
- "Frame range {0[frameStart]} - {0[frameEnd]}"
- ).format(frame_data)
- if frame_data.get("step", 1.0) != 1.0:
- message += " with step {0[step]}".format(frame_data)
- self.log.info(message)
+ # Log debug message about the collected frame range
+ self.log.debug(
+ "Collected frame_data: {}".format(frame_data)
+ )
- instance.data.update(frame_data)
-
- # Add frame range to label if the instance has a frame range.
- label = instance.data.get("label", instance.data["name"])
- instance.data["label"] = (
- "{0} [{1[frameStart]} - {1[frameEnd]}]".format(label,
- frame_data)
- )
+ instance.data.update(frame_data)
diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
index 277f922ba4..64de2079cd 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py
@@ -24,7 +24,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
"""
label = "VRay ROP Render Products"
- order = pyblish.api.CollectorOrder + 0.4
+ # This specific order value is used so that
+ # this plugin runs after CollectFrames
+ order = pyblish.api.CollectorOrder + 0.11
hosts = ["houdini"]
families = ["vray_rop"]
@@ -115,8 +117,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin):
return path
expected_files = []
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
+
for i in range(int(start), (int(end) + 1)):
expected_files.append(
os.path.join(dir, (file % i)).replace("\\", "/"))
diff --git a/openpype/hosts/houdini/plugins/publish/extract_ass.py b/openpype/hosts/houdini/plugins/publish/extract_ass.py
index 0d246625ba..be60217055 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_ass.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_ass.py
@@ -56,7 +56,7 @@ class ExtractAss(publish.Extractor):
'ext': ext,
"files": files,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py
index c9625ec880..d13141b426 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_bgeo.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_bgeo.py
@@ -47,7 +47,7 @@ class ExtractBGEO(publish.Extractor):
"ext": ext.lstrip("."),
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"]
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"]
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/extract_composite.py b/openpype/hosts/houdini/plugins/publish/extract_composite.py
index 7a1ab36b93..11cf83a46d 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_composite.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_composite.py
@@ -41,8 +41,8 @@ class ExtractComposite(publish.Extractor):
"ext": ext,
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
}
from pprint import pformat
diff --git a/openpype/hosts/houdini/plugins/publish/extract_fbx.py b/openpype/hosts/houdini/plugins/publish/extract_fbx.py
index 7993b3352f..7dc193c6a9 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_fbx.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_fbx.py
@@ -40,9 +40,9 @@ class ExtractFBX(publish.Extractor):
}
# 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"]
+ if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa
+ representation["frameStart"] = instance.data["frameStartHandle"]
+ representation["frameEnd"] = instance.data["frameEndHandle"]
# set value type for 'representations' key to list
if "representations" not in instance.data:
diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py
index 6c36dec5f5..38808089ac 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py
@@ -39,8 +39,8 @@ class ExtractOpenGL(publish.Extractor):
"ext": instance.data["imageFormat"],
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
"tags": tags,
"preview": True,
"camera_name": instance.data.get("review_camera")
diff --git a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py
index 1d99ac665c..ef5991924f 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_redshift_proxy.py
@@ -44,8 +44,8 @@ class ExtractRedshiftProxy(publish.Extractor):
}
# 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"]
+ if "frameStartHandle" in instance.data and "frameEndHandle" in instance.data: # noqa
+ representation["frameStart"] = instance.data["frameStartHandle"]
+ representation["frameEnd"] = instance.data["frameEndHandle"]
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py
index 4bca758f08..89af8e1756 100644
--- a/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py
+++ b/openpype/hosts/houdini/plugins/publish/extract_vdb_cache.py
@@ -40,7 +40,7 @@ class ExtractVDBCache(publish.Extractor):
"ext": "vdb",
"files": output,
"stagingDir": staging_dir,
- "frameStart": instance.data["frameStart"],
- "frameEnd": instance.data["frameEnd"],
+ "frameStart": instance.data["frameStartHandle"],
+ "frameEnd": instance.data["frameEndHandle"],
}
instance.data["representations"].append(representation)
diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py
new file mode 100644
index 0000000000..1b12fa7096
--- /dev/null
+++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+import pyblish.api
+from openpype.pipeline import PublishValidationError
+from openpype.pipeline.publish import RepairAction
+from openpype.hosts.houdini.api.action import SelectInvalidAction
+
+import hou
+
+
+class DisableUseAssetHandlesAction(RepairAction):
+ label = "Disable use asset handles"
+ icon = "mdi.toggle-switch-off"
+
+
+class ValidateFrameRange(pyblish.api.InstancePlugin):
+ """Validate Frame Range.
+
+ Due to the usage of start and end handles,
+ then Frame Range must be >= (start handle + end handle)
+ which results that frameEnd be smaller than frameStart
+ """
+
+ order = pyblish.api.ValidatorOrder - 0.1
+ hosts = ["houdini"]
+ label = "Validate Frame Range"
+ actions = [DisableUseAssetHandlesAction, SelectInvalidAction]
+
+ def process(self, instance):
+
+ invalid = self.get_invalid(instance)
+ if invalid:
+ raise PublishValidationError(
+ title="Invalid Frame Range",
+ message=(
+ "Invalid frame range because the instance "
+ "start frame ({0[frameStart]}) is higher than "
+ "the end frame ({0[frameEnd]})"
+ .format(instance.data)
+ ),
+ description=(
+ "## Invalid Frame Range\n"
+ "The frame range for the instance is invalid because "
+ "the start frame is higher than the end frame.\n\nThis "
+ "is likely due to asset handles being applied to your "
+ "instance or the ROP node's start frame "
+ "is set higher than the end frame.\n\nIf your ROP frame "
+ "range is correct and you do not want to apply asset "
+ "handles make sure to disable Use asset handles on the "
+ "publish instance."
+ )
+ )
+
+ @classmethod
+ def get_invalid(cls, instance):
+
+ if not instance.data.get("instance_node"):
+ return
+
+ rop_node = hou.node(instance.data["instance_node"])
+ frame_start = instance.data.get("frameStart")
+ frame_end = instance.data.get("frameEnd")
+
+ if frame_start is None or frame_end is None:
+ cls.log.debug(
+ "Skipping frame range validation for "
+ "instance without frame data: {}".format(rop_node.path())
+ )
+ return
+
+ if frame_start > frame_end:
+ cls.log.info(
+ "The ROP node render range is set to "
+ "{0[frameStartHandle]} - {0[frameEndHandle]} "
+ "The asset handles applied to the instance are start handle "
+ "{0[handleStart]} and end handle {0[handleEnd]}"
+ .format(instance.data)
+ )
+ return [rop_node]
+
+ @classmethod
+ def repair(cls, instance):
+
+ if not cls.get_invalid(instance):
+ # Already fixed
+ return
+
+ # Disable use asset handles
+ context = instance.context
+ create_context = context.data["create_context"]
+ instance_id = instance.data.get("instance_id")
+ if not instance_id:
+ cls.log.debug("'{}' must have instance id"
+ .format(instance))
+ return
+
+ created_instance = create_context.get_instance_by_id(instance_id)
+ if not instance_id:
+ cls.log.debug("Unable to find instance '{}' by id"
+ .format(instance))
+ return
+
+ created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa
+
+ create_context.save_changes()
+ cls.log.debug("use asset handles is turned off for '{}'"
+ .format(instance))
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index b2e32a70f9..0903aef7bc 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -4,7 +4,7 @@