Merge branch 'develop' into enhancement/OP-5920_abc-options-for-Pointcache-Animation-family

This commit is contained in:
Libor Batek 2024-02-22 11:27:03 +01:00 committed by GitHub
commit 8fee7b5240
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 1591 additions and 637 deletions

View file

@ -35,6 +35,14 @@ body:
label: Version
description: What version are you running? Look to OpenPype Tray
options:
- 3.18.8-nightly.1
- 3.18.7
- 3.18.7-nightly.5
- 3.18.7-nightly.4
- 3.18.7-nightly.3
- 3.18.7-nightly.2
- 3.18.7-nightly.1
- 3.18.6
- 3.18.6-nightly.2
- 3.18.6-nightly.1
- 3.18.5
@ -127,14 +135,6 @@ body:
- 3.15.11-nightly.3
- 3.15.11-nightly.2
- 3.15.11-nightly.1
- 3.15.10
- 3.15.10-nightly.2
- 3.15.10-nightly.1
- 3.15.9
- 3.15.9-nightly.2
- 3.15.9-nightly.1
- 3.15.8
- 3.15.8-nightly.3
validations:
required: true
- type: dropdown

View file

@ -1,6 +1,533 @@
# Changelog
## [3.18.7](https://github.com/ynput/OpenPype/tree/3.18.7)
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.6...3.18.7)
### **🆕 New features**
<details>
<summary>Chore: Wrapper for click proposal <a href="https://github.com/ynput/OpenPype/pull/5928">#5928</a></summary>
This is a proposal how to resolve issues with `click` python module. Issue https://github.com/ynput/OpenPype/issues/5921 reported that in Houdini 20+ is our click clashing with click in houdini, where is expected higher version. We can't update our version to support older pythons (NOTE older Python 3).
___
</details>
### **🚀 Enhancements**
<details>
<summary>Maya: Add repair action to hidden joints validator <a href="https://github.com/ynput/OpenPype/pull/6214">#6214</a></summary>
Joints Hidden is missing repair action, this adds it back
___
</details>
<details>
<summary>Blender: output node and EXR <a href="https://github.com/ynput/OpenPype/pull/6086">#6086</a></summary>
Output node now works correctly for Multilayer EXR and keeps existing links. The output now is handled entirely by the compositor node tree.
___
</details>
<details>
<summary>AYON Switch tool: Keep version after switch <a href="https://github.com/ynput/OpenPype/pull/6104">#6104</a></summary>
Keep version if only representation did change. The AYON variant of https://github.com/ynput/OpenPype/pull/4629
___
</details>
<details>
<summary>Loader AYON: Reset loader window on open <a href="https://github.com/ynput/OpenPype/pull/6170">#6170</a></summary>
Make sure loader tool is reset on each show.
___
</details>
<details>
<summary>Publisher: Show message with error on action failure <a href="https://github.com/ynput/OpenPype/pull/6179">#6179</a></summary>
This PR adds support for the publisher to show error message from running actions.Errors from actions will otherwise be hidden from user in various console outputs.Also include card for when action is finished.
___
</details>
<details>
<summary>AYON Applications: Remove djvview group from default applications <a href="https://github.com/ynput/OpenPype/pull/6188">#6188</a></summary>
The djv does not have group defined in models so the values are not used anywhere.
___
</details>
<details>
<summary>General: added fallback for broken ffprobe return <a href="https://github.com/ynput/OpenPype/pull/6189">#6189</a></summary>
Customer provided .exr returned width and height equal to 0 which caused error in `extract_thumbnail`. This tries to use oiiotool to get metadata about file, in our case it read it correctly.
___
</details>
<details>
<summary>Photoshop: High scaling in UIs <a href="https://github.com/ynput/OpenPype/pull/6190">#6190</a></summary>
Use `get_openpype_qt_app` to create `QApplication` in Photoshop.
___
</details>
<details>
<summary>Ftrack: Status update settings are not case insensitive. <a href="https://github.com/ynput/OpenPype/pull/6195">#6195</a></summary>
Make values for project_settings/ftrack/events/status_update case insensitive.
___
</details>
<details>
<summary>Thumbnail product filtering <a href="https://github.com/ynput/OpenPype/pull/6197">#6197</a></summary>
This PR introduces subset filtering for thumbnail extraction. This is to skip passes like zdepth which is not needed and can cause issues with extraction. Also speeds up publishing.
___
</details>
<details>
<summary>TimersManager: Idle dialog always on top <a href="https://github.com/ynput/OpenPype/pull/6201">#6201</a></summary>
Make stop timer dialog always on tophttps://app.clickup.com/t/6658547/OP-8033
___
</details>
<details>
<summary>AfterEffects: added toggle for applying values from DB during creation <a href="https://github.com/ynput/OpenPype/pull/6204">#6204</a></summary>
Previously values (resolution, duration) from Asset (eg. DB) were applied explicitly when instance of `render` product type was created. This PR adds toggle to Settings to disable this. (This allows artist to publish non standard length of composition, disabling of `Validate Scene Settings` is still required.)
___
</details>
<details>
<summary>Unreal: Update plugin commit <a href="https://github.com/ynput/OpenPype/pull/6208">#6208</a></summary>
Updated unreal plugin to latest main.
___
</details>
### **🐛 Bug fixes**
<details>
<summary>Traypublisher: editorial avoid audio tracks processing <a href="https://github.com/ynput/OpenPype/pull/6038">#6038</a></summary>
Avoiding audio tracks from EDL editorial publishing.
___
</details>
<details>
<summary>Resolve Inventory offsets clips when swapping versions <a href="https://github.com/ynput/OpenPype/pull/6128">#6128</a></summary>
Swapped version retain the offset and IDT of the timelime clip.closes: https://github.com/ynput/OpenPype/issues/6125
___
</details>
<details>
<summary>Publisher window as dialog <a href="https://github.com/ynput/OpenPype/pull/6176">#6176</a></summary>
Changing back Publisher window to QDialog.
___
</details>
<details>
<summary>Nuke: Validate write node fix error report - OP-8088 <a href="https://github.com/ynput/OpenPype/pull/6183">#6183</a></summary>
Report error was not printing the expected values from settings, but instead the values on the write node, leading to confusing messages like:
```
Traceback (most recent call last):
File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 135, in process
self._make_error(check)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 149, in _make_error
raise PublishXmlValidationError(
openpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!
Knob 'channels' > Correct: `rgb` > Wrong: `rgb`
```
This PR changes the error report to:
```
Traceback (most recent call last):
File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process
runner(*args)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 135, in process
self._make_error(check)
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\nuke\plugins\publish\validate_write_nodes.py", line 149, in _make_error
raise PublishXmlValidationError(
openpype.pipeline.publish.publish_plugins.PublishXmlValidationError: Write node's knobs values are not correct!
Knob 'channels' > Expected: `['rg']` > Current: `rgb`
```
___
</details>
<details>
<summary>Nuke: Camera product type loaded is not updating - OP-7973 <a href="https://github.com/ynput/OpenPype/pull/6184">#6184</a></summary>
When updating the camera this error would appear:
```
(...)openpype/hosts/nuke/plugins/load/load_camera_abc.py", line 142, in update
camera_node = nuke.toNode(object_name)
TypeError: toNode() argument 1 must be str, not Node
```
___
</details>
<details>
<summary>AYON settings: Use bundle name as variant in dev mode <a href="https://github.com/ynput/OpenPype/pull/6187">#6187</a></summary>
Make sure the bundle name is used in dev mode for settings variant.
___
</details>
<details>
<summary>Fusion: fix unwanted change to field name in Settings <a href="https://github.com/ynput/OpenPype/pull/6193">#6193</a></summary>
It should be `image_format` but in previous refactoring PR it fell back to original `output_formats` which caused enum not to show up and propagate into plugin.
___
</details>
<details>
<summary>Bugfix: AYON menu disappeared when the workspace has been changed in 3dsMax <a href="https://github.com/ynput/OpenPype/pull/6200">#6200</a></summary>
AYON plugins are not correctly registered when switching to different workspaces.
___
</details>
<details>
<summary>TrayPublisher: adding settings category to base creator classes <a href="https://github.com/ynput/OpenPype/pull/6202">#6202</a></summary>
Settings are resolving correctly as they suppose to.
___
</details>
<details>
<summary>Nuke: expose knobs backward compatibility fix - OP-8164 <a href="https://github.com/ynput/OpenPype/pull/6211">#6211</a></summary>
Fix backwards compatibility for settings `project_settings/nuke/create/CreateWriteRender/exposed_knobs`.
___
</details>
<details>
<summary>AE: fix local render doesn't push thumbnail to Ftrack <a href="https://github.com/ynput/OpenPype/pull/6212">#6212</a></summary>
Without thumbnail review is not clickable from main Versions list
___
</details>
<details>
<summary>Nuke: openpype expose knobs validator - OP-8166 <a href="https://github.com/ynput/OpenPype/pull/6213">#6213</a></summary>
Fix exposed knobs validator for backwards compatibility with missing settings.
___
</details>
<details>
<summary>Ftrack: Post-launch hook fix value lowering <a href="https://github.com/ynput/OpenPype/pull/6221">#6221</a></summary>
Fix lowerin of values in status mapping.
___
</details>
### **🔀 Refactored code**
<details>
<summary>Maya: Remove `shelf` class and shelf build on maya `userSetup.py` <a href="https://github.com/ynput/OpenPype/pull/5837">#5837</a></summary>
Remove shelf builder logic. It appeared to be unused and had bugs.
___
</details>
### **Merged pull requests**
<details>
<summary>Max: updated implementation of save_scene + small QOL improvements to host <a href="https://github.com/ynput/OpenPype/pull/6186">#6186</a></summary>
- Removed `has_unsaved_changes` from Max host as it looks to have been unused and unimplemented.
- Added and implemented `workfile_has_unsaved_changes` to Max host.
- Mirrored the Houdini host to implement the above into `save_scene` publish for Max.
- Added a line to `startup.ms` which opens the usual 'default' menu inside of Max (see screenshots).Current (Likely opens this menu due to one or more of the startup scripts used to insert OP menu):New:
___
</details>
<details>
<summary>Fusion: Use better resolution of Ayon apps on 4k display <a href="https://github.com/ynput/OpenPype/pull/6199">#6199</a></summary>
Changes size (makes it smaller) of Ayon apps (Workfiles, Loader) in Fusion on high definitions displays.
___
</details>
<details>
<summary>Update CONTRIBUTING.md <a href="https://github.com/ynput/OpenPype/pull/6210">#6210</a></summary>
Updating contributing guidelines to reflect the EOL state of repository
___
</details>
<details>
<summary>Deadline: Remove redundant instance_skeleton_data code - OP-8269 <a href="https://github.com/ynput/OpenPype/pull/6219">#6219</a></summary>
This PR https://github.com/ynput/OpenPype/pull/5186 re-introduced code about for the `instance_skeleton_data` but its actually not used since this variable gets overwritten later.
___
</details>
## [3.18.6](https://github.com/ynput/OpenPype/tree/3.18.6)
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.5...3.18.6)
### **🚀 Enhancements**
<details>
<summary>AYON: Use `SettingsField` from ayon server <a href="https://github.com/ynput/OpenPype/pull/6173">#6173</a></summary>
This is preparation for new version of pydantic which will require to customize the field class for AYON purposes as raw pydantic Field could not be used.
___
</details>
<details>
<summary>Nuke: Expose write knobs - OP-7592 <a href="https://github.com/ynput/OpenPype/pull/6137">#6137</a></summary>
This PR adds `exposed_knobs` to the creator plugins settings at `ayon+settings://nuke/create/CreateWriteRender/exposed_knobs`.When exposed knobs will be linked from the write node to the outside publish group, for users to adjust.
___
</details>
<details>
<summary>AYON: Remove kitsu addon <a href="https://github.com/ynput/OpenPype/pull/6172">#6172</a></summary>
Removed kitsu addon from server addons because already has own repository.
___
</details>
### **🐛 Bug fixes**
<details>
<summary>Fusion: provide better logging for validate saver crash due type error <a href="https://github.com/ynput/OpenPype/pull/6082">#6082</a></summary>
Handles reported issue for `NoneType` error thrown in conversion `int(tool["Comments"][frame])`. It is most likely happening when saver node has no input connections.There is a validator for that, but it might be not obvious, that this error is caused by missing input connections and it has been already reported by `"Validate Saver Has Input"`.
___
</details>
<details>
<summary>Workfile Template Builder: Use correct variable in create placeholder <a href="https://github.com/ynput/OpenPype/pull/6141">#6141</a></summary>
Use correct variable where failed instances are stored for validation.
___
</details>
<details>
<summary>ExtractOIIOTranscode: Missing product_names to subsets conversion <a href="https://github.com/ynput/OpenPype/pull/6159">#6159</a></summary>
The `Product Names` filtering should be fixed with this.
___
</details>
<details>
<summary>Blender: Fix missing animation data when updating blend assets <a href="https://github.com/ynput/OpenPype/pull/6165">#6165</a></summary>
Fix missing animation data when updating blend assets.
___
</details>
<details>
<summary>TrayPublisher: Pre-fill of version works in AYON <a href="https://github.com/ynput/OpenPype/pull/6180">#6180</a></summary>
Use `folderPath` instead of `asset` in AYON mode to calculate next available version.
___
</details>
### **🔀 Refactored code**
<details>
<summary>Chore: remove Muster <a href="https://github.com/ynput/OpenPype/pull/6085">#6085</a></summary>
Muster isn't maintained for a long time and it wasn't working anyway. This is removing related code from the code base. If there is renewed interest in Muster, it needs to be re-implemented in modern AYON compatible way.
___
</details>
### **Merged pull requests**
<details>
<summary>Maya: change label in the render settings to be more readable <a href="https://github.com/ynput/OpenPype/pull/6134">#6134</a></summary>
AYON replacement for #5713.
___
</details>
## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5)

View file

@ -1,53 +1,12 @@
## How to contribute to Pype
## How to contribute to OpenPype
We are always happy for any contributions for OpenPype improvements. Before making a PR and starting working on an issue, please read these simple guidelines.
OpenPype has reached the end of its life and is now in a limited maintenance mode (read more at https://community.ynput.io/t/openpype-end-of-life-timeline/877). As such we're no longer accepting contributions unless they are also ported to AYON at the same time.
#### **Did you find a bug?**
## Getting my PR merged during this period
1. Check in the issues and our [bug triage[(https://github.com/pypeclub/pype/projects/2) to make sure it wasn't reported already.
2. Ask on our [discord](http://pype.community/chat) Often, what appears as a bug, might be the intended behaviour for someone else.
3. Create a new issue.
4. Use the issue template for you PR please.
- Each OpenPype PR MUST have a corresponding AYON PR in github. Without AYON compatibility features will not be merged! Luckily most of the code is compatible, albeit sometimes in a different place after refactor. Porting from OpenPype to AYON should be really easy.
- Please keep the corresponding OpenPype and AYON PR names the same so they can be easily identified.
Inside each PR, put a link to the corresponding PR from the other product. OpenPype PRs should point to AYON PR and vice versa.
#### **Did you write a patch that fixes a bug?**
- Open a new GitHub pull request with the patch.
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
#### **Do you intend to add a new feature or change an existing one?**
- Open a new thread in the [github discussions](https://github.com/pypeclub/pype/discussions/new)
- Do not open issue until the suggestion is discussed. We will convert accepted suggestions into backlog and point them to the relevant discussion thread to keep the context.
- If you are already working on a new feature and you'd like it eventually merged to the main codebase, please consider making a DRAFT PR as soon as possible. This makes it a lot easier to give feedback, discuss the code and functionalit, plus it prevents multiple people tackling the same problem independently.
#### **Do you have questions about the source code?**
Open a new question on [github discussions](https://github.com/pypeclub/pype/discussions/new)
## Branching Strategy
As we move to 3.x as the primary supported version of pype and only keep 2.15 on bug bugfixes and client sponsored feature requests, we need to be very careful with merging strategy.
We also use this opportunity to switch the branch naming. 3.0 production branch will no longer be called MASTER, but will be renamed to MAIN. Develop will stay as it is.
A few important notes about 2.x and 3.x development:
- 3.x features are not backported to 2.x unless specifically requested
- 3.x bugs and hotfixes can be ported to 2.x if they are relevant or severe
- 2.x features and bugs MUST be ported to 3.x at the same time
## Pull Requests
- Each 2.x PR MUST have a corresponding 3.x PR in github. Without 3.x PR, 2.x features will not be merged! Luckily most of the code is compatible, albeit sometimes in a different place after refactor. Porting from 2.x to 3.x should be really easy.
- Please keep the corresponding 2 and 3 PR names the same so they can be easily identified from the PR list page.
- Each 2.x PR should be labeled with `2.x-dev` label.
Inside each PR, put a link to the corresponding PR for the other version
Of course if you want to contribute, feel free to make a PR to only 2.x/develop or develop, based on what you are using. While reviewing the PRs, we might convert the code to corresponding PR for the other release ourselves.
We might also change the target of you PR to and intermediate branch, rather than `develop` if we feel it requires some extra work on our end. That way, we preserve all your commits so you don't loose out on the contribution credits.
If a PR is targeted at 2.x release it must be labelled with 2x-dev label in Github.
AYON repository structure is a lot more granular compared to OpenPype. If you're unsure what repository your AYON equivalent PR should target, feel free to make OpenPype PR first and ask.

View file

@ -9,8 +9,13 @@ OpenPype
## Important Notice!
OpenPype as a standalone product has reach end of it's life and this repository is now used as a pipeline core code for [AYON](https://ynput.io/ayon/). You can read more details about the end of life process here https://community.ynput.io/t/openpype-end-of-life-timeline/877
OpenPype as a standalone product has reach end of it's life and this repository is now being phased out in favour of [ayon-core](https://github.com/ynput/ayon-core). You can read more details about the end of life process here https://community.ynput.io/t/openpype-end-of-life-timeline/877
As such, we no longer accept Pull Requests that are not ported to AYON at the same time!
```
Please refer to https://github.com/ynput/OpenPype/blob/develop/CONTRIBUTING.md for more information about the current PR process.
```
Introduction
------------

View file

@ -29,6 +29,7 @@ class RenderCreator(Creator):
# Settings
mark_for_review = True
force_setting_values = True
def create(self, subset_name_from_ui, data, pre_create_data):
stub = api.get_stub() # only after After Effects is up
@ -96,7 +97,9 @@ class RenderCreator(Creator):
self._add_instance_to_context(new_instance)
stub.rename_item(comp.id, subset_name)
set_settings(True, True, [comp.id], print_msg=False)
if self.force_setting_values:
set_settings(True, True, [comp.id], print_msg=False)
def get_pre_create_attr_defs(self):
output = [
@ -173,6 +176,7 @@ class RenderCreator(Creator):
)
self.mark_for_review = plugin_settings["mark_for_review"]
self.force_setting_values = plugin_settings["force_setting_values"]
self.default_variants = plugin_settings.get(
"default_variants",
plugin_settings.get("defaults") or []

View file

@ -2,6 +2,7 @@ from pathlib import Path
import bpy
from openpype import AYON_SERVER_ENABLED
from openpype.settings import get_project_settings
from openpype.pipeline import get_current_project_name
@ -47,6 +48,22 @@ def get_multilayer(settings):
["multilayer_exr"])
def get_renderer(settings):
"""Get renderer from blender settings."""
return (settings["blender"]
["RenderSettings"]
["renderer"])
def get_compositing(settings):
"""Get compositing from blender settings."""
return (settings["blender"]
["RenderSettings"]
["compositing"])
def get_render_product(output_path, name, aov_sep):
"""
Generate the path to the render product. Blender interprets the `#`
@ -91,66 +108,121 @@ def set_render_format(ext, multilayer):
image_settings.file_format = "TIFF"
def set_render_passes(settings):
aov_list = (settings["blender"]
["RenderSettings"]
["aov_list"])
custom_passes = (settings["blender"]
["RenderSettings"]
["custom_passes"])
def set_render_passes(settings, renderer):
aov_list = set(settings["blender"]["RenderSettings"]["aov_list"])
custom_passes = settings["blender"]["RenderSettings"]["custom_passes"]
# Common passes for both renderers
vl = bpy.context.view_layer
# Data Passes
vl.use_pass_combined = "combined" in aov_list
vl.use_pass_z = "z" in aov_list
vl.use_pass_mist = "mist" in aov_list
vl.use_pass_normal = "normal" in aov_list
# Light Passes
vl.use_pass_diffuse_direct = "diffuse_light" in aov_list
vl.use_pass_diffuse_color = "diffuse_color" in aov_list
vl.use_pass_glossy_direct = "specular_light" in aov_list
vl.use_pass_glossy_color = "specular_color" in aov_list
vl.eevee.use_pass_volume_direct = "volume_light" in aov_list
vl.use_pass_emit = "emission" in aov_list
vl.use_pass_environment = "environment" in aov_list
vl.use_pass_shadow = "shadow" in aov_list
vl.use_pass_ambient_occlusion = "ao" in aov_list
cycles = vl.cycles
# Cryptomatte Passes
vl.use_pass_cryptomatte_object = "cryptomatte_object" in aov_list
vl.use_pass_cryptomatte_material = "cryptomatte_material" in aov_list
vl.use_pass_cryptomatte_asset = "cryptomatte_asset" in aov_list
cycles.denoising_store_passes = "denoising" in aov_list
cycles.use_pass_volume_direct = "volume_direct" in aov_list
cycles.use_pass_volume_indirect = "volume_indirect" in aov_list
if renderer == "BLENDER_EEVEE":
# Eevee exclusive passes
eevee = vl.eevee
# Light Passes
vl.use_pass_shadow = "shadow" in aov_list
eevee.use_pass_volume_direct = "volume_light" in aov_list
# Effects Passes
eevee.use_pass_bloom = "bloom" in aov_list
eevee.use_pass_transparent = "transparent" in aov_list
# Cryptomatte Passes
vl.use_pass_cryptomatte_accurate = "cryptomatte_accurate" in aov_list
elif renderer == "CYCLES":
# Cycles exclusive passes
cycles = vl.cycles
# Data Passes
vl.use_pass_position = "position" in aov_list
vl.use_pass_vector = "vector" in aov_list
vl.use_pass_uv = "uv" in aov_list
cycles.denoising_store_passes = "denoising" in aov_list
vl.use_pass_object_index = "object_index" in aov_list
vl.use_pass_material_index = "material_index" in aov_list
cycles.pass_debug_sample_count = "sample_count" in aov_list
# Light Passes
vl.use_pass_diffuse_indirect = "diffuse_indirect" in aov_list
vl.use_pass_glossy_indirect = "specular_indirect" in aov_list
vl.use_pass_transmission_direct = "transmission_direct" in aov_list
vl.use_pass_transmission_indirect = "transmission_indirect" in aov_list
vl.use_pass_transmission_color = "transmission_color" in aov_list
cycles.use_pass_volume_direct = "volume_light" in aov_list
cycles.use_pass_volume_indirect = "volume_indirect" in aov_list
cycles.use_pass_shadow_catcher = "shadow" in aov_list
aovs_names = [aov.name for aov in vl.aovs]
for cp in custom_passes:
cp_name = cp[0]
cp_name = cp["attribute"] if AYON_SERVER_ENABLED else cp[0]
if cp_name not in aovs_names:
aov = vl.aovs.add()
aov.name = cp_name
else:
aov = vl.aovs[cp_name]
aov.type = cp[1].get("type", "VALUE")
aov.type = (cp["value"]
if AYON_SERVER_ENABLED else cp[1].get("type", "VALUE"))
return aov_list, custom_passes
return list(aov_list), custom_passes
def set_node_tree(output_path, name, aov_sep, ext, multilayer):
def _create_aov_slot(name, aov_sep, slots, rpass_name, multi_exr, output_path):
filename = f"{name}{aov_sep}{rpass_name}.####"
slot = slots.new(rpass_name if multi_exr else filename)
filepath = str(output_path / filename.lstrip("/"))
return slot, filepath
def set_node_tree(
output_path, render_product, name, aov_sep, ext, multilayer, compositing
):
# Set the scene to use the compositor node tree to render
bpy.context.scene.use_nodes = True
tree = bpy.context.scene.node_tree
# Get the Render Layers node
rl_node = None
comp_layer_type = "CompositorNodeRLayers"
output_type = "CompositorNodeOutputFile"
compositor_type = "CompositorNodeComposite"
# Get the Render Layer, Composite and the previous output nodes
render_layer_node = None
composite_node = None
old_output_node = None
for node in tree.nodes:
if node.bl_idname == "CompositorNodeRLayers":
rl_node = node
if node.bl_idname == comp_layer_type:
render_layer_node = node
elif node.bl_idname == compositor_type:
composite_node = node
elif node.bl_idname == output_type and "AYON" in node.name:
old_output_node = node
if render_layer_node and composite_node and old_output_node:
break
# If there's not a Render Layers node, we create it
if not rl_node:
rl_node = tree.nodes.new("CompositorNodeRLayers")
if not render_layer_node:
render_layer_node = tree.nodes.new(comp_layer_type)
# Get the enabled output sockets, that are the active passes for the
# render.
@ -158,48 +230,81 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer):
exclude_sockets = ["Image", "Alpha", "Noisy Image"]
passes = [
socket
for socket in rl_node.outputs
for socket in render_layer_node.outputs
if socket.enabled and socket.name not in exclude_sockets
]
# Remove all output nodes
for node in tree.nodes:
if node.bl_idname == "CompositorNodeOutputFile":
tree.nodes.remove(node)
# Create a new output node
output = tree.nodes.new("CompositorNodeOutputFile")
output = tree.nodes.new(output_type)
image_settings = bpy.context.scene.render.image_settings
output.format.file_format = image_settings.file_format
slots = None
# In case of a multilayer exr, we don't need to use the output node,
# because the blender render already outputs a multilayer exr.
if ext == "exr" and multilayer:
output.layer_slots.clear()
return []
multi_exr = ext == "exr" and multilayer
slots = output.layer_slots if multi_exr else output.file_slots
output.base_path = render_product if multi_exr else str(output_path)
output.file_slots.clear()
output.base_path = str(output_path)
slots.clear()
aov_file_products = []
old_links = {
link.from_socket.name: link for link in tree.links
if link.to_node == old_output_node}
# Create a new socket for the beauty output
pass_name = "rgba" if multi_exr else "beauty"
slot, _ = _create_aov_slot(
name, aov_sep, slots, pass_name, multi_exr, output_path)
tree.links.new(render_layer_node.outputs["Image"], slot)
if compositing:
# Create a new socket for the composite output
pass_name = "composite"
comp_socket, filepath = _create_aov_slot(
name, aov_sep, slots, pass_name, multi_exr, output_path)
aov_file_products.append(("Composite", filepath))
# For each active render pass, we add a new socket to the output node
# and link it
for render_pass in passes:
filepath = f"{name}{aov_sep}{render_pass.name}.####"
for rpass in passes:
slot, filepath = _create_aov_slot(
name, aov_sep, slots, rpass.name, multi_exr, output_path)
aov_file_products.append((rpass.name, filepath))
output.file_slots.new(filepath)
# If the rpass was not connected with the old output node, we connect
# it with the new one.
if not old_links.get(rpass.name):
tree.links.new(rpass, slot)
filename = str(output_path / filepath.lstrip("/"))
for link in list(old_links.values()):
# Check if the socket is still available in the new output node.
socket = output.inputs.get(link.to_socket.name)
# If it is, we connect it with the new output node.
if socket:
tree.links.new(link.from_socket, socket)
# Then, we remove the old link.
tree.links.remove(link)
aov_file_products.append((render_pass.name, filename))
# If there's a composite node, we connect its input with the new output
if compositing and composite_node:
for link in tree.links:
if link.to_node == composite_node:
tree.links.new(link.from_socket, comp_socket)
break
node_input = output.inputs[-1]
if old_output_node:
output.location = old_output_node.location
tree.nodes.remove(old_output_node)
tree.links.new(render_pass, node_input)
output.name = "AYON File Output"
output.label = "AYON File Output"
return aov_file_products
return [] if multi_exr else aov_file_products
def imprint_render_settings(node, data):
@ -228,17 +333,23 @@ def prepare_rendering(asset_group):
aov_sep = get_aov_separator(settings)
ext = get_image_format(settings)
multilayer = get_multilayer(settings)
renderer = get_renderer(settings)
compositing = get_compositing(settings)
set_render_format(ext, multilayer)
aov_list, custom_passes = set_render_passes(settings)
bpy.context.scene.render.engine = renderer
aov_list, custom_passes = set_render_passes(settings, renderer)
output_path = Path.joinpath(dirpath, render_folder, file_name)
render_product = get_render_product(output_path, name, aov_sep)
aov_file_product = set_node_tree(
output_path, name, aov_sep, ext, multilayer)
output_path, render_product, name, aov_sep,
ext, multilayer, compositing)
bpy.context.scene.render.filepath = render_product
# Clear the render filepath, so that the output is handled only by the
# output node in the compositor.
bpy.context.scene.render.filepath = ""
render_settings = {
"render_folder": render_folder,

View file

@ -1,8 +1,10 @@
"""Create render."""
import bpy
from openpype.lib import version_up
from openpype.hosts.blender.api import plugin
from openpype.hosts.blender.api.render_lib import prepare_rendering
from openpype.hosts.blender.api.workio import save_file
class CreateRenderlayer(plugin.BaseCreator):
@ -37,6 +39,7 @@ class CreateRenderlayer(plugin.BaseCreator):
# settings. Even the validator to check that the file is saved will
# detect the file as saved, even if it isn't. The only solution for
# now it is to force the file to be saved.
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
filepath = version_up(bpy.data.filepath)
save_file(filepath, copy=False)
return collection

View file

@ -28,15 +28,27 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
def process(self, instance):
if not self.is_active(instance.data):
return
tree = bpy.context.scene.node_tree
output_type = "CompositorNodeOutputFile"
output_node = None
# Remove all output nodes that inlcude "AYON" in the name.
# There should be only one.
for node in tree.nodes:
if node.bl_idname == output_type and "AYON" in node.name:
output_node = node
break
if not output_node:
raise PublishValidationError(
"No output node found in the compositor tree."
)
filepath = bpy.data.filepath
file = os.path.basename(filepath)
filename, ext = os.path.splitext(file)
if filename not in bpy.context.scene.render.filepath:
if filename not in output_node.base_path:
raise PublishValidationError(
"Render output folder "
"doesn't match the blender scene name! "
"Use Repair action to "
"fix the folder file path."
"Render output folder doesn't match the blender scene name! "
"Use Repair action to fix the folder file path."
)
@classmethod

View file

@ -15,6 +15,7 @@ from openpype.hosts.fusion.api.lib import (
)
from openpype.pipeline import get_current_asset_name
from openpype.resources import get_openpype_icon_filepath
from openpype.tools.utils import get_qt_app
from .pipeline import FusionEventHandler
from .pulse import FusionPulse
@ -174,7 +175,8 @@ class OpenPypeMenu(QtWidgets.QWidget):
def launch_openpype_menu():
app = QtWidgets.QApplication(sys.argv)
app = get_qt_app()
pype_menu = OpenPypeMenu()

View file

@ -59,10 +59,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
rt.callbacks.addScript(rt.Name('filePostOpen'),
lib.check_colorspace)
rt.callbacks.addScript(rt.Name('postWorkspaceChange'),
self._deferred_menu_creation)
def has_unsaved_changes(self):
# TODO: how to get it from 3dsmax?
return True
def workfile_has_unsaved_changes(self):
return rt.getSaveRequired()
def get_workfile_extensions(self):
return [".max"]

View file

@ -1,11 +1,9 @@
import pyblish.api
import os
from openpype.pipeline import registered_host
class SaveCurrentScene(pyblish.api.ContextPlugin):
"""Save current scene
"""
"""Save current scene"""
label = "Save current file"
order = pyblish.api.ExtractorOrder - 0.49
@ -13,9 +11,13 @@ class SaveCurrentScene(pyblish.api.ContextPlugin):
families = ["maxrender", "workfile"]
def process(self, context):
from pymxs import runtime as rt
folder = rt.maxFilePath
file = rt.maxFileName
current = os.path.join(folder, file)
assert context.data["currentFile"] == current
rt.saveMaxFile(current)
host = registered_host()
current_file = host.get_current_workfile()
assert context.data["currentFile"] == current_file
if host.workfile_has_unsaved_changes():
self.log.info(f"Saving current file: {current_file}")
host.save_workfile(current_file)
else:
self.log.debug("No unsaved changes, skipping file save..")

View file

@ -7,6 +7,9 @@
local pythonpath = systemTools.getEnvVariable "MAX_PYTHONPATH"
systemTools.setEnvVariable "PYTHONPATH" pythonpath
/*opens the create menu on startup to ensure users are presented with a useful default view.*/
max create mode
python.ExecuteFile startup
)
)

View file

@ -2939,119 +2939,6 @@ def fix_incompatible_containers():
"ReferenceLoader", type="string")
def _null(*args):
pass
class shelf():
'''A simple class to build shelves in maya. Since the build method is empty,
it should be extended by the derived class to build the necessary shelf
elements. By default it creates an empty shelf called "customShelf".'''
###########################################################################
'''This is an example shelf.'''
# class customShelf(_shelf):
# def build(self):
# self.addButon(label="button1")
# self.addButon("button2")
# self.addButon("popup")
# p = cmds.popupMenu(b=1)
# self.addMenuItem(p, "popupMenuItem1")
# self.addMenuItem(p, "popupMenuItem2")
# sub = self.addSubMenu(p, "subMenuLevel1")
# self.addMenuItem(sub, "subMenuLevel1Item1")
# sub2 = self.addSubMenu(sub, "subMenuLevel2")
# self.addMenuItem(sub2, "subMenuLevel2Item1")
# self.addMenuItem(sub2, "subMenuLevel2Item2")
# self.addMenuItem(sub, "subMenuLevel1Item2")
# self.addMenuItem(p, "popupMenuItem3")
# self.addButon("button3")
# customShelf()
###########################################################################
def __init__(self, name="customShelf", iconPath="", preset={}):
self.name = name
self.iconPath = iconPath
self.labelBackground = (0, 0, 0, 0)
self.labelColour = (.9, .9, .9)
self.preset = preset
self._cleanOldShelf()
cmds.setParent(self.name)
self.build()
def build(self):
'''This method should be overwritten in derived classes to actually
build the shelf elements. Otherwise, nothing is added to the shelf.'''
for item in self.preset['items']:
if not item.get('command'):
item['command'] = self._null
if item['type'] == 'button':
self.addButon(item['name'],
command=item['command'],
icon=item['icon'])
if item['type'] == 'menuItem':
self.addMenuItem(item['parent'],
item['name'],
command=item['command'],
icon=item['icon'])
if item['type'] == 'subMenu':
self.addMenuItem(item['parent'],
item['name'],
command=item['command'],
icon=item['icon'])
def addButon(self, label, icon="commandButton.png",
command=_null, doubleCommand=_null):
'''
Adds a shelf button with the specified label, command,
double click command and image.
'''
cmds.setParent(self.name)
if icon:
icon = os.path.join(self.iconPath, icon)
print(icon)
cmds.shelfButton(width=37, height=37, image=icon, label=label,
command=command, dcc=doubleCommand,
imageOverlayLabel=label, olb=self.labelBackground,
olc=self.labelColour)
def addMenuItem(self, parent, label, command=_null, icon=""):
'''
Adds a shelf button with the specified label, command,
double click command and image.
'''
if icon:
icon = os.path.join(self.iconPath, icon)
print(icon)
return cmds.menuItem(p=parent, label=label, c=command, i="")
def addSubMenu(self, parent, label, icon=None):
'''
Adds a sub menu item with the specified label and icon to
the specified parent popup menu.
'''
if icon:
icon = os.path.join(self.iconPath, icon)
print(icon)
return cmds.menuItem(p=parent, label=label, i=icon, subMenu=1)
def _cleanOldShelf(self):
'''
Checks if the shelf exists and empties it if it does
or creates it if it does not.
'''
if cmds.shelfLayout(self.name, ex=1):
if cmds.shelfLayout(self.name, q=1, ca=1):
for each in cmds.shelfLayout(self.name, q=1, ca=1):
cmds.deleteUI(each)
else:
cmds.shelfLayout(self.name, p="ShelfLayout")
def update_content_on_context_change():
"""
This will update scene content to match new asset on context change

View file

@ -7,6 +7,7 @@ from openpype.hosts.maya.api import lib
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
PublishValidationError
)
@ -38,7 +39,8 @@ class ValidateRigJointsHidden(pyblish.api.InstancePlugin):
invalid = self.get_invalid(instance)
if invalid:
raise ValueError("Visible joints found: {0}".format(invalid))
raise PublishValidationError(
"Visible joints found: {0}".format(invalid))
@classmethod
def repair(cls, instance):

View file

@ -46,24 +46,5 @@ if bool(int(os.environ.get(key, "0"))):
lowestPriority=True
)
# Build a shelf.
shelf_preset = settings['maya'].get('project_shelf')
if shelf_preset:
icon_path = os.path.join(
os.environ['OPENPYPE_PROJECT_SCRIPTS'],
project_name,
"icons")
icon_path = os.path.abspath(icon_path)
for i in shelf_preset['imports']:
import_string = "from {} import {}".format(project_name, i)
print(import_string)
exec(import_string)
cmds.evalDeferred(
"mlib.shelf(name=shelf_preset['name'], iconPath=icon_path,"
" preset=shelf_preset)"
)
print("Finished OpenPype usersetup.")

View file

@ -1348,7 +1348,9 @@ def _remove_old_knobs(node):
def exposed_write_knobs(settings, plugin_name, instance_node):
exposed_knobs = settings["nuke"]["create"][plugin_name]["exposed_knobs"]
exposed_knobs = settings["nuke"]["create"][plugin_name].get(
"exposed_knobs", []
)
if exposed_knobs:
instance_node.addKnob(nuke.Text_Knob('', 'Write Knobs'))
write_node = nuke.allNodes(group=instance_node, filter="Write")[0]

View file

@ -112,8 +112,6 @@ class AlembicCameraLoader(load.LoaderPlugin):
project_name = get_current_project_name()
version_doc = get_version_by_id(project_name, representation["parent"])
object_name = container["node"]
# get main variables
version_data = version_doc.get("data", {})
vname = version_doc.get("name", None)
@ -139,7 +137,7 @@ class AlembicCameraLoader(load.LoaderPlugin):
file = get_representation_path(representation).replace("\\", "/")
with maintained_selection():
camera_node = nuke.toNode(object_name)
camera_node = container["node"]
camera_node['selected'].setValue(True)
# collect input output dependencies
@ -154,9 +152,10 @@ class AlembicCameraLoader(load.LoaderPlugin):
xpos = camera_node.xpos()
ypos = camera_node.ypos()
nuke.nodeCopy("%clipboard%")
camera_name = camera_node.name()
nuke.delete(camera_node)
nuke.nodePaste("%clipboard%")
camera_node = nuke.toNode(object_name)
camera_node = nuke.toNode(camera_name)
camera_node.setXYpos(xpos, ypos)
# link to original input nodes

View file

@ -65,7 +65,7 @@ class ValidateExposedKnobs(
group_node = instance.data["transientData"]["node"]
nuke_settings = instance.context.data["project_settings"]["nuke"]
create_settings = nuke_settings["create"][plugin]
exposed_knobs = create_settings["exposed_knobs"]
exposed_knobs = create_settings.get("exposed_knobs", [])
unexposed_knobs = []
for knob in exposed_knobs:
if knob not in group_node.knobs():

View file

@ -129,7 +129,7 @@ class ValidateNukeWriteNode(
and key != "file"
and key != "tile_color"
):
check.append([key, node_value, write_node[key].value()])
check.append([key, fixed_values, write_node[key].value()])
if check:
self._make_error(check)
@ -137,7 +137,7 @@ class ValidateNukeWriteNode(
def _make_error(self, check):
# sourcery skip: merge-assign-and-aug-assign, move-assign-in-block
dbg_msg = "Write node's knobs values are not correct!\n"
msg_add = "Knob '{0}' > Correct: `{1}` > Wrong: `{2}`"
msg_add = "Knob '{0}' > Expected: `{1}` > Current: `{2}`"
details = [
msg_add.format(item[0], item[1], item[2])

View file

@ -3,12 +3,11 @@ import sys
import contextlib
import traceback
from qtpy import QtWidgets
from openpype.lib import env_value_to_bool, Logger
from openpype.modules import ModulesManager
from openpype.pipeline import install_host
from openpype.tools.utils import host_tools
from openpype.tools.utils import get_openpype_qt_app
from openpype.tests.lib import is_in_tests
from .launch_logic import ProcessLauncher, stub
@ -30,7 +29,7 @@ def main(*subprocess_args):
# coloring in StdOutBroker
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
app = QtWidgets.QApplication([])
app = get_openpype_qt_app()
app.setQuitOnLastWindowClosed(False)
launcher = ProcessLauncher(subprocess_args)

View file

@ -713,6 +713,11 @@ def swap_clips(from_clip, to_clip, to_in_frame, to_out_frame):
bool: True if successfully replaced
"""
# copy ACES input transform from timeline clip to new media item
mediapool_item_from_timeline = from_clip.GetMediaPoolItem()
_idt = mediapool_item_from_timeline.GetClipProperty('IDT')
to_clip.SetClipProperty('IDT', _idt)
_clip_prop = to_clip.GetClipProperty
to_clip_name = _clip_prop("File Name")
# add clip item as take to timeline

View file

@ -477,14 +477,16 @@ class ClipLoader:
)
_clip_property = media_pool_item.GetClipProperty
source_in = int(_clip_property("Start"))
source_out = int(_clip_property("End"))
# Read trimming from timeline item
timeline_item_in = timeline_item.GetLeftOffset()
timeline_item_len = timeline_item.GetDuration()
timeline_item_out = timeline_item_in + timeline_item_len
lib.swap_clips(
timeline_item,
media_pool_item,
source_in,
source_out
timeline_item_in,
timeline_item_out
)
print("Loading clips: `{}`".format(self.data["clip_name"]))

View file

@ -1,10 +1,13 @@
import os
import click
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
from openpype.modules import (
click_wrap,
OpenPypeModule,
ITrayAction,
IHostAddon,
)
STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@ -37,10 +40,10 @@ class StandAlonePublishAddon(OpenPypeModule, ITrayAction, IHostAddon):
run_detached_process(args)
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
@click.group(
@click_wrap.group(
StandAlonePublishAddon.name,
help="StandalonePublisher related commands.")
def cli_main():

View file

@ -1,10 +1,13 @@
import os
import click
from openpype.lib import get_openpype_execute_args
from openpype.lib.execute import run_detached_process
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
from openpype.modules import (
click_wrap,
OpenPypeModule,
ITrayAction,
IHostAddon,
)
TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@ -38,10 +41,12 @@ class TrayPublishAddon(OpenPypeModule, IHostAddon, ITrayAction):
run_detached_process(args)
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
@click.group(TrayPublishAddon.name, help="TrayPublisher related commands.")
@click_wrap.group(
TrayPublishAddon.name,
help="TrayPublisher related commands.")
def cli_main():
pass

View file

@ -32,6 +32,7 @@ SHARED_DATA_KEY = "openpype.traypublisher.instances"
class HiddenTrayPublishCreator(HiddenCreator):
host_name = "traypublisher"
settings_category = "traypublisher"
def collect_instances(self):
instances_by_identifier = cache_and_get_instances(
@ -68,6 +69,7 @@ class HiddenTrayPublishCreator(HiddenCreator):
class TrayPublishCreator(Creator):
create_allow_context_change = True
host_name = "traypublisher"
settings_category = "traypublisher"
def collect_instances(self):
instances_by_identifier = cache_and_get_instances(

View file

@ -381,15 +381,19 @@ or updating already created. Publishing will create OTIO file.
"""
self.asset_name_check = []
tracks = otio_timeline.each_child(
descended_from_type=otio.schema.Track
)
tracks = [
track for track in otio_timeline.each_child(
descended_from_type=otio.schema.Track)
if track.kind == "Video"
]
# media data for audio sream and reference solving
# media data for audio stream and reference solving
media_data = self._get_media_source_metadata(media_path)
for track in tracks:
# set track name
track.name = f"{sequence_file_name} - {otio_timeline.name}"
try:
track_start_frame = (
abs(track.source_range.start_time.value)
@ -398,19 +402,19 @@ or updating already created. Publishing will create OTIO file.
except AttributeError:
track_start_frame = 0
for clip in track.each_child():
if not self._validate_clip_for_processing(clip):
for otio_clip in track.each_child():
if not self._validate_clip_for_processing(otio_clip):
continue
# get available frames info to clip data
self._create_otio_reference(clip, media_path, media_data)
self._create_otio_reference(otio_clip, media_path, media_data)
# convert timeline range to source range
self._restore_otio_source_range(clip)
self._restore_otio_source_range(otio_clip)
base_instance_data = self._get_base_instance_data(
clip,
otio_clip,
instance_data,
track_start_frame
)
@ -429,7 +433,7 @@ or updating already created. Publishing will create OTIO file.
continue
instance = self._make_subset_instance(
clip,
otio_clip,
_fpreset,
deepcopy(base_instance_data),
parenting_data

View file

@ -79,6 +79,7 @@ class CollectShotInstance(pyblish.api.InstancePlugin):
clip for clip in otio_timeline.each_child(
descended_from_type=otio.schema.Clip)
if clip.name == otio_clip.name
if clip.parent().kind == "Video"
]
otio_clip = clips.pop()

@ -1 +1 @@
Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757
Subproject commit a4755d2869694fcf58c98119298cde8d204e2ce4

View file

@ -1,8 +1,6 @@
import os
import click
from openpype.modules import OpenPypeModule, IHostAddon
from openpype.modules import click_wrap, OpenPypeModule, IHostAddon
WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
@ -38,10 +36,10 @@ class WebpublisherAddon(OpenPypeModule, IHostAddon):
)
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
@click.group(
@click_wrap.group(
WebpublisherAddon.name,
help="Webpublisher related commands.")
def cli_main():
@ -49,10 +47,10 @@ def cli_main():
@cli_main.command()
@click.argument("path")
@click.option("-u", "--user", help="User email address")
@click.option("-p", "--project", help="Project")
@click.option("-t", "--targets", help="Targets", default=None,
@click_wrap.argument("path")
@click_wrap.option("-u", "--user", help="User email address")
@click_wrap.option("-p", "--project", help="Project")
@click_wrap.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def publish(project, path, user=None, targets=None):
"""Start publishing (Inner command).
@ -67,11 +65,11 @@ def publish(project, path, user=None, targets=None):
@cli_main.command()
@click.argument("path")
@click.option("-p", "--project", help="Project")
@click.option("-h", "--host", help="Host")
@click.option("-u", "--user", help="User email address")
@click.option("-t", "--targets", help="Targets", default=None,
@click_wrap.argument("path")
@click_wrap.option("-p", "--project", help="Project")
@click_wrap.option("-h", "--host", help="Host")
@click_wrap.option("-u", "--user", help="User email address")
@click_wrap.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def publishfromapp(project, path, host, user=None, targets=None):
"""Start publishing through application (Inner command).
@ -86,10 +84,10 @@ def publishfromapp(project, path, host, user=None, targets=None):
@cli_main.command()
@click.option("-e", "--executable", help="Executable")
@click.option("-u", "--upload_dir", help="Upload dir")
@click.option("-h", "--host", help="Host", default=None)
@click.option("-p", "--port", help="Port", default=None)
@click_wrap.option("-e", "--executable", help="Executable")
@click_wrap.option("-u", "--upload_dir", help="Upload dir")
@click_wrap.option("-h", "--host", help="Host", default=None)
@click_wrap.option("-p", "--port", help="Port", default=None)
def webserver(executable, upload_dir, host=None, port=None):
"""Start service for communication with Webpublish Front end.

View file

@ -1227,12 +1227,8 @@ def get_rescaled_command_arguments(
target_par = target_par or 1.0
input_par = 1.0
# ffmpeg command
input_file_metadata = get_ffprobe_data(input_path, logger=log)
stream = input_file_metadata["streams"][0]
input_width = int(stream["width"])
input_height = int(stream["height"])
stream_input_par = stream.get("sample_aspect_ratio")
input_height, input_width, stream_input_par = _get_image_dimensions(
application, input_path, log)
if stream_input_par:
input_par = (
float(stream_input_par.split(":")[0])
@ -1345,6 +1341,48 @@ def get_rescaled_command_arguments(
return command_args
def _get_image_dimensions(application, input_path, log):
"""Uses 'ffprobe' first and then 'oiiotool' if available to get dim.
Args:
application (str): "oiiotool"|"ffmpeg"
input_path (str): path to image file
log (Optional[logging.Logger]): Logger used for logging.
Returns:
(tuple) (int, int, dict) - (height, width, sample_aspect_ratio)
Raises:
RuntimeError if image dimensions couldn't be parsed out.
"""
# ffmpeg command
input_file_metadata = get_ffprobe_data(input_path, logger=log)
input_width = input_height = 0
stream = next(
(
s for s in input_file_metadata["streams"]
if s.get("codec_type") == "video"
),
{}
)
if stream:
input_width = int(stream["width"])
input_height = int(stream["height"])
# fallback for weird files with width=0, height=0
if (input_width == 0 or input_height == 0) and application == "oiiotool":
# Load info about file from oiio tool
input_info = get_oiio_info_for_input(input_path, logger=log)
if input_info:
input_width = int(input_info["width"])
input_height = int(input_info["height"])
if input_width == 0 or input_height == 0:
raise RuntimeError("Couldn't read {} either "
"with ffprobe or oiiotool".format(input_path))
stream_input_par = stream.get("sample_aspect_ratio")
return input_height, input_width, stream_input_par
def convert_color_values(application, color_value):
"""Get color mapping for ffmpeg and oiiotool.
Args:

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from . import click_wrap
from .interfaces import (
ILaunchHookPaths,
IPluginPaths,
@ -28,6 +29,8 @@ from .base import (
__all__ = (
"click_wrap",
"ILaunchHookPaths",
"IPluginPaths",
"ITrayModule",

View file

@ -0,0 +1,365 @@
"""Simplified wrapper for 'click' python module.
Module 'click' is used as main cli handler in AYON/OpenPype. Addons can
register their own subcommands with options. This wrapper allows to define
commands and options as with 'click', but without any dependency.
Why not to use 'click' directly? Version of 'click' used in AYON/OpenPype
is not compatible with 'click' version used in some DCCs (e.g. Houdini 20+).
And updating 'click' would break other DCCs.
How to use it? If you already have cli commands defined in addon, just replace
'click' with 'click_wrap' and it should work and modify your addon's cli
method to convert 'click_wrap' object to 'click' object.
Before
```python
import click
from openpype.modules import OpenPypeModule
class ExampleAddon(OpenPypeModule):
name = "example"
def cli(self, click_group):
click_group.add_command(cli_main)
@click.group(ExampleAddon.name, help="Example addon")
def cli_main():
pass
@cli_main.command(help="Example command")
@click.option("--arg1", help="Example argument 1", default="default1")
@click.option("--arg2", help="Example argument 2", is_flag=True)
def mycommand(arg1, arg2):
print(arg1, arg2)
```
Now
```
from openpype import click_wrap
from openpype.modules import OpenPypeModule
class ExampleAddon(OpenPypeModule):
name = "example"
def cli(self, click_group):
click_group.add_command(cli_main.to_click_obj())
@click_wrap.group(ExampleAddon.name, help="Example addon")
def cli_main():
pass
@cli_main.command(help="Example command")
@click_wrap.option("--arg1", help="Example argument 1", default="default1")
@click_wrap.option("--arg2", help="Example argument 2", is_flag=True)
def mycommand(arg1, arg2):
print(arg1, arg2)
```
Added small enhancements:
- most of the methods can be used as chained calls
- functions/methods 'command' and 'group' can be used in a way that
first argument is callback function and the rest are arguments
for click
Example:
```python
from openpype import click_wrap
from openpype.modules import OpenPypeModule
class ExampleAddon(OpenPypeModule):
name = "example"
def cli(self, click_group):
# Define main command (name 'example')
main = click_wrap.group(
self._cli_main, name=self.name, help="Example addon"
)
# Add subcommand (name 'mycommand')
(
main.command(
self._cli_command, name="mycommand", help="Example command"
)
.option(
"--arg1", help="Example argument 1", default="default1"
)
.option(
"--arg2", help="Example argument 2", is_flag=True,
)
)
# Convert main command to click object and add it to parent group
click_group.add_command(main.to_click_obj())
def _cli_main(self):
pass
def _cli_command(self, arg1, arg2):
print(arg1, arg2)
```
```shell
openpype_console addon example mycommand --arg1 value1 --arg2
```
"""
import collections
FUNC_ATTR_NAME = "__ayon_cli_options__"
class Command(object):
def __init__(self, func, *args, **kwargs):
# Command function
self._func = func
# Command definition arguments
self._args = args
# Command definition kwargs
self._kwargs = kwargs
# Both 'options' and 'arguments' are stored to the same variable
# - keep order of options and arguments
self._options = getattr(func, FUNC_ATTR_NAME, [])
def to_click_obj(self):
"""Converts this object to click object.
Returns:
click.Command: Click command object.
"""
return convert_to_click(self)
# --- Methods for 'convert_to_click' function ---
def get_args(self):
"""
Returns:
tuple: Command definition arguments.
"""
return self._args
def get_kwargs(self):
"""
Returns:
dict[str, Any]: Command definition kwargs.
"""
return self._kwargs
def get_func(self):
"""
Returns:
Function: Function to invoke on command trigger.
"""
return self._func
def iter_options(self):
"""
Yields:
tuple[str, tuple, dict]: Option type name with args and kwargs.
"""
for item in self._options:
yield item
# -----------------------------------------------
def add_option(self, *args, **kwargs):
return self.add_option_by_type("option", *args, **kwargs)
def add_argument(self, *args, **kwargs):
return self.add_option_by_type("argument", *args, **kwargs)
option = add_option
argument = add_argument
def add_option_by_type(self, option_name, *args, **kwargs):
self._options.append((option_name, args, kwargs))
return self
class Group(Command):
def __init__(self, func, *args, **kwargs):
super(Group, self).__init__(func, *args, **kwargs)
# Store sub-groupd and sub-commands to the same variable
self._commands = []
# --- Methods for 'convert_to_click' function ---
def iter_commands(self):
for command in self._commands:
yield command
# -----------------------------------------------
def add_command(self, command):
"""Add prepared command object as child.
Args:
command (Command): Prepared command object.
"""
if command not in self._commands:
self._commands.append(command)
def add_group(self, group):
"""Add prepared group object as child.
Args:
group (Group): Prepared group object.
"""
if group not in self._commands:
self._commands.append(group)
def command(self, *args, **kwargs):
"""Add child command.
Returns:
Union[Command, Function]: New command object, or wrapper function.
"""
return self._add_new(Command, *args, **kwargs)
def group(self, *args, **kwargs):
"""Add child group.
Returns:
Union[Group, Function]: New group object, or wrapper function.
"""
return self._add_new(Group, *args, **kwargs)
def _add_new(self, target_cls, *args, **kwargs):
func = None
if args and callable(args[0]):
args = list(args)
func = args.pop(0)
args = tuple(args)
def decorator(_func):
out = target_cls(_func, *args, **kwargs)
self._commands.append(out)
return out
if func is not None:
return decorator(func)
return decorator
def convert_to_click(obj_to_convert):
"""Convert wrapped object to click object.
Args:
obj_to_convert (Command): Object to convert to click object.
Returns:
click.Command: Click command object.
"""
import click
commands_queue = collections.deque()
commands_queue.append((obj_to_convert, None))
top_obj = None
while commands_queue:
item = commands_queue.popleft()
command_obj, parent_obj = item
if not isinstance(command_obj, Command):
raise TypeError(
"Invalid type '{}' expected 'Command'".format(
type(command_obj)
)
)
if isinstance(command_obj, Group):
click_obj = (
click.group(
*command_obj.get_args(),
**command_obj.get_kwargs()
)(command_obj.get_func())
)
else:
click_obj = (
click.command(
*command_obj.get_args(),
**command_obj.get_kwargs()
)(command_obj.get_func())
)
for item in command_obj.iter_options():
option_name, args, kwargs = item
if option_name == "option":
click.option(*args, **kwargs)(click_obj)
elif option_name == "argument":
click.argument(*args, **kwargs)(click_obj)
else:
raise ValueError(
"Invalid option name '{}'".format(option_name)
)
if top_obj is None:
top_obj = click_obj
if parent_obj is not None:
parent_obj.add_command(click_obj)
if isinstance(command_obj, Group):
for command in command_obj.iter_commands():
commands_queue.append((command, click_obj))
return top_obj
def group(*args, **kwargs):
func = None
if args and callable(args[0]):
args = list(args)
func = args.pop(0)
args = tuple(args)
def decorator(_func):
return Group(_func, *args, **kwargs)
if func is not None:
return decorator(func)
return decorator
def command(*args, **kwargs):
func = None
if args and callable(args[0]):
args = list(args)
func = args.pop(0)
args = tuple(args)
def decorator(_func):
return Command(_func, *args, **kwargs)
if func is not None:
return decorator(func)
return decorator
def argument(*args, **kwargs):
def decorator(func):
return _add_option_to_func(
func, "argument", *args, **kwargs
)
return decorator
def option(*args, **kwargs):
def decorator(func):
return _add_option_to_func(
func, "option", *args, **kwargs
)
return decorator
def _add_option_to_func(func, option_name, *args, **kwargs):
if isinstance(func, Command):
func.add_option_by_type(option_name, *args, **kwargs)
return func
if not hasattr(func, FUNC_ATTR_NAME):
setattr(func, FUNC_ATTR_NAME, [])
cli_options = getattr(func, FUNC_ATTR_NAME)
cli_options.append((option_name, args, kwargs))
return func

View file

@ -321,7 +321,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
return deadline_publish_job_id
def process(self, instance):
# type: (pyblish.api.Instance) -> None
"""Process plugin.
@ -338,151 +337,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
self.log.debug("Skipping local instance.")
return
data = instance.data.copy()
context = instance.context
self.context = context
self.anatomy = instance.context.data["anatomy"]
asset = data.get("asset") or context.data["asset"]
subset = data.get("subset")
start = instance.data.get("frameStart")
if start is None:
start = context.data["frameStart"]
end = instance.data.get("frameEnd")
if end is None:
end = context.data["frameEnd"]
handle_start = instance.data.get("handleStart")
if handle_start is None:
handle_start = context.data["handleStart"]
handle_end = instance.data.get("handleEnd")
if handle_end is None:
handle_end = context.data["handleEnd"]
fps = instance.data.get("fps")
if fps is None:
fps = context.data["fps"]
if data.get("extendFrames", False):
start, end = self._extend_frames(
asset,
subset,
start,
end,
data["overrideExistingFrame"])
try:
source = data["source"]
except KeyError:
source = context.data["currentFile"]
success, rootless_path = (
self.anatomy.find_root_template_from_path(source)
)
if success:
source = rootless_path
else:
# `rootless_path` is not set to `source` if none of roots match
self.log.warning((
"Could not find root path for remapping \"{}\"."
" This may cause issues."
).format(source))
family = "render"
if ("prerender" in instance.data["families"] or
"prerender.farm" in instance.data["families"]):
family = "prerender"
families = [family]
# pass review to families if marked as review
do_not_add_review = False
if data.get("review"):
families.append("review")
elif data.get("review") is False:
self.log.debug("Instance has review explicitly disabled.")
do_not_add_review = True
instance_skeleton_data = {
"family": family,
"subset": subset,
"families": families,
"asset": asset,
"frameStart": start,
"frameEnd": end,
"handleStart": handle_start,
"handleEnd": handle_end,
"frameStartHandle": start - handle_start,
"frameEndHandle": end + handle_end,
"comment": instance.data["comment"],
"fps": fps,
"source": source,
"extendFrames": data.get("extendFrames"),
"overrideExistingFrame": data.get("overrideExistingFrame"),
"pixelAspect": data.get("pixelAspect", 1),
"resolutionWidth": data.get("resolutionWidth", 1920),
"resolutionHeight": data.get("resolutionHeight", 1080),
"multipartExr": data.get("multipartExr", False),
"jobBatchName": data.get("jobBatchName", ""),
"useSequenceForReview": data.get("useSequenceForReview", True),
# map inputVersions `ObjectId` -> `str` so json supports it
"inputVersions": list(map(str, data.get("inputVersions", []))),
"colorspace": instance.data.get("colorspace"),
"stagingDir_persistent": instance.data.get(
"stagingDir_persistent", False
)
}
# skip locking version if we are creating v01
instance_version = instance.data.get("version") # take this if exists
if instance_version != 1:
instance_skeleton_data["version"] = instance_version
# transfer specific families from original instance to new render
for item in self.families_transfer:
if item in instance.data.get("families", []):
instance_skeleton_data["families"] += [item]
# transfer specific properties from original instance based on
# mapping dictionary `instance_transfer`
for key, values in self.instance_transfer.items():
if key in instance.data.get("families", []):
for v in values:
instance_skeleton_data[v] = instance.data.get(v)
# look into instance data if representations are not having any
# which are having tag `publish_on_farm` and include them
for repre in instance.data.get("representations", []):
staging_dir = repre.get("stagingDir")
if staging_dir:
success, rootless_staging_dir = (
self.anatomy.find_root_template_from_path(
staging_dir
)
)
if success:
repre["stagingDir"] = rootless_staging_dir
else:
self.log.warning((
"Could not find root path for remapping \"{}\"."
" This may cause issues on farm."
).format(staging_dir))
repre["stagingDir"] = staging_dir
if "publish_on_farm" in repre.get("tags"):
# create representations attribute of not there
if "representations" not in instance_skeleton_data.keys():
instance_skeleton_data["representations"] = []
instance_skeleton_data["representations"].append(repre)
instances = None
assert data.get("expectedFiles"), ("Submission from old Pype version"
" - missing expectedFiles")
anatomy = instance.context.data["anatomy"]
instance_skeleton_data = create_skeleton_instance(

View file

@ -8,9 +8,9 @@ in global space here until are required or used.
"""
import os
import click
from openpype.modules import (
click_wrap,
JsonFilesSettingsDef,
OpenPypeAddOn,
ModulesManager,
@ -115,10 +115,12 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction):
}
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
@click.group(ExampleAddon.name, help="Example addon dynamic cli commands.")
@click_wrap.group(
ExampleAddon.name,
help="Example addon dynamic cli commands.")
def cli_main():
pass

View file

@ -3,9 +3,8 @@ import json
import collections
import platform
import click
from openpype.modules import (
click_wrap,
OpenPypeModule,
ITrayModule,
IPluginPaths,
@ -489,7 +488,7 @@ class FtrackModule(
return cred.get("username"), cred.get("api_key")
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
def _check_ftrack_url(url):
@ -540,24 +539,24 @@ def resolve_ftrack_url(url, logger=None):
return ftrack_url
@click.group(FtrackModule.name, help="Ftrack module related commands.")
@click_wrap.group(FtrackModule.name, help="Ftrack module related commands.")
def cli_main():
pass
@cli_main.command()
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
@click.option("--ftrack-url", envvar="FTRACK_SERVER",
@click_wrap.option("-d", "--debug", is_flag=True, help="Print debug messages")
@click_wrap.option("--ftrack-url", envvar="FTRACK_SERVER",
help="Ftrack server url")
@click.option("--ftrack-user", envvar="FTRACK_API_USER",
@click_wrap.option("--ftrack-user", envvar="FTRACK_API_USER",
help="Ftrack api user")
@click.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
@click_wrap.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
help="Ftrack api key")
@click.option("--legacy", is_flag=True,
@click_wrap.option("--legacy", is_flag=True,
help="run event server without mongo storing")
@click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
@click_wrap.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
help="Clockify API key.")
@click.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE",
@click_wrap.option("--clockify-workspace", envvar="CLOCKIFY_WORKSPACE",
help="Clockify workspace")
def eventserver(
debug,

View file

@ -131,6 +131,8 @@ class PostFtrackHook(PostLaunchHook):
for key, value in status_mapping.items():
if key in already_tested:
continue
value = [i.lower() for i in value]
if actual_status in value or "__any__" in value:
if key != "__ignore__":
next_status_name = key

View file

@ -41,8 +41,7 @@ import json
import copy
import platform
import click
from openpype.modules import OpenPypeModule
from openpype.modules import OpenPypeModule, click_wrap
from openpype.settings import get_system_settings
@ -153,7 +152,7 @@ class JobQueueModule(OpenPypeModule):
return requests.get(api_path).json()
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
@classmethod
def get_server_url_from_settings(cls):
@ -213,7 +212,7 @@ class JobQueueModule(OpenPypeModule):
return main(str(executable), server_url)
@click.group(
@click_wrap.group(
JobQueueModule.name,
help="Application job server. Can be used as render farm."
)
@ -225,8 +224,8 @@ def cli_main():
"start_server",
help="Start server handling workers and their jobs."
)
@click.option("--port", help="Server port")
@click.option("--host", help="Server host (ip address)")
@click_wrap.option("--port", help="Server port")
@click_wrap.option("--host", help="Server host (ip address)")
def cli_start_server(port, host):
JobQueueModule.start_server(port, host)
@ -236,7 +235,9 @@ def cli_start_server(port, host):
"Start a worker for a specific application. (e.g. \"tvpaint/11.5\")"
)
)
@click.argument("app_name")
@click.option("--server_url", help="Server url which handle workers and jobs.")
@click_wrap.argument("app_name")
@click_wrap.option(
"--server_url",
help="Server url which handle workers and jobs.")
def cli_start_worker(app_name, server_url):
JobQueueModule.start_worker(app_name, server_url)

View file

@ -1,9 +1,9 @@
"""Kitsu module."""
import click
import os
from openpype.modules import (
click_wrap,
OpenPypeModule,
IPluginPaths,
ITrayAction,
@ -98,17 +98,17 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):
}
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
@click.group(KitsuModule.name, help="Kitsu dynamic cli commands.")
@click_wrap.group(KitsuModule.name, help="Kitsu dynamic cli commands.")
def cli_main():
pass
@cli_main.command()
@click.option("--login", envvar="KITSU_LOGIN", help="Kitsu login")
@click.option(
@click_wrap.option("--login", envvar="KITSU_LOGIN", help="Kitsu login")
@click_wrap.option(
"--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
def push_to_zou(login, password):
@ -124,11 +124,11 @@ def push_to_zou(login, password):
@cli_main.command()
@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login")
@click.option(
@click_wrap.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login")
@click_wrap.option(
"-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username"
)
@click.option(
@click_wrap.option(
"-prj",
"--project",
"projects",
@ -136,7 +136,7 @@ def push_to_zou(login, password):
default=[],
help="Sync specific kitsu projects",
)
@click.option(
@click_wrap.option(
"-lo",
"--listen-only",
"listen_only",

View file

@ -7,7 +7,6 @@ import copy
import signal
from collections import deque, defaultdict
import click
from bson.objectid import ObjectId
from openpype.client import (
@ -15,7 +14,12 @@ from openpype.client import (
get_representations,
get_representation_by_id,
)
from openpype.modules import OpenPypeModule, ITrayModule, IPluginPaths
from openpype.modules import (
OpenPypeModule,
ITrayModule,
IPluginPaths,
click_wrap,
)
from openpype.settings import (
get_project_settings,
get_system_settings,
@ -2405,7 +2409,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
return presets[project_name]['sites'][site_name]['root']
def cli(self, click_group):
click_group.add_command(cli_main)
click_group.add_command(cli_main.to_click_obj())
# Webserver module implementation
def webserver_initialization(self, server_manager):
@ -2417,13 +2421,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule, IPluginPaths):
)
@click.group(SyncServerModule.name, help="SyncServer module related commands.")
@click_wrap.group(
SyncServerModule.name,
help="SyncServer module related commands.")
def cli_main():
pass
@cli_main.command()
@click.option(
@click_wrap.option(
"-a",
"--active_site",
required=True,

View file

@ -17,6 +17,7 @@ class WidgetUserIdle(QtWidgets.QWidget):
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
| QtCore.Qt.WindowStaysOnTopHint
)
self._is_showed = False

View file

@ -2,6 +2,7 @@ import copy
import os
import subprocess
import tempfile
import re
import pyblish.api
from openpype.lib import (
@ -14,9 +15,10 @@ from openpype.lib import (
path_to_subprocess_arg,
run_subprocess,
)
from openpype.lib.transcoding import convert_colorspace
from openpype.lib.transcoding import VIDEO_EXTENSIONS
from openpype.lib.transcoding import (
convert_colorspace,
VIDEO_EXTENSIONS,
)
class ExtractThumbnail(pyblish.api.InstancePlugin):
@ -35,6 +37,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
"traypublisher",
"substancepainter",
"nuke",
"aftereffects"
]
enabled = False
@ -49,6 +52,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
# attribute presets from settings
oiiotool_defaults = None
ffmpeg_args = None
subsets = []
product_names = []
def process(self, instance):
# run main process
@ -103,6 +108,26 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
self.log.debug("Skipping crypto passes.")
return
# We only want to process the subsets needed from settings.
def validate_string_against_patterns(input_str, patterns):
for pattern in patterns:
if re.match(pattern, input_str):
return True
return False
product_names = self.subsets + self.product_names
if product_names:
result = validate_string_against_patterns(
instance.data["subset"], product_names
)
if not result:
self.log.debug(
"Subset \"{}\" did not match any valid subsets: {}".format(
instance.data["subset"], product_names
)
)
return
# first check for any explicitly marked representations for thumbnail
explicit_repres = self._get_explicit_repres_for_thumbnail(instance)
if explicit_repres:

View file

@ -17,7 +17,7 @@ class ValidateResources(pyblish.api.InstancePlugin):
"""
order = ValidateContentsOrder
label = "Resources"
label = "Validate Resources"
def process(self, instance):

View file

@ -1458,7 +1458,7 @@ class _AyonSettingsCache:
variant = "production"
if is_dev_mode_enabled():
variant = cls._get_dev_mode_settings_variant()
variant = cls._get_bundle_name()
elif is_staging_enabled():
variant = "staging"
@ -1474,28 +1474,6 @@ class _AyonSettingsCache:
def _get_bundle_name(cls):
return os.environ["AYON_BUNDLE_NAME"]
@classmethod
def _get_dev_mode_settings_variant(cls):
"""Develop mode settings variant.
Returns:
str: Name of settings variant.
"""
con = get_ayon_server_api_connection()
bundles = con.get_bundles()
user = con.get_user()
username = user["name"]
for bundle in bundles["bundles"]:
if (
bundle.get("isDev")
and bundle.get("activeUser") == username
):
return bundle["name"]
# Return fake variant - distribution logic will tell user that he
# does not have set any dev bundle
return "dev"
@classmethod
def get_value_by_project(cls, project_name):
cache_item = _AyonSettingsCache.cache_by_project_name[project_name]

View file

@ -15,7 +15,8 @@
"default_variants": [
"Main"
],
"mark_for_review": true
"mark_for_review": true,
"force_setting_values": true
}
},
"publish": {

View file

@ -22,7 +22,9 @@
"aov_separator": "underscore",
"image_format": "exr",
"multilayer_exr": true,
"aov_list": [],
"renderer": "CYCLES",
"compositing": true,
"aov_list": ["combined"],
"custom_passes": []
},
"workfile_builder": {

View file

@ -129,6 +129,7 @@
"deadline_priority": 50,
"publishing_script": "",
"skip_integration_repre_list": [],
"families_transfer": ["render3d", "render2d", "ftrack", "slate"],
"aov_filter": {
"maya": [
".*([Bb]eauty).*"

View file

@ -70,6 +70,7 @@
},
"ExtractThumbnail": {
"enabled": true,
"subsets": [],
"integrate_thumbnail": false,
"background_color": [
0,

View file

@ -42,6 +42,12 @@
"key": "mark_for_review",
"label": "Review",
"default": true
},
{
"type": "boolean",
"key": "force_setting_values",
"label": "Force resolution and duration values from Asset",
"default": true
}
]
}

View file

@ -103,6 +103,22 @@
"type": "label",
"label": "Note: Multilayer EXR is only used when output format type set to EXR."
},
{
"key": "renderer",
"label": "Renderer",
"type": "enum",
"multiselection": false,
"defaults": "CYCLES",
"enum_items": [
{"CYCLES": "Cycles"},
{"BLENDER_EEVEE": "Eevee"}
]
},
{
"key": "compositing",
"type": "boolean",
"label": "Enable Compositing"
},
{
"key": "aov_list",
"label": "AOVs to create",
@ -110,23 +126,38 @@
"multiselection": true,
"defaults": "empty",
"enum_items": [
{"empty": "< empty >"},
{"combined": "Combined"},
{"z": "Z"},
{"mist": "Mist"},
{"normal": "Normal"},
{"diffuse_light": "Diffuse Light"},
{"position": "Position (Cycles Only)"},
{"vector": "Vector (Cycles Only)"},
{"uv": "UV (Cycles Only)"},
{"denoising": "Denoising Data (Cycles Only)"},
{"object_index": "Object Index (Cycles Only)"},
{"material_index": "Material Index (Cycles Only)"},
{"sample_count": "Sample Count (Cycles Only)"},
{"diffuse_light": "Diffuse Light/Direct"},
{"diffuse_indirect": "Diffuse Indirect (Cycles Only)"},
{"diffuse_color": "Diffuse Color"},
{"specular_light": "Specular Light"},
{"specular_color": "Specular Color"},
{"volume_light": "Volume Light"},
{"specular_light": "Specular (Glossy) Light/Direct"},
{"specular_indirect": "Specular (Glossy) Indirect (Cycles Only)"},
{"specular_color": "Specular (Glossy) Color"},
{"transmission_light": "Transmission Light/Direct (Cycles Only)"},
{"transmission_indirect": "Transmission Indirect (Cycles Only)"},
{"transmission_color": "Transmission Color (Cycles Only)"},
{"volume_light": "Volume Light/Direct"},
{"volume_indirect": "Volume Indirect (Cycles Only)"},
{"emission": "Emission"},
{"environment": "Environment"},
{"shadow": "Shadow"},
{"shadow": "Shadow/Shadow Catcher"},
{"ao": "Ambient Occlusion"},
{"denoising": "Denoising"},
{"volume_direct": "Direct Volumetric Scattering"},
{"volume_indirect": "Indirect Volumetric Scattering"}
{"bloom": "Bloom (Eevee Only)"},
{"transparent": "Transparent (Eevee Only)"},
{"cryptomatte_object": "Cryptomatte Object"},
{"cryptomatte_material": "Cryptomatte Material"},
{"cryptomatte_asset": "Cryptomatte Asset"},
{"cryptomatte_accurate": "Cryptomatte Accurate Mode (Eevee Only)"}
]
},
{

View file

@ -693,6 +693,14 @@
"type": "text"
}
},
{
"type": "list",
"key": "families_transfer",
"label": "List of family names to transfer\nto generated instances (AOVs for example).",
"object_type": {
"type": "text"
}
},
{
"type": "dict-modifiable",
"docstring": "Regular expression to filter for which subset review should be created in publish job.",

View file

@ -202,6 +202,12 @@
"key": "enabled",
"label": "Enabled"
},
{
"type": "list",
"object_type": "text",
"key": "subsets",
"label": "Subsets"
},
{
"type": "boolean",
"key": "integrate_thumbnail",

View file

@ -531,6 +531,9 @@ class FrontendLoaderController(_BaseLoaderController):
Product types have defined if are checked for filtering or not.
Args:
project_name (Union[str, None]): Project name.
Returns:
list[ProductTypeItem]: List of product type items for a project.
"""

View file

@ -179,12 +179,15 @@ class ProductsModel:
"""Product type items for project.
Args:
project_name (str): Project name.
project_name (Union[str, None]): Project name.
Returns:
list[ProductTypeItem]: Product type items.
"""
if not project_name:
return []
cache = self._product_type_items_cache[project_name]
if not cache.is_valid:
product_types = ayon_api.get_project_product_types(project_name)

View file

@ -322,6 +322,7 @@ class LoaderWindow(QtWidgets.QWidget):
)
def refresh(self):
self._reset_on_show = False
self._controller.reset()
def showEvent(self, event):
@ -332,6 +333,13 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.start()
def closeEvent(self, event):
super(LoaderWindow, self).closeEvent(event)
# Deselect project so current context will be selected
# on next 'showEvent'
self._controller.set_selected_project(None)
self._reset_on_show = True
def keyPressEvent(self, event):
modifiers = event.modifiers()
ctrl_pressed = QtCore.Qt.ControlModifier & modifiers
@ -378,8 +386,7 @@ class LoaderWindow(QtWidgets.QWidget):
self._show_timer.stop()
if self._reset_on_show:
self._reset_on_show = False
self._controller.reset()
self.refresh()
def _show_group_dialog(self):
project_name = self._projects_combobox.get_selected_project_name()

View file

@ -1212,12 +1212,12 @@ class SwitchAssetDialog(QtWidgets.QDialog):
))
version_ids = set()
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.add(version_doc["_id"])
version_docs_by_parent_id[parent_id] = version_doc
version_ids.add(version_doc["_id"])
product_id = version_doc["parent"]
name = version_doc["name"]
version_docs_by_parent_id_and_name[product_id][name] = version_doc
hero_version_docs_by_parent_id = {}
for hero_version_doc in hero_version_docs:
@ -1242,7 +1242,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
version_docs_by_parent_id,
version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
)
@ -1256,10 +1256,10 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container,
loader,
selected_folder_id,
product_name,
selected_product_name,
selected_representation,
product_docs_by_parent_and_name,
version_docs_by_parent_id,
version_docs_by_parent_id_and_name,
hero_version_docs_by_parent_id,
repre_docs_by_parent_id_by_name,
):
@ -1272,15 +1272,18 @@ class SwitchAssetDialog(QtWidgets.QDialog):
container_product_id = container_version["parent"]
container_product = self._product_docs_by_id[container_product_id]
container_product_name = container_product["name"]
container_folder_id = container_product["parent"]
if selected_folder_id:
folder_id = selected_folder_id
else:
folder_id = container_product["parent"]
folder_id = container_folder_id
products_by_name = product_docs_by_parent_and_name[folder_id]
if product_name:
product_doc = products_by_name[product_name]
if selected_product_name:
product_doc = products_by_name[selected_product_name]
else:
product_doc = products_by_name[container_product["name"]]
@ -1300,7 +1303,26 @@ class SwitchAssetDialog(QtWidgets.QDialog):
repre_doc = _repres.get(container_repre_name)
if not repre_doc:
version_doc = version_docs_by_parent_id[product_id]
version_docs_by_name = (
version_docs_by_parent_id_and_name[product_id]
)
# If asset or subset are selected for switching, we use latest
# version else we try to keep the current container version.
version_name = None
if (
selected_folder_id in (None, container_folder_id)
and selected_product_name in (None, container_product_name)
):
version_name = container_version.get("name")
version_doc = None
if version_name is not None:
version_doc = version_docs_by_name.get(version_name)
if version_doc is None:
version_name = max(version_docs_by_name)
version_doc = version_docs_by_name[version_name]
version_id = version_doc["_id"]
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
if selected_representation:

View file

@ -2329,8 +2329,25 @@ class PublisherController(BasePublisherController):
result = pyblish.plugin.process(
plugin, self._publish_context, None, action.id
)
exception = result.get("error")
if exception:
self._emit_event(
"publish.action.failed",
{
"title": "Action failed",
"message": "Action failed.",
"traceback": "".join(
traceback.format_exception(exception)
),
"label": action.__name__,
"identifier": action.id
}
)
self._publish_report.add_action_result(action, result)
self.emit_card_message("Action finished.")
def _publish_next_process(self):
# Validations of progress before using iterator
# - same conditions may be inside iterator but they may be used

View file

@ -42,7 +42,7 @@ from .widgets import (
)
class PublisherWindow(QtWidgets.QWidget):
class PublisherWindow(QtWidgets.QDialog):
"""Main window of publisher."""
default_width = 1300
default_height = 800
@ -50,7 +50,7 @@ class PublisherWindow(QtWidgets.QWidget):
publish_footer_spacer = 2
def __init__(self, parent=None, controller=None, reset_on_show=None):
super(PublisherWindow, self).__init__()
super(PublisherWindow, self).__init__(parent)
self.setObjectName("PublishWindow")
@ -294,12 +294,6 @@ class PublisherWindow(QtWidgets.QWidget):
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
)
@ -321,6 +315,9 @@ class PublisherWindow(QtWidgets.QWidget):
controller.event_system.add_callback(
"convertors.find.failed", self._on_convertor_error
)
controller.event_system.add_callback(
"publish.action.failed", self._on_action_error
)
controller.event_system.add_callback(
"export_report.request", self._export_report
)
@ -328,7 +325,6 @@ class PublisherWindow(QtWidgets.QWidget):
"copy_report.request", self._copy_report
)
# Store extra header widget for TrayPublisher
# - can be used to add additional widgets to header between context
# label and help button
@ -491,8 +487,14 @@ class PublisherWindow(QtWidgets.QWidget):
app.removeEventFilter(self)
def keyPressEvent(self, event):
# Ignore escape button to close window
if event.key() == QtCore.Qt.Key_Escape:
if event.key() in {
# Ignore escape button to close window
QtCore.Qt.Key_Escape,
# Ignore enter keyboard event which by default triggers
# first available button in QDialog
QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
}:
event.accept()
return
@ -558,18 +560,6 @@ class PublisherWindow(QtWidgets.QWidget):
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.
@ -882,12 +872,6 @@ class PublisherWindow(QtWidgets.QWidget):
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)
@ -898,7 +882,6 @@ class PublisherWindow(QtWidgets.QWidget):
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)
@ -1012,6 +995,18 @@ class PublisherWindow(QtWidgets.QWidget):
event["title"], new_failed_info, "Convertor:"
)
def _on_action_error(self, event):
self.add_error_message_dialog(
event["title"],
[{
"message": event["message"],
"traceback": event["traceback"],
"label": event["label"],
"identifier": event["identifier"]
}],
"Action:"
)
def _update_create_overlay_size(self):
metrics = self._create_overlay_button.fontMetrics()
height = int(metrics.height())

View file

@ -1299,15 +1299,21 @@ class SwitchAssetDialog(QtWidgets.QDialog):
# If asset or subset are selected for switching, we use latest
# version else we try to keep the current container version.
version_name = None
if (
selected_asset not in (None, container_asset_name)
or selected_subset not in (None, container_subset_name)
selected_asset in (None, container_asset_name)
and selected_subset in (None, container_subset_name)
):
version_name = max(version_docs_by_name)
else:
version_name = container_version["name"]
version_name = container_version.get("name")
version_doc = None
if version_name is not None:
version_doc = version_docs_by_name.get(version_name)
if version_doc is None:
version_name = max(version_docs_by_name)
version_doc = version_docs_by_name[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

View file

@ -32,6 +32,7 @@ from .lib import (
set_style_property,
DynamicQThread,
qt_app_context,
get_qt_app,
get_openpype_qt_app,
get_asset_icon,
get_asset_icon_by_name,

View file

@ -154,11 +154,15 @@ def qt_app_context():
yield app
def get_openpype_qt_app():
"""Main Qt application initialized for OpenPype processed.
def get_qt_app():
"""Get Qt application.
This function should be used only inside OpenPype process and never inside
other processes.
The function initializes new Qt application if it is not already
initialized. It also sets some attributes to the application to
ensure that it will work properly on high DPI displays.
Returns:
QtWidgets.QApplication: Current Qt application.
"""
app = QtWidgets.QApplication.instance()
@ -184,6 +188,17 @@ def get_openpype_qt_app():
app = QtWidgets.QApplication(sys.argv)
return app
def get_openpype_qt_app():
"""Main Qt application initialized for OpenPype processed.
This function should be used only inside OpenPype process and never inside
other processes.
"""
app = get_qt_app()
app.setWindowIcon(QtGui.QIcon(get_app_icon_path()))
return app

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.18.6-nightly.2"
__version__ = "3.18.8-nightly.1"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.18.5" # OpenPype
version = "3.18.7" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

View file

@ -7,6 +7,8 @@ class CreateRenderPlugin(BaseSettingsModel):
default_factory=list,
title="Default Variants"
)
force_setting_values: bool = SettingsField(
True, title="Force resolution and duration values from Asset")
class AfterEffectsCreatorPlugins(BaseSettingsModel):

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring addon version."""
__version__ = "0.1.2"
__version__ = "0.1.3"

View file

@ -1175,30 +1175,6 @@
}
]
},
"djvview": {
"enabled": true,
"label": "DJV View",
"icon": "{}/app_icons/djvView.png",
"host_name": "",
"environment": "{}",
"variants": [
{
"name": "1-1",
"label": "1.1",
"executables": {
"windows": [],
"darwin": [],
"linux": []
},
"arguments": {
"windows": [],
"darwin": [],
"linux": []
},
"environment": "{}"
}
]
},
"wrap": {
"enabled": true,
"label": "Wrap",

View file

@ -23,6 +23,13 @@ def image_format_enum():
]
def renderers_enum():
return [
{"value": "CYCLES", "label": "Cycles"},
{"value": "BLENDER_EEVEE", "label": "Eevee"},
]
def aov_list_enum():
return [
{"value": "empty", "label": "< none >"},
@ -30,18 +37,52 @@ def aov_list_enum():
{"value": "z", "label": "Z"},
{"value": "mist", "label": "Mist"},
{"value": "normal", "label": "Normal"},
{"value": "diffuse_light", "label": "Diffuse Light"},
{"value": "position", "label": "Position (Cycles Only)"},
{"value": "vector", "label": "Vector (Cycles Only)"},
{"value": "uv", "label": "UV (Cycles Only)"},
{"value": "denoising", "label": "Denoising Data (Cycles Only)"},
{"value": "object_index", "label": "Object Index (Cycles Only)"},
{"value": "material_index", "label": "Material Index (Cycles Only)"},
{"value": "sample_count", "label": "Sample Count (Cycles Only)"},
{"value": "diffuse_light", "label": "Diffuse Light/Direct"},
{
"value": "diffuse_indirect",
"label": "Diffuse Indirect (Cycles Only)"
},
{"value": "diffuse_color", "label": "Diffuse Color"},
{"value": "specular_light", "label": "Specular Light"},
{"value": "specular_color", "label": "Specular Color"},
{"value": "volume_light", "label": "Volume Light"},
{"value": "specular_light", "label": "Specular (Glossy) Light/Direct"},
{
"value": "specular_indirect",
"label": "Specular (Glossy) Indirect (Cycles Only)"
},
{"value": "specular_color", "label": "Specular (Glossy) Color"},
{
"value": "transmission_light",
"label": "Transmission Light/Direct (Cycles Only)"
},
{
"value": "transmission_indirect",
"label": "Transmission Indirect (Cycles Only)"
},
{
"value": "transmission_color",
"label": "Transmission Color (Cycles Only)"
},
{"value": "volume_light", "label": "Volume Light/Direct"},
{"value": "volume_indirect", "label": "Volume Indirect (Cycles Only)"},
{"value": "emission", "label": "Emission"},
{"value": "environment", "label": "Environment"},
{"value": "shadow", "label": "Shadow"},
{"value": "shadow", "label": "Shadow/Shadow Catcher"},
{"value": "ao", "label": "Ambient Occlusion"},
{"value": "denoising", "label": "Denoising"},
{"value": "volume_direct", "label": "Direct Volumetric Scattering"},
{"value": "volume_indirect", "label": "Indirect Volumetric Scattering"}
{"value": "bloom", "label": "Bloom (Eevee Only)"},
{"value": "transparent", "label": "Transparent (Eevee Only)"},
{"value": "cryptomatte_object", "label": "Cryptomatte Object"},
{"value": "cryptomatte_material", "label": "Cryptomatte Material"},
{"value": "cryptomatte_asset", "label": "Cryptomatte Asset"},
{
"value": "cryptomatte_accurate",
"label": "Cryptomatte Accurate Mode (Eevee Only)"
},
]
@ -81,6 +122,14 @@ class RenderSettingsModel(BaseSettingsModel):
multilayer_exr: bool = SettingsField(
title="Multilayer (EXR)"
)
renderer: str = SettingsField(
"CYCLES",
title="Renderer",
enum_resolver=renderers_enum
)
compositing: bool = SettingsField(
title="Enable Compositing"
)
aov_list: list[str] = SettingsField(
default_factory=list,
enum_resolver=aov_list_enum,
@ -102,6 +151,8 @@ DEFAULT_RENDER_SETTINGS = {
"aov_separator": "underscore",
"image_format": "exr",
"multilayer_exr": True,
"aov_list": [],
"renderer": "CYCLES",
"compositing": True,
"aov_list": ["combined"],
"custom_passes": []
}

View file

@ -1 +1 @@
__version__ = "0.1.5"
__version__ = "0.1.6"

View file

@ -176,6 +176,10 @@ class ExtractThumbnailOIIODefaultsModel(BaseSettingsModel):
class ExtractThumbnailModel(BaseSettingsModel):
_isGroup = True
enabled: bool = SettingsField(True)
product_names: list[str] = SettingsField(
default_factory=list,
title="Product names"
)
integrate_thumbnail: bool = SettingsField(
True,
title="Integrate Thumbnail Representation"
@ -844,6 +848,7 @@ DEFAULT_PUBLISH_VALUES = {
},
"ExtractThumbnail": {
"enabled": True,
"product_names": [],
"integrate_thumbnail": True,
"target_size": {
"type": "source"

View file

@ -1 +1 @@
__version__ = "0.1.4"
__version__ = "0.1.5"

View file

@ -57,9 +57,9 @@ class CreateSaverPluginModel(BaseSettingsModel):
enum_resolver=_create_saver_instance_attributes_enum,
title="Instance attributes"
)
output_formats: list[str] = SettingsField(
default_factory=list,
title="Output formats"
image_format: str = SettingsField(
enum_resolver=_image_format_enum,
title="Output Image Format"
)
@ -90,6 +90,8 @@ class CreateImageSaverModel(CreateSaverPluginModel):
0,
title="Default rendered frame"
)
class CreatPluginsModel(BaseSettingsModel):
CreateSaver: CreateSaverModel = SettingsField(
default_factory=CreateSaverModel,

View file

@ -1 +1 @@
__version__ = "0.1.3"
__version__ = "0.1.4"