mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
🔀 Merge remote-tracking branch 'origin/develop' into maya_new_publisher
This commit is contained in:
commit
92d18eb970
71 changed files with 1810 additions and 380 deletions
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,10 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.15.12
|
||||
- 3.15.12-nightly.4
|
||||
- 3.15.12-nightly.3
|
||||
- 3.15.12-nightly.2
|
||||
- 3.15.12-nightly.1
|
||||
- 3.15.11
|
||||
- 3.15.11-nightly.5
|
||||
|
|
@ -131,10 +135,6 @@ body:
|
|||
- 3.14.5-nightly.2
|
||||
- 3.14.5-nightly.1
|
||||
- 3.14.4
|
||||
- 3.14.4-nightly.4
|
||||
- 3.14.4-nightly.3
|
||||
- 3.14.4-nightly.2
|
||||
- 3.14.4-nightly.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
603
CHANGELOG.md
603
CHANGELOG.md
|
|
@ -1,6 +1,609 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.15.12](https://github.com/ynput/OpenPype/tree/3.15.12)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.11...3.15.12)
|
||||
|
||||
### **🆕 New features**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Tray Publisher: User can set colorspace per instance explicitly <a href="https://github.com/ynput/OpenPype/pull/4901">#4901</a></summary>
|
||||
|
||||
With this feature a user can set/override the colorspace for the representations of an instance explicitly instead of relying on the File Rules from project settings or alike. This way you can ingest any file and explicitly say "this file is colorspace X".
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Review Family in Max <a href="https://github.com/ynput/OpenPype/pull/5001">#5001</a></summary>
|
||||
|
||||
Review Feature by creating preview animation in 3dsmax(The code is still cleaning up so there is going to be some updates until it is ready for review)
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AfterEffects: support for workfile template builder <a href="https://github.com/ynput/OpenPype/pull/5163">#5163</a></summary>
|
||||
|
||||
This PR add functionality of templated workfile builder. It allows someone to prepare AE workfile with placeholders as for automatically loading particular representation of particular subset of particular asset from context where workfile is opened.Selection from multiple prepared workfiles is provided with usage of templates, specific type of tasks could use particular workfile template etc.Artists then can build workfile from template when opening new workfile.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>CreatePlugin: Get next version helper <a href="https://github.com/ynput/OpenPype/pull/5242">#5242</a></summary>
|
||||
|
||||
Implemented helper functions to get next available versions for create instances.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Improve Templates <a href="https://github.com/ynput/OpenPype/pull/4854">#4854</a></summary>
|
||||
|
||||
Use library method for fetching reference node and support parent in hierarchy.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bug: Maya - xgen sidecar files arent moved when saving workfile as an new asset workfile changing context - OP-6222 <a href="https://github.com/ynput/OpenPype/pull/5215">#5215</a></summary>
|
||||
|
||||
This PR manages the Xgen files when switching context in the Workfiles app.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>node references to check for duplicates in Max <a href="https://github.com/ynput/OpenPype/pull/5192">#5192</a></summary>
|
||||
|
||||
No duplicates for node references in Max when users trying to select nodes before publishing
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Tweak profiles logging to debug level <a href="https://github.com/ynput/OpenPype/pull/5194">#5194</a></summary>
|
||||
|
||||
Tweak profiles logging to debug level since they aren't artist facing logs.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement: Reduce more visual clutter for artists in new publisher reports <a href="https://github.com/ynput/OpenPype/pull/5208">#5208</a></summary>
|
||||
|
||||
Got this from one of our artists' reports - figured some of these logs were definitely not for the artist, reduced those logs to debug level.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Cosmetics: Tweak pyblish repair actions (icon, logs, docstring) <a href="https://github.com/ynput/OpenPype/pull/5213">#5213</a></summary>
|
||||
|
||||
- Add icon to RepairContextAction
|
||||
- logs to debug level
|
||||
- also add attempt repair for RepairAction for consistency
|
||||
- fix RepairContextAction docstring to mention correct argument name
|
||||
|
||||
#### Additional info
|
||||
|
||||
We should not forget to remove this ["deprecated" actions.py file](https://github.com/ynput/OpenPype/blob/3501d0d23a78fbaef106da2fffe946cb49bef855/openpype/action.py) in 3.16 (next-minor)
|
||||
|
||||
## Testing notes:
|
||||
|
||||
1. Run some fabulous repairs!
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: fix save file prompt on launch last workfile with color management enabled + restructure `set_colorspace` <a href="https://github.com/ynput/OpenPype/pull/5225">#5225</a></summary>
|
||||
|
||||
- Only set `configFilePath` when OCIO env var is not set since it doesn't do anything if OCIO var is set anyway.
|
||||
- Set the Maya 2022+ default OCIO path using the resources path instead of "" to avoid Maya Save File on new file after launch
|
||||
- **Bugfix: This is what fixes the Save prompt on open last workfile feature with Global color management enabled**
|
||||
- Move all code related to applying the maya settings together after querying the settings
|
||||
- Swap around the `if use_workfile_settings` since the check was reversed
|
||||
- Use `get_current_project_name()` instead of environment vars
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement: More descriptive error messages for Loaders <a href="https://github.com/ynput/OpenPype/pull/5227">#5227</a></summary>
|
||||
|
||||
Tweak raised errors and error messages for loader errors.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: add select invalid action for ValidateSopOutputNode <a href="https://github.com/ynput/OpenPype/pull/5231">#5231</a></summary>
|
||||
|
||||
This PR adds `SelectROPAction` action to `houdini\api\action.py`and it's used in `Validate Output Node``SelectROPAction` is used to select the associated ROPs with the errored instances.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Remove new lines from the delivery template string <a href="https://github.com/ynput/OpenPype/pull/5235">#5235</a></summary>
|
||||
|
||||
If the delivery template has a new line symbol at the end, say it was copied from the text editor, the delivery process will fail with `OSError` due to incorrect destination path. To avoid that I added `rstrip()` to the `delivery_path` processing.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: better selection on pointcache creation <a href="https://github.com/ynput/OpenPype/pull/5250">#5250</a></summary>
|
||||
|
||||
Houdini allows `ObjNode` path as `sop_path` in the `ROP` unlike OP/ Ayon require `sop_path` to be set to a sop node path explicitly In this code, better selection is used to filter out invalid selections from OP/ Ayon point of viewValid selections are
|
||||
- `SopNode` that has parent of type `geo` or `subnet`
|
||||
- `ObjNode` of type `geo` that has
|
||||
- `SopNode` of type `output`
|
||||
- `SopNode` with render flag `on` (if no `Sopnode` of type `output`)this effectively filter
|
||||
- empty `ObjNode`
|
||||
- `ObjNode`(s) of other types like `cam` and `dopnet`
|
||||
- `SopNode`(s) that thier parents of other types like `cam` and `sop solver`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Update scene inventory even if any errors occurred during update <a href="https://github.com/ynput/OpenPype/pull/5252">#5252</a></summary>
|
||||
|
||||
When selecting many items in the scene inventory to update versions and one of the items would error out the updating stops. However, before this PR the scene inventory would also NOT refresh making you think it did nothing.Also implemented as method to allow some code deduplication.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Convert frame values to integers <a href="https://github.com/ynput/OpenPype/pull/5188">#5188</a></summary>
|
||||
|
||||
Convert frame values to integers.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: fix the register_event_callback correctly collecting workfile save after <a href="https://github.com/ynput/OpenPype/pull/5214">#5214</a></summary>
|
||||
|
||||
fixing the bug of register_event_callback not being able to collect action of "workfile_save_after" for lock file action
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: aligning default settings to distributed aces 1.2 config <a href="https://github.com/ynput/OpenPype/pull/5233">#5233</a></summary>
|
||||
|
||||
Maya colorspace setttings defaults are set the way they align our distributed ACES 1.2 config file set in global colorspace configs.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>RepairAction and SelectInvalidAction filter instances failed on the exact plugin <a href="https://github.com/ynput/OpenPype/pull/5240">#5240</a></summary>
|
||||
|
||||
RepairAction and SelectInvalidAction actually filter to instances that failed on the exact plugin - not on "any failure"
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Bugfix look update nodes by id with non-unique shape names (query with `fullPath`) <a href="https://github.com/ynput/OpenPype/pull/5257">#5257</a></summary>
|
||||
|
||||
Fixes a bug where updating attributes on nodes with assigned shader if shape name existed more than once in the scene due to `cmds.listRelatives` call not being done with the `fullPath=True` flag.Original error:
|
||||
```python
|
||||
# Traceback (most recent call last):
|
||||
# File "E:\openpype\OpenPype\openpype\tools\sceneinventory\view.py", line 264, in <lambda>
|
||||
# lambda: self._show_version_dialog(items))
|
||||
# File "E:\openpype\OpenPype\openpype\tools\sceneinventory\view.py", line 722, in _show_version_dialog
|
||||
# self._update_containers(items, version)
|
||||
# File "E:\openpype\OpenPype\openpype\tools\sceneinventory\view.py", line 849, in _update_containers
|
||||
# update_container(item, item_version)
|
||||
# File "E:\openpype\OpenPype\openpype\pipeline\load\utils.py", line 502, in update_container
|
||||
# return loader.update(container, new_representation)
|
||||
# File "E:\openpype\OpenPype\openpype\hosts\maya\plugins\load\load_look.py", line 119, in update
|
||||
# nodes_by_id[lib.get_id(n)].append(n)
|
||||
# File "E:\openpype\OpenPype\openpype\hosts\maya\api\lib.py", line 1420, in get_id
|
||||
# sel.add(node)
|
||||
```
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Create nodes with inpanel=False <a href="https://github.com/ynput/OpenPype/pull/5051">#5051</a></summary>
|
||||
|
||||
This PR is meant to remove the annoyance of the UI changing focus to the properties window just for the property window of the newly created node to disappear. Instead of using node.hideControlPanel I'm implementing the concealment during the creation of the node which will not change the focus of the current window.
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix the reset frame range not setting up the right timeline in Max <a href="https://github.com/ynput/OpenPype/pull/5187">#5187</a></summary>
|
||||
|
||||
Resolve #5181
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Resolve: after launch automatization fixes <a href="https://github.com/ynput/OpenPype/pull/5193">#5193</a></summary>
|
||||
|
||||
Workfile is no correctly created and aligned witch actual project. Also the launching mechanism is now fixed so even no workfile had been saved yet it will open OpenPype menu automatically.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: Revert backward incompatible change of path to template to multiplatform <a href="https://github.com/ynput/OpenPype/pull/5197">#5197</a></summary>
|
||||
|
||||
Now platformity is still handed by usage of `work[root]` (or any other root that is accessible across platforms.)
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: root set format updating in node graph <a href="https://github.com/ynput/OpenPype/pull/5198">#5198</a></summary>
|
||||
|
||||
Nuke root node needs to be reset on some values so any knobs could be updated in node graph. This works the same way as an user would change frame number so expressions would update its values in knobs.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Hiero: fixing otio current project and cosmetics <a href="https://github.com/ynput/OpenPype/pull/5200">#5200</a></summary>
|
||||
|
||||
Otio were not returning correct current project once additional Untitled project was open in project manager stack.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Publisher instances dont hold its enabled disabled states when Publisher reopened again <a href="https://github.com/ynput/OpenPype/pull/5202">#5202</a></summary>
|
||||
|
||||
Resolve #5183, general maxscript conversion issue to python (e.g. bool conversion, true in maxscript while True in Python)(Also resolve the ValueError when you change the subset to publish into list view menu)
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Burnins: Filter script is defined only for video streams <a href="https://github.com/ynput/OpenPype/pull/5205">#5205</a></summary>
|
||||
|
||||
Burnins are working for inputs with audio.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Colorspace lib fix compatible python version comparison <a href="https://github.com/ynput/OpenPype/pull/5212">#5212</a></summary>
|
||||
|
||||
Fix python version comparison.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: Fix `get_color_management_preferences` <a href="https://github.com/ynput/OpenPype/pull/5217">#5217</a></summary>
|
||||
|
||||
Fix the issue described here where the logic for retrieving the current OCIO display and view was incorrectly trying to apply a regex to it.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: Redshift ROP image format bug <a href="https://github.com/ynput/OpenPype/pull/5218">#5218</a></summary>
|
||||
|
||||
Problem :
|
||||
"RS_outputFileFormat" parm value was missing
|
||||
and there were more "image_format" than redshift rop supports
|
||||
|
||||
Fix:
|
||||
1) removed unnecessary formats from `image_format_enum`
|
||||
2) add the selected format value to `RS_outputFileFormat`
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Colorspace: check PyOpenColorIO rather then python version <a href="https://github.com/ynput/OpenPype/pull/5223">#5223</a></summary>
|
||||
|
||||
Fixing previously merged PR (https://github.com/ynput/OpenPype/pull/5212) And applying better way to check compatibility with PyOpenColorIO python api.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Validate delivery action representations status <a href="https://github.com/ynput/OpenPype/pull/5228">#5228</a></summary>
|
||||
|
||||
- disable delivery button if no representations checked
|
||||
- fix macos combobox layout
|
||||
- add error message if no delivery templates found
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Houdini: Add geometry check for pointcache family <a href="https://github.com/ynput/OpenPype/pull/5230">#5230</a></summary>
|
||||
|
||||
When `sop_path` on ABC ROP node points to a non `SopNode`, these validators `validate_abc_primitive_to_detail.py`, `validate_primitive_hierarchy_paths.py` will error and crash when this line is executed `geo = output_node.geometryAtFrame(frame)`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: Add geometry check for VDB family <a href="https://github.com/ynput/OpenPype/pull/5232">#5232</a></summary>
|
||||
|
||||
When `sop_path` on Geometry ROP node points to a non SopNode, this validator `validate_vdb_output_node.py` will error and crash when this line is executed`sop_node.geometryAtFrame(frame)`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Substance Painter: Include the setting only in publish tab <a href="https://github.com/ynput/OpenPype/pull/5234">#5234</a></summary>
|
||||
|
||||
Instead of having two settings in both create and publish tab, there is solely one setting in the publish tab for users to set up the parameters.Resolve #5172
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Fix collecting arnold prefix when none <a href="https://github.com/ynput/OpenPype/pull/5243">#5243</a></summary>
|
||||
|
||||
When no prefix is specified in render settings, the renderlayer collector would error.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Deadline: OPENPYPE_VERSION should only be added when running from build <a href="https://github.com/ynput/OpenPype/pull/5244">#5244</a></summary>
|
||||
|
||||
When running from source the environment variable `OPENPYPE_VERSION` should not be added. This is a bugfix for the feature #4489
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix no prompt for "unsaved changes" showing when opening workfile in Houdini <a href="https://github.com/ynput/OpenPype/pull/5246">#5246</a></summary>
|
||||
|
||||
Fix no prompt for "unsaved changes" showing when opening workfile in Houdini.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix no prompt for "unsaved changes" showing when opening workfile in Substance Painter <a href="https://github.com/ynput/OpenPype/pull/5248">#5248</a></summary>
|
||||
|
||||
Fix no prompt for "unsaved changes" showing when opening workfile in Substance Painter.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>General: add the os library before os.environ.get <a href="https://github.com/ynput/OpenPype/pull/5249">#5249</a></summary>
|
||||
|
||||
Adding os library into `creator_plugins.py` due to `os.environ.get` in line 667
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Fix set_attribute for enum attributes <a href="https://github.com/ynput/OpenPype/pull/5261">#5261</a></summary>
|
||||
|
||||
Fix for #5260
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Unreal: Move Qt imports away from module init <a href="https://github.com/ynput/OpenPype/pull/5268">#5268</a></summary>
|
||||
|
||||
Importing `Window` creates errors in headless mode.
|
||||
```
|
||||
*** WRN: >>> { ModulesLoader }: [ FAILED to import host folder unreal ]
|
||||
=============================
|
||||
No Qt bindings could be found
|
||||
=============================
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\tokejepsen\OpenPype\.venv\lib\site-packages\qtpy\__init__.py", line 252, in <module>
|
||||
from PySide6 import __version__ as PYSIDE_VERSION # analysis:ignore
|
||||
ModuleNotFoundERROR: No module named 'PySide6'
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\tokejepsen\OpenPype\openpype\modules\base.py", line 385, in _load_modules
|
||||
default_module = __import__(
|
||||
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\unreal\__init__.py", line 1, in <module>
|
||||
from .addon import UnrealAddon
|
||||
File "C:\Users\tokejepsen\OpenPype\openpype\hosts\unreal\addon.py", line 4, in <module>
|
||||
from openpype.widgets.message_window import Window
|
||||
File "C:\Users\tokejepsen\OpenPype\openpype\widgets\__init__.py", line 1, in <module>
|
||||
from .password_dialog import PasswordDialog
|
||||
File "C:\Users\tokejepsen\OpenPype\openpype\widgets\password_dialog.py", line 1, in <module>
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
File "C:\Users\tokejepsen\OpenPype\.venv\lib\site-packages\qtpy\__init__.py", line 259, in <module>
|
||||
raise QtBindingsNotFoundERROR()
|
||||
qtpy.QtBindingsNotFoundERROR: No Qt bindings could be found
|
||||
```
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🔀 Refactored code**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Minor refactoring and code cleanup <a href="https://github.com/ynput/OpenPype/pull/5226">#5226</a></summary>
|
||||
|
||||
Some small cleanup and refactoring of logic. Removing old comments, unused imports and some minor optimization. Also removed the prints of the loader names of each container the scene in `fix_incompatible_containers` + optimizing by using `set` and defining only once. Moved some UI related code/tweaks to run `on_init` only if not in headless mode. Removed an empty `obj.py` file.Each commit message kind of describes why the change was made.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **Merged pull requests**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bug: Template builder fails when loading data without outliner representation <a href="https://github.com/ynput/OpenPype/pull/5222">#5222</a></summary>
|
||||
|
||||
I add an assertion management in case the container does not have a represention in outliner.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AfterEffects - add container check validator to AE settings <a href="https://github.com/ynput/OpenPype/pull/5203">#5203</a></summary>
|
||||
|
||||
Adds check if scene contains only latest version of loaded containers.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.15.11](https://github.com/ynput/OpenPype/tree/3.15.11)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ def deprecated(new_destination):
|
|||
|
||||
|
||||
@deprecated("openpype.pipeline.publish.get_errored_instances_from_context")
|
||||
def get_errored_instances_from_context(context):
|
||||
def get_errored_instances_from_context(context, plugin=None):
|
||||
"""
|
||||
Deprecated:
|
||||
Since 3.14.* will be removed in 3.16.* or later.
|
||||
|
|
@ -57,7 +57,7 @@ def get_errored_instances_from_context(context):
|
|||
|
||||
from openpype.pipeline.publish import get_errored_instances_from_context
|
||||
|
||||
return get_errored_instances_from_context(context)
|
||||
return get_errored_instances_from_context(context, plugin=plugin)
|
||||
|
||||
|
||||
@deprecated("openpype.pipeline.publish.get_errored_plugins_from_context")
|
||||
|
|
@ -97,11 +97,9 @@ class RepairAction(pyblish.api.Action):
|
|||
|
||||
# Get the errored instances
|
||||
self.log.info("Finding failed instances..")
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
for instance in instances:
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
for instance in errored_instances:
|
||||
plugin.repair(instance)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
icon = "search"
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes...")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -18,15 +18,13 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
icon = "search" # Icon from Awesome Icon
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -17,15 +17,13 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
|
||||
def process(self, context, plugin):
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
|
|
@ -44,3 +42,42 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
node.setCurrent(True)
|
||||
else:
|
||||
self.log.info("No invalid nodes found.")
|
||||
|
||||
|
||||
class SelectROPAction(pyblish.api.Action):
|
||||
"""Select ROP.
|
||||
|
||||
It's used to select the associated ROPs with the errored instances.
|
||||
"""
|
||||
|
||||
label = "Select ROP"
|
||||
on = "failed" # This action is only available on a failed plug-in
|
||||
icon = "mdi.cursor-default-click"
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context, plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding ROP nodes..")
|
||||
rop_nodes = list()
|
||||
for instance in errored_instances:
|
||||
node_path = instance.data.get("instance_node")
|
||||
if not node_path:
|
||||
continue
|
||||
|
||||
node = hou.node(node_path)
|
||||
if not node:
|
||||
continue
|
||||
|
||||
rop_nodes.append(node)
|
||||
|
||||
hou.clearAllSelected()
|
||||
if rop_nodes:
|
||||
self.log.info("Selecting ROP nodes: {}".format(
|
||||
", ".join(node.path() for node in rop_nodes)
|
||||
))
|
||||
for node in rop_nodes:
|
||||
node.setSelected(True)
|
||||
node.setCurrent(True)
|
||||
else:
|
||||
self.log.info("No ROP nodes found.")
|
||||
|
|
|
|||
|
|
@ -633,23 +633,8 @@ def evalParmNoFrame(node, parm, pad_character="#"):
|
|||
|
||||
def get_color_management_preferences():
|
||||
"""Get default OCIO preferences"""
|
||||
data = {
|
||||
"config": hou.Color.ocio_configPath()
|
||||
|
||||
return {
|
||||
"config": hou.Color.ocio_configPath(),
|
||||
"display": hou.Color.ocio_defaultDisplay(),
|
||||
"view": hou.Color.ocio_defaultView()
|
||||
}
|
||||
|
||||
# Get default display and view from OCIO
|
||||
display = hou.Color.ocio_defaultDisplay()
|
||||
disp_regex = re.compile(r"^(?P<name>.+-)(?P<display>.+)$")
|
||||
disp_match = disp_regex.match(display)
|
||||
|
||||
view = hou.Color.ocio_defaultView()
|
||||
view_regex = re.compile(r"^(?P<name>.+- )(?P<view>.+)$")
|
||||
view_match = view_regex.match(view)
|
||||
data.update({
|
||||
"display": disp_match.group("display"),
|
||||
"view": view_match.group("view")
|
||||
|
||||
})
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
import hdefereval # noqa, hdefereval is only available in ui mode
|
||||
hdefereval.executeDeferred(creator_node_shelves.install)
|
||||
|
||||
def has_unsaved_changes(self):
|
||||
def workfile_has_unsaved_changes(self):
|
||||
return hou.hipFile.hasUnsavedChanges()
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating pointcache alembics."""
|
||||
from openpype.hosts.houdini.api import plugin
|
||||
from openpype.pipeline import CreatedInstance
|
||||
|
||||
import hou
|
||||
|
||||
|
|
@ -14,15 +13,13 @@ class CreatePointCache(plugin.HoudiniCreator):
|
|||
icon = "gears"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
import hou
|
||||
|
||||
instance_data.pop("active", None)
|
||||
instance_data.update({"node_type": "alembic"})
|
||||
|
||||
instance = super(CreatePointCache, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data) # type: CreatedInstance
|
||||
pre_create_data)
|
||||
|
||||
instance_node = hou.node(instance.get("instance_node"))
|
||||
parms = {
|
||||
|
|
@ -37,13 +34,44 @@ class CreatePointCache(plugin.HoudiniCreator):
|
|||
}
|
||||
|
||||
if self.selected_nodes:
|
||||
parms["sop_path"] = self.selected_nodes[0].path()
|
||||
selected_node = self.selected_nodes[0]
|
||||
|
||||
# try to find output node
|
||||
for child in self.selected_nodes[0].children():
|
||||
if child.type().name() == "output":
|
||||
parms["sop_path"] = child.path()
|
||||
break
|
||||
# Although Houdini allows ObjNode path on `sop_path` for the
|
||||
# the ROP node we prefer it set to the SopNode path explicitly
|
||||
|
||||
# Allow sop level paths (e.g. /obj/geo1/box1)
|
||||
if isinstance(selected_node, hou.SopNode):
|
||||
parms["sop_path"] = selected_node.path()
|
||||
self.log.debug(
|
||||
"Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'."
|
||||
% selected_node.path()
|
||||
)
|
||||
|
||||
# Allow object level paths to Geometry nodes (e.g. /obj/geo1)
|
||||
# but do not allow other object level nodes types like cameras, etc.
|
||||
elif isinstance(selected_node, hou.ObjNode) and \
|
||||
selected_node.type().name() in ["geo"]:
|
||||
|
||||
# get the output node with the minimum
|
||||
# 'outputidx' or the node with display flag
|
||||
sop_path = self.get_obj_output(selected_node)
|
||||
|
||||
if sop_path:
|
||||
parms["sop_path"] = sop_path.path()
|
||||
self.log.debug(
|
||||
"Valid ObjNode selection, 'SOP Path' in ROP will be set to "
|
||||
"the child path '%s'."
|
||||
% sop_path.path()
|
||||
)
|
||||
|
||||
if not parms.get("sop_path", None):
|
||||
self.log.debug(
|
||||
"Selection isn't valid. 'SOP Path' in ROP will be empty."
|
||||
)
|
||||
else:
|
||||
self.log.debug(
|
||||
"No Selection. 'SOP Path' in ROP will be empty."
|
||||
)
|
||||
|
||||
instance_node.setParms(parms)
|
||||
instance_node.parm("trange").set(1)
|
||||
|
|
@ -57,3 +85,23 @@ class CreatePointCache(plugin.HoudiniCreator):
|
|||
hou.ropNodeTypeCategory(),
|
||||
hou.sopNodeTypeCategory()
|
||||
]
|
||||
|
||||
def get_obj_output(self, obj_node):
|
||||
"""Find output node with the smallest 'outputidx'."""
|
||||
|
||||
outputs = obj_node.subnetOutputs()
|
||||
|
||||
# if obj_node is empty
|
||||
if not outputs:
|
||||
return
|
||||
|
||||
# if obj_node has one output child whether its
|
||||
# sop output node or a node with the render flag
|
||||
elif len(outputs) == 1:
|
||||
return outputs[0]
|
||||
|
||||
# if there are more than one, then it have multiple ouput nodes
|
||||
# return the one with the minimum 'outputidx'
|
||||
else:
|
||||
return min(outputs,
|
||||
key=lambda node: node.evalParm('outputidx'))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin):
|
|||
"Collected filename from current scene name."
|
||||
)
|
||||
|
||||
if host.has_unsaved_changes():
|
||||
if host.workfile_has_unsaved_changes():
|
||||
self.log.info("Saving current file: {}".format(current_file))
|
||||
host.save_workfile(current_file)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -73,6 +73,14 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
|
|||
cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern)
|
||||
cls.log.debug("Checking with path attribute: %s" % path_attr)
|
||||
|
||||
if not hasattr(output_node, "geometry"):
|
||||
# In the case someone has explicitly set an Object
|
||||
# node instead of a SOP node in Geometry context
|
||||
# then for now we ignore - this allows us to also
|
||||
# export object transforms.
|
||||
cls.log.warning("No geometry output node found, skipping check..")
|
||||
return
|
||||
|
||||
# Check if the primitive attribute exists
|
||||
frame = instance.data.get("frameStart", 0)
|
||||
geo = output_node.geometryAtFrame(frame)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
|
|||
|
||||
cls.log.debug("Checking for attribute: %s" % path_attr)
|
||||
|
||||
if not hasattr(output_node, "geometry"):
|
||||
# In the case someone has explicitly set an Object
|
||||
# node instead of a SOP node in Geometry context
|
||||
# then for now we ignore - this allows us to also
|
||||
# export object transforms.
|
||||
cls.log.warning("No geometry output node found, skipping check..")
|
||||
return
|
||||
|
||||
# Check if the primitive attribute exists
|
||||
frame = instance.data.get("frameStart", 0)
|
||||
geo = output_node.geometryAtFrame(frame)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
from openpype.hosts.houdini.api.action import (
|
||||
SelectInvalidAction,
|
||||
SelectROPAction,
|
||||
)
|
||||
|
||||
import hou
|
||||
|
||||
|
||||
class ValidateSopOutputNode(pyblish.api.InstancePlugin):
|
||||
|
|
@ -19,6 +25,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
|
|||
families = ["pointcache", "vdbcache"]
|
||||
hosts = ["houdini"]
|
||||
label = "Validate Output Node"
|
||||
actions = [SelectROPAction, SelectInvalidAction]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
|
|
@ -31,9 +38,6 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
|
|||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
|
||||
import hou
|
||||
|
||||
output_node = instance.data.get("output_node")
|
||||
|
||||
if output_node is None:
|
||||
|
|
@ -43,7 +47,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
|
|||
"Ensure a valid SOP output path is set." % node.path()
|
||||
)
|
||||
|
||||
return [node.path()]
|
||||
return [node]
|
||||
|
||||
# Output node must be a Sop node.
|
||||
if not isinstance(output_node, hou.SopNode):
|
||||
|
|
@ -53,7 +57,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
|
|||
"instead found category type: %s"
|
||||
% (output_node.path(), output_node.type().category().name())
|
||||
)
|
||||
return [output_node.path()]
|
||||
return [output_node]
|
||||
|
||||
# For the sake of completeness also assert the category type
|
||||
# is Sop to avoid potential edge case scenarios even though
|
||||
|
|
@ -73,11 +77,11 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin):
|
|||
except hou.Error as exc:
|
||||
cls.log.error("Cook failed: %s" % exc)
|
||||
cls.log.error(output_node.errors()[0])
|
||||
return [output_node.path()]
|
||||
return [output_node]
|
||||
|
||||
# Ensure the output node has at least Geometry data
|
||||
if not output_node.geometry():
|
||||
cls.log.error(
|
||||
"Output node `%s` has no geometry data." % output_node.path()
|
||||
)
|
||||
return [output_node.path()]
|
||||
return [output_node]
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ def update_mode_context(mode):
|
|||
|
||||
def get_geometry_at_frame(sop_node, frame, force=True):
|
||||
"""Return geometry at frame but force a cooked value."""
|
||||
if not hasattr(sop_node, "geometry"):
|
||||
return
|
||||
with update_mode_context(hou.updateMode.AutoUpdate):
|
||||
sop_node.cook(force=force, frame_range=(frame, frame))
|
||||
return sop_node.geometryAtFrame(frame)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,14 @@ def read(container) -> dict:
|
|||
value.startswith(JSON_PREFIX):
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
value = json.loads(value[len(JSON_PREFIX):])
|
||||
|
||||
# default value behavior
|
||||
# convert maxscript boolean values
|
||||
if value == "true":
|
||||
value = True
|
||||
elif value == "false":
|
||||
value = False
|
||||
|
||||
data[key.strip()] = value
|
||||
|
||||
data["instance_node"] = container.Name
|
||||
|
|
@ -284,6 +292,21 @@ def get_max_version():
|
|||
return max_info[7]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_camera(camera):
|
||||
original = rt.viewport.getCamera()
|
||||
if not original:
|
||||
# if there is no original camera
|
||||
# use the current camera as original
|
||||
original = rt.getNodeByName(camera)
|
||||
review_camera = rt.getNodeByName(camera)
|
||||
try:
|
||||
rt.viewport.setCamera(review_camera)
|
||||
yield
|
||||
finally:
|
||||
rt.viewport.setCamera(original)
|
||||
|
||||
|
||||
def set_timeline(frameStart, frameEnd):
|
||||
"""Set frame range for timeline editor in Max
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData"
|
|||
(
|
||||
handle_name = node_to_name c
|
||||
node_ref = NodeTransformMonitor node:c
|
||||
idx = finditem list_node.items handle_name
|
||||
if idx do (
|
||||
continue
|
||||
)
|
||||
append temp_arr handle_name
|
||||
append i_node_arr node_ref
|
||||
)
|
||||
|
|
|
|||
57
openpype/hosts/max/plugins/create/create_review.py
Normal file
57
openpype/hosts/max/plugins/create/create_review.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating review in Max."""
|
||||
from openpype.hosts.max.api import plugin
|
||||
from openpype.lib import BoolDef, EnumDef, NumberDef
|
||||
|
||||
|
||||
class CreateReview(plugin.MaxCreator):
|
||||
"""Review in 3dsMax"""
|
||||
|
||||
identifier = "io.openpype.creators.max.review"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
||||
instance_data["imageFormat"] = pre_create_data.get("imageFormat")
|
||||
instance_data["keepImages"] = pre_create_data.get("keepImages")
|
||||
instance_data["percentSize"] = pre_create_data.get("percentSize")
|
||||
instance_data["rndLevel"] = pre_create_data.get("rndLevel")
|
||||
|
||||
super(CreateReview, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
attrs = super(CreateReview, self).get_pre_create_attr_defs()
|
||||
|
||||
image_format_enum = [
|
||||
"bmp", "cin", "exr", "jpg", "hdr", "rgb", "png",
|
||||
"rla", "rpf", "dds", "sgi", "tga", "tif", "vrimg"
|
||||
]
|
||||
|
||||
rndLevel_enum = [
|
||||
"smoothhighlights", "smooth", "facethighlights",
|
||||
"facet", "flat", "litwireframe", "wireframe", "box"
|
||||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("keepImages",
|
||||
label="Keep Image Sequences",
|
||||
default=False),
|
||||
EnumDef("imageFormat",
|
||||
image_format_enum,
|
||||
default="png",
|
||||
label="Image Format Options"),
|
||||
NumberDef("percentSize",
|
||||
label="Percent of Output",
|
||||
default=100,
|
||||
minimum=1,
|
||||
decimals=0),
|
||||
EnumDef("rndLevel",
|
||||
rndLevel_enum,
|
||||
default="smoothhighlights",
|
||||
label="Preference")
|
||||
]
|
||||
92
openpype/hosts/max/plugins/publish/collect_review.py
Normal file
92
openpype/hosts/max/plugins/publish/collect_review.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# dont forget getting the focal length for burnin
|
||||
"""Collect Review"""
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.lib import BoolDef
|
||||
from openpype.pipeline.publish import OpenPypePyblishPluginMixin
|
||||
|
||||
|
||||
class CollectReview(pyblish.api.InstancePlugin,
|
||||
OpenPypePyblishPluginMixin):
|
||||
"""Collect Review Data for Preview Animation"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.02
|
||||
label = "Collect Review Data"
|
||||
hosts = ['max']
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
nodes = instance.data["members"]
|
||||
focal_length = None
|
||||
camera_name = None
|
||||
for node in nodes:
|
||||
if rt.classOf(node) in rt.Camera.classes:
|
||||
camera_name = node.name
|
||||
focal_length = node.fov
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
data = {
|
||||
"review_camera": camera_name,
|
||||
"frameStart": instance.context.data["frameStart"],
|
||||
"frameEnd": instance.context.data["frameEnd"],
|
||||
"fps": instance.context.data["fps"],
|
||||
"dspGeometry": attr_values.get("dspGeometry"),
|
||||
"dspShapes": attr_values.get("dspShapes"),
|
||||
"dspLights": attr_values.get("dspLights"),
|
||||
"dspCameras": attr_values.get("dspCameras"),
|
||||
"dspHelpers": attr_values.get("dspHelpers"),
|
||||
"dspParticles": attr_values.get("dspParticles"),
|
||||
"dspBones": attr_values.get("dspBones"),
|
||||
"dspBkg": attr_values.get("dspBkg"),
|
||||
"dspGrid": attr_values.get("dspGrid"),
|
||||
"dspSafeFrame": attr_values.get("dspSafeFrame"),
|
||||
"dspFrameNums": attr_values.get("dspFrameNums")
|
||||
}
|
||||
# Enable ftrack functionality
|
||||
instance.data.setdefault("families", []).append('ftrack')
|
||||
|
||||
burnin_members = instance.data.setdefault("burninDataMembers", {})
|
||||
burnin_members["focalLength"] = focal_length
|
||||
|
||||
self.log.debug(f"data:{data}")
|
||||
instance.data.update(data)
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
|
||||
return [
|
||||
BoolDef("dspGeometry",
|
||||
label="Geometry",
|
||||
default=True),
|
||||
BoolDef("dspShapes",
|
||||
label="Shapes",
|
||||
default=False),
|
||||
BoolDef("dspLights",
|
||||
label="Lights",
|
||||
default=False),
|
||||
BoolDef("dspCameras",
|
||||
label="Cameras",
|
||||
default=False),
|
||||
BoolDef("dspHelpers",
|
||||
label="Helpers",
|
||||
default=False),
|
||||
BoolDef("dspParticles",
|
||||
label="Particle Systems",
|
||||
default=True),
|
||||
BoolDef("dspBones",
|
||||
label="Bone Objects",
|
||||
default=False),
|
||||
BoolDef("dspBkg",
|
||||
label="Background",
|
||||
default=True),
|
||||
BoolDef("dspGrid",
|
||||
label="Active Grid",
|
||||
default=False),
|
||||
BoolDef("dspSafeFrame",
|
||||
label="Safe Frames",
|
||||
default=False),
|
||||
BoolDef("dspFrameNums",
|
||||
label="Frame Numbers",
|
||||
default=False)
|
||||
]
|
||||
102
openpype/hosts/max/plugins/publish/extract_review_animation.py
Normal file
102
openpype/hosts/max/plugins/publish/extract_review_animation.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.max.api.lib import viewport_camera, get_max_version
|
||||
|
||||
|
||||
class ExtractReviewAnimation(publish.Extractor):
|
||||
"""
|
||||
Extract Review by Review Animation
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder + 0.001
|
||||
label = "Extract Review Animation"
|
||||
hosts = ["max"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
staging_dir = self.staging_dir(instance)
|
||||
ext = instance.data.get("imageFormat")
|
||||
filename = "{0}..{1}".format(instance.name, ext)
|
||||
start = int(instance.data["frameStart"])
|
||||
end = int(instance.data["frameEnd"])
|
||||
fps = int(instance.data["fps"])
|
||||
filepath = os.path.join(staging_dir, filename)
|
||||
filepath = filepath.replace("\\", "/")
|
||||
filenames = self.get_files(
|
||||
instance.name, start, end, ext)
|
||||
|
||||
self.log.debug(
|
||||
"Writing Review Animation to"
|
||||
" '%s' to '%s'" % (filename, staging_dir))
|
||||
|
||||
review_camera = instance.data["review_camera"]
|
||||
with viewport_camera(review_camera):
|
||||
preview_arg = self.set_preview_arg(
|
||||
instance, filepath, start, end, fps)
|
||||
rt.execute(preview_arg)
|
||||
|
||||
tags = ["review"]
|
||||
if not instance.data.get("keepImages"):
|
||||
tags.append("delete")
|
||||
|
||||
self.log.debug("Performing Extraction ...")
|
||||
|
||||
representation = {
|
||||
"name": instance.data["imageFormat"],
|
||||
"ext": instance.data["imageFormat"],
|
||||
"files": filenames,
|
||||
"stagingDir": staging_dir,
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"tags": tags,
|
||||
"preview": True,
|
||||
"camera_name": review_camera
|
||||
}
|
||||
self.log.debug(f"{representation}")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
def get_files(self, filename, start, end, ext):
|
||||
file_list = []
|
||||
for frame in range(int(start), int(end) + 1):
|
||||
actual_name = "{}.{:04}.{}".format(
|
||||
filename, frame, ext)
|
||||
file_list.append(actual_name)
|
||||
|
||||
return file_list
|
||||
|
||||
def set_preview_arg(self, instance, filepath,
|
||||
start, end, fps):
|
||||
job_args = list()
|
||||
default_option = f'CreatePreview filename:"{filepath}"'
|
||||
job_args.append(default_option)
|
||||
frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa
|
||||
job_args.append(frame_option)
|
||||
rndLevel = instance.data.get("rndLevel")
|
||||
if rndLevel:
|
||||
option = f"rndLevel:#{rndLevel}"
|
||||
job_args.append(option)
|
||||
options = [
|
||||
"percentSize", "dspGeometry", "dspShapes",
|
||||
"dspLights", "dspCameras", "dspHelpers", "dspParticles",
|
||||
"dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums"
|
||||
]
|
||||
|
||||
for key in options:
|
||||
enabled = instance.data.get(key)
|
||||
if enabled:
|
||||
job_args.append(f"{key}:{enabled}")
|
||||
|
||||
if get_max_version() == 2024:
|
||||
# hardcoded for current stage
|
||||
auto_play_option = "autoPlay:false"
|
||||
job_args.append(auto_play_option)
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
self.log.debug(job_str)
|
||||
|
||||
return job_str
|
||||
91
openpype/hosts/max/plugins/publish/extract_thumbnail.py
Normal file
91
openpype/hosts/max/plugins/publish/extract_thumbnail.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import os
|
||||
import tempfile
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.max.api.lib import viewport_camera, get_max_version
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""
|
||||
Extract Thumbnail for Review
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Thumbnail"
|
||||
hosts = ["max"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
# TODO: Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
self.log.debug(
|
||||
f"Create temp directory {tmp_staging} for thumbnail"
|
||||
)
|
||||
fps = int(instance.data["fps"])
|
||||
frame = int(instance.data["frameStart"])
|
||||
instance.context.data["cleanupFullPaths"].append(tmp_staging)
|
||||
filename = "{name}_thumbnail..png".format(**instance.data)
|
||||
filepath = os.path.join(tmp_staging, filename)
|
||||
filepath = filepath.replace("\\", "/")
|
||||
thumbnail = self.get_filename(instance.name, frame)
|
||||
|
||||
self.log.debug(
|
||||
"Writing Thumbnail to"
|
||||
" '%s' to '%s'" % (filename, tmp_staging))
|
||||
review_camera = instance.data["review_camera"]
|
||||
with viewport_camera(review_camera):
|
||||
preview_arg = self.set_preview_arg(
|
||||
instance, filepath, fps, frame)
|
||||
rt.execute(preview_arg)
|
||||
|
||||
representation = {
|
||||
"name": "thumbnail",
|
||||
"ext": "png",
|
||||
"files": thumbnail,
|
||||
"stagingDir": tmp_staging,
|
||||
"thumbnail": True
|
||||
}
|
||||
|
||||
self.log.debug(f"{representation}")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
def get_filename(self, filename, target_frame):
|
||||
thumbnail_name = "{}_thumbnail.{:04}.png".format(
|
||||
filename, target_frame
|
||||
)
|
||||
return thumbnail_name
|
||||
|
||||
def set_preview_arg(self, instance, filepath, fps, frame):
|
||||
job_args = list()
|
||||
default_option = f'CreatePreview filename:"{filepath}"'
|
||||
job_args.append(default_option)
|
||||
frame_option = f"outputAVI:false start:{frame} end:{frame} fps:{fps}" # noqa
|
||||
job_args.append(frame_option)
|
||||
rndLevel = instance.data.get("rndLevel")
|
||||
if rndLevel:
|
||||
option = f"rndLevel:#{rndLevel}"
|
||||
job_args.append(option)
|
||||
options = [
|
||||
"percentSize", "dspGeometry", "dspShapes",
|
||||
"dspLights", "dspCameras", "dspHelpers", "dspParticles",
|
||||
"dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums"
|
||||
]
|
||||
|
||||
for key in options:
|
||||
enabled = instance.data.get(key)
|
||||
if enabled:
|
||||
job_args.append(f"{key}:{enabled}")
|
||||
if get_max_version() == 2024:
|
||||
# hardcoded for current stage
|
||||
auto_play_option = "autoPlay:false"
|
||||
job_args.append(auto_play_option)
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
self.log.debug(job_str)
|
||||
|
||||
return job_str
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError
|
||||
)
|
||||
from openpype.hosts.max.api.lib import get_frame_range, set_timeline
|
||||
|
||||
|
||||
class ValidateAnimationTimeline(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Validates Animation Timeline for Preview Animation in Max
|
||||
"""
|
||||
|
||||
label = "Animation Timeline for Review"
|
||||
order = ValidateContentsOrder
|
||||
families = ["review"]
|
||||
hosts = ["max"]
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(
|
||||
frame_range["handleEnd"]
|
||||
)
|
||||
if rt.animationRange.start != frame_start_handle or (
|
||||
rt.animationRange.end != frame_end_handle
|
||||
):
|
||||
raise PublishValidationError("Incorrect animation timeline "
|
||||
"set for preview animation.. "
|
||||
"\nYou can use repair action to "
|
||||
"the correct animation timeline")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(
|
||||
frame_range["handleEnd"]
|
||||
)
|
||||
set_timeline(frame_start_handle, frame_end_handle)
|
||||
|
|
@ -11,7 +11,7 @@ class ValidateCameraContent(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera"]
|
||||
families = ["camera", "review"]
|
||||
hosts = ["max"]
|
||||
label = "Camera Contents"
|
||||
camera_type = ["$Free_Camera", "$Target_Camera",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ class ValidateMaxContents(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera",
|
||||
"maxScene",
|
||||
"maxrender"]
|
||||
"maxrender",
|
||||
"review"]
|
||||
hosts = ["max"]
|
||||
label = "Max Scene Contents"
|
||||
|
||||
|
|
|
|||
|
|
@ -111,15 +111,13 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
except ImportError:
|
||||
raise ImportError("Current host is not Maya")
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import os
|
||||
from pprint import pformat
|
||||
import sys
|
||||
import platform
|
||||
import uuid
|
||||
import re
|
||||
|
||||
|
|
@ -1544,7 +1543,15 @@ def set_attribute(attribute, value, node):
|
|||
cmds.addAttr(node, longName=attribute, **kwargs)
|
||||
|
||||
node_attr = "{}.{}".format(node, attribute)
|
||||
if "dataType" in kwargs:
|
||||
enum_type = cmds.attributeQuery(attribute, node=node, enum=True)
|
||||
if enum_type and value_type == "str":
|
||||
enum_string_values = cmds.attributeQuery(
|
||||
attribute, node=node, listEnum=True
|
||||
)[0].split(":")
|
||||
cmds.setAttr(
|
||||
"{}.{}".format(node, attribute), enum_string_values.index(value)
|
||||
)
|
||||
elif "dataType" in kwargs:
|
||||
attr_type = kwargs["dataType"]
|
||||
cmds.setAttr(node_attr, value, type=attr_type)
|
||||
else:
|
||||
|
|
@ -2832,19 +2839,22 @@ def get_attr_in_layer(attr, layer):
|
|||
|
||||
def fix_incompatible_containers():
|
||||
"""Backwards compatibility: old containers to use new ReferenceLoader"""
|
||||
|
||||
old_loaders = {
|
||||
"MayaAsciiLoader",
|
||||
"AbcLoader",
|
||||
"ModelLoader",
|
||||
"CameraLoader",
|
||||
"RigLoader",
|
||||
"FBXLoader"
|
||||
}
|
||||
host = registered_host()
|
||||
for container in host.ls():
|
||||
loader = container['loader']
|
||||
|
||||
print(container['loader'])
|
||||
|
||||
if loader in ["MayaAsciiLoader",
|
||||
"AbcLoader",
|
||||
"ModelLoader",
|
||||
"CameraLoader",
|
||||
"RigLoader",
|
||||
"FBXLoader"]:
|
||||
if loader in old_loaders:
|
||||
log.info(
|
||||
"Converting legacy container loader {} to "
|
||||
"ReferenceLoader: {}".format(loader, container["objectName"])
|
||||
)
|
||||
cmds.setAttr(container["objectName"] + ".loader",
|
||||
"ReferenceLoader", type="string")
|
||||
|
||||
|
|
@ -2972,7 +2982,7 @@ def _get_render_instances():
|
|||
list: list of instances
|
||||
|
||||
"""
|
||||
objectset = cmds.ls("*.id", long=True, type="objectSet",
|
||||
objectset = cmds.ls("*.id", long=True, exactType="objectSet",
|
||||
recursive=True, objectsOnly=True)
|
||||
|
||||
instances = []
|
||||
|
|
@ -3259,36 +3269,21 @@ def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None):
|
|||
|
||||
|
||||
def set_colorspace():
|
||||
"""Set Colorspace from project configuration
|
||||
"""
|
||||
"""Set Colorspace from project configuration"""
|
||||
|
||||
# set color spaces for rendering space and view transforms
|
||||
def _colormanage(**kwargs):
|
||||
"""Wrapper around `cmds.colorManagementPrefs`.
|
||||
|
||||
This logs errors instead of raising an error so color management
|
||||
settings get applied as much as possible.
|
||||
|
||||
"""
|
||||
assert len(kwargs) == 1, "Must receive one keyword argument"
|
||||
try:
|
||||
cmds.colorManagementPrefs(edit=True, **kwargs)
|
||||
log.debug("Setting Color Management Preference: {}".format(kwargs))
|
||||
except RuntimeError as exc:
|
||||
log.error(exc)
|
||||
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
project_name = get_current_project_name()
|
||||
imageio = get_project_settings(project_name)["maya"]["imageio"]
|
||||
|
||||
# ocio compatibility variables
|
||||
ocio_v2_maya_version = 2022
|
||||
maya_version = int(cmds.about(version=True))
|
||||
ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version
|
||||
is_ocio_set = bool(os.environ.get("OCIO"))
|
||||
|
||||
root_dict = {}
|
||||
use_workfile_settings = imageio.get("workfile", {}).get("enabled")
|
||||
|
||||
if use_workfile_settings:
|
||||
root_dict = imageio["workfile"]
|
||||
else:
|
||||
# TODO: deprecated code from 3.15.5 - remove
|
||||
# Maya 2022+ introduces new OCIO v2 color management settings that
|
||||
# can override the old color management preferences. OpenPype has
|
||||
|
|
@ -3311,40 +3306,63 @@ def set_colorspace():
|
|||
if not isinstance(root_dict, dict):
|
||||
msg = "set_colorspace(): argument should be dictionary"
|
||||
log.error(msg)
|
||||
return
|
||||
|
||||
else:
|
||||
root_dict = imageio["workfile"]
|
||||
# backward compatibility
|
||||
# TODO: deprecated code from 3.15.5 - remove with deprecated code above
|
||||
view_name = root_dict.get("viewTransform")
|
||||
if view_name is None:
|
||||
view_name = root_dict.get("viewName")
|
||||
|
||||
log.debug(">> root_dict: {}".format(pformat(root_dict)))
|
||||
if not root_dict:
|
||||
return
|
||||
|
||||
if root_dict:
|
||||
# enable color management
|
||||
cmds.colorManagementPrefs(e=True, cmEnabled=True)
|
||||
cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True)
|
||||
# set color spaces for rendering space and view transforms
|
||||
def _colormanage(**kwargs):
|
||||
"""Wrapper around `cmds.colorManagementPrefs`.
|
||||
|
||||
# backward compatibility
|
||||
# TODO: deprecated code from 3.15.5 - refactor to use new settings
|
||||
view_name = root_dict.get("viewTransform")
|
||||
if view_name is None:
|
||||
view_name = root_dict.get("viewName")
|
||||
This logs errors instead of raising an error so color management
|
||||
settings get applied as much as possible.
|
||||
|
||||
if use_ocio_v2:
|
||||
# Use Maya 2022+ default OCIO v2 config
|
||||
"""
|
||||
assert len(kwargs) == 1, "Must receive one keyword argument"
|
||||
try:
|
||||
cmds.colorManagementPrefs(edit=True, **kwargs)
|
||||
log.debug("Setting Color Management Preference: {}".format(kwargs))
|
||||
except RuntimeError as exc:
|
||||
log.error(exc)
|
||||
|
||||
# enable color management
|
||||
cmds.colorManagementPrefs(edit=True, cmEnabled=True)
|
||||
cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True)
|
||||
|
||||
if use_ocio_v2:
|
||||
log.info("Using Maya OCIO v2")
|
||||
if not is_ocio_set:
|
||||
# Set the Maya 2022+ default OCIO v2 config file path
|
||||
log.info("Setting default Maya OCIO v2 config")
|
||||
cmds.colorManagementPrefs(edit=True, configFilePath="")
|
||||
# Note: Setting "" as value also sets this default however
|
||||
# introduces a bug where launching a file on startup will prompt
|
||||
# to save the empty scene before it, so we set using the path.
|
||||
# This value has been the same for 2022, 2023 and 2024
|
||||
path = "<MAYA_RESOURCES>/OCIO-configs/Maya2022-default/config.ocio"
|
||||
cmds.colorManagementPrefs(edit=True, configFilePath=path)
|
||||
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewName=view_name)
|
||||
_colormanage(displayName=root_dict["displayName"])
|
||||
else:
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewName=view_name)
|
||||
_colormanage(displayName=root_dict["displayName"])
|
||||
else:
|
||||
log.info("Using Maya OCIO v1 (legacy)")
|
||||
if not is_ocio_set:
|
||||
# Set the Maya default config file path
|
||||
log.info("Setting default Maya OCIO v1 legacy config")
|
||||
cmds.colorManagementPrefs(edit=True, configFilePath="legacy")
|
||||
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewTransformName=view_name)
|
||||
# set rendering space and view transform
|
||||
_colormanage(renderingSpaceName=root_dict["renderSpace"])
|
||||
_colormanage(viewTransformName=view_name)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -274,12 +274,14 @@ class ARenderProducts:
|
|||
"Unsupported renderer {}".format(self.renderer)
|
||||
)
|
||||
|
||||
# Note: When this attribute is never set (e.g. on maya launch) then
|
||||
# this can return None even though it is a string attribute
|
||||
prefix = self._get_attr(prefix_attr)
|
||||
|
||||
if not prefix:
|
||||
# Fall back to scene name by default
|
||||
log.debug("Image prefix not set, using <Scene>")
|
||||
file_prefix = "<Scene>"
|
||||
log.warning("Image prefix not set, using <Scene>")
|
||||
prefix = "<Scene>"
|
||||
|
||||
return prefix
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
import errno
|
||||
import logging
|
||||
import contextlib
|
||||
import shutil
|
||||
|
||||
from maya import utils, cmds, OpenMaya
|
||||
import maya.api.OpenMaya as om
|
||||
|
|
@ -116,6 +117,9 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
register_event_callback("taskChanged", on_task_changed)
|
||||
register_event_callback("workfile.open.before", before_workfile_open)
|
||||
register_event_callback("workfile.save.before", before_workfile_save)
|
||||
register_event_callback(
|
||||
"workfile.save.before", workfile_save_before_xgen
|
||||
)
|
||||
register_event_callback("workfile.save.after", after_workfile_save)
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
|
|
@ -497,18 +501,16 @@ def on_init():
|
|||
# Force load objExport plug-in (requested by artists)
|
||||
cmds.loadPlugin("objExport", quiet=True)
|
||||
|
||||
from .customize import (
|
||||
override_component_mask_commands,
|
||||
override_toolbox_ui
|
||||
)
|
||||
safe_deferred(override_component_mask_commands)
|
||||
|
||||
launch_workfiles = os.environ.get("WORKFILES_STARTUP")
|
||||
|
||||
if launch_workfiles:
|
||||
safe_deferred(host_tools.show_workfiles)
|
||||
|
||||
if not lib.IS_HEADLESS:
|
||||
launch_workfiles = os.environ.get("WORKFILES_STARTUP")
|
||||
if launch_workfiles:
|
||||
safe_deferred(host_tools.show_workfiles)
|
||||
|
||||
from .customize import (
|
||||
override_component_mask_commands,
|
||||
override_toolbox_ui
|
||||
)
|
||||
safe_deferred(override_component_mask_commands)
|
||||
safe_deferred(override_toolbox_ui)
|
||||
|
||||
|
||||
|
|
@ -566,37 +568,29 @@ def on_save():
|
|||
Any transform of a mesh, without an existing ID, is given one
|
||||
automatically on file save.
|
||||
"""
|
||||
|
||||
log.info("Running callback on save..")
|
||||
# remove lockfile if users jumps over from one scene to another
|
||||
_remove_workfile_lock()
|
||||
|
||||
# # Update current task for the current scene
|
||||
# update_task_from_path(cmds.file(query=True, sceneName=True))
|
||||
|
||||
# Generate ids of the current context on nodes in the scene
|
||||
nodes = lib.get_id_required_nodes(referenced_nodes=False)
|
||||
for node, new_id in lib.generate_ids(nodes):
|
||||
lib.set_id(node, new_id, overwrite=False)
|
||||
|
||||
|
||||
def _update_render_layer_observers():
|
||||
# Helper to trigger update for all renderlayer observer logic
|
||||
lib.remove_render_layer_observer()
|
||||
lib.add_render_layer_observer()
|
||||
lib.add_render_layer_change_observer()
|
||||
|
||||
|
||||
def on_open():
|
||||
"""On scene open let's assume the containers have changed."""
|
||||
|
||||
from qtpy import QtWidgets
|
||||
from openpype.widgets import popup
|
||||
|
||||
cmds.evalDeferred(
|
||||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.remove_render_layer_observer()")
|
||||
cmds.evalDeferred(
|
||||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.add_render_layer_observer()")
|
||||
cmds.evalDeferred(
|
||||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.add_render_layer_change_observer()")
|
||||
# # Update current task for the current scene
|
||||
# update_task_from_path(cmds.file(query=True, sceneName=True))
|
||||
utils.executeDeferred(_update_render_layer_observers)
|
||||
|
||||
# Validate FPS after update_task_from_path to
|
||||
# ensure it is using correct FPS for the asset
|
||||
|
|
@ -607,10 +601,7 @@ def on_open():
|
|||
log.warning("Scene has outdated content.")
|
||||
|
||||
# Find maya main window
|
||||
top_level_widgets = {w.objectName(): w for w in
|
||||
QtWidgets.QApplication.topLevelWidgets()}
|
||||
parent = top_level_widgets.get("MayaWindow", None)
|
||||
|
||||
parent = lib.get_main_window()
|
||||
if parent is None:
|
||||
log.info("Skipping outdated content pop-up "
|
||||
"because Maya window can't be found.")
|
||||
|
|
@ -635,16 +626,9 @@ def on_new():
|
|||
"""Set project resolution and fps when create a new file"""
|
||||
log.info("Running callback on new..")
|
||||
with lib.suspended_refresh():
|
||||
cmds.evalDeferred(
|
||||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.remove_render_layer_observer()")
|
||||
cmds.evalDeferred(
|
||||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.add_render_layer_observer()")
|
||||
cmds.evalDeferred(
|
||||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.add_render_layer_change_observer()")
|
||||
lib.set_context_settings()
|
||||
|
||||
utils.executeDeferred(_update_render_layer_observers)
|
||||
_remove_workfile_lock()
|
||||
|
||||
|
||||
|
|
@ -698,6 +682,91 @@ def before_workfile_save(event):
|
|||
create_workspace_mel(workdir_path, project_name)
|
||||
|
||||
|
||||
def workfile_save_before_xgen(event):
|
||||
"""Manage Xgen external files when switching context.
|
||||
|
||||
Xgen has various external files that needs to be unique and relative to the
|
||||
workfile, so we need to copy and potentially overwrite these files when
|
||||
switching context.
|
||||
|
||||
Args:
|
||||
event (Event) - openpype/lib/events.py
|
||||
"""
|
||||
if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True):
|
||||
return
|
||||
|
||||
import xgenm
|
||||
|
||||
current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/")
|
||||
expected_work_dir = event.data["workdir_path"].replace("\\", "/")
|
||||
if current_work_dir == expected_work_dir:
|
||||
return
|
||||
|
||||
palettes = cmds.ls(type="xgmPalette", long=True)
|
||||
if not palettes:
|
||||
return
|
||||
|
||||
transfers = []
|
||||
overwrites = []
|
||||
attribute_changes = {}
|
||||
attrs = ["xgFileName", "xgBaseFile"]
|
||||
for palette in palettes:
|
||||
sanitized_palette = palette.replace("|", "")
|
||||
project_path = xgenm.getAttr("xgProjectPath", sanitized_palette)
|
||||
_, maya_extension = os.path.splitext(event.data["filename"])
|
||||
|
||||
for attr in attrs:
|
||||
node_attr = "{}.{}".format(palette, attr)
|
||||
attr_value = cmds.getAttr(node_attr)
|
||||
|
||||
if not attr_value:
|
||||
continue
|
||||
|
||||
source = os.path.join(project_path, attr_value)
|
||||
|
||||
attr_value = event.data["filename"].replace(
|
||||
maya_extension,
|
||||
"__{}{}".format(
|
||||
sanitized_palette.replace(":", "__"),
|
||||
os.path.splitext(attr_value)[1]
|
||||
)
|
||||
)
|
||||
target = os.path.join(expected_work_dir, attr_value)
|
||||
|
||||
transfers.append((source, target))
|
||||
attribute_changes[node_attr] = attr_value
|
||||
|
||||
relative_path = xgenm.getAttr(
|
||||
"xgDataPath", sanitized_palette
|
||||
).split(os.pathsep)[0]
|
||||
absolute_path = relative_path.replace("${PROJECT}", project_path)
|
||||
for root, _, files in os.walk(absolute_path):
|
||||
for f in files:
|
||||
source = os.path.join(root, f).replace("\\", "/")
|
||||
target = source.replace(project_path, expected_work_dir + "/")
|
||||
transfers.append((source, target))
|
||||
if os.path.exists(target):
|
||||
overwrites.append(target)
|
||||
|
||||
# Ask user about overwriting files.
|
||||
if overwrites:
|
||||
log.warning(
|
||||
"WARNING! Potential loss of data.\n\n"
|
||||
"Found duplicate Xgen files in new context.\n{}".format(
|
||||
"\n".join(overwrites)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
for source, destination in transfers:
|
||||
if not os.path.exists(os.path.dirname(destination)):
|
||||
os.makedirs(os.path.dirname(destination))
|
||||
shutil.copy(source, destination)
|
||||
|
||||
for attribute, value in attribute_changes.items():
|
||||
cmds.setAttr(attribute, value, type="string")
|
||||
|
||||
|
||||
def after_workfile_save(event):
|
||||
workfile_name = event["filename"]
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,36 +1,24 @@
|
|||
import os
|
||||
import json
|
||||
from abc import (
|
||||
ABCMeta
|
||||
)
|
||||
import six
|
||||
import re
|
||||
import os
|
||||
from abc import ABCMeta
|
||||
|
||||
import qargparse
|
||||
import six
|
||||
from maya import cmds
|
||||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
import qargparse
|
||||
|
||||
from openpype.lib import Logger
|
||||
from openpype.lib import BoolDef, Logger
|
||||
from openpype.pipeline import AVALON_CONTAINER_ID, Anatomy, CreatedInstance
|
||||
from openpype.pipeline import Creator as NewCreator
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
LoaderPlugin,
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
Anatomy,
|
||||
LegacyCreator,
|
||||
Creator as NewCreator,
|
||||
CreatedInstance,
|
||||
CreatorError
|
||||
)
|
||||
from openpype.lib import BoolDef
|
||||
from .lib import imprint, read
|
||||
CreatorError, LegacyCreator, LoaderPlugin, get_representation_path,
|
||||
legacy_io)
|
||||
from openpype.pipeline.load import LoadError
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
from .pipeline import containerise
|
||||
from . import lib
|
||||
|
||||
from .lib import imprint, read
|
||||
from .pipeline import containerise
|
||||
|
||||
log = Logger.get_logger()
|
||||
|
||||
|
|
@ -581,6 +569,7 @@ class ReferenceLoader(Loader):
|
|||
|
||||
def update(self, container, representation):
|
||||
from maya import cmds
|
||||
|
||||
from openpype.hosts.maya.api.lib import get_container_members
|
||||
|
||||
node = container["objectName"]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import contextlib
|
|||
from maya import cmds
|
||||
from maya.app.renderSetup.model import renderSetup
|
||||
|
||||
# from colorbleed.maya import lib
|
||||
from .lib import pairwise
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -272,7 +272,12 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
|
|||
return
|
||||
|
||||
roots = cmds.sets(container, q=True)
|
||||
ref_node = get_reference_node(roots)
|
||||
ref_node = None
|
||||
try:
|
||||
ref_node = get_reference_node(roots)
|
||||
except AssertionError as e:
|
||||
self.log.info(e.args[0])
|
||||
|
||||
nodes_to_parent = []
|
||||
for root in roots:
|
||||
if ref_node:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
color = "orange"
|
||||
|
||||
def process_reference(self, context, name, namespace, options):
|
||||
import maya.cmds as cmds
|
||||
from maya import cmds
|
||||
|
||||
with lib.maintained_selection():
|
||||
file_url = self.prepare_root_value(self.fname,
|
||||
|
|
@ -113,8 +113,8 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
|
||||
# region compute lookup
|
||||
nodes_by_id = defaultdict(list)
|
||||
for n in nodes:
|
||||
nodes_by_id[lib.get_id(n)].append(n)
|
||||
for node in nodes:
|
||||
nodes_by_id[lib.get_id(node)].append(node)
|
||||
lib.apply_attributes(attributes, nodes_by_id)
|
||||
|
||||
def _get_nodes_with_shader(self, shader_nodes):
|
||||
|
|
@ -125,14 +125,16 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
|
|||
Returns
|
||||
<list> node names
|
||||
"""
|
||||
import maya.cmds as cmds
|
||||
from maya import cmds
|
||||
|
||||
nodes_list = []
|
||||
for shader in shader_nodes:
|
||||
connections = cmds.listConnections(cmds.listHistory(shader, f=1),
|
||||
future = cmds.listHistory(shader, future=True)
|
||||
connections = cmds.listConnections(future,
|
||||
type='mesh')
|
||||
if connections:
|
||||
for connection in connections:
|
||||
nodes_list.extend(cmds.listRelatives(connection,
|
||||
shapes=True))
|
||||
return nodes_list
|
||||
# Ensure unique entries only to optimize query and results
|
||||
connections = list(set(connections))
|
||||
return cmds.listRelatives(connections,
|
||||
shapes=True,
|
||||
fullPath=True) or []
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import os
|
||||
|
||||
from maya import cmds
|
||||
# import maya.mel as mel
|
||||
import pyblish.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ from openpype.client import get_last_version_by_subset_id
|
|||
from openpype import style
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.tools.utils.lib import qt_app_context
|
||||
from openpype.hosts.maya.api.lib import assign_look_by_version
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
assign_look_by_version,
|
||||
get_main_window
|
||||
)
|
||||
|
||||
from maya import cmds
|
||||
# old api for MFileIO
|
||||
|
|
@ -297,9 +300,7 @@ def show():
|
|||
pass
|
||||
|
||||
# Get Maya main window
|
||||
top_level_widgets = QtWidgets.QApplication.topLevelWidgets()
|
||||
mainwindow = next(widget for widget in top_level_widgets
|
||||
if widget.objectName() == "MayaWindow")
|
||||
mainwindow = get_main_window()
|
||||
|
||||
with qt_app_context():
|
||||
window = MayaLookAssignerWindow(parent=mainwindow)
|
||||
|
|
|
|||
|
|
@ -25,15 +25,13 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
except ImportError:
|
||||
raise ImportError("Current host is not Nuke")
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
|
||||
if invalid_nodes:
|
||||
|
|
|
|||
|
|
@ -553,7 +553,9 @@ def add_write_node_legacy(name, **kwarg):
|
|||
|
||||
w = nuke.createNode(
|
||||
"Write",
|
||||
"name {}".format(name))
|
||||
"name {}".format(name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
w["file"].setValue(kwarg["file"])
|
||||
|
||||
|
|
@ -589,7 +591,9 @@ def add_write_node(name, file_path, knobs, **kwarg):
|
|||
|
||||
w = nuke.createNode(
|
||||
"Write",
|
||||
"name {}".format(name))
|
||||
"name {}".format(name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
w["file"].setValue(file_path)
|
||||
|
||||
|
|
@ -1192,8 +1196,10 @@ def create_prenodes(
|
|||
|
||||
# create node
|
||||
now_node = nuke.createNode(
|
||||
nodeclass, "name {}".format(name))
|
||||
now_node.hideControlPanel()
|
||||
nodeclass,
|
||||
"name {}".format(name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# add for dependency linking
|
||||
for_dependency[name] = {
|
||||
|
|
@ -1317,12 +1323,17 @@ def create_write_node(
|
|||
input_name = str(input.name()).replace(" ", "")
|
||||
# if connected input node was defined
|
||||
prev_node = nuke.createNode(
|
||||
"Input", "name {}".format(input_name))
|
||||
"Input",
|
||||
"name {}".format(input_name),
|
||||
inpanel=False
|
||||
)
|
||||
else:
|
||||
# generic input node connected to nothing
|
||||
prev_node = nuke.createNode(
|
||||
"Input", "name {}".format("rgba"))
|
||||
prev_node.hideControlPanel()
|
||||
"Input",
|
||||
"name {}".format("rgba"),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# creating pre-write nodes `prenodes`
|
||||
last_prenode = create_prenodes(
|
||||
|
|
@ -1342,15 +1353,13 @@ def create_write_node(
|
|||
imageio_writes["knobs"],
|
||||
**data
|
||||
)
|
||||
write_node.hideControlPanel()
|
||||
# connect to previous node
|
||||
now_node.setInput(0, prev_node)
|
||||
|
||||
# switch actual node to previous
|
||||
prev_node = now_node
|
||||
|
||||
now_node = nuke.createNode("Output", "name Output1")
|
||||
now_node.hideControlPanel()
|
||||
now_node = nuke.createNode("Output", "name Output1", inpanel=False)
|
||||
|
||||
# connect to previous node
|
||||
now_node.setInput(0, prev_node)
|
||||
|
|
@ -1517,8 +1526,10 @@ def create_write_node_legacy(
|
|||
else:
|
||||
# generic input node connected to nothing
|
||||
prev_node = nuke.createNode(
|
||||
"Input", "name {}".format("rgba"))
|
||||
prev_node.hideControlPanel()
|
||||
"Input",
|
||||
"name {}".format("rgba"),
|
||||
inpanel=False
|
||||
)
|
||||
# creating pre-write nodes `prenodes`
|
||||
if prenodes:
|
||||
for node in prenodes:
|
||||
|
|
@ -1530,8 +1541,10 @@ def create_write_node_legacy(
|
|||
|
||||
# create node
|
||||
now_node = nuke.createNode(
|
||||
klass, "name {}".format(pre_node_name))
|
||||
now_node.hideControlPanel()
|
||||
klass,
|
||||
"name {}".format(pre_node_name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# add data to knob
|
||||
for _knob in knobs:
|
||||
|
|
@ -1561,14 +1574,18 @@ def create_write_node_legacy(
|
|||
if isinstance(dependent, (tuple or list)):
|
||||
for i, node_name in enumerate(dependent):
|
||||
input_node = nuke.createNode(
|
||||
"Input", "name {}".format(node_name))
|
||||
input_node.hideControlPanel()
|
||||
"Input",
|
||||
"name {}".format(node_name),
|
||||
inpanel=False
|
||||
)
|
||||
now_node.setInput(1, input_node)
|
||||
|
||||
elif isinstance(dependent, str):
|
||||
input_node = nuke.createNode(
|
||||
"Input", "name {}".format(node_name))
|
||||
input_node.hideControlPanel()
|
||||
"Input",
|
||||
"name {}".format(node_name),
|
||||
inpanel=False
|
||||
)
|
||||
now_node.setInput(0, input_node)
|
||||
|
||||
else:
|
||||
|
|
@ -1583,15 +1600,13 @@ def create_write_node_legacy(
|
|||
"inside_{}".format(name),
|
||||
**_data
|
||||
)
|
||||
write_node.hideControlPanel()
|
||||
# connect to previous node
|
||||
now_node.setInput(0, prev_node)
|
||||
|
||||
# switch actual node to previous
|
||||
prev_node = now_node
|
||||
|
||||
now_node = nuke.createNode("Output", "name Output1")
|
||||
now_node.hideControlPanel()
|
||||
now_node = nuke.createNode("Output", "name Output1", inpanel=False)
|
||||
|
||||
# connect to previous node
|
||||
now_node.setInput(0, prev_node)
|
||||
|
|
|
|||
|
|
@ -66,8 +66,6 @@ class AlembicCameraLoader(load.LoaderPlugin):
|
|||
object_name, file),
|
||||
inpanel=False
|
||||
)
|
||||
# hide property panel
|
||||
camera_node.hideControlPanel()
|
||||
|
||||
camera_node.forceValidate()
|
||||
camera_node["frame_rate"].setValue(float(fps))
|
||||
|
|
|
|||
|
|
@ -144,10 +144,9 @@ class LoadClip(plugin.NukeLoader):
|
|||
# Create the Loader with the filename path set
|
||||
read_node = nuke.createNode(
|
||||
"Read",
|
||||
"name {}".format(read_name))
|
||||
|
||||
# hide property panel
|
||||
read_node.hideControlPanel()
|
||||
"name {}".format(read_name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# to avoid multiple undo steps for rest of process
|
||||
# we will switch off undo-ing
|
||||
|
|
@ -165,8 +164,8 @@ class LoadClip(plugin.NukeLoader):
|
|||
"handleStart", "handleEnd"]
|
||||
|
||||
data_imprint = {}
|
||||
for k in add_keys:
|
||||
if k == 'version':
|
||||
for key in add_keys:
|
||||
if key == 'version':
|
||||
version_doc = context["version"]
|
||||
if version_doc["type"] == "hero_version":
|
||||
version = "hero"
|
||||
|
|
@ -174,17 +173,20 @@ class LoadClip(plugin.NukeLoader):
|
|||
version = version_doc.get("name")
|
||||
|
||||
if version:
|
||||
data_imprint[k] = version
|
||||
data_imprint[key] = version
|
||||
|
||||
elif k == 'colorspace':
|
||||
colorspace = representation["data"].get(k)
|
||||
colorspace = colorspace or version_data.get(k)
|
||||
elif key == 'colorspace':
|
||||
colorspace = representation["data"].get(key)
|
||||
colorspace = colorspace or version_data.get(key)
|
||||
data_imprint["db_colorspace"] = colorspace
|
||||
if used_colorspace:
|
||||
data_imprint["used_colorspace"] = used_colorspace
|
||||
else:
|
||||
data_imprint[k] = context["version"]['data'].get(
|
||||
k, str(None))
|
||||
value_ = context["version"]['data'].get(
|
||||
key, str(None))
|
||||
if isinstance(value_, (str)):
|
||||
value_ = value_.replace("\\", "/")
|
||||
data_imprint[key] = value_
|
||||
|
||||
data_imprint["objectName"] = read_name
|
||||
|
||||
|
|
|
|||
|
|
@ -88,10 +88,9 @@ class LoadEffects(load.LoaderPlugin):
|
|||
|
||||
GN = nuke.createNode(
|
||||
"Group",
|
||||
"name {}_1".format(object_name))
|
||||
|
||||
# hide property panel
|
||||
GN.hideControlPanel()
|
||||
"name {}_1".format(object_name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# adding content to the group node
|
||||
with GN:
|
||||
|
|
|
|||
|
|
@ -89,10 +89,9 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
|
|||
|
||||
GN = nuke.createNode(
|
||||
"Group",
|
||||
"name {}_1".format(object_name))
|
||||
|
||||
# hide property panel
|
||||
GN.hideControlPanel()
|
||||
"name {}_1".format(object_name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# adding content to the group node
|
||||
with GN:
|
||||
|
|
|
|||
|
|
@ -119,10 +119,9 @@ class LoadImage(load.LoaderPlugin):
|
|||
with viewer_update_and_undo_stop():
|
||||
r = nuke.createNode(
|
||||
"Read",
|
||||
"name {}".format(read_name))
|
||||
|
||||
# hide property panel
|
||||
r.hideControlPanel()
|
||||
"name {}".format(read_name),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
r["file"].setValue(file)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,9 +65,6 @@ class AlembicModelLoader(load.LoaderPlugin):
|
|||
inpanel=False
|
||||
)
|
||||
|
||||
# hide property panel
|
||||
model_node.hideControlPanel()
|
||||
|
||||
model_node.forceValidate()
|
||||
|
||||
# Ensure all items are imported and selected.
|
||||
|
|
|
|||
|
|
@ -70,10 +70,9 @@ class LinkAsGroup(load.LoaderPlugin):
|
|||
# P = nuke.nodes.LiveGroup("file {}".format(file))
|
||||
P = nuke.createNode(
|
||||
"Precomp",
|
||||
"file {}".format(file))
|
||||
|
||||
# hide property panel
|
||||
P.hideControlPanel()
|
||||
"file {}".format(file),
|
||||
inpanel=False
|
||||
)
|
||||
|
||||
# Set colorspace defined in version data
|
||||
colorspace = context["version"]["data"].get("colorspace", None)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import pyblish.api
|
||||
import clique
|
||||
from openpype.pipeline import PublishXmlValidationError
|
||||
from openpype.pipeline.publish import get_errored_instances_from_context
|
||||
|
||||
|
||||
class RepairActionBase(pyblish.api.Action):
|
||||
|
|
@ -11,14 +12,7 @@ class RepairActionBase(pyblish.api.Action):
|
|||
@staticmethod
|
||||
def get_instance(context, plugin):
|
||||
# Get the errored instances
|
||||
failed = []
|
||||
for result in context.data["results"]:
|
||||
if (result["error"] is not None and result["instance"] is not None
|
||||
and result["instance"] not in failed):
|
||||
failed.append(result["instance"])
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
return pyblish.api.instances_by_plugin(failed, plugin)
|
||||
return get_errored_instances_from_context(context, plugin=plugin)
|
||||
|
||||
def repair_knob(self, instances, state):
|
||||
for instance in instances:
|
||||
|
|
|
|||
|
|
@ -27,15 +27,13 @@ class SelectInvalidAction(pyblish.api.Action):
|
|||
except ImportError:
|
||||
raise ImportError("Current host is not Resolve")
|
||||
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid clips..")
|
||||
invalid = list()
|
||||
for instance in instances:
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
self._uninstall_menu()
|
||||
self._deregister_callbacks()
|
||||
|
||||
def has_unsaved_changes(self):
|
||||
def workfile_has_unsaved_changes(self):
|
||||
|
||||
if not substance_painter.project.is_open():
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ class CreateTextures(Creator):
|
|||
if not substance_painter.project.is_open():
|
||||
raise CreatorError("Can't create a Texture Set instance without "
|
||||
"an open project.")
|
||||
# Transfer settings from pre create to instance
|
||||
creator_attributes = instance_data.setdefault(
|
||||
"creator_attributes", dict())
|
||||
for key in [
|
||||
"exportPresetUrl",
|
||||
"exportFileFormat",
|
||||
"exportSize",
|
||||
"exportPadding",
|
||||
"exportDilationDistance"
|
||||
]:
|
||||
if key in pre_create_data:
|
||||
creator_attributes[key] = pre_create_data[key]
|
||||
|
||||
instance = self.create_instance_in_context(subset_name,
|
||||
instance_data)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin):
|
|||
if context.data["currentFile"] != current:
|
||||
raise KnownPublishError("Workfile has changed during publishing!")
|
||||
|
||||
if host.has_unsaved_changes():
|
||||
if host.workfile_has_unsaved_changes():
|
||||
self.log.info("Saving current file: {}".format(current))
|
||||
host.save_workfile()
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
from openpype.modules import IHostAddon, OpenPypeModule
|
||||
from openpype.widgets.message_window import Window
|
||||
|
||||
UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
@ -21,6 +20,8 @@ class UnrealAddon(OpenPypeModule, IHostAddon):
|
|||
|
||||
from .lib import get_compatible_integration
|
||||
|
||||
from openpype.widgets.message_window import Window
|
||||
|
||||
pattern = re.compile(r'^\d+-\d+$')
|
||||
|
||||
if not pattern.match(app.name):
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
|
|||
"FTRACK_SERVER",
|
||||
"AVALON_APP_NAME",
|
||||
"OPENPYPE_USERNAME",
|
||||
"OPENPYPE_VERSION",
|
||||
"OPENPYPE_SG_USER"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from copy import deepcopy
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import platform
|
||||
import contextlib
|
||||
|
|
@ -237,12 +236,13 @@ def get_data_subprocess(config_path, data_type):
|
|||
return json.loads(return_json_data)
|
||||
|
||||
|
||||
def compatible_python():
|
||||
"""Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper"""
|
||||
compatible = False
|
||||
if sys.version_info.major == 3 and sys.version_info.minor >= 9:
|
||||
compatible = True
|
||||
return compatible
|
||||
def compatibility_check():
|
||||
"""Making sure PyOpenColorIO is importable"""
|
||||
try:
|
||||
import PyOpenColorIO # noqa: F401
|
||||
except ImportError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_ocio_config_colorspaces(config_path):
|
||||
|
|
@ -257,12 +257,15 @@ def get_ocio_config_colorspaces(config_path):
|
|||
Returns:
|
||||
dict: colorspace and family in couple
|
||||
"""
|
||||
if compatible_python():
|
||||
from ..scripts.ocio_wrapper import _get_colorspace_data
|
||||
return _get_colorspace_data(config_path)
|
||||
else:
|
||||
if not compatibility_check():
|
||||
# python environment is not compatible with PyOpenColorIO
|
||||
# needs to be run in subprocess
|
||||
return get_colorspace_data_subprocess(config_path)
|
||||
|
||||
from openpype.scripts.ocio_wrapper import _get_colorspace_data
|
||||
|
||||
return _get_colorspace_data(config_path)
|
||||
|
||||
|
||||
def get_colorspace_data_subprocess(config_path):
|
||||
"""Get colorspace data via subprocess
|
||||
|
|
@ -290,12 +293,15 @@ def get_ocio_config_views(config_path):
|
|||
Returns:
|
||||
dict: `display/viewer` and viewer data
|
||||
"""
|
||||
if compatible_python():
|
||||
from ..scripts.ocio_wrapper import _get_views_data
|
||||
return _get_views_data(config_path)
|
||||
else:
|
||||
if not compatibility_check():
|
||||
# python environment is not compatible with PyOpenColorIO
|
||||
# needs to be run in subprocess
|
||||
return get_views_data_subprocess(config_path)
|
||||
|
||||
from openpype.scripts.ocio_wrapper import _get_views_data
|
||||
|
||||
return _get_views_data(config_path)
|
||||
|
||||
|
||||
def get_views_data_subprocess(config_path):
|
||||
"""Get viewers data via subprocess
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ from .constants import (
|
|||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
)
|
||||
|
||||
from .utils import (
|
||||
get_last_versions_for_instances,
|
||||
get_next_versions_for_instances,
|
||||
)
|
||||
|
||||
from .subset_name import (
|
||||
TaskNotSetError,
|
||||
get_subset_name_template,
|
||||
|
|
@ -46,6 +51,9 @@ __all__ = (
|
|||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
|
||||
"get_last_versions_for_instances",
|
||||
"get_next_versions_for_instances",
|
||||
|
||||
"TaskNotSetError",
|
||||
"get_subset_name_template",
|
||||
"get_subset_name",
|
||||
|
|
|
|||
|
|
@ -1122,10 +1122,10 @@ class CreatedInstance:
|
|||
|
||||
@property
|
||||
def creator_attribute_defs(self):
|
||||
"""Attribute defintions defined by creator plugin.
|
||||
"""Attribute definitions defined by creator plugin.
|
||||
|
||||
Returns:
|
||||
List[AbstractAttrDef]: Attribute defitions.
|
||||
List[AbstractAttrDef]: Attribute definitions.
|
||||
"""
|
||||
|
||||
return self.creator_attributes.attr_defs
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from openpype.pipeline.plugin_discover import (
|
|||
)
|
||||
|
||||
from .subset_name import get_subset_name
|
||||
from .utils import get_next_versions_for_instances
|
||||
from .legacy_create import LegacyCreator
|
||||
|
||||
|
||||
|
|
@ -483,6 +484,27 @@ class BaseCreator:
|
|||
thumbnail_path
|
||||
)
|
||||
|
||||
def get_next_versions_for_instances(self, instances):
|
||||
"""Prepare next versions for instances.
|
||||
|
||||
This is helper method to receive next possible versions for instances.
|
||||
It is using context information on instance to receive them, 'asset'
|
||||
and 'subset'.
|
||||
|
||||
Output will contain version by each instance id.
|
||||
|
||||
Args:
|
||||
instances (list[CreatedInstance]): Instances for which to get next
|
||||
versions.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Next versions by instance id.
|
||||
"""
|
||||
|
||||
return get_next_versions_for_instances(
|
||||
self.create_context.project_name, instances
|
||||
)
|
||||
|
||||
|
||||
class Creator(BaseCreator):
|
||||
"""Creator that has more information for artist to show in UI.
|
||||
|
|
|
|||
|
|
@ -74,12 +74,12 @@ class LegacyCreator(object):
|
|||
if not plugin_settings:
|
||||
return
|
||||
|
||||
print(">>> We have preset for {}".format(plugin_name))
|
||||
cls.log.debug(">>> We have preset for {}".format(plugin_name))
|
||||
for option, value in plugin_settings.items():
|
||||
if option == "enabled" and value is False:
|
||||
print(" - is disabled by preset")
|
||||
cls.log.debug(" - is disabled by preset")
|
||||
else:
|
||||
print(" - setting `{}`: `{}`".format(option, value))
|
||||
cls.log.debug(" - setting `{}`: `{}`".format(option, value))
|
||||
setattr(cls, option, value)
|
||||
|
||||
def process(self):
|
||||
|
|
|
|||
122
openpype/pipeline/create/utils.py
Normal file
122
openpype/pipeline/create/utils.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import collections
|
||||
|
||||
from openpype.client import get_assets, get_subsets, get_last_versions
|
||||
|
||||
|
||||
def get_last_versions_for_instances(
|
||||
project_name, instances, use_value_for_missing=False
|
||||
):
|
||||
"""Get last versions for instances by their asset and subset name.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
instances (list[CreatedInstance]): Instances to get next versions for.
|
||||
use_value_for_missing (Optional[bool]): Missing values are replaced
|
||||
with negative value if True. Otherwise None is used. -2 is used
|
||||
for instances without filled asset or subset name. -1 is used
|
||||
for missing entities.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[int, None]]: Last versions by instance id.
|
||||
"""
|
||||
|
||||
output = {
|
||||
instance.id: -1 if use_value_for_missing else None
|
||||
for instance in instances
|
||||
}
|
||||
subset_names_by_asset_name = collections.defaultdict(set)
|
||||
instances_by_hierarchy = {}
|
||||
for instance in instances:
|
||||
asset_name = instance.data.get("asset")
|
||||
subset_name = instance.subset_name
|
||||
if not asset_name or not subset_name:
|
||||
if use_value_for_missing:
|
||||
output[instance.id] = -2
|
||||
continue
|
||||
|
||||
(
|
||||
instances_by_hierarchy
|
||||
.setdefault(asset_name, {})
|
||||
.setdefault(subset_name, [])
|
||||
.append(instance)
|
||||
)
|
||||
subset_names_by_asset_name[asset_name].add(subset_name)
|
||||
|
||||
subset_names = set()
|
||||
for names in subset_names_by_asset_name.values():
|
||||
subset_names |= names
|
||||
|
||||
if not subset_names:
|
||||
return output
|
||||
|
||||
asset_docs = get_assets(
|
||||
project_name,
|
||||
asset_names=subset_names_by_asset_name.keys(),
|
||||
fields=["name", "_id"]
|
||||
)
|
||||
asset_names_by_id = {
|
||||
asset_doc["_id"]: asset_doc["name"]
|
||||
for asset_doc in asset_docs
|
||||
}
|
||||
if not asset_names_by_id:
|
||||
return output
|
||||
|
||||
subset_docs = get_subsets(
|
||||
project_name,
|
||||
asset_ids=asset_names_by_id.keys(),
|
||||
subset_names=subset_names,
|
||||
fields=["_id", "name", "parent"]
|
||||
)
|
||||
subset_docs_by_id = {}
|
||||
for subset_doc in subset_docs:
|
||||
# Filter subset docs by subset names under parent
|
||||
asset_id = subset_doc["parent"]
|
||||
asset_name = asset_names_by_id[asset_id]
|
||||
subset_name = subset_doc["name"]
|
||||
if subset_name not in subset_names_by_asset_name[asset_name]:
|
||||
continue
|
||||
subset_docs_by_id[subset_doc["_id"]] = subset_doc
|
||||
|
||||
if not subset_docs_by_id:
|
||||
return output
|
||||
|
||||
last_versions_by_subset_id = get_last_versions(
|
||||
project_name,
|
||||
subset_docs_by_id.keys(),
|
||||
fields=["name", "parent"]
|
||||
)
|
||||
for subset_id, version_doc in last_versions_by_subset_id.items():
|
||||
subset_doc = subset_docs_by_id[subset_id]
|
||||
asset_id = subset_doc["parent"]
|
||||
asset_name = asset_names_by_id[asset_id]
|
||||
_instances = instances_by_hierarchy[asset_name][subset_doc["name"]]
|
||||
for instance in _instances:
|
||||
output[instance.id] = version_doc["name"]
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def get_next_versions_for_instances(project_name, instances):
|
||||
"""Get next versions for instances by their asset and subset name.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
instances (list[CreatedInstance]): Instances to get next versions for.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[int, None]]: Next versions by instance id. Version is
|
||||
'None' if instance has no asset or subset name.
|
||||
"""
|
||||
|
||||
last_versions = get_last_versions_for_instances(
|
||||
project_name, instances, True)
|
||||
|
||||
output = {}
|
||||
for instance_id, version in last_versions.items():
|
||||
if version == -2:
|
||||
output[instance_id] = None
|
||||
elif version == -1:
|
||||
output[instance_id] = 1
|
||||
else:
|
||||
output[instance_id] = version + 1
|
||||
return output
|
||||
|
|
@ -157,6 +157,8 @@ def deliver_single_file(
|
|||
delivery_path = delivery_path.replace("..", ".")
|
||||
# Make sure path is valid for all platforms
|
||||
delivery_path = os.path.normpath(delivery_path.replace("\\", "/"))
|
||||
# Remove newlines from the end of the string to avoid OSError during copy
|
||||
delivery_path = delivery_path.rstrip()
|
||||
|
||||
delivery_folder = os.path.dirname(delivery_path)
|
||||
if not os.path.exists(delivery_folder):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from .utils import (
|
|||
LoadError,
|
||||
IncompatibleLoaderError,
|
||||
InvalidRepresentationContext,
|
||||
LoaderSwitchNotImplementedError,
|
||||
LoaderNotFoundError,
|
||||
|
||||
get_repres_contexts,
|
||||
get_contexts_for_repre_docs,
|
||||
|
|
@ -55,6 +57,8 @@ __all__ = (
|
|||
"LoadError",
|
||||
"IncompatibleLoaderError",
|
||||
"InvalidRepresentationContext",
|
||||
"LoaderSwitchNotImplementedError",
|
||||
"LoaderNotFoundError",
|
||||
|
||||
"get_repres_contexts",
|
||||
"get_contexts_for_repre_docs",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,16 @@ class InvalidRepresentationContext(ValueError):
|
|||
pass
|
||||
|
||||
|
||||
class LoaderSwitchNotImplementedError(NotImplementedError):
|
||||
"""Error when `switch` is used with Loader that has no implementation."""
|
||||
pass
|
||||
|
||||
|
||||
class LoaderNotFoundError(RuntimeError):
|
||||
"""Error when Loader plugin for a loader name is not found."""
|
||||
pass
|
||||
|
||||
|
||||
def get_repres_contexts(representation_ids, dbcon=None):
|
||||
"""Return parenthood context for representation.
|
||||
|
||||
|
|
@ -432,7 +442,10 @@ def remove_container(container):
|
|||
|
||||
Loader = _get_container_loader(container)
|
||||
if not Loader:
|
||||
raise RuntimeError("Can't remove container. See log for details.")
|
||||
raise LoaderNotFoundError(
|
||||
"Can't remove container because loader '{}' was not found."
|
||||
.format(container.get("loader"))
|
||||
)
|
||||
|
||||
loader = Loader(get_representation_context(container["representation"]))
|
||||
return loader.remove(container)
|
||||
|
|
@ -480,7 +493,10 @@ def update_container(container, version=-1):
|
|||
# Run update on the Loader for this container
|
||||
Loader = _get_container_loader(container)
|
||||
if not Loader:
|
||||
raise RuntimeError("Can't update container. See log for details.")
|
||||
raise LoaderNotFoundError(
|
||||
"Can't update container because loader '{}' was not found."
|
||||
.format(container.get("loader"))
|
||||
)
|
||||
|
||||
loader = Loader(get_representation_context(container["representation"]))
|
||||
return loader.update(container, new_representation)
|
||||
|
|
@ -502,15 +518,18 @@ def switch_container(container, representation, loader_plugin=None):
|
|||
loader_plugin = _get_container_loader(container)
|
||||
|
||||
if not loader_plugin:
|
||||
raise RuntimeError("Can't switch container. See log for details.")
|
||||
raise LoaderNotFoundError(
|
||||
"Can't switch container because loader '{}' was not found."
|
||||
.format(container.get("loader"))
|
||||
)
|
||||
|
||||
if not hasattr(loader_plugin, "switch"):
|
||||
# Backwards compatibility (classes without switch support
|
||||
# might be better to just have "switch" raise NotImplementedError
|
||||
# on the base class of Loader\
|
||||
raise RuntimeError("Loader '{}' does not support 'switch'".format(
|
||||
loader_plugin.label
|
||||
))
|
||||
raise LoaderSwitchNotImplementedError(
|
||||
"Loader {} does not support 'switch'".format(loader_plugin.label)
|
||||
)
|
||||
|
||||
# Get the new representation to switch to
|
||||
project_name = legacy_io.active_project()
|
||||
|
|
@ -520,7 +539,11 @@ def switch_container(container, representation, loader_plugin=None):
|
|||
|
||||
new_context = get_representation_context(new_representation)
|
||||
if not is_compatible_loader(loader_plugin, new_context):
|
||||
raise AssertionError("Must be compatible Loader")
|
||||
raise IncompatibleLoaderError(
|
||||
"Loader {} is incompatible with {}".format(
|
||||
loader_plugin.__name__, new_context["subset"]["name"]
|
||||
)
|
||||
)
|
||||
|
||||
loader = loader_plugin(new_context)
|
||||
|
||||
|
|
|
|||
|
|
@ -577,12 +577,14 @@ def remote_publish(log, close_plugin_name=None, raise_error=False):
|
|||
raise RuntimeError(error_message)
|
||||
|
||||
|
||||
def get_errored_instances_from_context(context):
|
||||
def get_errored_instances_from_context(context, plugin=None):
|
||||
"""Collect failed instances from pyblish context.
|
||||
|
||||
Args:
|
||||
context (pyblish.api.Context): Publish context where we're looking
|
||||
for failed instances.
|
||||
plugin (pyblish.api.Plugin): If provided then only consider errors
|
||||
related to that plug-in.
|
||||
|
||||
Returns:
|
||||
List[pyblish.lib.Instance]: Instances which failed during processing.
|
||||
|
|
@ -594,6 +596,9 @@ def get_errored_instances_from_context(context):
|
|||
# When instance is None we are on the "context" result
|
||||
continue
|
||||
|
||||
if plugin is not None and result.get("plugin") != plugin:
|
||||
continue
|
||||
|
||||
if result["error"]:
|
||||
instances.append(result["instance"])
|
||||
|
||||
|
|
|
|||
|
|
@ -234,11 +234,9 @@ class RepairAction(pyblish.api.Action):
|
|||
|
||||
# Get the errored instances
|
||||
self.log.debug("Finding failed instances..")
|
||||
errored_instances = get_errored_instances_from_context(context)
|
||||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
|
||||
for instance in instances:
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
for instance in errored_instances:
|
||||
self.log.debug(
|
||||
"Attempting repair for instance: {} ...".format(instance)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import copy
|
||||
import platform
|
||||
from collections import defaultdict
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -83,6 +84,12 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
self.templates = self._get_templates(self.anatomy)
|
||||
for name, _ in self.templates.items():
|
||||
dropdown.addItem(name)
|
||||
if self.templates and platform.system() == "Darwin":
|
||||
# fix macos QCombobox Style
|
||||
dropdown.setItemDelegate(QtWidgets.QStyledItemDelegate())
|
||||
# update combo box length to longest entry
|
||||
longest_key = max(self.templates.keys(), key=len)
|
||||
dropdown.setMinimumContentsLength(len(longest_key))
|
||||
|
||||
template_label = QtWidgets.QLabel()
|
||||
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||
|
|
@ -115,7 +122,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
input_layout.addRow("Representations", repre_checkboxes_layout)
|
||||
|
||||
btn_delivery = QtWidgets.QPushButton("Deliver")
|
||||
btn_delivery.setEnabled(bool(dropdown.currentText()))
|
||||
btn_delivery.setEnabled(False)
|
||||
|
||||
progress_bar = QtWidgets.QProgressBar(self)
|
||||
progress_bar.setMinimum = 0
|
||||
|
|
@ -152,6 +159,15 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
btn_delivery.clicked.connect(self.deliver)
|
||||
dropdown.currentIndexChanged.connect(self._update_template_value)
|
||||
|
||||
if not self.dropdown.count():
|
||||
self.text_area.setVisible(True)
|
||||
error_message = (
|
||||
"No Delivery Templates found!\n"
|
||||
"Add Template in [project_anatomy/templates/delivery]"
|
||||
)
|
||||
self.text_area.setText(error_message)
|
||||
self.log.error(error_message.replace("\n", " "))
|
||||
|
||||
def deliver(self):
|
||||
"""Main method to loop through all selected representations"""
|
||||
self.progress_bar.setVisible(True)
|
||||
|
|
@ -287,14 +303,17 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
|
|||
self.files_selected, self.size_selected = \
|
||||
self._get_counts(selected_repres)
|
||||
self.selected_label.setText(self._prepare_label())
|
||||
# update delivery button state if any templates found
|
||||
if self.dropdown.count():
|
||||
self.btn_delivery.setEnabled(bool(selected_repres))
|
||||
|
||||
def _update_template_value(self, _index=None):
|
||||
"""Sets template value to label after selection in dropdown."""
|
||||
name = self.dropdown.currentText()
|
||||
template_value = self.templates.get(name)
|
||||
if template_value:
|
||||
self.btn_delivery.setEnabled(True)
|
||||
self.template_label.setText(template_value)
|
||||
self.btn_delivery.setEnabled(bool(self._get_selected_repres()))
|
||||
|
||||
def _update_progress(self, uploaded):
|
||||
"""Update progress bar after each repre copied."""
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"aftereffects",
|
||||
"photoshop",
|
||||
"flame",
|
||||
"houdini"
|
||||
"houdini",
|
||||
"max"
|
||||
# "resolve"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"maya",
|
||||
"blender",
|
||||
"houdini",
|
||||
"max",
|
||||
"shell",
|
||||
"hiero",
|
||||
"premiere",
|
||||
|
|
|
|||
|
|
@ -262,7 +262,8 @@
|
|||
],
|
||||
"hosts": [
|
||||
"maya",
|
||||
"houdini"
|
||||
"houdini",
|
||||
"max"
|
||||
],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
|
|
|
|||
|
|
@ -421,9 +421,9 @@
|
|||
},
|
||||
"workfile": {
|
||||
"enabled": false,
|
||||
"renderSpace": "ACEScg",
|
||||
"displayName": "sRGB",
|
||||
"viewName": "ACES 1.0 SDR-video"
|
||||
"renderSpace": "ACES - ACEScg",
|
||||
"displayName": "ACES",
|
||||
"viewName": "sRGB"
|
||||
},
|
||||
"colorManagementPreference_v2": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
"imageio": {
|
||||
"activate_host_color_management": true,
|
||||
"ocio_config": {
|
||||
"override_global_config": true,
|
||||
"override_global_config": false,
|
||||
"filepath": []
|
||||
},
|
||||
"file_rules": {
|
||||
"activate_host_rules": true,
|
||||
"activate_host_rules": false,
|
||||
"rules": {}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ from openpype.pipeline.load import (
|
|||
switch_container,
|
||||
get_repres_contexts,
|
||||
loaders_from_repre_context,
|
||||
LoaderSwitchNotImplementedError,
|
||||
IncompatibleLoaderError,
|
||||
LoaderNotFoundError
|
||||
)
|
||||
|
||||
from .widgets import (
|
||||
|
|
@ -1298,19 +1301,28 @@ class SwitchAssetDialog(QtWidgets.QDialog):
|
|||
else:
|
||||
repre_doc = repres_by_name[container_repre_name]
|
||||
|
||||
error = None
|
||||
try:
|
||||
switch_container(container, repre_doc, loader)
|
||||
except (
|
||||
LoaderSwitchNotImplementedError,
|
||||
IncompatibleLoaderError,
|
||||
LoaderNotFoundError,
|
||||
) as exc:
|
||||
error = str(exc)
|
||||
except Exception:
|
||||
msg = (
|
||||
error = (
|
||||
"Switch asset failed. "
|
||||
"Search console log for more details."
|
||||
)
|
||||
if error is not None:
|
||||
log.warning((
|
||||
"Couldn't switch asset."
|
||||
"See traceback for more information."
|
||||
)
|
||||
log.warning(msg, exc_info=True)
|
||||
), exc_info=True)
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Switch asset failed")
|
||||
dialog.setText(
|
||||
"Switch asset failed. Search console log for more details"
|
||||
)
|
||||
dialog.setText(error)
|
||||
dialog.exec_()
|
||||
|
||||
self.switched.emit()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import collections
|
||||
import logging
|
||||
import itertools
|
||||
from functools import partial
|
||||
|
||||
from qtpy import QtWidgets, QtCore
|
||||
|
|
@ -195,20 +196,17 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
version_name_by_id[version_doc["_id"]] = \
|
||||
version_doc["name"]
|
||||
|
||||
# Specify version per item to update to
|
||||
update_items = []
|
||||
update_versions = []
|
||||
for item in items:
|
||||
repre_id = item["representation"]
|
||||
version_id = version_id_by_repre_id.get(repre_id)
|
||||
version_name = version_name_by_id.get(version_id)
|
||||
if version_name is not None:
|
||||
try:
|
||||
update_container(item, version_name)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(
|
||||
version_name, [item]
|
||||
)
|
||||
log.warning("Update failed", exc_info=True)
|
||||
|
||||
self.data_changed.emit()
|
||||
update_items.append(item)
|
||||
update_versions.append(version_name)
|
||||
self._update_containers(update_items, update_versions)
|
||||
|
||||
update_icon = qtawesome.icon(
|
||||
"fa.asterisk",
|
||||
|
|
@ -225,16 +223,6 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
|
||||
update_to_latest_action = None
|
||||
if has_outdated or has_loaded_hero_versions:
|
||||
# update to latest version
|
||||
def _on_update_to_latest(items):
|
||||
for item in items:
|
||||
try:
|
||||
update_container(item, -1)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(None, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
self.data_changed.emit()
|
||||
|
||||
update_icon = qtawesome.icon(
|
||||
"fa.angle-double-up",
|
||||
color=DEFAULT_COLOR
|
||||
|
|
@ -245,21 +233,11 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
menu
|
||||
)
|
||||
update_to_latest_action.triggered.connect(
|
||||
lambda: _on_update_to_latest(items)
|
||||
lambda: self._update_containers(items, version=-1)
|
||||
)
|
||||
|
||||
change_to_hero = None
|
||||
if has_available_hero_version:
|
||||
# change to hero version
|
||||
def _on_update_to_hero(items):
|
||||
for item in items:
|
||||
try:
|
||||
update_container(item, HeroVersionType(-1))
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog('hero', [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
self.data_changed.emit()
|
||||
|
||||
# TODO change icon
|
||||
change_icon = qtawesome.icon(
|
||||
"fa.asterisk",
|
||||
|
|
@ -271,7 +249,8 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
menu
|
||||
)
|
||||
change_to_hero.triggered.connect(
|
||||
lambda: _on_update_to_hero(items)
|
||||
lambda: self._update_containers(items,
|
||||
version=HeroVersionType(-1))
|
||||
)
|
||||
|
||||
# set version
|
||||
|
|
@ -740,14 +719,7 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
|
||||
if label:
|
||||
version = versions_by_label[label]
|
||||
for item in items:
|
||||
try:
|
||||
update_container(item, version)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(version, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
# refresh model when done
|
||||
self.data_changed.emit()
|
||||
self._update_containers(items, version)
|
||||
|
||||
def _show_switch_dialog(self, items):
|
||||
"""Display Switch dialog"""
|
||||
|
|
@ -782,9 +754,9 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
Args:
|
||||
version: str or int or None
|
||||
"""
|
||||
if not version:
|
||||
if version == -1:
|
||||
version_str = "latest"
|
||||
elif version == "hero":
|
||||
elif isinstance(version, HeroVersionType):
|
||||
version_str = "hero"
|
||||
elif isinstance(version, int):
|
||||
version_str = "v{:03d}".format(version)
|
||||
|
|
@ -841,10 +813,43 @@ class SceneInventoryView(QtWidgets.QTreeView):
|
|||
return
|
||||
|
||||
# Trigger update to latest
|
||||
for item in outdated_items:
|
||||
try:
|
||||
update_container(item, -1)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(None, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
self.data_changed.emit()
|
||||
self._update_containers(outdated_items, version=-1)
|
||||
|
||||
def _update_containers(self, items, version):
|
||||
"""Helper to update items to given version (or version per item)
|
||||
|
||||
If at least one item is specified this will always try to refresh
|
||||
the inventory even if errors occurred on any of the items.
|
||||
|
||||
Arguments:
|
||||
items (list): Items to update
|
||||
version (int or list): Version to set to.
|
||||
This can be a list specifying a version for each item.
|
||||
Like `update_container` version -1 sets the latest version
|
||||
and HeroTypeVersion instances set the hero version.
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(version, (list, tuple)):
|
||||
# We allow a unique version to be specified per item. In that case
|
||||
# the length must match with the items
|
||||
assert len(items) == len(version), (
|
||||
"Number of items mismatches number of versions: "
|
||||
"{} items - {} versions".format(len(items), len(version))
|
||||
)
|
||||
versions = version
|
||||
else:
|
||||
# Repeat the same version infinitely
|
||||
versions = itertools.repeat(version)
|
||||
|
||||
# Trigger update to latest
|
||||
try:
|
||||
for item, item_version in zip(items, versions):
|
||||
try:
|
||||
update_container(item, item_version)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(item_version, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
finally:
|
||||
# Always update the scene inventory view, even if errors occurred
|
||||
self.data_changed.emit()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.15.12-nightly.1"
|
||||
__version__ = "3.15.12"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.15.11" # OpenPype
|
||||
version = "3.15.12" # OpenPype
|
||||
description = "Open VFX and Animation pipeline with support."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue