mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
Merge branch 'develop' into enhancement/OP-7120-blender_output-node-exr
This commit is contained in:
commit
9f5ea0f106
75 changed files with 2428 additions and 816 deletions
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
16
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,14 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.18.3
|
||||
- 3.18.3-nightly.2
|
||||
- 3.18.3-nightly.1
|
||||
- 3.18.2
|
||||
- 3.18.2-nightly.6
|
||||
- 3.18.2-nightly.5
|
||||
- 3.18.2-nightly.4
|
||||
- 3.18.2-nightly.3
|
||||
- 3.18.2-nightly.2
|
||||
- 3.18.2-nightly.1
|
||||
- 3.18.1
|
||||
|
|
@ -127,14 +135,6 @@ body:
|
|||
- 3.15.7-nightly.1
|
||||
- 3.15.6
|
||||
- 3.15.6-nightly.3
|
||||
- 3.15.6-nightly.2
|
||||
- 3.15.6-nightly.1
|
||||
- 3.15.5
|
||||
- 3.15.5-nightly.2
|
||||
- 3.15.5-nightly.1
|
||||
- 3.15.4
|
||||
- 3.15.4-nightly.3
|
||||
- 3.15.4-nightly.2
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
573
CHANGELOG.md
573
CHANGELOG.md
|
|
@ -1,6 +1,579 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.18.3](https://github.com/ynput/OpenPype/tree/3.18.3)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.2...3.18.3)
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Apply initial viewport shader for Redshift Proxy after loading <a href="https://github.com/ynput/OpenPype/pull/6102">#6102</a></summary>
|
||||
|
||||
When the published redshift proxy is being loaded, the shader of the proxy is missing. This is different from the manual load through creating redshift proxy for files. This PR is to assign the default lambert to the redshift proxy, which replicates the same approach when the user manually loads the proxy with filepath.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: We should keep current subset version when we switch only the representation type <a href="https://github.com/ynput/OpenPype/pull/4629">#4629</a></summary>
|
||||
|
||||
When we switch only the representation type of subsets, we should not get the representation from the last version of the subset.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: Add loader for redshift proxy family <a href="https://github.com/ynput/OpenPype/pull/5948">#5948</a></summary>
|
||||
|
||||
Loader for Redshift Proxy in Houdini (Thanks for @BigRoy contribution)
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AfterEffects: exposing Deadline pools fields in Publisher UI <a href="https://github.com/ynput/OpenPype/pull/6079">#6079</a></summary>
|
||||
|
||||
Deadline pools might be adhoc set by an artist during publishing. AfterEffects implementation wasn't providing this.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Event callbacks can have order <a href="https://github.com/ynput/OpenPype/pull/6080">#6080</a></summary>
|
||||
|
||||
Event callbacks can have order in which are called, and fixed issue with getting function name and file when using `partial` function as callback.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: OpenPype addon defines runtime dependencies <a href="https://github.com/ynput/OpenPype/pull/6095">#6095</a></summary>
|
||||
|
||||
Moved runtime dependencies from ayon-launcher to openpype addon.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: User's setting for scene unit scale <a href="https://github.com/ynput/OpenPype/pull/6097">#6097</a></summary>
|
||||
|
||||
Options for users to set the default scene unit scale for their scenes.AYONLegacy OP
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Remove deprecated templates profiles <a href="https://github.com/ynput/OpenPype/pull/6103">#6103</a></summary>
|
||||
|
||||
Remove deprecated usage of template profiles from settings.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Window is not always on top <a href="https://github.com/ynput/OpenPype/pull/6107">#6107</a></summary>
|
||||
|
||||
Goal of this PR is to avoid using `WindowStaysOnTopHint` which causes issues, especially in cases when DCC shows a popup dialog that is behind the window, in that case both Publisher and DCC are frozen and there is nothing to do.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: add split job export support for Redshift ROP <a href="https://github.com/ynput/OpenPype/pull/6108">#6108</a></summary>
|
||||
|
||||
This is adding support for splitting of export and render jobs for Redshift as is already implemented for Vray, Mantra and Arnold.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: automatic installation of PySide2 <a href="https://github.com/ynput/OpenPype/pull/6111">#6111</a></summary>
|
||||
|
||||
This PR adds hook which tries to check if PySide2 is installed in Python used by Fusion and if not, it tries to install it automatically.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: OpenPype addon dependencies <a href="https://github.com/ynput/OpenPype/pull/6113">#6113</a></summary>
|
||||
|
||||
Added `click` and `six` to requirements of openpype addon, and removed `Qt.py` requirement, which is not used anywhere.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Thumbnail representation has 'outputName' <a href="https://github.com/ynput/OpenPype/pull/6114">#6114</a></summary>
|
||||
|
||||
Add thumbnail output name to thumbnail representation to prevent same output filename during integration.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Kitsu: Clear credentials is safe <a href="https://github.com/ynput/OpenPype/pull/6116">#6116</a></summary>
|
||||
|
||||
Do not remove not existing keyring items.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: bug fix the playblast without textures <a href="https://github.com/ynput/OpenPype/pull/5942">#5942</a></summary>
|
||||
|
||||
Bug fix the texture not being displayed when users enable texture placement in the OP/AYON setting
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Workfile instance update fix <a href="https://github.com/ynput/OpenPype/pull/6048">#6048</a></summary>
|
||||
|
||||
Make sure workfile instance has always available 'instance_node' in transient data.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Fix issue with parenting of widgets <a href="https://github.com/ynput/OpenPype/pull/6106">#6106</a></summary>
|
||||
|
||||
Don't use publisher window parent (usually main DCC window) as parent for report widget.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>:wrench: fix and update pydocstyle configuration <a href="https://github.com/ynput/OpenPype/pull/6109">#6109</a></summary>
|
||||
|
||||
Fix pydocstyle configuration and move it to `pyproject.toml`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Create camera node with the latest camera node class in Nuke 14 <a href="https://github.com/ynput/OpenPype/pull/6118">#6118</a></summary>
|
||||
|
||||
Creating instance fails for certain cameras, and it seems to only exist in Nuke 14. The reason of causing that contributes to the new camera node class `Camera4` while the camera creator is working with the `Camera2` class.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Site Sync: small fixes in Loader <a href="https://github.com/ynput/OpenPype/pull/6119">#6119</a></summary>
|
||||
|
||||
Resolves issue:
|
||||
- local and studio icons were same, they should be different
|
||||
- `TypeError: string indices must be integers` error when downloading/uploading workfiles
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Template data for editorial publishing <a href="https://github.com/ynput/OpenPype/pull/6120">#6120</a></summary>
|
||||
|
||||
Template data for editorial publishing are filled during `CollectInstanceAnatomyData`. The structure for editorial is determined, as it's required for ExtractHierarchy AYON/OpenPype plugins.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>SceneInventory: Fix site sync icon conversion <a href="https://github.com/ynput/OpenPype/pull/6123">#6123</a></summary>
|
||||
|
||||
Use 'get_qt_icon' to convert icon definitions from site sync.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.18.2](https://github.com/ynput/OpenPype/tree/3.18.2)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.1...3.18.2)
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Testing: Release Maya/Deadline job from pending when testing. <a href="https://github.com/ynput/OpenPype/pull/5988">#5988</a></summary>
|
||||
|
||||
When testing we wont put the Deadline jobs into pending with dependencies, so the worker can start as soon as possible.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Tweaks on Extractions for the exporters <a href="https://github.com/ynput/OpenPype/pull/5814">#5814</a></summary>
|
||||
|
||||
With this PR
|
||||
- Suspend Refresh would be introduced in abc & obj extractors for optimization.
|
||||
- Allow users to choose the custom attributes to be included in abc exports
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Optional preserve references. <a href="https://github.com/ynput/OpenPype/pull/5994">#5994</a></summary>
|
||||
|
||||
Optional preserve references when publishing Maya scenes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON ftrack: Expect 'ayon' group in custom attributes <a href="https://github.com/ynput/OpenPype/pull/6066">#6066</a></summary>
|
||||
|
||||
Expect `ayon` group as one of options to get custom attributes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Chore: Remove dependencies related to separated addons <a href="https://github.com/ynput/OpenPype/pull/6074">#6074</a></summary>
|
||||
|
||||
Removed dependencies from openpype client pyproject.toml that are already defined by addons which require them.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Editorial & chore: Stop using pathlib2 <a href="https://github.com/ynput/OpenPype/pull/6075">#6075</a></summary>
|
||||
|
||||
Do not use `pathlib2` which is Python 2 backport for `pathlib` module in python 3.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Traypublisher: Correct validator label <a href="https://github.com/ynput/OpenPype/pull/6084">#6084</a></summary>
|
||||
|
||||
Use correct label for Validate filepaths.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Extract Review Intermediate disabled when both Extract Review Mov and Extract Review Intermediate disabled in setting <a href="https://github.com/ynput/OpenPype/pull/6089">#6089</a></summary>
|
||||
|
||||
Report in Discord https://discord.com/channels/517362899170230292/563751989075378201/1187874498234556477
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Bug fix the file from texture node not being collected correctly in Yeti Rig <a href="https://github.com/ynput/OpenPype/pull/5990">#5990</a></summary>
|
||||
|
||||
Fix the bug of collect Yeti Rig not being able to get the file parameter(s) from the texture node(s), resulting to the failure of publishing the textures to the resource directory.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bug: fix AYON settings for Maya workspace <a href="https://github.com/ynput/OpenPype/pull/6069">#6069</a></summary>
|
||||
|
||||
This is changing bug in default AYON setting for Maya workspace, where missing semicolumn caused workspace not being set. This is also syncing default workspace settings to OpenPype
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Refactor colorspace handling in CollectColorspace plugin <a href="https://github.com/ynput/OpenPype/pull/6033">#6033</a></summary>
|
||||
|
||||
Traypublisher is now capable set available colorspaces or roles to publishing images sequence or video. This is fix of new implementation where we allowed to use roles in the enumerator selector.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: Houdini render split bugs <a href="https://github.com/ynput/OpenPype/pull/6037">#6037</a></summary>
|
||||
|
||||
This PR is a follow up PR to https://github.com/ynput/OpenPype/pull/5420This PR does:
|
||||
- refactor `get_output_parameter` to what is used to be.
|
||||
- fix a bug with split render
|
||||
- rename `exportJob` flag to `split_render`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: fix for single frame rendering <a href="https://github.com/ynput/OpenPype/pull/6056">#6056</a></summary>
|
||||
|
||||
Fixes publishes of single frame of `render` product type.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Photoshop: fix layer publish thumbnail missing in loader <a href="https://github.com/ynput/OpenPype/pull/6061">#6061</a></summary>
|
||||
|
||||
Thumbnails from any products (either `review` nor separate layer instances) weren't stored in Ayon.This resulted in not showing them in Loader and Server UI. After this PR thumbnails should be shown in the Loader and on the Server (`http://YOUR_AYON_HOSTNAME:5000/projects/YOUR_PROJECT/browser`).
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Chore: Do not use thumbnailSource for thumbnail integration <a href="https://github.com/ynput/OpenPype/pull/6063">#6063</a></summary>
|
||||
|
||||
Do not use `thumbnailSource` for thumbnail integration.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Photoshop: fix creation of .mov <a href="https://github.com/ynput/OpenPype/pull/6064">#6064</a></summary>
|
||||
|
||||
Generation of .mov file with 1 frame per published layer was failing.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Photoshop: fix Collect Color Coded settings <a href="https://github.com/ynput/OpenPype/pull/6065">#6065</a></summary>
|
||||
|
||||
Fix for wrong default value for `Collect Color Coded Instances` Settings
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bug: Fix Publisher parent window in Nuke <a href="https://github.com/ynput/OpenPype/pull/6067">#6067</a></summary>
|
||||
|
||||
Fixing issue where publisher parent window wasn't set because wrong use of version constant.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Python console widget: Save registry fix <a href="https://github.com/ynput/OpenPype/pull/6076">#6076</a></summary>
|
||||
|
||||
Do not save registry until there is something to save.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Ftrack: update asset names for multiple reviewable items <a href="https://github.com/ynput/OpenPype/pull/6077">#6077</a></summary>
|
||||
|
||||
Multiple reviewable assetVersion components with better grouping to asset version name.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Ftrack: DJV action fixes <a href="https://github.com/ynput/OpenPype/pull/6098">#6098</a></summary>
|
||||
|
||||
Fix bugs in DJV ftrack action.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON Workfiles tool: Fix arrow to timezone typo <a href="https://github.com/ynput/OpenPype/pull/6099">#6099</a></summary>
|
||||
|
||||
Fix parenthesis typo with arrow local timezone function.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🔀 Refactored code**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Update folder-favorite icon to ayon icon <a href="https://github.com/ynput/OpenPype/pull/5718">#5718</a></summary>
|
||||
|
||||
Updates old "Pype-2.0-era" (from ancient greece times) to AYON logo equivalent.I believe it's only used in Nuke.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **Merged pull requests**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Maya / Nuke remove publish gui filters from settings <a href="https://github.com/ynput/OpenPype/pull/5570">#5570</a></summary>
|
||||
|
||||
- Remove Publish GUI Filters from Nuke settings
|
||||
- Remove Publish GUI Filters from Maya settings
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: Project/User option for output format (create_saver) <a href="https://github.com/ynput/OpenPype/pull/6045">#6045</a></summary>
|
||||
|
||||
Adds "Output Image Format" option which can be set via project settings and overwritten by users in "Create" menu. This replaces the current behaviour of being hardcoded to "exr". Replacing the need for people to manually edit the saver path if they require a different extension.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: Output Image Format Updating Instances (create_saver) <a href="https://github.com/ynput/OpenPype/pull/6060">#6060</a></summary>
|
||||
|
||||
Adds the ability to update Saver image output format if changed in the Publish UI.~~Adds an optional validator that compares "Output Image Format" in the Publish menu against the one currently found on the saver. It then offers a repair action to update the output extension on the saver.~~
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Tests: Fix representation count for AE legacy test <a href="https://github.com/ynput/OpenPype/pull/6072">#6072</a></summary>
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.18.1](https://github.com/ynput/OpenPype/tree/3.18.1)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -124,23 +124,24 @@ def get_linked_representation_id(
|
|||
if not versions_to_check:
|
||||
break
|
||||
|
||||
links = con.get_versions_links(
|
||||
versions_links = con.get_versions_links(
|
||||
project_name,
|
||||
versions_to_check,
|
||||
link_types=link_types,
|
||||
link_direction="out")
|
||||
|
||||
versions_to_check = set()
|
||||
for link in links:
|
||||
# Care only about version links
|
||||
if link["entityType"] != "version":
|
||||
continue
|
||||
entity_id = link["entityId"]
|
||||
# Skip already found linked version ids
|
||||
if entity_id in linked_version_ids:
|
||||
continue
|
||||
linked_version_ids.add(entity_id)
|
||||
versions_to_check.add(entity_id)
|
||||
for links in versions_links.values():
|
||||
for link in links:
|
||||
# Care only about version links
|
||||
if link["entityType"] != "version":
|
||||
continue
|
||||
entity_id = link["entityId"]
|
||||
# Skip already found linked version ids
|
||||
if entity_id in linked_version_ids:
|
||||
continue
|
||||
linked_version_ids.add(entity_id)
|
||||
versions_to_check.add(entity_id)
|
||||
|
||||
linked_version_ids.remove(version_id)
|
||||
if not linked_version_ids:
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ def prepare_scene_name(
|
|||
if namespace:
|
||||
name = f"{name}_{namespace}"
|
||||
name = f"{name}_{subset}"
|
||||
|
||||
# Blender name for a collection or object cannot be longer than 63
|
||||
# characters. If the name is longer, it will raise an error.
|
||||
if len(name) > 63:
|
||||
raise ValueError(f"Scene name '{name}' would be too long.")
|
||||
|
||||
return name
|
||||
|
||||
|
||||
|
|
@ -226,7 +232,7 @@ class BaseCreator(Creator):
|
|||
|
||||
# Create asset group
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = instance_data["folderPath"]
|
||||
asset_name = instance_data["folderPath"].split("/")[-1]
|
||||
else:
|
||||
asset_name = instance_data["asset"]
|
||||
|
||||
|
|
@ -305,12 +311,16 @@ class BaseCreator(Creator):
|
|||
)
|
||||
return
|
||||
|
||||
# Rename the instance node in the scene if subset or asset changed
|
||||
# Rename the instance node in the scene if subset or asset changed.
|
||||
# Do not rename the instance if the family is workfile, as the
|
||||
# workfile instance is included in the AVALON_CONTAINER collection.
|
||||
if (
|
||||
"subset" in changes.changed_keys
|
||||
or asset_name_key in changes.changed_keys
|
||||
):
|
||||
) and created_instance.family != "workfile":
|
||||
asset_name = data[asset_name_key]
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = asset_name.split("/")[-1]
|
||||
name = prepare_scene_name(
|
||||
asset=asset_name, subset=data["subset"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class CreateWorkfile(BaseCreator, AutoCreator):
|
|||
|
||||
def create(self):
|
||||
"""Create workfile instances."""
|
||||
existing_instance = next(
|
||||
workfile_instance = next(
|
||||
(
|
||||
instance for instance in self.create_context.instances
|
||||
if instance.creator_identifier == self.identifier
|
||||
|
|
@ -39,14 +39,14 @@ class CreateWorkfile(BaseCreator, AutoCreator):
|
|||
host_name = self.create_context.host_name
|
||||
|
||||
existing_asset_name = None
|
||||
if existing_instance is not None:
|
||||
if workfile_instance is not None:
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_asset_name = existing_instance.get("folderPath")
|
||||
existing_asset_name = workfile_instance.get("folderPath")
|
||||
|
||||
if existing_asset_name is None:
|
||||
existing_asset_name = existing_instance["asset"]
|
||||
existing_asset_name = workfile_instance["asset"]
|
||||
|
||||
if not existing_instance:
|
||||
if not workfile_instance:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
task_name, task_name, asset_doc, project_name, host_name
|
||||
|
|
@ -66,19 +66,18 @@ class CreateWorkfile(BaseCreator, AutoCreator):
|
|||
asset_doc,
|
||||
project_name,
|
||||
host_name,
|
||||
existing_instance,
|
||||
workfile_instance,
|
||||
)
|
||||
)
|
||||
self.log.info("Auto-creating workfile instance...")
|
||||
current_instance = CreatedInstance(
|
||||
workfile_instance = CreatedInstance(
|
||||
self.family, subset_name, data, self
|
||||
)
|
||||
instance_node = bpy.data.collections.get(AVALON_CONTAINERS, {})
|
||||
current_instance.transient_data["instance_node"] = instance_node
|
||||
self._add_instance_to_context(current_instance)
|
||||
self._add_instance_to_context(workfile_instance)
|
||||
|
||||
elif (
|
||||
existing_asset_name != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
or workfile_instance["task"] != task_name
|
||||
):
|
||||
# Update instance context if it's different
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
|
@ -86,12 +85,17 @@ class CreateWorkfile(BaseCreator, AutoCreator):
|
|||
task_name, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
workfile_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
workfile_instance["asset"] = asset_name
|
||||
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
workfile_instance["task"] = task_name
|
||||
workfile_instance["subset"] = subset_name
|
||||
|
||||
instance_node = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not instance_node:
|
||||
instance_node = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
workfile_instance.transient_data["instance_node"] = instance_node
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -61,5 +61,10 @@ class BlendAnimationLoader(plugin.AssetLoader):
|
|||
|
||||
bpy.data.objects.remove(container)
|
||||
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
filename = bpy.path.basename(libpath)
|
||||
# Blender has a limit of 63 characters for any data name.
|
||||
# If the filename is longer, it will be truncated.
|
||||
if len(filename) > 63:
|
||||
filename = filename[:63]
|
||||
library = bpy.data.libraries.get(filename)
|
||||
bpy.data.libraries.remove(library)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,12 @@ class BlendLoader(plugin.AssetLoader):
|
|||
bpy.context.scene.collection.objects.link(obj)
|
||||
|
||||
# Remove the library from the blend file
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
filepath = bpy.path.basename(libpath)
|
||||
# Blender has a limit of 63 characters for any data name.
|
||||
# If the filepath is longer, it will be truncated.
|
||||
if len(filepath) > 63:
|
||||
filepath = filepath[:63]
|
||||
library = bpy.data.libraries.get(filepath)
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
return container, members
|
||||
|
|
|
|||
|
|
@ -60,7 +60,12 @@ class BlendSceneLoader(plugin.AssetLoader):
|
|||
bpy.context.scene.collection.children.link(container)
|
||||
|
||||
# Remove the library from the blend file
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
filepath = bpy.path.basename(libpath)
|
||||
# Blender has a limit of 63 characters for any data name.
|
||||
# If the filepath is longer, it will be truncated.
|
||||
if len(filepath) > 63:
|
||||
filepath = filepath[:63]
|
||||
library = bpy.data.libraries.get(filepath)
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
return container, members
|
||||
|
|
|
|||
|
|
@ -64,5 +64,8 @@ class FusionPrelaunch(PreLaunchHook):
|
|||
|
||||
self.launch_context.env[py3_var] = py3_dir
|
||||
|
||||
# for hook installing PySide2
|
||||
self.data["fusion_python3_home"] = py3_dir
|
||||
|
||||
self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}")
|
||||
self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR
|
||||
|
|
|
|||
186
openpype/hosts/fusion/hooks/pre_pyside_install.py
Normal file
186
openpype/hosts/fusion/hooks/pre_pyside_install.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import os
|
||||
import subprocess
|
||||
import platform
|
||||
import uuid
|
||||
|
||||
from openpype.lib.applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InstallPySideToFusion(PreLaunchHook):
|
||||
"""Automatically installs Qt binding to fusion's python packages.
|
||||
|
||||
Check if fusion has installed PySide2 and will try to install if not.
|
||||
|
||||
For pipeline implementation is required to have Qt binding installed in
|
||||
fusion's python packages.
|
||||
"""
|
||||
|
||||
app_groups = {"fusion"}
|
||||
order = 2
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
# Prelaunch hook is not crucial
|
||||
try:
|
||||
settings = self.data["project_settings"][self.host_name]
|
||||
if not settings["hooks"]["InstallPySideToFusion"]["enabled"]:
|
||||
return
|
||||
self.inner_execute()
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Processing of {} crashed.".format(self.__class__.__name__),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def inner_execute(self):
|
||||
self.log.debug("Check for PySide2 installation.")
|
||||
|
||||
fusion_python3_home = self.data.get("fusion_python3_home")
|
||||
if not fusion_python3_home:
|
||||
self.log.warning("'fusion_python3_home' was not provided. "
|
||||
"Installation of PySide2 not possible")
|
||||
return
|
||||
|
||||
if platform.system().lower() == "windows":
|
||||
exe_filenames = ["python.exe"]
|
||||
else:
|
||||
exe_filenames = ["python3", "python"]
|
||||
|
||||
for exe_filename in exe_filenames:
|
||||
python_executable = os.path.join(fusion_python3_home, exe_filename)
|
||||
if os.path.exists(python_executable):
|
||||
break
|
||||
|
||||
if not os.path.exists(python_executable):
|
||||
self.log.warning(
|
||||
"Couldn't find python executable for fusion. {}".format(
|
||||
python_executable
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Check if PySide2 is installed and skip if yes
|
||||
if self._is_pyside_installed(python_executable):
|
||||
self.log.debug("Fusion has already installed PySide2.")
|
||||
return
|
||||
|
||||
self.log.debug("Installing PySide2.")
|
||||
# Install PySide2 in fusion's python
|
||||
if self._windows_require_permissions(
|
||||
os.path.dirname(python_executable)):
|
||||
result = self._install_pyside_windows(python_executable)
|
||||
else:
|
||||
result = self._install_pyside(python_executable)
|
||||
|
||||
if result:
|
||||
self.log.info("Successfully installed PySide2 module to fusion.")
|
||||
else:
|
||||
self.log.warning("Failed to install PySide2 module to fusion.")
|
||||
|
||||
def _install_pyside_windows(self, python_executable):
|
||||
"""Install PySide2 python module to fusion's python.
|
||||
|
||||
Installation requires administration rights that's why it is required
|
||||
to use "pywin32" module which can execute command's and ask for
|
||||
administration rights.
|
||||
"""
|
||||
try:
|
||||
import win32api
|
||||
import win32con
|
||||
import win32process
|
||||
import win32event
|
||||
import pywintypes
|
||||
from win32comext.shell.shell import ShellExecuteEx
|
||||
from win32comext.shell import shellcon
|
||||
except Exception:
|
||||
self.log.warning("Couldn't import \"pywin32\" modules")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Parameters
|
||||
# - use "-m pip" as module pip to install PySide2 and argument
|
||||
# "--ignore-installed" is to force install module to fusion's
|
||||
# site-packages and make sure it is binary compatible
|
||||
parameters = "-m pip install --ignore-installed PySide2"
|
||||
|
||||
# Execute command and ask for administrator's rights
|
||||
process_info = ShellExecuteEx(
|
||||
nShow=win32con.SW_SHOWNORMAL,
|
||||
fMask=shellcon.SEE_MASK_NOCLOSEPROCESS,
|
||||
lpVerb="runas",
|
||||
lpFile=python_executable,
|
||||
lpParameters=parameters,
|
||||
lpDirectory=os.path.dirname(python_executable)
|
||||
)
|
||||
process_handle = process_info["hProcess"]
|
||||
win32event.WaitForSingleObject(process_handle,
|
||||
win32event.INFINITE)
|
||||
returncode = win32process.GetExitCodeProcess(process_handle)
|
||||
return returncode == 0
|
||||
except pywintypes.error:
|
||||
return False
|
||||
|
||||
def _install_pyside(self, python_executable):
|
||||
"""Install PySide2 python module to fusion's python."""
|
||||
try:
|
||||
# Parameters
|
||||
# - use "-m pip" as module pip to install PySide2 and argument
|
||||
# "--ignore-installed" is to force install module to fusion's
|
||||
# site-packages and make sure it is binary compatible
|
||||
env = dict(os.environ)
|
||||
del env['PYTHONPATH']
|
||||
args = [
|
||||
python_executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--ignore-installed",
|
||||
"PySide2",
|
||||
]
|
||||
process = subprocess.Popen(
|
||||
args, stdout=subprocess.PIPE, universal_newlines=True,
|
||||
env=env
|
||||
)
|
||||
process.communicate()
|
||||
return process.returncode == 0
|
||||
except PermissionError:
|
||||
self.log.warning(
|
||||
"Permission denied with command:"
|
||||
"\"{}\".".format(" ".join(args))
|
||||
)
|
||||
except OSError as error:
|
||||
self.log.warning(f"OS error has occurred: \"{error}\".")
|
||||
except subprocess.SubprocessError:
|
||||
pass
|
||||
|
||||
def _is_pyside_installed(self, python_executable):
|
||||
"""Check if PySide2 module is in fusion's pip list."""
|
||||
args = [python_executable, "-c", "from qtpy import QtWidgets"]
|
||||
process = subprocess.Popen(args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
_, stderr = process.communicate()
|
||||
stderr = stderr.decode()
|
||||
if stderr:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _windows_require_permissions(self, dirpath):
|
||||
if platform.system().lower() != "windows":
|
||||
return False
|
||||
|
||||
try:
|
||||
# Attempt to create a temporary file in the folder
|
||||
temp_file_path = os.path.join(dirpath, uuid.uuid4().hex)
|
||||
with open(temp_file_path, "w"):
|
||||
pass
|
||||
os.remove(temp_file_path) # Clean up temporary file
|
||||
return False
|
||||
|
||||
except PermissionError:
|
||||
return True
|
||||
|
||||
except BaseException as exc:
|
||||
print(("Failed to determine if root requires permissions."
|
||||
"Unexpected error: {}").format(exc))
|
||||
return False
|
||||
|
|
@ -15,6 +15,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
icon = "magic"
|
||||
ext = "exr"
|
||||
|
||||
# Default to split export and render jobs
|
||||
split_render = True
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
||||
instance_data.pop("active", None)
|
||||
|
|
@ -36,12 +39,15 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
# Also create the linked Redshift IPR Rop
|
||||
try:
|
||||
ipr_rop = instance_node.parent().createNode(
|
||||
"Redshift_IPR", node_name=basename + "_IPR"
|
||||
"Redshift_IPR", node_name=f"{basename}_IPR"
|
||||
)
|
||||
except hou.OperationFailed:
|
||||
except hou.OperationFailed as e:
|
||||
raise plugin.OpenPypeCreatorError(
|
||||
("Cannot create Redshift node. Is Redshift "
|
||||
"installed and enabled?"))
|
||||
(
|
||||
"Cannot create Redshift node. Is Redshift "
|
||||
"installed and enabled?"
|
||||
)
|
||||
) from e
|
||||
|
||||
# Move it to directly under the Redshift ROP
|
||||
ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1))
|
||||
|
|
@ -74,8 +80,15 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
for node in self.selected_nodes:
|
||||
if node.type().name() == "cam":
|
||||
camera = node.path()
|
||||
parms.update({
|
||||
"RS_renderCamera": camera or ""})
|
||||
parms["RS_renderCamera"] = camera or ""
|
||||
|
||||
export_dir = hou.text.expandString("$HIP/pyblish/rs/")
|
||||
rs_filepath = f"{export_dir}{subset_name}/{subset_name}.$F4.rs"
|
||||
parms["RS_archive_file"] = rs_filepath
|
||||
|
||||
if pre_create_data.get("split_render", self.split_render):
|
||||
parms["RS_archive_enable"] = 1
|
||||
|
||||
instance_node.setParms(parms)
|
||||
|
||||
# Lock some Avalon attributes
|
||||
|
|
@ -102,6 +115,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
|
|||
BoolDef("farm",
|
||||
label="Submitting to Farm",
|
||||
default=True),
|
||||
BoolDef("split_render",
|
||||
label="Split export and render jobs",
|
||||
default=self.split_render),
|
||||
EnumDef("image_format",
|
||||
image_format_enum,
|
||||
default=self.ext,
|
||||
|
|
|
|||
112
openpype/hosts/houdini/plugins/load/load_redshift_proxy.py
Normal file
112
openpype/hosts/houdini/plugins/load/load_redshift_proxy.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import os
|
||||
import re
|
||||
from openpype.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
)
|
||||
from openpype.hosts.houdini.api import pipeline
|
||||
from openpype.pipeline.load import LoadError
|
||||
|
||||
import hou
|
||||
|
||||
|
||||
class RedshiftProxyLoader(load.LoaderPlugin):
|
||||
"""Load Redshift Proxy"""
|
||||
|
||||
families = ["redshiftproxy"]
|
||||
label = "Load Redshift Proxy"
|
||||
representations = ["rs"]
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
|
||||
# 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)
|
||||
|
||||
# Check whether the Redshift parameters exist - if not, then likely
|
||||
# redshift is not set up or initialized correctly
|
||||
if not container.parm("RS_objprop_proxy_enable"):
|
||||
container.destroy()
|
||||
raise LoadError("Unable to initialize geo node with Redshift "
|
||||
"attributes. Make sure you have the Redshift "
|
||||
"plug-in set up correctly for Houdini.")
|
||||
|
||||
# Enable by default
|
||||
container.setParms({
|
||||
"RS_objprop_proxy_enable": True,
|
||||
"RS_objprop_proxy_file": self.format_path(
|
||||
self.filepath_from_context(context),
|
||||
context["representation"])
|
||||
})
|
||||
|
||||
# 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()
|
||||
|
||||
# Add this stub node inside so it previews ok
|
||||
proxy_sop = container.createNode("redshift_proxySOP",
|
||||
node_name=node_name)
|
||||
proxy_sop.setDisplayFlag(True)
|
||||
|
||||
nodes = [container, proxy_sop]
|
||||
|
||||
self[:] = nodes
|
||||
|
||||
return pipeline.containerise(
|
||||
node_name,
|
||||
namespace,
|
||||
nodes,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
suffix="",
|
||||
)
|
||||
|
||||
def update(self, container, representation):
|
||||
|
||||
# Update the file path
|
||||
file_path = get_representation_path(representation)
|
||||
|
||||
node = container["node"]
|
||||
node.setParms({
|
||||
"RS_objprop_proxy_file": self.format_path(
|
||||
file_path, representation)
|
||||
})
|
||||
|
||||
# Update attribute
|
||||
node.setParms({"representation": str(representation["_id"])})
|
||||
|
||||
def remove(self, container):
|
||||
|
||||
node = container["node"]
|
||||
node.destroy()
|
||||
|
||||
@staticmethod
|
||||
def format_path(path, representation):
|
||||
"""Format file path correctly for single redshift proxy
|
||||
or redshift proxy 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 is_sequence:
|
||||
filename = re.sub(r"(.*)\.(\d+)\.(rs.*)", "\\1.$F4.\\3", path)
|
||||
filename = os.path.join(path, filename)
|
||||
else:
|
||||
filename = path
|
||||
|
||||
filename = os.path.normpath(filename)
|
||||
filename = filename.replace("\\", "/")
|
||||
|
||||
return filename
|
||||
|
|
@ -31,7 +31,6 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
families = ["redshift_rop"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
rop = hou.node(instance.data.get("instance_node"))
|
||||
|
||||
# Collect chunkSize
|
||||
|
|
@ -43,13 +42,29 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
|
||||
default_prefix = evalParmNoFrame(rop, "RS_outputFileNamePrefix")
|
||||
beauty_suffix = rop.evalParm("RS_outputBeautyAOVSuffix")
|
||||
render_products = []
|
||||
# Store whether we are splitting the render job (export + render)
|
||||
split_render = bool(rop.parm("RS_archive_enable").eval())
|
||||
instance.data["splitRender"] = split_render
|
||||
export_products = []
|
||||
if split_render:
|
||||
export_prefix = evalParmNoFrame(
|
||||
rop, "RS_archive_file", pad_character="0"
|
||||
)
|
||||
beauty_export_product = self.get_render_product_name(
|
||||
prefix=export_prefix,
|
||||
suffix=None)
|
||||
export_products.append(beauty_export_product)
|
||||
self.log.debug(
|
||||
"Found export product: {}".format(beauty_export_product)
|
||||
)
|
||||
instance.data["ifdFile"] = beauty_export_product
|
||||
instance.data["exportFiles"] = list(export_products)
|
||||
|
||||
# Default beauty AOV
|
||||
beauty_product = self.get_render_product_name(
|
||||
prefix=default_prefix, suffix=beauty_suffix
|
||||
)
|
||||
render_products.append(beauty_product)
|
||||
render_products = [beauty_product]
|
||||
files_by_aov = {
|
||||
"_": self.generate_expected_files(instance,
|
||||
beauty_product)}
|
||||
|
|
@ -59,11 +74,11 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
i = index + 1
|
||||
|
||||
# Skip disabled AOVs
|
||||
if not rop.evalParm("RS_aovEnable_%s" % i):
|
||||
if not rop.evalParm(f"RS_aovEnable_{i}"):
|
||||
continue
|
||||
|
||||
aov_suffix = rop.evalParm("RS_aovSuffix_%s" % i)
|
||||
aov_prefix = evalParmNoFrame(rop, "RS_aovCustomPrefix_%s" % i)
|
||||
aov_suffix = rop.evalParm(f"RS_aovSuffix_{i}")
|
||||
aov_prefix = evalParmNoFrame(rop, f"RS_aovCustomPrefix_{i}")
|
||||
if not aov_prefix:
|
||||
aov_prefix = default_prefix
|
||||
|
||||
|
|
@ -85,7 +100,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
|
|||
instance.data["attachTo"] = [] # stub required data
|
||||
|
||||
if "expectedFiles" not in instance.data:
|
||||
instance.data["expectedFiles"] = list()
|
||||
instance.data["expectedFiles"] = []
|
||||
instance.data["expectedFiles"].append(files_by_aov)
|
||||
|
||||
# update the colorspace data
|
||||
|
|
|
|||
|
|
@ -294,6 +294,37 @@ def reset_frame_range(fps: bool = True):
|
|||
frame_range["frameStartHandle"], frame_range["frameEndHandle"])
|
||||
|
||||
|
||||
def reset_unit_scale():
|
||||
"""Apply the unit scale setting to 3dsMax
|
||||
"""
|
||||
project_name = get_current_project_name()
|
||||
settings = get_project_settings(project_name).get("max")
|
||||
scene_scale = settings.get("unit_scale_settings",
|
||||
{}).get("scene_unit_scale")
|
||||
if scene_scale:
|
||||
rt.units.DisplayType = rt.Name("Metric")
|
||||
rt.units.MetricType = rt.Name(scene_scale)
|
||||
else:
|
||||
rt.units.DisplayType = rt.Name("Generic")
|
||||
|
||||
|
||||
def convert_unit_scale():
|
||||
"""Convert system unit scale in 3dsMax
|
||||
for fbx export
|
||||
|
||||
Returns:
|
||||
str: unit scale
|
||||
"""
|
||||
unit_scale_dict = {
|
||||
"millimeters": "mm",
|
||||
"centimeters": "cm",
|
||||
"meters": "m",
|
||||
"kilometers": "km"
|
||||
}
|
||||
current_unit_scale = rt.Execute("units.MetricType as string")
|
||||
return unit_scale_dict[current_unit_scale]
|
||||
|
||||
|
||||
def set_context_setting():
|
||||
"""Apply the project settings from the project definition
|
||||
|
||||
|
|
@ -310,6 +341,7 @@ def set_context_setting():
|
|||
reset_scene_resolution()
|
||||
reset_frame_range()
|
||||
reset_colorspace()
|
||||
reset_unit_scale()
|
||||
|
||||
|
||||
def get_max_version():
|
||||
|
|
|
|||
|
|
@ -124,6 +124,10 @@ class OpenPypeMenu(object):
|
|||
colorspace_action.triggered.connect(self.colorspace_callback)
|
||||
openpype_menu.addAction(colorspace_action)
|
||||
|
||||
unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu)
|
||||
unit_scale_action.triggered.connect(self.unit_scale_callback)
|
||||
openpype_menu.addAction(unit_scale_action)
|
||||
|
||||
return openpype_menu
|
||||
|
||||
def load_callback(self):
|
||||
|
|
@ -157,3 +161,7 @@ class OpenPypeMenu(object):
|
|||
def colorspace_callback(self):
|
||||
"""Callback to reset colorspace"""
|
||||
return lib.reset_colorspace()
|
||||
|
||||
def unit_scale_callback(self):
|
||||
"""Callback to reset unit scale"""
|
||||
return lib.reset_unit_scale()
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from openpype.hosts.max.api import maintained_selection
|
||||
from openpype.pipeline import OptionalPyblishPluginMixin, publish
|
||||
|
||||
|
||||
class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
"""Extract Camera with FbxExporter."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Extract Fbx Camera"
|
||||
hosts = ["max"]
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
rt.FBXExporterSetParam("Animation", True)
|
||||
rt.FBXExporterSetParam("Cameras", True)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
rt.ExportFile(
|
||||
filepath,
|
||||
rt.Name("noPrompt"),
|
||||
selectedOnly=True,
|
||||
using=rt.FBXEXP,
|
||||
)
|
||||
|
||||
self.log.info("Performing Extraction ...")
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": "fbx",
|
||||
"ext": "fbx",
|
||||
"files": filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info(f"Extracted instance '{instance.name}' to: {filepath}")
|
||||
|
|
@ -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.hosts.max.api.lib import convert_unit_scale
|
||||
|
||||
|
||||
class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
|
|
@ -23,14 +24,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
rt.FBXExporterSetParam("Animation", False)
|
||||
rt.FBXExporterSetParam("Cameras", False)
|
||||
rt.FBXExporterSetParam("Lights", False)
|
||||
rt.FBXExporterSetParam("PointCache", False)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
self._set_fbx_attributes()
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
|
|
@ -56,3 +50,34 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
|||
self.log.info(
|
||||
"Extracted instance '%s' to: %s" % (instance.name, filepath)
|
||||
)
|
||||
|
||||
def _set_fbx_attributes(self):
|
||||
unit_scale = convert_unit_scale()
|
||||
rt.FBXExporterSetParam("Animation", False)
|
||||
rt.FBXExporterSetParam("Cameras", False)
|
||||
rt.FBXExporterSetParam("Lights", False)
|
||||
rt.FBXExporterSetParam("PointCache", False)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
if unit_scale:
|
||||
rt.FBXExporterSetParam("ConvertUnit", unit_scale)
|
||||
|
||||
|
||||
class ExtractCameraFbx(ExtractModelFbx):
|
||||
"""Extract Camera with FbxExporter."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Extract Fbx Camera"
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def _set_fbx_attributes(self):
|
||||
unit_scale = convert_unit_scale()
|
||||
rt.FBXExporterSetParam("Animation", True)
|
||||
rt.FBXExporterSetParam("Cameras", True)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
if unit_scale:
|
||||
rt.FBXExporterSetParam("ConvertUnit", unit_scale)
|
||||
139
openpype/hosts/maya/api/exitstack.py
Normal file
139
openpype/hosts/maya/api/exitstack.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""Backwards compatible implementation of ExitStack for Python 2.
|
||||
|
||||
ExitStack contextmanager was implemented with Python 3.3.
|
||||
As long as we supportPython 2 hosts we can use this backwards
|
||||
compatible implementation to support bothPython 2 and Python 3.
|
||||
|
||||
Instead of using ExitStack from contextlib, use it from this module:
|
||||
|
||||
>>> from openpype.hosts.maya.api.exitstack import ExitStack
|
||||
|
||||
It will provide the appropriate ExitStack implementation for the current
|
||||
running Python version.
|
||||
|
||||
"""
|
||||
# TODO: Remove the entire script once dropping Python 2 support.
|
||||
import contextlib
|
||||
if getattr(contextlib, "nested", None):
|
||||
from contextlib import ExitStack # noqa
|
||||
else:
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
class ExitStack(object):
|
||||
|
||||
"""Context manager for dynamic management of a stack of exit callbacks
|
||||
|
||||
For example:
|
||||
|
||||
with ExitStack() as stack:
|
||||
files = [stack.enter_context(open(fname))
|
||||
for fname in filenames]
|
||||
# All opened files will automatically be closed at the end of
|
||||
# the with statement, even if attempts to open files later
|
||||
# in the list raise an exception
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self._exit_callbacks = deque()
|
||||
|
||||
def pop_all(self):
|
||||
"""Preserve the context stack by transferring
|
||||
it to a new instance"""
|
||||
new_stack = type(self)()
|
||||
new_stack._exit_callbacks = self._exit_callbacks
|
||||
self._exit_callbacks = deque()
|
||||
return new_stack
|
||||
|
||||
def _push_cm_exit(self, cm, cm_exit):
|
||||
"""Helper to correctly register callbacks
|
||||
to __exit__ methods"""
|
||||
def _exit_wrapper(*exc_details):
|
||||
return cm_exit(cm, *exc_details)
|
||||
_exit_wrapper.__self__ = cm
|
||||
self.push(_exit_wrapper)
|
||||
|
||||
def push(self, exit):
|
||||
"""Registers a callback with the standard __exit__ method signature
|
||||
|
||||
Can suppress exceptions the same way __exit__ methods can.
|
||||
|
||||
Also accepts any object with an __exit__ method (registering a call
|
||||
to the method instead of the object itself)
|
||||
"""
|
||||
# We use an unbound method rather than a bound method to follow
|
||||
# the standard lookup behaviour for special methods
|
||||
_cb_type = type(exit)
|
||||
try:
|
||||
exit_method = _cb_type.__exit__
|
||||
except AttributeError:
|
||||
# Not a context manager, so assume its a callable
|
||||
self._exit_callbacks.append(exit)
|
||||
else:
|
||||
self._push_cm_exit(exit, exit_method)
|
||||
return exit # Allow use as a decorator
|
||||
|
||||
def callback(self, callback, *args, **kwds):
|
||||
"""Registers an arbitrary callback and arguments.
|
||||
|
||||
Cannot suppress exceptions.
|
||||
"""
|
||||
def _exit_wrapper(exc_type, exc, tb):
|
||||
callback(*args, **kwds)
|
||||
# We changed the signature, so using @wraps is not appropriate, but
|
||||
# setting __wrapped__ may still help with introspection
|
||||
_exit_wrapper.__wrapped__ = callback
|
||||
self.push(_exit_wrapper)
|
||||
return callback # Allow use as a decorator
|
||||
|
||||
def enter_context(self, cm):
|
||||
"""Enters the supplied context manager
|
||||
|
||||
If successful, also pushes its __exit__ method as a callback and
|
||||
returns the result of the __enter__ method.
|
||||
"""
|
||||
# We look up the special methods on the type to
|
||||
# match the with statement
|
||||
_cm_type = type(cm)
|
||||
_exit = _cm_type.__exit__
|
||||
result = _cm_type.__enter__(cm)
|
||||
self._push_cm_exit(cm, _exit)
|
||||
return result
|
||||
|
||||
def close(self):
|
||||
"""Immediately unwind the context stack"""
|
||||
self.__exit__(None, None, None)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_details):
|
||||
# We manipulate the exception state so it behaves as though
|
||||
# we were actually nesting multiple with statements
|
||||
frame_exc = sys.exc_info()[1]
|
||||
|
||||
def _fix_exception_context(new_exc, old_exc):
|
||||
while 1:
|
||||
exc_context = new_exc.__context__
|
||||
if exc_context in (None, frame_exc):
|
||||
break
|
||||
new_exc = exc_context
|
||||
new_exc.__context__ = old_exc
|
||||
|
||||
# Callbacks are invoked in LIFO order to match the behaviour of
|
||||
# nested context managers
|
||||
suppressed_exc = False
|
||||
while self._exit_callbacks:
|
||||
cb = self._exit_callbacks.pop()
|
||||
try:
|
||||
if cb(*exc_details):
|
||||
suppressed_exc = True
|
||||
exc_details = (None, None, None)
|
||||
except Exception:
|
||||
new_exc_details = sys.exc_info()
|
||||
# simulate the stack of exceptions by setting the context
|
||||
_fix_exception_context(new_exc_details[1], exc_details[1])
|
||||
if not self._exit_callbacks:
|
||||
raise
|
||||
exc_details = new_exc_details
|
||||
return suppressed_exc
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"""Standalone helper functions"""
|
||||
|
||||
import os
|
||||
import copy
|
||||
from pprint import pformat
|
||||
import sys
|
||||
import uuid
|
||||
|
|
@ -9,6 +10,8 @@ import re
|
|||
import json
|
||||
import logging
|
||||
import contextlib
|
||||
import capture
|
||||
from .exitstack import ExitStack
|
||||
from collections import OrderedDict, defaultdict
|
||||
from math import ceil
|
||||
from six import string_types
|
||||
|
|
@ -172,6 +175,216 @@ def maintained_selection():
|
|||
cmds.select(clear=True)
|
||||
|
||||
|
||||
def reload_all_udim_tile_previews():
|
||||
"""Regenerate all UDIM tile preview in texture file"""
|
||||
for texture_file in cmds.ls(type="file"):
|
||||
if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0:
|
||||
cmds.ogs(regenerateUVTilePreview=texture_file)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def panel_camera(panel, camera):
|
||||
"""Set modelPanel's camera during the context.
|
||||
|
||||
Arguments:
|
||||
panel (str): modelPanel name.
|
||||
camera (str): camera name.
|
||||
|
||||
"""
|
||||
original_camera = cmds.modelPanel(panel, query=True, camera=True)
|
||||
try:
|
||||
cmds.modelPanel(panel, edit=True, camera=camera)
|
||||
yield
|
||||
finally:
|
||||
cmds.modelPanel(panel, edit=True, camera=original_camera)
|
||||
|
||||
|
||||
def render_capture_preset(preset):
|
||||
"""Capture playblast with a preset.
|
||||
|
||||
To generate the preset use `generate_capture_preset`.
|
||||
|
||||
Args:
|
||||
preset (dict): preset options
|
||||
|
||||
Returns:
|
||||
str: Output path of `capture.capture`
|
||||
"""
|
||||
|
||||
# Force a refresh at the start of the timeline
|
||||
# TODO (Question): Why do we need to do this? What bug does it solve?
|
||||
# Is this for simulations?
|
||||
cmds.refresh(force=True)
|
||||
refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True))
|
||||
cmds.currentTime(refresh_frame_int - 1, edit=True)
|
||||
cmds.currentTime(refresh_frame_int, edit=True)
|
||||
log.debug(
|
||||
"Using preset: {}".format(
|
||||
json.dumps(preset, indent=4, sort_keys=True)
|
||||
)
|
||||
)
|
||||
preset = copy.deepcopy(preset)
|
||||
# not supported by `capture` so we pop it off of the preset
|
||||
reload_textures = preset["viewport_options"].pop("loadTextures", False)
|
||||
panel = preset.pop("panel")
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(maintained_time())
|
||||
stack.enter_context(panel_camera(panel, preset["camera"]))
|
||||
stack.enter_context(viewport_default_options(panel, preset))
|
||||
if reload_textures:
|
||||
# Force immediate texture loading when to ensure
|
||||
# all textures have loaded before the playblast starts
|
||||
stack.enter_context(material_loading_mode(mode="immediate"))
|
||||
# Regenerate all UDIM tiles previews
|
||||
reload_all_udim_tile_previews()
|
||||
path = capture.capture(log=self.log, **preset)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def generate_capture_preset(instance, camera, path,
|
||||
start=None, end=None, capture_preset=None):
|
||||
"""Function for getting all the data of preset options for
|
||||
playblast capturing
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): instance
|
||||
camera (str): review camera
|
||||
path (str): filepath
|
||||
start (int): frameStart
|
||||
end (int): frameEnd
|
||||
capture_preset (dict): capture preset
|
||||
|
||||
Returns:
|
||||
dict: Resulting preset
|
||||
"""
|
||||
preset = load_capture_preset(data=capture_preset)
|
||||
|
||||
preset["camera"] = camera
|
||||
preset["start_frame"] = start
|
||||
preset["end_frame"] = end
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
preset["panel"] = instance.data["panel"]
|
||||
|
||||
# Disable viewer since we use the rendering logic for publishing
|
||||
# We don't want to open the generated playblast in a viewer directly.
|
||||
preset["viewer"] = False
|
||||
|
||||
# "isolate_view" will already have been applied at creation, so we'll
|
||||
# ignore it here.
|
||||
preset.pop("isolate_view")
|
||||
|
||||
# Set resolution variables from capture presets
|
||||
width_preset = capture_preset["Resolution"]["width"]
|
||||
height_preset = capture_preset["Resolution"]["height"]
|
||||
|
||||
# Set resolution variables from asset values
|
||||
asset_data = instance.data["assetEntity"]["data"]
|
||||
asset_width = asset_data.get("resolutionWidth")
|
||||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
|
||||
# Use resolution from instance if review width/height is set
|
||||
# Otherwise use the resolution from preset if it has non-zero values
|
||||
# Otherwise fall back to asset width x height
|
||||
# Else define no width, then `capture.capture` will use render resolution
|
||||
if review_instance_width and review_instance_height:
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera. If there is only 1 member it'll be the camera because we
|
||||
# validate to have 1 camera only.
|
||||
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Override camera options
|
||||
# Enforce persisting camera depth of field
|
||||
camera_options = preset.setdefault("camera_options", {})
|
||||
camera_options["depthOfField"] = cmds.getAttr(
|
||||
"{0}.depthOfField".format(camera)
|
||||
)
|
||||
|
||||
# Use Pan/Zoom from instance data instead of from preset
|
||||
preset.pop("pan_zoom", None)
|
||||
camera_options["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
# Override viewport options by instance data
|
||||
viewport_options = preset.setdefault("viewport_options", {})
|
||||
viewport_options["displayLights"] = instance.data["displayLights"]
|
||||
viewport_options["imagePlane"] = instance.data.get("imagePlane", True)
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
if transparency != 0:
|
||||
preset["viewport2_options"]["transparencyAlgorithm"] = transparency
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
if not capture_preset["Viewport Options"]["override_viewport_options"]:
|
||||
panel_preset = capture.parse_view(preset["panel"])
|
||||
panel_preset.pop("camera")
|
||||
preset.update(panel_preset)
|
||||
|
||||
return preset
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_default_options(panel, preset):
|
||||
"""Context manager used by `render_capture_preset`.
|
||||
|
||||
We need to explicitly enable some viewport changes so the viewport is
|
||||
refreshed ahead of playblasting.
|
||||
|
||||
"""
|
||||
# TODO: Clarify in the docstring WHY we need to set it ahead of
|
||||
# playblasting. What issues does it solve?
|
||||
viewport_defaults = {}
|
||||
try:
|
||||
keys = [
|
||||
"useDefaultMaterial",
|
||||
"wireframeOnShaded",
|
||||
"xray",
|
||||
"jointXray",
|
||||
"backfaceCulling",
|
||||
"textures"
|
||||
]
|
||||
for key in keys:
|
||||
viewport_defaults[key] = cmds.modelEditor(
|
||||
panel, query=True, **{key: True}
|
||||
)
|
||||
if preset["viewport_options"].get(key):
|
||||
cmds.modelEditor(
|
||||
panel, edit=True, **{key: True}
|
||||
)
|
||||
yield
|
||||
finally:
|
||||
# Restoring viewport options.
|
||||
if viewport_defaults:
|
||||
cmds.modelEditor(
|
||||
panel, edit=True, **viewport_defaults
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def material_loading_mode(mode="immediate"):
|
||||
"""Set material loading mode during context"""
|
||||
original = cmds.displayPref(query=True, materialLoadingMode=True)
|
||||
cmds.displayPref(materialLoadingMode=mode)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cmds.displayPref(materialLoadingMode=original)
|
||||
|
||||
|
||||
def get_namespace(node):
|
||||
"""Return namespace of given node"""
|
||||
node_name = node.rsplit("|", 1)[-1]
|
||||
|
|
@ -2677,7 +2890,7 @@ def bake_to_world_space(nodes,
|
|||
return world_space_nodes
|
||||
|
||||
|
||||
def load_capture_preset(data=None):
|
||||
def load_capture_preset(data):
|
||||
"""Convert OpenPype Extract Playblast settings to `capture` arguments
|
||||
|
||||
Input data is the settings from:
|
||||
|
|
@ -2691,8 +2904,6 @@ def load_capture_preset(data=None):
|
|||
|
||||
"""
|
||||
|
||||
import capture
|
||||
|
||||
options = dict()
|
||||
viewport_options = dict()
|
||||
viewport2_options = dict()
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ class RedshiftProxyLoader(load.LoaderPlugin):
|
|||
cmds.connectAttr("{}.outMesh".format(rs_mesh),
|
||||
"{}.inMesh".format(mesh_shape))
|
||||
|
||||
# TODO: use the assigned shading group as shaders if existed
|
||||
# assign default shader to redshift proxy
|
||||
if cmds.ls("initialShadingGroup", type="shadingEngine"):
|
||||
cmds.sets(mesh_shape, forceElement="initialShadingGroup")
|
||||
|
||||
group_node = cmds.group(empty=True, name="{}_GRP".format(name))
|
||||
mesh_transform = cmds.listRelatives(mesh_shape,
|
||||
parent=True, fullPath=True)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from maya import cmds
|
|||
import pyblish.api
|
||||
|
||||
from openpype.hosts.maya.api import lib
|
||||
from openpype.pipeline.publish import KnownPublishError
|
||||
|
||||
|
||||
SETTINGS = {"renderDensity",
|
||||
|
|
@ -116,7 +117,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin):
|
|||
resources = []
|
||||
|
||||
image_search_paths = cmds.getAttr("{}.imageSearchPath".format(node))
|
||||
texture_filenames = []
|
||||
if image_search_paths:
|
||||
|
||||
# TODO: Somehow this uses OS environment path separator, `:` vs `;`
|
||||
|
|
@ -127,9 +127,16 @@ class CollectYetiRig(pyblish.api.InstancePlugin):
|
|||
# find all ${TOKEN} tokens and replace them with $TOKEN env. variable
|
||||
image_search_paths = self._replace_tokens(image_search_paths)
|
||||
|
||||
# List all related textures
|
||||
texture_filenames = cmds.pgYetiCommand(node, listTextures=True)
|
||||
self.log.debug("Found %i texture(s)" % len(texture_filenames))
|
||||
# List all related textures
|
||||
texture_nodes = cmds.pgYetiGraph(
|
||||
node, listNodes=True, type="texture")
|
||||
texture_filenames = [
|
||||
cmds.pgYetiGraph(
|
||||
node, node=texture_node,
|
||||
param="file_name", getParamValue=True)
|
||||
for texture_node in texture_nodes
|
||||
]
|
||||
self.log.debug("Found %i texture(s)" % len(texture_filenames))
|
||||
|
||||
# Get all reference nodes
|
||||
reference_nodes = cmds.pgYetiGraph(node,
|
||||
|
|
@ -137,11 +144,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin):
|
|||
type="reference")
|
||||
self.log.debug("Found %i reference node(s)" % len(reference_nodes))
|
||||
|
||||
if texture_filenames and not image_search_paths:
|
||||
raise ValueError("pgYetiMaya node '%s' is missing the path to the "
|
||||
"files in the 'imageSearchPath "
|
||||
"atttribute'" % node)
|
||||
|
||||
# Collect all texture files
|
||||
# find all ${TOKEN} tokens and replace them with $TOKEN env. variable
|
||||
texture_filenames = self._replace_tokens(texture_filenames)
|
||||
|
|
@ -161,7 +163,7 @@ class CollectYetiRig(pyblish.api.InstancePlugin):
|
|||
break
|
||||
|
||||
if not files:
|
||||
self.log.warning(
|
||||
raise KnownPublishError(
|
||||
"No texture found for: %s "
|
||||
"(searched: %s)" % (texture, image_search_paths))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import os
|
||||
import json
|
||||
import contextlib
|
||||
|
||||
import clique
|
||||
import capture
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
|
@ -11,16 +8,6 @@ from openpype.hosts.maya.api import lib
|
|||
from maya import cmds
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def panel_camera(panel, camera):
|
||||
original_camera = cmds.modelPanel(panel, query=True, camera=True)
|
||||
try:
|
||||
cmds.modelPanel(panel, edit=True, camera=camera)
|
||||
yield
|
||||
finally:
|
||||
cmds.modelPanel(panel, edit=True, camera=original_camera)
|
||||
|
||||
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
"""Extract viewport playblast.
|
||||
|
||||
|
|
@ -36,19 +23,8 @@ class ExtractPlayblast(publish.Extractor):
|
|||
capture_preset = {}
|
||||
profiles = None
|
||||
|
||||
def _capture(self, preset):
|
||||
if os.environ.get("OPENPYPE_DEBUG") == "1":
|
||||
self.log.debug(
|
||||
"Using preset: {}".format(
|
||||
json.dumps(preset, indent=4, sort_keys=True)
|
||||
)
|
||||
)
|
||||
|
||||
path = capture.capture(log=self.log, **preset)
|
||||
self.log.debug("playblast path {}".format(path))
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("Extracting capture..")
|
||||
self.log.debug("Extracting playblast..")
|
||||
|
||||
# get scene fps
|
||||
fps = instance.data.get("fps") or instance.context.data.get("fps")
|
||||
|
|
@ -63,10 +39,6 @@ class ExtractPlayblast(publish.Extractor):
|
|||
end = cmds.playbackOptions(query=True, animationEndTime=True)
|
||||
|
||||
self.log.debug("start: {}, end: {}".format(start, end))
|
||||
|
||||
# get cameras
|
||||
camera = instance.data["review_camera"]
|
||||
|
||||
task_data = instance.data["anatomyData"].get("task", {})
|
||||
capture_preset = lib.get_capture_preset(
|
||||
task_data.get("name"),
|
||||
|
|
@ -75,174 +47,35 @@ class ExtractPlayblast(publish.Extractor):
|
|||
instance.context.data["project_settings"],
|
||||
self.log
|
||||
)
|
||||
|
||||
preset = lib.load_capture_preset(data=capture_preset)
|
||||
|
||||
# "isolate_view" will already have been applied at creation, so we'll
|
||||
# ignore it here.
|
||||
preset.pop("isolate_view")
|
||||
|
||||
# Set resolution variables from capture presets
|
||||
width_preset = capture_preset["Resolution"]["width"]
|
||||
height_preset = capture_preset["Resolution"]["height"]
|
||||
|
||||
# Set resolution variables from asset values
|
||||
asset_data = instance.data["assetEntity"]["data"]
|
||||
asset_width = asset_data.get("resolutionWidth")
|
||||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
preset["camera"] = camera
|
||||
|
||||
# Tests if project resolution is set,
|
||||
# if it is a value other than zero, that value is
|
||||
# used, if not then the asset resolution is
|
||||
# used
|
||||
if review_instance_width and review_instance_height:
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
preset["start_frame"] = start
|
||||
preset["end_frame"] = end
|
||||
|
||||
# Enforce persisting camera depth of field
|
||||
camera_options = preset.setdefault("camera_options", {})
|
||||
camera_options["depthOfField"] = cmds.getAttr(
|
||||
"{0}.depthOfField".format(camera))
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{0}".format(instance.name)
|
||||
filename = instance.name
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.debug("Outputting images to %s" % path)
|
||||
# get cameras
|
||||
camera = instance.data["review_camera"]
|
||||
preset = lib.generate_capture_preset(
|
||||
instance, camera, path,
|
||||
start=start, end=end,
|
||||
capture_preset=capture_preset)
|
||||
lib.render_capture_preset(preset)
|
||||
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
cmds.refresh(force=True)
|
||||
|
||||
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
|
||||
cmds.currentTime(refreshFrameInt - 1, edit=True)
|
||||
cmds.currentTime(refreshFrameInt, edit=True)
|
||||
|
||||
# Use displayLights setting from instance
|
||||
key = "displayLights"
|
||||
preset["viewport_options"][key] = instance.data[key]
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
if transparency != 0:
|
||||
preset["viewport2_options"]["transparencyAlgorithm"] = transparency
|
||||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera. If there is only 1 member it'll be the camera because we
|
||||
# validate to have 1 camera only.
|
||||
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Show/Hide image planes on request.
|
||||
image_plane = instance.data.get("imagePlane", True)
|
||||
if "viewport_options" in preset:
|
||||
preset["viewport_options"]["imagePlane"] = image_plane
|
||||
else:
|
||||
preset["viewport_options"] = {"imagePlane": image_plane}
|
||||
|
||||
# Disable Pan/Zoom.
|
||||
pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"]))
|
||||
preset.pop("pan_zoom", None)
|
||||
preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
# Need to explicitly enable some viewport changes so the viewport is
|
||||
# refreshed ahead of playblasting.
|
||||
keys = [
|
||||
"useDefaultMaterial",
|
||||
"wireframeOnShaded",
|
||||
"xray",
|
||||
"jointXray",
|
||||
"backfaceCulling"
|
||||
]
|
||||
viewport_defaults = {}
|
||||
for key in keys:
|
||||
viewport_defaults[key] = cmds.modelEditor(
|
||||
instance.data["panel"], query=True, **{key: True}
|
||||
)
|
||||
if preset["viewport_options"][key]:
|
||||
cmds.modelEditor(
|
||||
instance.data["panel"], edit=True, **{key: True}
|
||||
)
|
||||
|
||||
override_viewport_options = (
|
||||
capture_preset["Viewport Options"]["override_viewport_options"]
|
||||
)
|
||||
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset["viewer"] = False
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
if not override_viewport_options:
|
||||
panel_preset = capture.parse_view(instance.data["panel"])
|
||||
panel_preset.pop("camera")
|
||||
preset.update(panel_preset)
|
||||
|
||||
# Need to ensure Python 2 compatibility.
|
||||
# TODO: Remove once dropping Python 2.
|
||||
if getattr(contextlib, "nested", None):
|
||||
# Python 3 compatibility.
|
||||
with contextlib.nested(
|
||||
lib.maintained_time(),
|
||||
panel_camera(instance.data["panel"], preset["camera"])
|
||||
):
|
||||
self._capture(preset)
|
||||
else:
|
||||
# Python 2 compatibility.
|
||||
with contextlib.ExitStack() as stack:
|
||||
stack.enter_context(lib.maintained_time())
|
||||
stack.enter_context(
|
||||
panel_camera(instance.data["panel"], preset["camera"])
|
||||
)
|
||||
|
||||
self._capture(preset)
|
||||
|
||||
# Restoring viewport options.
|
||||
if viewport_defaults:
|
||||
cmds.modelEditor(
|
||||
instance.data["panel"], edit=True, **viewport_defaults
|
||||
)
|
||||
|
||||
try:
|
||||
cmds.setAttr(
|
||||
"{}.panZoomEnabled".format(preset["camera"]), pan_zoom)
|
||||
except RuntimeError:
|
||||
self.log.warning("Cannot restore Pan/Zoom settings.")
|
||||
|
||||
# Find playblast sequence
|
||||
collected_files = os.listdir(stagingdir)
|
||||
patterns = [clique.PATTERNS["frames"]]
|
||||
collections, remainder = clique.assemble(collected_files,
|
||||
minimum_items=1,
|
||||
patterns=patterns)
|
||||
|
||||
filename = preset.get("filename", "%TEMP%")
|
||||
self.log.debug("filename {}".format(filename))
|
||||
self.log.debug("Searching playblast collection for: %s", path)
|
||||
frame_collection = None
|
||||
for collection in collections:
|
||||
filebase = collection.format("{head}").rstrip(".")
|
||||
self.log.debug("collection head {}".format(filebase))
|
||||
if filebase in filename:
|
||||
self.log.debug("Checking collection head: %s", filebase)
|
||||
if filebase in path:
|
||||
frame_collection = collection
|
||||
self.log.debug(
|
||||
"we found collection of interest {}".format(
|
||||
str(frame_collection)))
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
"Found playblast collection: %s", frame_collection
|
||||
)
|
||||
|
||||
tags = ["review"]
|
||||
if not instance.data.get("keepImages"):
|
||||
|
|
@ -256,6 +89,9 @@ class ExtractPlayblast(publish.Extractor):
|
|||
if len(collected_files) == 1:
|
||||
collected_files = collected_files[0]
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": capture_preset["Codec"]["compression"],
|
||||
"ext": capture_preset["Codec"]["compression"],
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
import os
|
||||
import glob
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
import capture
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""Extract viewport thumbnail.
|
||||
|
|
@ -24,7 +19,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
self.log.debug("Extracting capture..")
|
||||
self.log.debug("Extracting thumbnail..")
|
||||
|
||||
camera = instance.data["review_camera"]
|
||||
|
||||
|
|
@ -37,20 +32,24 @@ class ExtractThumbnail(publish.Extractor):
|
|||
self.log
|
||||
)
|
||||
|
||||
preset = lib.load_capture_preset(data=capture_preset)
|
||||
|
||||
# "isolate_view" will already have been applied at creation, so we'll
|
||||
# ignore it here.
|
||||
preset.pop("isolate_view")
|
||||
|
||||
override_viewport_options = (
|
||||
capture_preset["Viewport Options"]["override_viewport_options"]
|
||||
# Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_thumbnail")
|
||||
self.log.debug(
|
||||
"Create temp directory {} for thumbnail".format(dst_staging)
|
||||
)
|
||||
# Store new staging to cleanup paths
|
||||
filename = instance.name
|
||||
path = os.path.join(dst_staging, filename)
|
||||
|
||||
preset["camera"] = camera
|
||||
preset["start_frame"] = instance.data["frameStart"]
|
||||
preset["end_frame"] = instance.data["frameStart"]
|
||||
preset["camera_options"] = {
|
||||
self.log.debug("Outputting images to %s" % path)
|
||||
|
||||
preset = lib.generate_capture_preset(
|
||||
instance, camera, path,
|
||||
start=1, end=1,
|
||||
capture_preset=capture_preset)
|
||||
|
||||
preset["camera_options"].update({
|
||||
"displayGateMask": False,
|
||||
"displayResolution": False,
|
||||
"displayFilmGate": False,
|
||||
|
|
@ -60,101 +59,10 @@ class ExtractThumbnail(publish.Extractor):
|
|||
"displayFilmPivot": False,
|
||||
"displayFilmOrigin": False,
|
||||
"overscan": 1.0,
|
||||
"depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)),
|
||||
}
|
||||
# Set resolution variables from capture presets
|
||||
width_preset = capture_preset["Resolution"]["width"]
|
||||
height_preset = capture_preset["Resolution"]["height"]
|
||||
# Set resolution variables from asset values
|
||||
asset_data = instance.data["assetEntity"]["data"]
|
||||
asset_width = asset_data.get("resolutionWidth")
|
||||
asset_height = asset_data.get("resolutionHeight")
|
||||
review_instance_width = instance.data.get("review_width")
|
||||
review_instance_height = instance.data.get("review_height")
|
||||
# Tests if project resolution is set,
|
||||
# if it is a value other than zero, that value is
|
||||
# used, if not then the asset resolution is
|
||||
# used
|
||||
if review_instance_width and review_instance_height:
|
||||
preset["width"] = review_instance_width
|
||||
preset["height"] = review_instance_height
|
||||
elif width_preset and height_preset:
|
||||
preset["width"] = width_preset
|
||||
preset["height"] = height_preset
|
||||
elif asset_width and asset_height:
|
||||
preset["width"] = asset_width
|
||||
preset["height"] = asset_height
|
||||
})
|
||||
path = lib.render_capture_preset(preset)
|
||||
|
||||
# Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
self.log.debug(
|
||||
"Create temp directory {} for thumbnail".format(dst_staging)
|
||||
)
|
||||
# Store new staging to cleanup paths
|
||||
filename = "{0}".format(instance.name)
|
||||
path = os.path.join(dst_staging, filename)
|
||||
|
||||
self.log.debug("Outputting images to %s" % path)
|
||||
|
||||
preset["filename"] = path
|
||||
preset["overwrite"] = True
|
||||
|
||||
cmds.refresh(force=True)
|
||||
|
||||
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
|
||||
cmds.currentTime(refreshFrameInt - 1, edit=True)
|
||||
cmds.currentTime(refreshFrameInt, edit=True)
|
||||
|
||||
# Use displayLights setting from instance
|
||||
key = "displayLights"
|
||||
preset["viewport_options"][key] = instance.data[key]
|
||||
|
||||
# Override transparency if requested.
|
||||
transparency = instance.data.get("transparency", 0)
|
||||
if transparency != 0:
|
||||
preset["viewport2_options"]["transparencyAlgorithm"] = transparency
|
||||
|
||||
# Isolate view is requested by having objects in the set besides a
|
||||
# camera. If there is only 1 member it'll be the camera because we
|
||||
# validate to have 1 camera only.
|
||||
if instance.data["isolate"] and len(instance.data["setMembers"]) > 1:
|
||||
preset["isolate"] = instance.data["setMembers"]
|
||||
|
||||
# Show or Hide Image Plane
|
||||
image_plane = instance.data.get("imagePlane", True)
|
||||
if "viewport_options" in preset:
|
||||
preset["viewport_options"]["imagePlane"] = image_plane
|
||||
else:
|
||||
preset["viewport_options"] = {"imagePlane": image_plane}
|
||||
|
||||
# Disable Pan/Zoom.
|
||||
preset.pop("pan_zoom", None)
|
||||
preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"]
|
||||
|
||||
with lib.maintained_time():
|
||||
# Force viewer to False in call to capture because we have our own
|
||||
# viewer opening call to allow a signal to trigger between
|
||||
# playblast and viewer
|
||||
preset["viewer"] = False
|
||||
|
||||
# Update preset with current panel setting
|
||||
# if override_viewport_options is turned off
|
||||
panel = cmds.getPanel(withFocus=True) or ""
|
||||
if not override_viewport_options and "modelPanel" in panel:
|
||||
panel_preset = capture.parse_active_view()
|
||||
preset.update(panel_preset)
|
||||
cmds.setFocus(panel)
|
||||
|
||||
if os.environ.get("OPENPYPE_DEBUG") == "1":
|
||||
self.log.debug(
|
||||
"Using preset: {}".format(
|
||||
json.dumps(preset, indent=4, sort_keys=True)
|
||||
)
|
||||
)
|
||||
|
||||
path = capture.capture(**preset)
|
||||
playblast = self._fix_playblast_output_path(path)
|
||||
playblast = self._fix_playblast_output_path(path)
|
||||
|
||||
_, thumbnail = os.path.split(playblast)
|
||||
|
||||
|
|
|
|||
|
|
@ -3483,3 +3483,19 @@ def get_filenames_without_hash(filename, frame_start, frame_end):
|
|||
new_filename = filename_without_hashes.format(frame)
|
||||
filenames.append(new_filename)
|
||||
return filenames
|
||||
|
||||
|
||||
def create_camera_node_by_version():
|
||||
"""Function to create the camera with the latest node class
|
||||
For Nuke version 14.0 or later, the Camera4 camera node class
|
||||
would be used
|
||||
For the version before, the Camera2 camera node class
|
||||
would be used
|
||||
Returns:
|
||||
Node: camera node
|
||||
"""
|
||||
nuke_number_version = nuke.NUKE_VERSION_MAJOR
|
||||
if nuke_number_version >= 14:
|
||||
return nuke.createNode("Camera4")
|
||||
else:
|
||||
return nuke.createNode("Camera2")
|
||||
|
|
|
|||
|
|
@ -259,9 +259,7 @@ def _install_menu():
|
|||
menu.addCommand(
|
||||
"Create...",
|
||||
lambda: host_tools.show_publisher(
|
||||
parent=(
|
||||
main_window if nuke.NUKE_VERSION_MAJOR >= 14 else None
|
||||
),
|
||||
parent=main_window,
|
||||
tab="create"
|
||||
)
|
||||
)
|
||||
|
|
@ -270,9 +268,7 @@ def _install_menu():
|
|||
menu.addCommand(
|
||||
"Publish...",
|
||||
lambda: host_tools.show_publisher(
|
||||
parent=(
|
||||
main_window if nuke.NUKE_VERSION_MAJOR >= 14 else None
|
||||
),
|
||||
parent=main_window,
|
||||
tab="publish"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ def set_context_favorites(favorites=None):
|
|||
favorites (dict): couples of {name:path}
|
||||
"""
|
||||
favorites = favorites or {}
|
||||
icon_path = resources.get_resource("icons", "folder-favorite3.png")
|
||||
icon_path = resources.get_resource("icons", "folder-favorite.png")
|
||||
for name, path in favorites.items():
|
||||
nuke.addFavoriteDir(
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ from openpype.hosts.nuke.api import (
|
|||
NukeCreatorError,
|
||||
maintained_selection
|
||||
)
|
||||
from openpype.hosts.nuke.api.lib import (
|
||||
create_camera_node_by_version
|
||||
)
|
||||
|
||||
|
||||
class CreateCamera(NukeCreator):
|
||||
|
|
@ -32,7 +35,7 @@ class CreateCamera(NukeCreator):
|
|||
"Creator error: Select only camera node type")
|
||||
created_node = self.selected_nodes[0]
|
||||
else:
|
||||
created_node = nuke.createNode("Camera2")
|
||||
created_node = create_camera_node_by_version()
|
||||
|
||||
created_node["tile_color"].setValue(
|
||||
int(self.node_color, 16))
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ class ExtractReviewIntermediates(publish.Extractor):
|
|||
nuke_publish = project_settings["nuke"]["publish"]
|
||||
deprecated_setting = nuke_publish["ExtractReviewDataMov"]
|
||||
current_setting = nuke_publish.get("ExtractReviewIntermediates")
|
||||
if not deprecated_setting["enabled"] and (
|
||||
not current_setting["enabled"]
|
||||
):
|
||||
cls.enabled = False
|
||||
|
||||
if deprecated_setting["enabled"]:
|
||||
# Use deprecated settings if they are still enabled
|
||||
cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,113 @@ class MissingEventSystem(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def _get_func_ref(func):
|
||||
if inspect.ismethod(func):
|
||||
return WeakMethod(func)
|
||||
return weakref.ref(func)
|
||||
|
||||
|
||||
def _get_func_info(func):
|
||||
path = "<unknown path>"
|
||||
if func is None:
|
||||
return "<unknown>", path
|
||||
|
||||
if hasattr(func, "__name__"):
|
||||
name = func.__name__
|
||||
else:
|
||||
name = str(func)
|
||||
|
||||
# Get path to file and fallback to '<unknown path>' if fails
|
||||
# NOTE This was added because of 'partial' functions which is handled,
|
||||
# but who knows what else can cause this to fail?
|
||||
try:
|
||||
path = os.path.abspath(inspect.getfile(func))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return name, path
|
||||
|
||||
|
||||
class weakref_partial:
|
||||
"""Partial function with weak reference to the wrapped function.
|
||||
|
||||
Can be used as 'functools.partial' but it will store weak reference to
|
||||
function. That means that the function must be reference counted
|
||||
to avoid garbage collecting the function itself.
|
||||
|
||||
When the referenced functions is garbage collected then calling the
|
||||
weakref partial (no matter the args/kwargs passed) will do nothing.
|
||||
It will fail silently, returning `None`. The `is_valid()` method can
|
||||
be used to detect whether the reference is still valid.
|
||||
|
||||
Is useful for object methods. In that case the callback is
|
||||
deregistered when object is destroyed.
|
||||
|
||||
Warnings:
|
||||
Values passed as *args and **kwargs are stored strongly in memory.
|
||||
That may "keep alive" objects that should be already destroyed.
|
||||
It is recommended to pass only immutable objects like 'str',
|
||||
'bool', 'int' etc.
|
||||
|
||||
Args:
|
||||
func (Callable): Function to wrap.
|
||||
*args: Arguments passed to the wrapped function.
|
||||
**kwargs: Keyword arguments passed to the wrapped function.
|
||||
"""
|
||||
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
self._func_ref = _get_func_ref(func)
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
func = self._func_ref()
|
||||
if func is None:
|
||||
return
|
||||
|
||||
new_args = tuple(list(self._args) + list(args))
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs.update(kwargs)
|
||||
return func(*new_args, **new_kwargs)
|
||||
|
||||
def get_func(self):
|
||||
"""Get wrapped function.
|
||||
|
||||
Returns:
|
||||
Union[Callable, None]: Wrapped function or None if it was
|
||||
destroyed.
|
||||
"""
|
||||
|
||||
return self._func_ref()
|
||||
|
||||
def is_valid(self):
|
||||
"""Check if wrapped function is still valid.
|
||||
|
||||
Returns:
|
||||
bool: Is wrapped function still valid.
|
||||
"""
|
||||
|
||||
return self._func_ref() is not None
|
||||
|
||||
def validate_signature(self, *args, **kwargs):
|
||||
"""Validate if passed arguments are supported by wrapped function.
|
||||
|
||||
Returns:
|
||||
bool: Are passed arguments supported by wrapped function.
|
||||
"""
|
||||
|
||||
func = self._func_ref()
|
||||
if func is None:
|
||||
return False
|
||||
|
||||
new_args = tuple(list(self._args) + list(args))
|
||||
new_kwargs = dict(self._kwargs)
|
||||
new_kwargs.update(kwargs)
|
||||
return is_func_signature_supported(
|
||||
func, *new_args, **new_kwargs
|
||||
)
|
||||
|
||||
|
||||
class EventCallback(object):
|
||||
"""Callback registered to a topic.
|
||||
|
||||
|
|
@ -34,20 +141,37 @@ class EventCallback(object):
|
|||
or none arguments. When 1 argument is expected then the processed 'Event'
|
||||
object is passed in.
|
||||
|
||||
The registered callbacks don't keep function in memory so it is not
|
||||
possible to store lambda function as callback.
|
||||
The callbacks are validated against their reference counter, that is
|
||||
achieved using 'weakref' module. That means that the callback must
|
||||
be stored in memory somewhere. e.g. lambda functions are not
|
||||
supported as valid callback.
|
||||
|
||||
You can use 'weakref_partial' functions. In that case is partial object
|
||||
stored in the callback object and reference counter is checked for
|
||||
the wrapped function.
|
||||
|
||||
Args:
|
||||
topic(str): Topic which will be listened.
|
||||
func(func): Callback to a topic.
|
||||
topic (str): Topic which will be listened.
|
||||
func (Callable): Callback to a topic.
|
||||
order (Union[int, None]): Order of callback. Lower number means higher
|
||||
priority.
|
||||
|
||||
Raises:
|
||||
TypeError: When passed function is not a callable object.
|
||||
"""
|
||||
|
||||
def __init__(self, topic, func):
|
||||
def __init__(self, topic, func, order):
|
||||
if not callable(func):
|
||||
raise TypeError((
|
||||
"Registered callback is not callable. \"{}\""
|
||||
).format(str(func)))
|
||||
|
||||
self._validate_order(order)
|
||||
|
||||
self._log = None
|
||||
self._topic = topic
|
||||
self._order = order
|
||||
self._enabled = True
|
||||
# Replace '*' with any character regex and escape rest of text
|
||||
# - when callback is registered for '*' topic it will receive all
|
||||
# events
|
||||
|
|
@ -63,37 +187,38 @@ class EventCallback(object):
|
|||
topic_regex = re.compile(topic_regex_str)
|
||||
self._topic_regex = topic_regex
|
||||
|
||||
# Convert callback into references
|
||||
# - deleted functions won't cause crashes
|
||||
if inspect.ismethod(func):
|
||||
func_ref = WeakMethod(func)
|
||||
elif callable(func):
|
||||
func_ref = weakref.ref(func)
|
||||
# Callback function prep
|
||||
if isinstance(func, weakref_partial):
|
||||
partial_func = func
|
||||
(name, path) = _get_func_info(func.get_func())
|
||||
func_ref = None
|
||||
expect_args = partial_func.validate_signature("fake")
|
||||
expect_kwargs = partial_func.validate_signature(event="fake")
|
||||
|
||||
else:
|
||||
raise TypeError((
|
||||
"Registered callback is not callable. \"{}\""
|
||||
).format(str(func)))
|
||||
partial_func = None
|
||||
(name, path) = _get_func_info(func)
|
||||
# Convert callback into references
|
||||
# - deleted functions won't cause crashes
|
||||
func_ref = _get_func_ref(func)
|
||||
|
||||
# Collect function name and path to file for logging
|
||||
func_name = func.__name__
|
||||
func_path = os.path.abspath(inspect.getfile(func))
|
||||
|
||||
# Get expected arguments from function spec
|
||||
# - positional arguments are always preferred
|
||||
expect_args = is_func_signature_supported(func, "fake")
|
||||
expect_kwargs = is_func_signature_supported(func, event="fake")
|
||||
# Get expected arguments from function spec
|
||||
# - positional arguments are always preferred
|
||||
expect_args = is_func_signature_supported(func, "fake")
|
||||
expect_kwargs = is_func_signature_supported(func, event="fake")
|
||||
|
||||
self._func_ref = func_ref
|
||||
self._func_name = func_name
|
||||
self._func_path = func_path
|
||||
self._partial_func = partial_func
|
||||
self._ref_is_valid = True
|
||||
self._expect_args = expect_args
|
||||
self._expect_kwargs = expect_kwargs
|
||||
self._ref_valid = func_ref is not None
|
||||
self._enabled = True
|
||||
|
||||
self._name = name
|
||||
self._path = path
|
||||
|
||||
def __repr__(self):
|
||||
return "< {} - {} > {}".format(
|
||||
self.__class__.__name__, self._func_name, self._func_path
|
||||
self.__class__.__name__, self._name, self._path
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -104,32 +229,83 @@ class EventCallback(object):
|
|||
|
||||
@property
|
||||
def is_ref_valid(self):
|
||||
return self._ref_valid
|
||||
"""
|
||||
|
||||
Returns:
|
||||
bool: Is reference to callback valid.
|
||||
"""
|
||||
|
||||
self._validate_ref()
|
||||
return self._ref_is_valid
|
||||
|
||||
def validate_ref(self):
|
||||
if not self._ref_valid:
|
||||
return
|
||||
"""Validate if reference to callback is valid.
|
||||
|
||||
callback = self._func_ref()
|
||||
if not callback:
|
||||
self._ref_valid = False
|
||||
Deprecated:
|
||||
Reference is always live checkd with 'is_ref_valid'.
|
||||
"""
|
||||
|
||||
# Trigger validate by getting 'is_valid'
|
||||
_ = self.is_ref_valid
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Is callback enabled."""
|
||||
"""Is callback enabled.
|
||||
|
||||
Returns:
|
||||
bool: Is callback enabled.
|
||||
"""
|
||||
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
"""Change if callback is enabled."""
|
||||
"""Change if callback is enabled.
|
||||
|
||||
Args:
|
||||
enabled (bool): Change enabled state of the callback.
|
||||
"""
|
||||
|
||||
self._enabled = enabled
|
||||
|
||||
def deregister(self):
|
||||
"""Calling this function will cause that callback will be removed."""
|
||||
# Fake reference
|
||||
self._ref_valid = False
|
||||
|
||||
self._ref_is_valid = False
|
||||
self._partial_func = None
|
||||
self._func_ref = None
|
||||
|
||||
def get_order(self):
|
||||
"""Get callback order.
|
||||
|
||||
Returns:
|
||||
Union[int, None]: Callback order.
|
||||
"""
|
||||
|
||||
return self._order
|
||||
|
||||
def set_order(self, order):
|
||||
"""Change callback order.
|
||||
|
||||
Args:
|
||||
order (Union[int, None]): Order of callback. Lower number means
|
||||
higher priority.
|
||||
"""
|
||||
|
||||
self._validate_order(order)
|
||||
self._order = order
|
||||
|
||||
order = property(get_order, set_order)
|
||||
|
||||
def topic_matches(self, topic):
|
||||
"""Check if event topic matches callback's topic."""
|
||||
"""Check if event topic matches callback's topic.
|
||||
|
||||
Args:
|
||||
topic (str): Topic name.
|
||||
|
||||
Returns:
|
||||
bool: Topic matches callback's topic.
|
||||
"""
|
||||
|
||||
return self._topic_regex.match(topic)
|
||||
|
||||
def process_event(self, event):
|
||||
|
|
@ -139,36 +315,69 @@ class EventCallback(object):
|
|||
event(Event): Event that was triggered.
|
||||
"""
|
||||
|
||||
# Skip if callback is not enabled or has invalid reference
|
||||
if not self._ref_valid or not self._enabled:
|
||||
# Skip if callback is not enabled
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
# Get reference
|
||||
callback = self._func_ref()
|
||||
# Check if reference is valid or callback's topic matches the event
|
||||
if not callback:
|
||||
# Change state if is invalid so the callback is removed
|
||||
self._ref_valid = False
|
||||
# Get reference and skip if is not available
|
||||
callback = self._get_callback()
|
||||
if callback is None:
|
||||
return
|
||||
|
||||
elif self.topic_matches(event.topic):
|
||||
# Try execute callback
|
||||
try:
|
||||
if self._expect_args:
|
||||
callback(event)
|
||||
if not self.topic_matches(event.topic):
|
||||
return
|
||||
|
||||
elif self._expect_kwargs:
|
||||
callback(event=event)
|
||||
# Try to execute callback
|
||||
try:
|
||||
if self._expect_args:
|
||||
callback(event)
|
||||
|
||||
else:
|
||||
callback()
|
||||
elif self._expect_kwargs:
|
||||
callback(event=event)
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to execute event callback {}".format(
|
||||
str(repr(self))
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
callback()
|
||||
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to execute event callback {}".format(
|
||||
str(repr(self))
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def _validate_order(self, order):
|
||||
if isinstance(order, int):
|
||||
return
|
||||
|
||||
raise TypeError(
|
||||
"Expected type 'int' got '{}'.".format(str(type(order)))
|
||||
)
|
||||
|
||||
def _get_callback(self):
|
||||
if self._partial_func is not None:
|
||||
return self._partial_func
|
||||
|
||||
if self._func_ref is not None:
|
||||
return self._func_ref()
|
||||
return None
|
||||
|
||||
def _validate_ref(self):
|
||||
if self._ref_is_valid is False:
|
||||
return
|
||||
|
||||
if self._func_ref is not None:
|
||||
self._ref_is_valid = self._func_ref() is not None
|
||||
|
||||
elif self._partial_func is not None:
|
||||
self._ref_is_valid = self._partial_func.is_valid()
|
||||
|
||||
else:
|
||||
self._ref_is_valid = False
|
||||
|
||||
if not self._ref_is_valid:
|
||||
self._func_ref = None
|
||||
self._partial_func = None
|
||||
|
||||
|
||||
# Inherit from 'object' for Python 2 hosts
|
||||
|
|
@ -282,30 +491,39 @@ class Event(object):
|
|||
class EventSystem(object):
|
||||
"""Encapsulate event handling into an object.
|
||||
|
||||
System wraps registered callbacks and triggered events into single object
|
||||
so it is possible to create mutltiple independent systems that have their
|
||||
System wraps registered callbacks and triggered events into single object,
|
||||
so it is possible to create multiple independent systems that have their
|
||||
topics and callbacks.
|
||||
|
||||
|
||||
Callbacks are stored by order of their registration, but it is possible to
|
||||
manually define order of callbacks using 'order' argument within
|
||||
'add_callback'.
|
||||
"""
|
||||
|
||||
default_order = 100
|
||||
|
||||
def __init__(self):
|
||||
self._registered_callbacks = []
|
||||
|
||||
def add_callback(self, topic, callback):
|
||||
def add_callback(self, topic, callback, order=None):
|
||||
"""Register callback in event system.
|
||||
|
||||
Args:
|
||||
topic (str): Topic for EventCallback.
|
||||
callback (Callable): Function or method that will be called
|
||||
when topic is triggered.
|
||||
callback (Union[Callable, weakref_partial]): Function or method
|
||||
that will be called when topic is triggered.
|
||||
order (Optional[int]): Order of callback. Lower number means
|
||||
higher priority.
|
||||
|
||||
Returns:
|
||||
EventCallback: Created callback object which can be used to
|
||||
stop listening.
|
||||
"""
|
||||
|
||||
callback = EventCallback(topic, callback)
|
||||
if order is None:
|
||||
order = self.default_order
|
||||
|
||||
callback = EventCallback(topic, callback, order)
|
||||
self._registered_callbacks.append(callback)
|
||||
return callback
|
||||
|
||||
|
|
@ -341,22 +559,6 @@ class EventSystem(object):
|
|||
event.emit()
|
||||
return event
|
||||
|
||||
def _process_event(self, event):
|
||||
"""Process event topic and trigger callbacks.
|
||||
|
||||
Args:
|
||||
event (Event): Prepared event with topic and data.
|
||||
"""
|
||||
|
||||
invalid_callbacks = []
|
||||
for callback in self._registered_callbacks:
|
||||
callback.process_event(event)
|
||||
if not callback.is_ref_valid:
|
||||
invalid_callbacks.append(callback)
|
||||
|
||||
for callback in invalid_callbacks:
|
||||
self._registered_callbacks.remove(callback)
|
||||
|
||||
def emit_event(self, event):
|
||||
"""Emit event object.
|
||||
|
||||
|
|
@ -366,6 +568,21 @@ class EventSystem(object):
|
|||
|
||||
self._process_event(event)
|
||||
|
||||
def _process_event(self, event):
|
||||
"""Process event topic and trigger callbacks.
|
||||
|
||||
Args:
|
||||
event (Event): Prepared event with topic and data.
|
||||
"""
|
||||
|
||||
callbacks = tuple(sorted(
|
||||
self._registered_callbacks, key=lambda x: x.order
|
||||
))
|
||||
for callback in callbacks:
|
||||
callback.process_event(event)
|
||||
if not callback.is_ref_valid:
|
||||
self._registered_callbacks.remove(callback)
|
||||
|
||||
|
||||
class QueuedEventSystem(EventSystem):
|
||||
"""Events are automatically processed in queue.
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ def is_func_signature_supported(func, *args, **kwargs):
|
|||
True
|
||||
|
||||
Args:
|
||||
func (function): A function where the signature should be tested.
|
||||
func (Callable): A function where the signature should be tested.
|
||||
*args (Any): Positional arguments for function signature.
|
||||
**kwargs (Any): Keyword arguments for function signature.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect Deadline pools. Choose default one from Settings
|
||||
|
||||
"""
|
||||
import pyblish.api
|
||||
from openpype.lib import TextDef
|
||||
from openpype.pipeline.publish import OpenPypePyblishPluginMixin
|
||||
|
|
@ -9,11 +6,35 @@ from openpype.pipeline.publish import OpenPypePyblishPluginMixin
|
|||
|
||||
class CollectDeadlinePools(pyblish.api.InstancePlugin,
|
||||
OpenPypePyblishPluginMixin):
|
||||
"""Collect pools from instance if present, from Setting otherwise."""
|
||||
"""Collect pools from instance or Publisher attributes, from Setting
|
||||
otherwise.
|
||||
|
||||
Pools are used to control which DL workers could render the job.
|
||||
|
||||
Pools might be set:
|
||||
- directly on the instance (set directly in DCC)
|
||||
- from Publisher attributes
|
||||
- from defaults from Settings.
|
||||
|
||||
Publisher attributes could be shown even for instances that should be
|
||||
rendered locally as visibility is driven by product type of the instance
|
||||
(which will be `render` most likely).
|
||||
(Might be resolved in the future and class attribute 'families' should
|
||||
be cleaned up.)
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.420
|
||||
label = "Collect Deadline Pools"
|
||||
families = ["rendering",
|
||||
hosts = ["aftereffects",
|
||||
"fusion",
|
||||
"harmony"
|
||||
"nuke",
|
||||
"maya",
|
||||
"max"]
|
||||
|
||||
families = ["render",
|
||||
"rendering",
|
||||
"render.farm",
|
||||
"renderFarm",
|
||||
"renderlayer",
|
||||
|
|
@ -30,7 +51,6 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin,
|
|||
cls.secondary_pool = settings.get("secondary_pool", None)
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
if not instance.data.get("primaryPool"):
|
||||
instance.data["primaryPool"] = (
|
||||
|
|
@ -60,8 +80,12 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin,
|
|||
return [
|
||||
TextDef("primaryPool",
|
||||
label="Primary Pool",
|
||||
default=cls.primary_pool),
|
||||
default=cls.primary_pool,
|
||||
tooltip="Deadline primary pool, "
|
||||
"applicable for farm rendering"),
|
||||
TextDef("secondaryPool",
|
||||
label="Secondary Pool",
|
||||
default=cls.secondary_pool)
|
||||
default=cls.secondary_pool,
|
||||
tooltip="Deadline secondary pool, "
|
||||
"applicable for farm rendering")
|
||||
]
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from openpype.lib import (
|
|||
NumberDef
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class DeadlinePluginInfo():
|
||||
SceneFile = attr.ib(default=None)
|
||||
|
|
@ -41,6 +42,12 @@ class VrayRenderPluginInfo():
|
|||
SeparateFilesPerFrame = attr.ib(default=True)
|
||||
|
||||
|
||||
@attr.s
|
||||
class RedshiftRenderPluginInfo():
|
||||
SceneFile = attr.ib(default=None)
|
||||
Version = attr.ib(default=None)
|
||||
|
||||
|
||||
class HoudiniSubmitDeadline(
|
||||
abstract_submit_deadline.AbstractSubmitDeadline,
|
||||
OpenPypePyblishPluginMixin
|
||||
|
|
@ -262,6 +269,25 @@ class HoudiniSubmitDeadline(
|
|||
plugin_info = VrayRenderPluginInfo(
|
||||
InputFilename=instance.data["ifdFile"],
|
||||
)
|
||||
elif family == "redshift_rop":
|
||||
plugin_info = RedshiftRenderPluginInfo(
|
||||
SceneFile=instance.data["ifdFile"]
|
||||
)
|
||||
# Note: To use different versions of Redshift on Deadline
|
||||
# set the `REDSHIFT_VERSION` env variable in the Tools
|
||||
# settings in the AYON Application plugin. You will also
|
||||
# need to set that version in `Redshift.param` file
|
||||
# of the Redshift Deadline plugin:
|
||||
# [Redshift_Executable_*]
|
||||
# where * is the version number.
|
||||
if os.getenv("REDSHIFT_VERSION"):
|
||||
plugin_info.Version = os.getenv("REDSHIFT_VERSION")
|
||||
else:
|
||||
self.log.warning((
|
||||
"REDSHIFT_VERSION env variable is not set"
|
||||
" - using version configured in Deadline"
|
||||
))
|
||||
|
||||
else:
|
||||
self.log.error(
|
||||
"Family '%s' not supported yet to split render job",
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1"
|
||||
|
||||
# Adding file dependencies.
|
||||
if self.asset_dependencies:
|
||||
if not bool(os.environ.get("IS_TEST")) and self.asset_dependencies:
|
||||
dependencies = instance.context.data["fileDependencies"]
|
||||
for dependency in dependencies:
|
||||
job_info.AssetDependency += dependency
|
||||
|
|
@ -570,7 +570,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
|
||||
job_info = copy.deepcopy(self.job_info)
|
||||
|
||||
if self.asset_dependencies:
|
||||
if not bool(os.environ.get("IS_TEST")) and self.asset_dependencies:
|
||||
# Asset dependency to wait for at least the scene file to sync.
|
||||
job_info.AssetDependency += self.scene_path
|
||||
|
||||
|
|
|
|||
|
|
@ -297,7 +297,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
job_index)] = assembly_id # noqa: E501
|
||||
job_index += 1
|
||||
elif instance.data.get("bakingSubmissionJobs"):
|
||||
self.log.info("Adding baking submission jobs as dependencies...")
|
||||
self.log.info(
|
||||
"Adding baking submission jobs as dependencies..."
|
||||
)
|
||||
job_index = 0
|
||||
for assembly_id in instance.data["bakingSubmissionJobs"]:
|
||||
payload["JobInfo"]["JobDependency{}".format(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class DJVViewAction(BaseAction):
|
|||
description = "DJV View Launcher"
|
||||
icon = statics_icon("app_icons", "djvView.png")
|
||||
|
||||
type = 'Application'
|
||||
type = "Application"
|
||||
|
||||
allowed_types = [
|
||||
"cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg",
|
||||
|
|
@ -60,7 +60,7 @@ class DJVViewAction(BaseAction):
|
|||
return False
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
if event['data'].get('values', {}):
|
||||
if event["data"].get("values", {}):
|
||||
return
|
||||
|
||||
entity = entities[0]
|
||||
|
|
@ -70,32 +70,32 @@ class DJVViewAction(BaseAction):
|
|||
if entity_type == "assetversion":
|
||||
if (
|
||||
entity[
|
||||
'components'
|
||||
][0]['file_type'][1:] in self.allowed_types
|
||||
"components"
|
||||
][0]["file_type"][1:] in self.allowed_types
|
||||
):
|
||||
versions.append(entity)
|
||||
else:
|
||||
master_entity = entity
|
||||
if entity_type == "task":
|
||||
master_entity = entity['parent']
|
||||
master_entity = entity["parent"]
|
||||
|
||||
for asset in master_entity['assets']:
|
||||
for version in asset['versions']:
|
||||
for asset in master_entity["assets"]:
|
||||
for version in asset["versions"]:
|
||||
# Get only AssetVersion of selected task
|
||||
if (
|
||||
entity_type == "task" and
|
||||
version['task']['id'] != entity['id']
|
||||
version["task"]["id"] != entity["id"]
|
||||
):
|
||||
continue
|
||||
# Get only components with allowed type
|
||||
filetype = version['components'][0]['file_type']
|
||||
filetype = version["components"][0]["file_type"]
|
||||
if filetype[1:] in self.allowed_types:
|
||||
versions.append(version)
|
||||
|
||||
if len(versions) < 1:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'There are no Asset Versions to open.'
|
||||
"success": False,
|
||||
"message": "There are no Asset Versions to open."
|
||||
}
|
||||
|
||||
# TODO sort them (somehow?)
|
||||
|
|
@ -134,68 +134,68 @@ class DJVViewAction(BaseAction):
|
|||
last_available = None
|
||||
select_value = None
|
||||
for version in versions:
|
||||
for component in version['components']:
|
||||
for component in version["components"]:
|
||||
label = base_label.format(
|
||||
str(version['version']).zfill(3),
|
||||
version['asset']['type']['name'],
|
||||
component['name']
|
||||
str(version["version"]).zfill(3),
|
||||
version["asset"]["type"]["name"],
|
||||
component["name"]
|
||||
)
|
||||
|
||||
try:
|
||||
location = component[
|
||||
'component_locations'
|
||||
][0]['location']
|
||||
"component_locations"
|
||||
][0]["location"]
|
||||
file_path = location.get_filesystem_path(component)
|
||||
except Exception:
|
||||
file_path = component[
|
||||
'component_locations'
|
||||
][0]['resource_identifier']
|
||||
"component_locations"
|
||||
][0]["resource_identifier"]
|
||||
|
||||
if os.path.isdir(os.path.dirname(file_path)):
|
||||
last_available = file_path
|
||||
if component['name'] == default_component:
|
||||
if component["name"] == default_component:
|
||||
select_value = file_path
|
||||
version_items.append(
|
||||
{'label': label, 'value': file_path}
|
||||
{"label": label, "value": file_path}
|
||||
)
|
||||
|
||||
if len(version_items) == 0:
|
||||
return {
|
||||
'success': False,
|
||||
'message': (
|
||||
'There are no Asset Versions with accessible path.'
|
||||
"success": False,
|
||||
"message": (
|
||||
"There are no Asset Versions with accessible path."
|
||||
)
|
||||
}
|
||||
|
||||
item = {
|
||||
'label': 'Items to view',
|
||||
'type': 'enumerator',
|
||||
'name': 'path',
|
||||
'data': sorted(
|
||||
"label": "Items to view",
|
||||
"type": "enumerator",
|
||||
"name": "path",
|
||||
"data": sorted(
|
||||
version_items,
|
||||
key=itemgetter('label'),
|
||||
key=itemgetter("label"),
|
||||
reverse=True
|
||||
)
|
||||
}
|
||||
if select_value is not None:
|
||||
item['value'] = select_value
|
||||
item["value"] = select_value
|
||||
else:
|
||||
item['value'] = last_available
|
||||
item["value"] = last_available
|
||||
|
||||
items.append(item)
|
||||
|
||||
return {'items': items}
|
||||
return {"items": items}
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
"""Callback method for DJVView action."""
|
||||
|
||||
# Launching application
|
||||
event_data = event["data"]
|
||||
if "values" not in event_data:
|
||||
event_values = event["data"].get("values")
|
||||
if not event_values:
|
||||
return
|
||||
|
||||
djv_app_name = event_data["djv_app_name"]
|
||||
app = self.applicaion_manager.applications.get(djv_app_name)
|
||||
djv_app_name = event_values["djv_app_name"]
|
||||
app = self.application_manager.applications.get(djv_app_name)
|
||||
executable = None
|
||||
if app is not None:
|
||||
executable = app.find_executable()
|
||||
|
|
@ -206,18 +206,21 @@ class DJVViewAction(BaseAction):
|
|||
"message": "Couldn't find DJV executable."
|
||||
}
|
||||
|
||||
filpath = os.path.normpath(event_data["values"]["path"])
|
||||
filpath = os.path.normpath(event_values["path"])
|
||||
|
||||
cmd = [
|
||||
# DJV path
|
||||
executable,
|
||||
str(executable),
|
||||
# PATH TO COMPONENT
|
||||
filpath
|
||||
]
|
||||
|
||||
try:
|
||||
# Run DJV with these commands
|
||||
subprocess.Popen(cmd)
|
||||
_process = subprocess.Popen(cmd)
|
||||
# Keep process in memory for some time
|
||||
time.sleep(0.1)
|
||||
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"success": False,
|
||||
|
|
|
|||
|
|
@ -64,8 +64,10 @@ def clear_credentials():
|
|||
user_registry = OpenPypeSecureRegistry("kitsu_user")
|
||||
|
||||
# Set local settings
|
||||
user_registry.delete_item("login")
|
||||
user_registry.delete_item("password")
|
||||
if user_registry.get_item("login", None) is not None:
|
||||
user_registry.delete_item("login")
|
||||
if user_registry.get_item("password", None) is not None:
|
||||
user_registry.delete_item("password")
|
||||
|
||||
|
||||
def save_credentials(login: str, password: str):
|
||||
|
|
@ -92,8 +94,9 @@ def load_credentials() -> Tuple[str, str]:
|
|||
# Get user registry
|
||||
user_registry = OpenPypeSecureRegistry("kitsu_user")
|
||||
|
||||
return user_registry.get_item("login", None), user_registry.get_item(
|
||||
"password", None
|
||||
return (
|
||||
user_registry.get_item("login", None),
|
||||
user_registry.get_item("password", None)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,41 +58,13 @@ def get_template_name_profiles(
|
|||
if not project_settings:
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
profiles = (
|
||||
return copy.deepcopy(
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["publish"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
if profiles:
|
||||
return copy.deepcopy(profiles)
|
||||
|
||||
# Use legacy approach for cases new settings are not filled yet for the
|
||||
# project
|
||||
legacy_profiles = (
|
||||
project_settings
|
||||
["global"]
|
||||
["publish"]
|
||||
["IntegrateAssetNew"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
if legacy_profiles:
|
||||
if not logger:
|
||||
logger = Logger.get_logger("get_template_name_profiles")
|
||||
|
||||
logger.warning((
|
||||
"Project \"{}\" is using legacy access to publish template."
|
||||
" It is recommended to move settings to new location"
|
||||
" 'project_settings/global/tools/publish/template_name_profiles'."
|
||||
).format(project_name))
|
||||
|
||||
# Replace "tasks" key with "task_names"
|
||||
profiles = []
|
||||
for profile in copy.deepcopy(legacy_profiles):
|
||||
profile["task_names"] = profile.pop("tasks", [])
|
||||
profiles.append(profile)
|
||||
return profiles
|
||||
|
||||
|
||||
def get_hero_template_name_profiles(
|
||||
|
|
@ -121,36 +93,13 @@ def get_hero_template_name_profiles(
|
|||
if not project_settings:
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
profiles = (
|
||||
return copy.deepcopy(
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["publish"]
|
||||
["hero_template_name_profiles"]
|
||||
)
|
||||
if profiles:
|
||||
return copy.deepcopy(profiles)
|
||||
|
||||
# Use legacy approach for cases new settings are not filled yet for the
|
||||
# project
|
||||
legacy_profiles = copy.deepcopy(
|
||||
project_settings
|
||||
["global"]
|
||||
["publish"]
|
||||
["IntegrateHeroVersion"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
if legacy_profiles:
|
||||
if not logger:
|
||||
logger = Logger.get_logger("get_hero_template_name_profiles")
|
||||
|
||||
logger.warning((
|
||||
"Project \"{}\" is using legacy access to hero publish template."
|
||||
" It is recommended to move settings to new location"
|
||||
" 'project_settings/global/tools/publish/"
|
||||
"hero_template_name_profiles'."
|
||||
).format(project_name))
|
||||
return legacy_profiles
|
||||
|
||||
|
||||
def get_publish_template_name(
|
||||
|
|
|
|||
|
|
@ -190,47 +190,18 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
project_task_types = project_doc["config"]["tasks"]
|
||||
|
||||
for instance in context:
|
||||
asset_doc = instance.data.get("assetEntity")
|
||||
anatomy_updates = {
|
||||
anatomy_data = copy.deepcopy(context.data["anatomyData"])
|
||||
anatomy_data.update({
|
||||
"family": instance.data["family"],
|
||||
"subset": instance.data["subset"],
|
||||
}
|
||||
if asset_doc:
|
||||
parents = asset_doc["data"].get("parents") or list()
|
||||
parent_name = project_doc["name"]
|
||||
if parents:
|
||||
parent_name = parents[-1]
|
||||
})
|
||||
|
||||
hierarchy = "/".join(parents)
|
||||
anatomy_updates.update({
|
||||
"asset": asset_doc["name"],
|
||||
"hierarchy": hierarchy,
|
||||
"parent": parent_name,
|
||||
"folder": {
|
||||
"name": asset_doc["name"],
|
||||
},
|
||||
})
|
||||
|
||||
# Task
|
||||
task_type = None
|
||||
task_name = instance.data.get("task")
|
||||
if task_name:
|
||||
asset_tasks = asset_doc["data"]["tasks"]
|
||||
task_type = asset_tasks.get(task_name, {}).get("type")
|
||||
task_code = (
|
||||
project_task_types
|
||||
.get(task_type, {})
|
||||
.get("short_name")
|
||||
)
|
||||
anatomy_updates["task"] = {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_code
|
||||
}
|
||||
self._fill_asset_data(instance, project_doc, anatomy_data)
|
||||
self._fill_task_data(instance, project_task_types, anatomy_data)
|
||||
|
||||
# Define version
|
||||
if self.follow_workfile_version:
|
||||
version_number = context.data('version')
|
||||
version_number = context.data("version")
|
||||
else:
|
||||
version_number = instance.data.get("version")
|
||||
|
||||
|
|
@ -242,6 +213,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
|
||||
# If version is not specified for instance or context
|
||||
if version_number is None:
|
||||
task_data = anatomy_data.get("task") or {}
|
||||
task_name = task_data.get("name")
|
||||
task_type = task_data.get("type")
|
||||
version_number = get_versioning_start(
|
||||
context.data["projectName"],
|
||||
instance.context.data["hostName"],
|
||||
|
|
@ -250,29 +224,26 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
family=instance.data["family"],
|
||||
subset=instance.data["subset"]
|
||||
)
|
||||
anatomy_updates["version"] = version_number
|
||||
anatomy_data["version"] = version_number
|
||||
|
||||
# Additional data
|
||||
resolution_width = instance.data.get("resolutionWidth")
|
||||
if resolution_width:
|
||||
anatomy_updates["resolution_width"] = resolution_width
|
||||
anatomy_data["resolution_width"] = resolution_width
|
||||
|
||||
resolution_height = instance.data.get("resolutionHeight")
|
||||
if resolution_height:
|
||||
anatomy_updates["resolution_height"] = resolution_height
|
||||
anatomy_data["resolution_height"] = resolution_height
|
||||
|
||||
pixel_aspect = instance.data.get("pixelAspect")
|
||||
if pixel_aspect:
|
||||
anatomy_updates["pixel_aspect"] = float(
|
||||
anatomy_data["pixel_aspect"] = float(
|
||||
"{:0.2f}".format(float(pixel_aspect))
|
||||
)
|
||||
|
||||
fps = instance.data.get("fps")
|
||||
if fps:
|
||||
anatomy_updates["fps"] = float("{:0.2f}".format(float(fps)))
|
||||
|
||||
anatomy_data = copy.deepcopy(context.data["anatomyData"])
|
||||
anatomy_data.update(anatomy_updates)
|
||||
anatomy_data["fps"] = float("{:0.2f}".format(float(fps)))
|
||||
|
||||
# Store anatomy data
|
||||
instance.data["projectEntity"] = project_doc
|
||||
|
|
@ -288,3 +259,157 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
instance_name,
|
||||
json.dumps(anatomy_data, indent=4)
|
||||
))
|
||||
|
||||
def _fill_asset_data(self, instance, project_doc, anatomy_data):
|
||||
# QUESTION should we make sure that all asset data are poped if asset
|
||||
# data cannot be found?
|
||||
# - 'asset', 'hierarchy', 'parent', 'folder'
|
||||
asset_doc = instance.data.get("assetEntity")
|
||||
if asset_doc:
|
||||
parents = asset_doc["data"].get("parents") or list()
|
||||
parent_name = project_doc["name"]
|
||||
if parents:
|
||||
parent_name = parents[-1]
|
||||
|
||||
hierarchy = "/".join(parents)
|
||||
anatomy_data.update({
|
||||
"asset": asset_doc["name"],
|
||||
"hierarchy": hierarchy,
|
||||
"parent": parent_name,
|
||||
"folder": {
|
||||
"name": asset_doc["name"],
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
if instance.data.get("newAssetPublishing"):
|
||||
hierarchy = instance.data["hierarchy"]
|
||||
anatomy_data["hierarchy"] = hierarchy
|
||||
|
||||
parent_name = project_doc["name"]
|
||||
if hierarchy:
|
||||
parent_name = hierarchy.split("/")[-1]
|
||||
|
||||
asset_name = instance.data["asset"].split("/")[-1]
|
||||
anatomy_data.update({
|
||||
"asset": asset_name,
|
||||
"hierarchy": hierarchy,
|
||||
"parent": parent_name,
|
||||
"folder": {
|
||||
"name": asset_name,
|
||||
},
|
||||
})
|
||||
|
||||
def _fill_task_data(self, instance, project_task_types, anatomy_data):
|
||||
# QUESTION should we make sure that all task data are poped if task
|
||||
# data cannot be resolved?
|
||||
# - 'task'
|
||||
|
||||
# Skip if there is no task
|
||||
task_name = instance.data.get("task")
|
||||
if not task_name:
|
||||
return
|
||||
|
||||
# Find task data based on asset entity
|
||||
asset_doc = instance.data.get("assetEntity")
|
||||
task_data = self._get_task_data_from_asset(
|
||||
asset_doc, task_name, project_task_types
|
||||
)
|
||||
if task_data:
|
||||
# Fill task data
|
||||
# - if we're in editorial, make sure the task type is filled
|
||||
if (
|
||||
not instance.data.get("newAssetPublishing")
|
||||
or task_data["type"]
|
||||
):
|
||||
anatomy_data["task"] = task_data
|
||||
return
|
||||
|
||||
# New hierarchy is not created, so we can only skip rest of the logic
|
||||
if not instance.data.get("newAssetPublishing"):
|
||||
return
|
||||
|
||||
# Try to find task data based on hierarchy context and asset name
|
||||
hierarchy_context = instance.context.data.get("hierarchyContext")
|
||||
asset_name = instance.data.get("asset")
|
||||
if not hierarchy_context or not asset_name:
|
||||
return
|
||||
|
||||
project_name = instance.context.data["projectName"]
|
||||
# OpenPype approach vs AYON approach
|
||||
if "/" not in asset_name:
|
||||
tasks_info = self._find_tasks_info_in_hierarchy(
|
||||
hierarchy_context, asset_name
|
||||
)
|
||||
else:
|
||||
current_data = hierarchy_context.get(project_name, {})
|
||||
for key in asset_name.split("/"):
|
||||
if key:
|
||||
current_data = current_data.get("childs", {}).get(key, {})
|
||||
tasks_info = current_data.get("tasks", {})
|
||||
|
||||
task_info = tasks_info.get(task_name, {})
|
||||
task_type = task_info.get("type")
|
||||
task_code = (
|
||||
project_task_types
|
||||
.get(task_type, {})
|
||||
.get("short_name")
|
||||
)
|
||||
anatomy_data["task"] = {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_code
|
||||
}
|
||||
|
||||
def _get_task_data_from_asset(
|
||||
self, asset_doc, task_name, project_task_types
|
||||
):
|
||||
"""
|
||||
|
||||
Args:
|
||||
asset_doc (Union[dict[str, Any], None]): Asset document.
|
||||
task_name (Union[str, None]): Task name.
|
||||
project_task_types (dict[str, dict[str, Any]]): Project task
|
||||
types.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, str], None]: Task data or None if not found.
|
||||
"""
|
||||
|
||||
if not asset_doc or not task_name:
|
||||
return None
|
||||
|
||||
asset_tasks = asset_doc["data"]["tasks"]
|
||||
task_type = asset_tasks.get(task_name, {}).get("type")
|
||||
task_code = (
|
||||
project_task_types
|
||||
.get(task_type, {})
|
||||
.get("short_name")
|
||||
)
|
||||
return {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_code
|
||||
}
|
||||
|
||||
def _find_tasks_info_in_hierarchy(self, hierarchy_context, asset_name):
|
||||
"""Find tasks info for an asset in editorial hierarchy.
|
||||
|
||||
Args:
|
||||
hierarchy_context (dict[str, Any]): Editorial hierarchy context.
|
||||
asset_name (str): Asset name.
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, Any]]: Tasks info by name.
|
||||
"""
|
||||
|
||||
hierarchy_queue = collections.deque()
|
||||
hierarchy_queue.append(hierarchy_context)
|
||||
while hierarchy_queue:
|
||||
item = hierarchy_context.popleft()
|
||||
if asset_name in item:
|
||||
return item[asset_name].get("tasks") or {}
|
||||
|
||||
for subitem in item.values():
|
||||
hierarchy_queue.extend(subitem.get("childs") or [])
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -79,19 +79,6 @@ class CollectResourcesPath(pyblish.api.InstancePlugin):
|
|||
"representation": "TEMP"
|
||||
})
|
||||
|
||||
# Add fill keys for editorial publishing creating new entity
|
||||
# TODO handle in editorial plugin
|
||||
if instance.data.get("newAssetPublishing"):
|
||||
if "hierarchy" not in template_data:
|
||||
template_data["hierarchy"] = instance.data["hierarchy"]
|
||||
|
||||
if "asset" not in template_data:
|
||||
asset_name = instance.data["asset"].split("/")[-1]
|
||||
template_data["asset"] = asset_name
|
||||
template_data["folder"] = {
|
||||
"name": asset_name
|
||||
}
|
||||
|
||||
publish_templates = anatomy.templates_obj["publish"]
|
||||
if "folder" in publish_templates:
|
||||
publish_folder = publish_templates["folder"].format_strict(
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
"files": dst_filename,
|
||||
"stagingDir": dst_staging,
|
||||
"thumbnail": True,
|
||||
"tags": ["thumbnail"]
|
||||
"tags": ["thumbnail"],
|
||||
"outputName": "thumbnail",
|
||||
}
|
||||
|
||||
# adding representation
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
# permissions error on files (files were used or user didn't have perms)
|
||||
# *but all other plugins must be sucessfully completed
|
||||
|
||||
template_name_profiles = []
|
||||
_default_template_name = "hero"
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 9.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
|
|
@ -15,6 +15,11 @@
|
|||
"copy_status": false,
|
||||
"force_sync": false
|
||||
},
|
||||
"hooks": {
|
||||
"InstallPySideToFusion": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"CreateSaver": {
|
||||
"temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
{
|
||||
"unit_scale_settings": {
|
||||
"enabled": true,
|
||||
"scene_unit_scale": "Meters"
|
||||
},
|
||||
"imageio": {
|
||||
"activate_host_color_management": true,
|
||||
"ocio_config": {
|
||||
|
|
|
|||
|
|
@ -1289,6 +1289,7 @@
|
|||
"twoSidedLighting": true,
|
||||
"lineAAEnable": true,
|
||||
"multiSample": 8,
|
||||
"loadTextures": false,
|
||||
"useDefaultMaterial": false,
|
||||
"wireframeOnShaded": false,
|
||||
"xray": false,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,29 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "hooks",
|
||||
"label": "Hooks",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"key": "InstallPySideToFusion",
|
||||
"label": "Install PySide2",
|
||||
"is_group": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,34 @@
|
|||
"label": "Max",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "unit_scale_settings",
|
||||
"type": "dict",
|
||||
"label": "Set Unit Scale",
|
||||
"collapsible": true,
|
||||
"is_group": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"key": "scene_unit_scale",
|
||||
"label": "Scene Unit Scale",
|
||||
"type": "enum",
|
||||
"multiselection": false,
|
||||
"defaults": "exr",
|
||||
"enum_items": [
|
||||
{"Millimeters": "mm"},
|
||||
{"Centimeters": "cm"},
|
||||
{"Meters": "m"},
|
||||
{"Kilometers": "km"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "imageio",
|
||||
"type": "dict",
|
||||
|
|
|
|||
|
|
@ -1023,49 +1023,6 @@
|
|||
{
|
||||
"type": "label",
|
||||
"label": "<b>NOTE:</b> Hero publish template profiles settings were moved to <a href=\"settings://project_settings/global/tools/publish/hero_template_name_profiles\"><b>Tools/Publish/Hero template name profiles</b></a>. Please move values there."
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "template_name_profiles",
|
||||
"label": "Template name profiles (DEPRECATED)",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "task_names",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "template_name",
|
||||
"label": "Template name",
|
||||
"tooltip": "Name of template from Anatomy templates"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -236,6 +236,11 @@
|
|||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "loadTextures",
|
||||
"label": "Load Textures"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "useDefaultMaterial",
|
||||
|
|
@ -908,6 +913,12 @@
|
|||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "loadTextures",
|
||||
"label": "Load Textures",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "useDefaultMaterial",
|
||||
|
|
|
|||
|
|
@ -140,12 +140,10 @@ class SiteSyncModel:
|
|||
Union[dict[str, Any], None]: Site icon definition.
|
||||
"""
|
||||
|
||||
if not project_name:
|
||||
if not project_name or not self.is_site_sync_enabled(project_name):
|
||||
return None
|
||||
|
||||
active_site = self.get_active_site(project_name)
|
||||
provider = self._get_provider_for_site(project_name, active_site)
|
||||
return self._get_provider_icon(provider)
|
||||
return self._get_site_icon_def(project_name, active_site)
|
||||
|
||||
def get_remote_site_icon_def(self, project_name):
|
||||
"""Remote site icon definition.
|
||||
|
|
@ -160,7 +158,14 @@ class SiteSyncModel:
|
|||
if not project_name or not self.is_site_sync_enabled(project_name):
|
||||
return None
|
||||
remote_site = self.get_remote_site(project_name)
|
||||
provider = self._get_provider_for_site(project_name, remote_site)
|
||||
return self._get_site_icon_def(project_name, remote_site)
|
||||
|
||||
def _get_site_icon_def(self, project_name, site_name):
|
||||
# use different icon for studio even if provider is 'local_drive'
|
||||
if site_name == self._site_sync_addon.DEFAULT_SITE:
|
||||
provider = "studio"
|
||||
else:
|
||||
provider = self._get_provider_for_site(project_name, site_name)
|
||||
return self._get_provider_icon(provider)
|
||||
|
||||
def get_version_sync_availability(self, project_name, version_ids):
|
||||
|
|
|
|||
|
|
@ -84,9 +84,9 @@ class SceneInventoryController:
|
|||
def get_containers(self):
|
||||
host = self._host
|
||||
if isinstance(host, ILoadHost):
|
||||
return host.get_containers()
|
||||
return list(host.get_containers())
|
||||
elif hasattr(host, "ls"):
|
||||
return host.ls()
|
||||
return list(host.ls())
|
||||
return []
|
||||
|
||||
# Site Sync methods
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from openpype.pipeline import (
|
|||
)
|
||||
from openpype.style import get_default_entity_icon_color
|
||||
from openpype.tools.utils.models import TreeModel, Item
|
||||
from openpype.tools.ayon_utils.widgets import get_qt_icon
|
||||
|
||||
|
||||
def walk_hierarchy(node):
|
||||
|
|
@ -71,8 +72,8 @@ class InventoryModel(TreeModel):
|
|||
site_icons = self._controller.get_site_provider_icons()
|
||||
|
||||
self._site_icons = {
|
||||
provider: QtGui.QIcon(icon_path)
|
||||
for provider, icon_path in site_icons.items()
|
||||
provider: get_qt_icon(icon_def)
|
||||
for provider, icon_def in site_icons.items()
|
||||
}
|
||||
|
||||
def outdated(self, item):
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ class SiteSyncModel:
|
|||
|
||||
if not self.is_sync_server_enabled():
|
||||
return {}
|
||||
site_sync = self._get_sync_server_module()
|
||||
return site_sync.get_site_icons()
|
||||
site_sync_addon = self._get_sync_server_module()
|
||||
return site_sync_addon.get_site_icons()
|
||||
|
||||
def get_sites_information(self):
|
||||
return {
|
||||
|
|
@ -150,23 +150,23 @@ class SiteSyncModel:
|
|||
return self._remote_site_provider
|
||||
|
||||
def _cache_sites(self):
|
||||
site_sync = self._get_sync_server_module()
|
||||
active_site = None
|
||||
remote_site = None
|
||||
active_site_provider = None
|
||||
remote_site_provider = None
|
||||
if site_sync is not None:
|
||||
if self.is_sync_server_enabled():
|
||||
site_sync = self._get_sync_server_module()
|
||||
project_name = self._controller.get_current_project_name()
|
||||
active_site = site_sync.get_active_site(project_name)
|
||||
remote_site = site_sync.get_remote_site(project_name)
|
||||
active_site_provider = "studio"
|
||||
remote_site_provider = "studio"
|
||||
if active_site != "studio":
|
||||
active_site_provider = site_sync.get_active_provider(
|
||||
active_site_provider = site_sync.get_provider_for_site(
|
||||
project_name, active_site
|
||||
)
|
||||
if remote_site != "studio":
|
||||
remote_site_provider = site_sync.get_active_provider(
|
||||
remote_site_provider = site_sync.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -606,7 +606,7 @@ class PublishWorkfilesModel:
|
|||
print("Failed to format workfile path: {}".format(exc))
|
||||
|
||||
dirpath, filename = os.path.split(workfile_path)
|
||||
created_at = arrow.get(repre_entity["createdAt"].to("local"))
|
||||
created_at = arrow.get(repre_entity["createdAt"]).to("local")
|
||||
return FileItem(
|
||||
dirpath,
|
||||
filename,
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
super(ScreenMarquee, self).__init__(parent=parent)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.FramelessWindowHint
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.FramelessWindowHint
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
| QtCore.Qt.Tool)
|
||||
)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
self.setMouseTracking(True)
|
||||
|
|
@ -210,6 +211,9 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
"""
|
||||
|
||||
tool = cls()
|
||||
# Activate so Escape event is not ignored.
|
||||
tool.setWindowState(QtCore.Qt.WindowActive)
|
||||
# Exec dialog and return captured pixmap.
|
||||
tool.exec_()
|
||||
return tool.get_captured_pixmap()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ from .widgets import (
|
|||
)
|
||||
|
||||
|
||||
class PublisherWindow(QtWidgets.QDialog):
|
||||
class PublisherWindow(QtWidgets.QWidget):
|
||||
"""Main window of publisher."""
|
||||
default_width = 1300
|
||||
default_height = 800
|
||||
|
|
@ -50,7 +50,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
publish_footer_spacer = 2
|
||||
|
||||
def __init__(self, parent=None, controller=None, reset_on_show=None):
|
||||
super(PublisherWindow, self).__init__(parent)
|
||||
super(PublisherWindow, self).__init__()
|
||||
|
||||
self.setObjectName("PublishWindow")
|
||||
|
||||
|
|
@ -64,17 +64,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
if reset_on_show is None:
|
||||
reset_on_show = True
|
||||
|
||||
if parent is None:
|
||||
on_top_flag = QtCore.Qt.WindowStaysOnTopHint
|
||||
else:
|
||||
on_top_flag = QtCore.Qt.Dialog
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowTitleHint
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.WindowTitleHint
|
||||
| QtCore.Qt.WindowMaximizeButtonHint
|
||||
| QtCore.Qt.WindowMinimizeButtonHint
|
||||
| QtCore.Qt.WindowCloseButtonHint
|
||||
| on_top_flag
|
||||
)
|
||||
|
||||
if controller is None:
|
||||
|
|
@ -189,7 +184,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller, content_stacked_widget
|
||||
)
|
||||
|
||||
report_widget = ReportPageWidget(controller, parent)
|
||||
report_widget = ReportPageWidget(controller, content_stacked_widget)
|
||||
|
||||
# Details - Publish details
|
||||
publish_details_widget = PublishReportViewerWidget(
|
||||
|
|
@ -299,6 +294,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.event_system.add_callback(
|
||||
"publish.process.stopped", self._on_publish_stop
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"publish.process.instance.changed", self._on_instance_change
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"publish.process.plugin.changed", self._on_plugin_change
|
||||
)
|
||||
controller.event_system.add_callback(
|
||||
"show.card.message", self._on_overlay_message
|
||||
)
|
||||
|
|
@ -557,6 +558,18 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._reset_on_show = False
|
||||
self.reset()
|
||||
|
||||
def _make_sure_on_top(self):
|
||||
"""Raise window to top and activate it.
|
||||
|
||||
This may not work for some DCCs without Qt.
|
||||
"""
|
||||
|
||||
if not self._window_is_visible:
|
||||
self.show()
|
||||
|
||||
self.setWindowState(QtCore.Qt.WindowActive)
|
||||
self.raise_()
|
||||
|
||||
def _checks_before_save(self, explicit_save):
|
||||
"""Save of changes may trigger some issues.
|
||||
|
||||
|
|
@ -869,6 +882,12 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
if self._is_on_create_tab():
|
||||
self._go_to_publish_tab()
|
||||
|
||||
def _on_instance_change(self):
|
||||
self._make_sure_on_top()
|
||||
|
||||
def _on_plugin_change(self):
|
||||
self._make_sure_on_top()
|
||||
|
||||
def _on_publish_validated_change(self, event):
|
||||
if event["value"]:
|
||||
self._validate_btn.setEnabled(False)
|
||||
|
|
@ -879,6 +898,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
self._comment_input.setText("")
|
||||
|
||||
def _on_publish_stop(self):
|
||||
self._make_sure_on_top()
|
||||
self._set_publish_overlay_visibility(False)
|
||||
self._reset_btn.setEnabled(True)
|
||||
self._stop_btn.setEnabled(False)
|
||||
|
|
|
|||
|
|
@ -1230,12 +1230,12 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
|
||||
version_ids = list()
|
||||
|
||||
version_docs_by_parent_id = {}
|
||||
version_docs_by_parent_id_and_name = collections.defaultdict(dict)
|
||||
for version_doc in version_docs:
|
||||
parent_id = version_doc["parent"]
|
||||
if parent_id not in version_docs_by_parent_id:
|
||||
version_ids.append(version_doc["_id"])
|
||||
version_docs_by_parent_id[parent_id] = version_doc
|
||||
version_ids.append(version_doc["_id"])
|
||||
name = version_doc["name"]
|
||||
version_docs_by_parent_id_and_name[parent_id][name] = version_doc
|
||||
|
||||
hero_version_docs_by_parent_id = {}
|
||||
for hero_version_doc in hero_version_docs:
|
||||
|
|
@ -1293,13 +1293,32 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
repre_doc = _repres.get(container_repre_name)
|
||||
|
||||
if not repre_doc:
|
||||
version_doc = version_docs_by_parent_id[subset_id]
|
||||
version_id = version_doc["_id"]
|
||||
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
|
||||
if selected_representation:
|
||||
repre_doc = repres_by_name[selected_representation]
|
||||
version_docs_by_name = version_docs_by_parent_id_and_name[
|
||||
subset_id
|
||||
]
|
||||
|
||||
# If asset or subset are selected for switching, we use latest
|
||||
# version else we try to keep the current container version.
|
||||
if (
|
||||
selected_asset not in (None, container_asset_name)
|
||||
or selected_subset not in (None, container_subset_name)
|
||||
):
|
||||
version_name = max(version_docs_by_name)
|
||||
else:
|
||||
repre_doc = repres_by_name[container_repre_name]
|
||||
version_name = container_version["name"]
|
||||
|
||||
version_doc = version_docs_by_name[version_name]
|
||||
version_id = version_doc["_id"]
|
||||
repres_docs_by_name = repre_docs_by_parent_id_by_name[
|
||||
version_id
|
||||
]
|
||||
|
||||
if selected_representation:
|
||||
repres_name = selected_representation
|
||||
else:
|
||||
repres_name = container_repre_name
|
||||
|
||||
repre_doc = repres_docs_by_name[repres_name]
|
||||
|
||||
error = None
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.18.2-nightly.2"
|
||||
__version__ = "3.18.3"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.18.1" # OpenPype
|
||||
version = "3.18.3" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
@ -181,3 +181,8 @@ reportMissingTypeStubs = false
|
|||
|
||||
[tool.poetry.extras]
|
||||
docs = ["Sphinx", "furo", "sphinxcontrib-napoleon"]
|
||||
|
||||
[tool.pydocstyle]
|
||||
inherit = false
|
||||
convetion = "google"
|
||||
match = "(?!test_).*\\.py"
|
||||
|
|
|
|||
|
|
@ -697,13 +697,6 @@ class IntegrateHeroVersionModel(BaseSettingsModel):
|
|||
optional: bool = Field(False, title="Optional")
|
||||
active: bool = Field(True, title="Active")
|
||||
families: list[str] = Field(default_factory=list, title="Families")
|
||||
# TODO remove when removed from client code
|
||||
template_name_profiles: list[IntegrateHeroTemplateNameProfileModel] = (
|
||||
Field(
|
||||
default_factory=list,
|
||||
title="Template name profiles"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CleanUpModel(BaseSettingsModel):
|
||||
|
|
@ -1049,19 +1042,6 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"layout",
|
||||
"mayaScene",
|
||||
"simpleUnrealTexture"
|
||||
],
|
||||
"template_name_profiles": [
|
||||
{
|
||||
"product_types": [
|
||||
"simpleUnrealTexture"
|
||||
],
|
||||
"hosts": [
|
||||
"standalonepublisher"
|
||||
],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
"template_name": "simpleUnrealTextureHero"
|
||||
}
|
||||
]
|
||||
},
|
||||
"CleanUp": {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.5"
|
||||
__version__ = "0.1.6"
|
||||
|
|
|
|||
|
|
@ -25,16 +25,6 @@ def _create_saver_instance_attributes_enum():
|
|||
]
|
||||
|
||||
|
||||
def _image_format_enum():
|
||||
return [
|
||||
{"value": "exr", "label": "exr"},
|
||||
{"value": "tga", "label": "tga"},
|
||||
{"value": "png", "label": "png"},
|
||||
{"value": "tif", "label": "tif"},
|
||||
{"value": "jpg", "label": "jpg"},
|
||||
]
|
||||
|
||||
|
||||
class CreateSaverPluginModel(BaseSettingsModel):
|
||||
_isGroup = True
|
||||
temp_rendering_path_template: str = Field(
|
||||
|
|
@ -49,9 +39,23 @@ class CreateSaverPluginModel(BaseSettingsModel):
|
|||
enum_resolver=_create_saver_instance_attributes_enum,
|
||||
title="Instance attributes"
|
||||
)
|
||||
image_format: str = Field(
|
||||
enum_resolver=_image_format_enum,
|
||||
title="Output Image Format"
|
||||
output_formats: list[str] = Field(
|
||||
default_factory=list,
|
||||
title="Output formats"
|
||||
)
|
||||
|
||||
|
||||
class HookOptionalModel(BaseSettingsModel):
|
||||
enabled: bool = Field(
|
||||
True,
|
||||
title="Enabled"
|
||||
)
|
||||
|
||||
|
||||
class HooksModel(BaseSettingsModel):
|
||||
InstallPySideToFusion: HookOptionalModel = Field(
|
||||
default_factory=HookOptionalModel,
|
||||
title="Install PySide2"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -71,6 +75,10 @@ class FusionSettings(BaseSettingsModel):
|
|||
default_factory=CopyFusionSettingsModel,
|
||||
title="Local Fusion profile settings"
|
||||
)
|
||||
hooks: HooksModel = Field(
|
||||
default_factory=HooksModel,
|
||||
title="Hooks"
|
||||
)
|
||||
create: CreatPluginsModel = Field(
|
||||
default_factory=CreatPluginsModel,
|
||||
title="Creator plugins"
|
||||
|
|
@ -93,6 +101,11 @@ DEFAULT_VALUES = {
|
|||
"copy_status": False,
|
||||
"force_sync": False
|
||||
},
|
||||
"hooks": {
|
||||
"InstallPySideToFusion": {
|
||||
"enabled": True
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"CreateSaver": {
|
||||
"temp_rendering_path_template": "{workdir}/renders/fusion/{product[name]}/{product[name]}.{frame}.{ext}",
|
||||
|
|
@ -104,7 +117,15 @@ DEFAULT_VALUES = {
|
|||
"reviewable",
|
||||
"farm_rendering"
|
||||
],
|
||||
"image_format": "exr"
|
||||
"output_formats": [
|
||||
"exr",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"tiff",
|
||||
"png",
|
||||
"tga"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.1"
|
||||
__version__ = "0.1.2"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.2.10"
|
||||
__version__ = "0.2.11"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,25 @@ from .publishers import (
|
|||
)
|
||||
|
||||
|
||||
def unit_scale_enum():
|
||||
"""Return enumerator for scene unit scale."""
|
||||
return [
|
||||
{"label": "mm", "value": "Millimeters"},
|
||||
{"label": "cm", "value": "Centimeters"},
|
||||
{"label": "m", "value": "Meters"},
|
||||
{"label": "km", "value": "Kilometers"}
|
||||
]
|
||||
|
||||
|
||||
class UnitScaleSettings(BaseSettingsModel):
|
||||
enabled: bool = Field(True, title="Enabled")
|
||||
scene_unit_scale: str = Field(
|
||||
"Centimeters",
|
||||
title="Scene Unit Scale",
|
||||
enum_resolver=unit_scale_enum
|
||||
)
|
||||
|
||||
|
||||
class PRTAttributesModel(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
name: str = Field(title="Name")
|
||||
|
|
@ -24,6 +43,10 @@ class PointCloudSettings(BaseSettingsModel):
|
|||
|
||||
|
||||
class MaxSettings(BaseSettingsModel):
|
||||
unit_scale_settings: UnitScaleSettings = Field(
|
||||
default_factory=UnitScaleSettings,
|
||||
title="Set Unit Scale"
|
||||
)
|
||||
imageio: ImageIOSettings = Field(
|
||||
default_factory=ImageIOSettings,
|
||||
title="Color Management (ImageIO)"
|
||||
|
|
@ -46,6 +69,10 @@ class MaxSettings(BaseSettingsModel):
|
|||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"unit_scale_settings": {
|
||||
"enabled": True,
|
||||
"scene_unit_scale": "Centimeters"
|
||||
},
|
||||
"RenderSettings": DEFAULT_RENDER_SETTINGS,
|
||||
"CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS,
|
||||
"PointCloud": {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.3"
|
||||
__version__ = "0.1.4"
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class ViewportOptionsSetting(BaseSettingsModel):
|
|||
True, title="Enable Anti-Aliasing", section="Anti-Aliasing"
|
||||
)
|
||||
multiSample: int = Field(8, title="Anti Aliasing Samples")
|
||||
loadTextures: bool = Field(False, title="Load Textures")
|
||||
useDefaultMaterial: bool = Field(False, title="Use Default Material")
|
||||
wireframeOnShaded: bool = Field(False, title="Wireframe On Shaded")
|
||||
xray: bool = Field(False, title="X-Ray")
|
||||
|
|
@ -302,6 +303,7 @@ DEFAULT_PLAYBLAST_SETTING = {
|
|||
"twoSidedLighting": True,
|
||||
"lineAAEnable": True,
|
||||
"multiSample": 8,
|
||||
"loadTextures": False,
|
||||
"useDefaultMaterial": False,
|
||||
"wireframeOnShaded": False,
|
||||
"xray": False,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,18 @@ python = ">=3.9.1,<3.10"
|
|||
aiohttp_json_rpc = "*" # TVPaint server
|
||||
aiohttp-middlewares = "^2.0.0"
|
||||
wsrpc_aiohttp = "^3.1.1" # websocket server
|
||||
Click = "^8"
|
||||
clique = "1.6.*"
|
||||
jsonschema = "^2.6.0"
|
||||
pymongo = "^3.11.2"
|
||||
log4mongo = "^1.7"
|
||||
pyblish-base = "^1.8.11"
|
||||
pynput = "^1.7.2" # Timers manager - TODO remove
|
||||
"Qt.py" = "^1.3.3"
|
||||
qtawesome = "0.7.3"
|
||||
speedcopy = "^2.1"
|
||||
six = "^1.15"
|
||||
qtawesome = "0.7.3"
|
||||
|
||||
[ayon.runtimeDependencies]
|
||||
OpenTimelineIO = "0.14.1"
|
||||
opencolorio = "2.2.1"
|
||||
Pillow = "9.5.0"
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@ max-complexity = 30
|
|||
[pylint.'MESSAGES CONTROL']
|
||||
disable = no-member
|
||||
|
||||
[pydocstyle]
|
||||
convention = google
|
||||
ignore = D107
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
omit = /tests
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ createNode objectSet -n "modelMain";
|
|||
addAttr -ci true -sn "attrPrefix" -ln "attrPrefix" -dt "string";
|
||||
addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string";
|
||||
addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string";
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
-dt "string";
|
||||
setAttr ".ihi" 0;
|
||||
setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:7364ea6776c9";
|
||||
|
|
@ -296,7 +296,7 @@ createNode objectSet -n "workfileMain";
|
|||
addAttr -ci true -sn "task" -ln "task" -dt "string";
|
||||
addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string";
|
||||
addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string";
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
-dt "string";
|
||||
setAttr ".ihi" 0;
|
||||
setAttr ".hio" yes;
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ createNode objectSet -n "modelMain";
|
|||
addAttr -ci true -sn "attrPrefix" -ln "attrPrefix" -dt "string";
|
||||
addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string";
|
||||
addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string";
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
-dt "string";
|
||||
setAttr ".ihi" 0;
|
||||
setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:7364ea6776c9";
|
||||
|
|
@ -296,7 +296,7 @@ createNode objectSet -n "workfileMain";
|
|||
addAttr -ci true -sn "task" -ln "task" -dt "string";
|
||||
addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string";
|
||||
addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string";
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
-dt "string";
|
||||
setAttr ".ihi" 0;
|
||||
setAttr ".hio" yes;
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ createNode objectSet -n "modelMain";
|
|||
addAttr -ci true -sn "attrPrefix" -ln "attrPrefix" -dt "string";
|
||||
addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string";
|
||||
addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string";
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
-dt "string";
|
||||
setAttr ".ihi" 0;
|
||||
setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:7364ea6776c9";
|
||||
|
|
@ -296,7 +296,7 @@ createNode objectSet -n "workfileMain";
|
|||
addAttr -ci true -sn "task" -ln "task" -dt "string";
|
||||
addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string";
|
||||
addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string";
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys"
|
||||
-dt "string";
|
||||
setAttr ".ihi" 0;
|
||||
setAttr ".hio" yes;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
from openpype.lib.events import EventSystem, QueuedEventSystem
|
||||
from functools import partial
|
||||
from openpype.lib.events import (
|
||||
EventSystem,
|
||||
QueuedEventSystem,
|
||||
weakref_partial,
|
||||
)
|
||||
|
||||
|
||||
def test_default_event_system():
|
||||
|
|
@ -81,3 +86,93 @@ def test_manual_event_system_queue():
|
|||
|
||||
assert output == expected_output, (
|
||||
"Callbacks were not called in correct order")
|
||||
|
||||
|
||||
def test_unordered_events():
|
||||
"""
|
||||
Validate if callbacks are triggered in order of their register.
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
def function_a():
|
||||
result.append("A")
|
||||
|
||||
def function_b():
|
||||
result.append("B")
|
||||
|
||||
def function_c():
|
||||
result.append("C")
|
||||
|
||||
# Without order
|
||||
event_system = QueuedEventSystem()
|
||||
event_system.add_callback("test", function_a)
|
||||
event_system.add_callback("test", function_b)
|
||||
event_system.add_callback("test", function_c)
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
assert result == ["A", "B", "C"]
|
||||
|
||||
|
||||
def test_ordered_events():
|
||||
"""
|
||||
Validate if callbacks are triggered by their order and order
|
||||
of their register.
|
||||
"""
|
||||
result = []
|
||||
|
||||
def function_a():
|
||||
result.append("A")
|
||||
|
||||
def function_b():
|
||||
result.append("B")
|
||||
|
||||
def function_c():
|
||||
result.append("C")
|
||||
|
||||
def function_d():
|
||||
result.append("D")
|
||||
|
||||
def function_e():
|
||||
result.append("E")
|
||||
|
||||
def function_f():
|
||||
result.append("F")
|
||||
|
||||
# Without order
|
||||
event_system = QueuedEventSystem()
|
||||
event_system.add_callback("test", function_a)
|
||||
event_system.add_callback("test", function_b, order=-10)
|
||||
event_system.add_callback("test", function_c, order=200)
|
||||
event_system.add_callback("test", function_d, order=150)
|
||||
event_system.add_callback("test", function_e)
|
||||
event_system.add_callback("test", function_f, order=200)
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
assert result == ["B", "A", "E", "D", "C", "F"]
|
||||
|
||||
|
||||
def test_events_partial_callbacks():
|
||||
"""
|
||||
Validate if partial callbacks are triggered.
|
||||
"""
|
||||
|
||||
result = []
|
||||
|
||||
def function(name):
|
||||
result.append(name)
|
||||
|
||||
def function_regular():
|
||||
result.append("regular")
|
||||
|
||||
event_system = QueuedEventSystem()
|
||||
event_system.add_callback("test", function_regular)
|
||||
event_system.add_callback("test", partial(function, "foo"))
|
||||
event_system.add_callback("test", weakref_partial(function, "bar"))
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
# Delete function should also make partial callbacks invalid
|
||||
del function
|
||||
event_system.emit("test", {}, "test")
|
||||
|
||||
assert result == ["regular", "bar", "regular"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue