mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/OP-7133_3dsMax--optional-custom-attributes-for-abc-extract
This commit is contained in:
commit
17d2c5414a
524 changed files with 23278 additions and 5696 deletions
28
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
28
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -35,6 +35,20 @@ body:
|
|||
label: Version
|
||||
description: What version are you running? Look to OpenPype Tray
|
||||
options:
|
||||
- 3.17.7-nightly.7
|
||||
- 3.17.7-nightly.6
|
||||
- 3.17.7-nightly.5
|
||||
- 3.17.7-nightly.4
|
||||
- 3.17.7-nightly.3
|
||||
- 3.17.7-nightly.2
|
||||
- 3.17.7-nightly.1
|
||||
- 3.17.6
|
||||
- 3.17.6-nightly.3
|
||||
- 3.17.6-nightly.2
|
||||
- 3.17.6-nightly.1
|
||||
- 3.17.5
|
||||
- 3.17.5-nightly.3
|
||||
- 3.17.5-nightly.2
|
||||
- 3.17.5-nightly.1
|
||||
- 3.17.4
|
||||
- 3.17.4-nightly.2
|
||||
|
|
@ -121,20 +135,6 @@ body:
|
|||
- 3.15.3-nightly.3
|
||||
- 3.15.3-nightly.2
|
||||
- 3.15.3-nightly.1
|
||||
- 3.15.2
|
||||
- 3.15.2-nightly.6
|
||||
- 3.15.2-nightly.5
|
||||
- 3.15.2-nightly.4
|
||||
- 3.15.2-nightly.3
|
||||
- 3.15.2-nightly.2
|
||||
- 3.15.2-nightly.1
|
||||
- 3.15.1
|
||||
- 3.15.1-nightly.6
|
||||
- 3.15.1-nightly.5
|
||||
- 3.15.1-nightly.4
|
||||
- 3.15.1-nightly.3
|
||||
- 3.15.1-nightly.2
|
||||
- 3.15.1-nightly.1
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
|
|
|||
837
CHANGELOG.md
837
CHANGELOG.md
|
|
@ -1,6 +1,843 @@
|
|||
# Changelog
|
||||
|
||||
|
||||
## [3.17.6](https://github.com/ynput/OpenPype/tree/3.17.6)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.5...3.17.6)
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Testing: Validate Maya Logs <a href="https://github.com/ynput/OpenPype/pull/5775">#5775</a></summary>
|
||||
|
||||
This PR adds testing of the logs within Maya such as Python and Pyblish errors.The reason why we need to touch so many files outside of Maya is because of the pyblish errors below;
|
||||
```
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "collect_otio_frame_ranges" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "collect_otio_frame_ranges" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "collect_otio_review" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "collect_otio_review" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "collect_otio_subset_resources" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "collect_otio_subset_resources" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "extract_otio_audio_tracks" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "extract_otio_audio_tracks" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "extract_otio_file" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "extract_otio_file" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "extract_otio_review" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "extract_otio_review" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "extract_otio_trimming_video" (No module named 'opentimelineio')
|
||||
# Error: pyblish.plugin : Skipped: "extract_otio_trimming_video" (No module named 'opentimelineio') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "submit_blender_deadline" (No module named 'bpy')
|
||||
# Error: pyblish.plugin : Skipped: "submit_blender_deadline" (No module named 'bpy') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "submit_houdini_remote_publish" (No module named 'hou')
|
||||
# Error: pyblish.plugin : Skipped: "submit_houdini_remote_publish" (No module named 'hou') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "submit_houdini_render_deadline" (No module named 'hou')
|
||||
# Error: pyblish.plugin : Skipped: "submit_houdini_render_deadline" (No module named 'hou') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "submit_max_deadline" (No module named 'pymxs')
|
||||
# Error: pyblish.plugin : Skipped: "submit_max_deadline" (No module named 'pymxs') #
|
||||
pyblish (ERROR) (line: 1371) pyblish.plugin:
|
||||
Skipped: "submit_nuke_deadline" (No module named 'nuke')
|
||||
# Error: pyblish.plugin : Skipped: "submit_nuke_deadline" (No module named 'nuke') #
|
||||
```
|
||||
We also needed to `stdout` and `stderr` from the launched application to capture the output.Split from #5644.Dependent on #5734
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Render Settings cleanup remove global `RENDER_ATTRS` <a href="https://github.com/ynput/OpenPype/pull/5801">#5801</a></summary>
|
||||
|
||||
Remove global `lib.RENDER_ATTRS` and implement a `RenderSettings.get_padding_attr(renderer)` method instead.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Testing: Ingest expected files and input workfile <a href="https://github.com/ynput/OpenPype/pull/5840">#5840</a></summary>
|
||||
|
||||
This ingests the Maya workfile from the Drive storage. Have changed the format to MayaAscii so its easier to see what changes are happening in a PR. This meant changing the expected files and database entries as well.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Create plugin auto-apply settings <a href="https://github.com/ynput/OpenPype/pull/5908">#5908</a></summary>
|
||||
|
||||
Create plugins can auto-apply settings.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Resolve: Add save current file button + "Save" shortcut when menu is active <a href="https://github.com/ynput/OpenPype/pull/5691">#5691</a></summary>
|
||||
|
||||
Adds a "Save current file" to the OpenPype menu.Also adds a "Save" shortcut key sequence (CTRL+S on Windows) to the button, so that clicking CTRL+S when the menu is active will save the current workfile. However this of course does not work if the menu does not receive the key press event (e.g. when Resolve UI is active instead)Resolves #5684
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Reference USD file as maya native geometry <a href="https://github.com/ynput/OpenPype/pull/5781">#5781</a></summary>
|
||||
|
||||
Add MayaUsdReferenceLoader to reference USD as Maya native geometry using `mayaUSDImport` file translator.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Bug fix on wrong aspect ratio and viewport not being maximized during context in review family <a href="https://github.com/ynput/OpenPype/pull/5839">#5839</a></summary>
|
||||
|
||||
This PR will fix the bug on wrong aspect ratio and viewport not being maximized when creating preview animationBesides, the support of tga image format and the options for AA quality are implemented in this PR
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Incorporate blender "Collections" into Publish/Load <a href="https://github.com/ynput/OpenPype/pull/5841">#5841</a></summary>
|
||||
|
||||
Allow `blendScene` family to include collections.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Allows user preset the setting of preview animation in OP/AYON Setting <a href="https://github.com/ynput/OpenPype/pull/5859">#5859</a></summary>
|
||||
|
||||
Allows user preset the setting of preview animation in OP/AYON Setting for review family.
|
||||
- [x] Openpype
|
||||
- [x] AYON
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Center publisher window on first show <a href="https://github.com/ynput/OpenPype/pull/5877">#5877</a></summary>
|
||||
|
||||
Move publisher window to center of a screen on first show.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publisher: Instance context changes confirm works <a href="https://github.com/ynput/OpenPype/pull/5881">#5881</a></summary>
|
||||
|
||||
Confirmation of context changes in publisher on existing instances does not cause glitches.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON workfiles tools: Revisit workfiles tool <a href="https://github.com/ynput/OpenPype/pull/5897">#5897</a></summary>
|
||||
|
||||
Revisited workfiles tool for AYON mode to reuse common models and widgets.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: updated colorspace settings <a href="https://github.com/ynput/OpenPype/pull/5906">#5906</a></summary>
|
||||
|
||||
Updating nuke colorspace settings into more convenient way with usage of ocio config roles rather then particular colorspace names. This way we should not have troubles to switch between linear Rec709 or ACES configs without any additional settings changes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Refactor to new publisher <a href="https://github.com/ynput/OpenPype/pull/5910">#5910</a></summary>
|
||||
|
||||
Refactor Blender integration to use the new publisher
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement: Some publish logs cosmetics <a href="https://github.com/ynput/OpenPype/pull/5917">#5917</a></summary>
|
||||
|
||||
General logging message tweaks:
|
||||
- Sort some lists of folder/filenames so they appear sorted in the logs
|
||||
- Fix some grammar / typos
|
||||
- In some cases provide slightly more information in a log
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Better name of 'asset_name' function <a href="https://github.com/ynput/OpenPype/pull/5927">#5927</a></summary>
|
||||
|
||||
Renamed function `asset_name` to `prepare_scene_name`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Maya: Bug fix the fbx animation export errored out when the skeletonAnim set is empty <a href="https://github.com/ynput/OpenPype/pull/5875">#5875</a></summary>
|
||||
|
||||
Resolve this bug discordIf the skeletonAnim SET is empty and fbx animation collect, the fbx animation extractor would skip the fbx extraction
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: fix few typos in houdini's and Maya's Ayon settings <a href="https://github.com/ynput/OpenPype/pull/5882">#5882</a></summary>
|
||||
|
||||
Fixing few typos
|
||||
- [x] Maya unreal static mesh
|
||||
- [x] Houdini static mesh
|
||||
- [x] Houdini collect asset handles
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: Ayon Deadline env vars + error message on no executable found <a href="https://github.com/ynput/OpenPype/pull/5815">#5815</a></summary>
|
||||
|
||||
Fix some Ayon x Deadline issues as came up in this topic:
|
||||
- missing Environment Variables issue explained here for `deadlinePlugin.RunProcess` for the AYON _extract environments_ call.
|
||||
- wrong error formatting described here with a `;` between each character like this: `Ayon executable was not found in the semicolon separated list "C;:;/;P;r;o;g;r;a;m; ;F;i;l;e;s;/;Y;n;p;u;t;/;A;Y;O;N; ;1;.;0;.;0;-;b;e;t;a;.;5;/;a;y;o;n;_;c;o;n;s;o;l;e;.;e;x;e". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor.`
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Fix bundles access in settings <a href="https://github.com/ynput/OpenPype/pull/5856">#5856</a></summary>
|
||||
|
||||
Fixed access to bundles data in settings to define correct develop variant.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON 3dsMax settings: 'ValidateAttributes' settings converte only if available <a href="https://github.com/ynput/OpenPype/pull/5878">#5878</a></summary>
|
||||
|
||||
Convert `ValidateAttributes` settings only if are available in AYON settings.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Fix TrayPublisher editorial settings <a href="https://github.com/ynput/OpenPype/pull/5880">#5880</a></summary>
|
||||
|
||||
Fixing Traypublisher settings for adding task in simple editorial.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>TrayPublisher: editorial frame range check not needed <a href="https://github.com/ynput/OpenPype/pull/5884">#5884</a></summary>
|
||||
|
||||
Validator for frame ranges is not needed during editorial publishing since entity data are not yet in database.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Update houdini license validator <a href="https://github.com/ynput/OpenPype/pull/5886">#5886</a></summary>
|
||||
|
||||
As reported in this community commentHoudini USD publishing is only restricted in Houdini apprentice.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Fix blend extraction and packed images <a href="https://github.com/ynput/OpenPype/pull/5888">#5888</a></summary>
|
||||
|
||||
Fixed a with blend extractor and packed images.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Initialize connection with all information <a href="https://github.com/ynput/OpenPype/pull/5890">#5890</a></summary>
|
||||
|
||||
Create global AYON api connection with all informations all the time.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Scene inventory tool without site sync <a href="https://github.com/ynput/OpenPype/pull/5896">#5896</a></summary>
|
||||
|
||||
Skip 'get_site_icons' if site sync addon is disabled.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Publish report tool: Fix PySide6 <a href="https://github.com/ynput/OpenPype/pull/5898">#5898</a></summary>
|
||||
|
||||
Use constants from classes instead of objects.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>fusion: removing hardcoded template name for saver <a href="https://github.com/ynput/OpenPype/pull/5907">#5907</a></summary>
|
||||
|
||||
Fusion is not hardcoded for `render` anatomy template only anymore. This was blocking AYON deployment.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.17.5](https://github.com/ynput/OpenPype/tree/3.17.5)
|
||||
|
||||
|
||||
[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.4...3.17.5)
|
||||
|
||||
### **🆕 New features**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: Add USD loader <a href="https://github.com/ynput/OpenPype/pull/4896">#4896</a></summary>
|
||||
|
||||
Add an OpenPype managed USD loader (`uLoader`) for Fusion.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fusion: Resolution validator <a href="https://github.com/ynput/OpenPype/pull/5325">#5325</a></summary>
|
||||
|
||||
Added a resolution validator.The code is from my old PR (https://github.com/ynput/OpenPype/pull/4921) that I closed because the PR also contained a frame range validator that no longer is needed.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Context Selection tool: Refactor Context tool (for AYON) <a href="https://github.com/ynput/OpenPype/pull/5766">#5766</a></summary>
|
||||
|
||||
Context selection tool has AYON variant.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Use AYON username for user in template data <a href="https://github.com/ynput/OpenPype/pull/5842">#5842</a></summary>
|
||||
|
||||
Use ayon username for template data in AYON mode.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Testing: app_group flag <a href="https://github.com/ynput/OpenPype/pull/5869">#5869</a></summary>
|
||||
|
||||
`app_group` command flag. This is for changing which flavour of the host to launch. In the case of Maya, you can launch Maya and MayaPy, but it can be used for the Nuke family as well.Split from #5644
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🚀 Enhancements**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement: Fusion fix saver creation + minor Blender/Fusion logging tweaks <a href="https://github.com/ynput/OpenPype/pull/5558">#5558</a></summary>
|
||||
|
||||
- Blender change logs to `debug` level in preparation for new publisher artist facing reports (note that it currently still uses the old publisher)
|
||||
- Fusion: Create Saver fix redeclaration of default_variants
|
||||
- Fusion: Fix saver being created in incorrect state without saving directly after create
|
||||
- Fusion: Allow reset frame range on render family
|
||||
- Fusion: Tweak logging level for artist-facing report
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Resolve: load clip to timeline at set time <a href="https://github.com/ynput/OpenPype/pull/5665">#5665</a></summary>
|
||||
|
||||
It is possible to load clip to correct place on timeline.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Optional Deadline workfile dependency. <a href="https://github.com/ynput/OpenPype/pull/5732">#5732</a></summary>
|
||||
|
||||
Adds option to add the workfile as dependency for the Deadline job.Think it used to have something like this, but it disappeared. Usecase is for remote workflow where the Nuke script needs to be synced before the job can start.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Enhancement/houdini rearrange ayon houdini settings files <a href="https://github.com/ynput/OpenPype/pull/5748">#5748</a></summary>
|
||||
|
||||
Rearranging Houdini Settings to be more readable, easier to edit, update settings (include all families/product types)This PR is mainly for Ayon Settings to have more organized files. For Openpype, I'll make sure that each Houdini setting in Ayon has an equivalent in Openpype.
|
||||
- [x] update Ayon settings, fix typos and remove deprecated settings.
|
||||
- [x] Sync with Openpype
|
||||
- [x] Test in Openpype
|
||||
- [x] Test in Ayon
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: updating create ayon addon script <a href="https://github.com/ynput/OpenPype/pull/5822">#5822</a></summary>
|
||||
|
||||
Adding developers environment options.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Implement Validator for Properties/Attributes Value Check <a href="https://github.com/ynput/OpenPype/pull/5824">#5824</a></summary>
|
||||
|
||||
Add optional validator which can check if the property attributes are valid in Max
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Remove unused 'get_render_path' function <a href="https://github.com/ynput/OpenPype/pull/5826">#5826</a></summary>
|
||||
|
||||
Remove unused function `get_render_path` from nuke integration.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Limit current context template data function <a href="https://github.com/ynput/OpenPype/pull/5845">#5845</a></summary>
|
||||
|
||||
Current implementation of `get_current_context_template_data` does return the same values as base template data function `get_template_data`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Max: Make sure Collect Render not ignoring instance asset <a href="https://github.com/ynput/OpenPype/pull/5847">#5847</a></summary>
|
||||
|
||||
- Make sure Collect Render is not always using asset from context.
|
||||
- Make sure Scene version being collected
|
||||
- Clean up unnecessary uses of code in the collector.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Ftrack: Events are not processed if project is not available in OpenPype <a href="https://github.com/ynput/OpenPype/pull/5853">#5853</a></summary>
|
||||
|
||||
Events that happened on project which is not in OpenPype is not processed.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Add Nuke 11.0 as default setting <a href="https://github.com/ynput/OpenPype/pull/5855">#5855</a></summary>
|
||||
|
||||
Found I needed Nuke 11.0 in the default settings to help with unit testing.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>TVPaint: Code cleanup <a href="https://github.com/ynput/OpenPype/pull/5857">#5857</a></summary>
|
||||
|
||||
Removed unused import. Use `AYON` label in ayon mode. Removed unused data in publish context `"previous_context"`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON settings: Use correct label for follow workfile version <a href="https://github.com/ynput/OpenPype/pull/5874">#5874</a></summary>
|
||||
|
||||
Follow workfile version label was marked as Collect Anatomy Instance Data label.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🐛 Bug fixes**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Fix workfile template builder so representations get loaded next to each other <a href="https://github.com/ynput/OpenPype/pull/5061">#5061</a></summary>
|
||||
|
||||
Refactor when the cleanup of the placeholder happens for the cases where multiple representations are loaded by a single placeholder.The existing code didn't take into account the case where a template placeholder can load multiple representations so it was trying to do the cleanup of the placeholder node and the re-arrangement of the imported nodes too early. I assume this was designed only for the cases where a single representation can load multiple nodes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: Dont update node name on update <a href="https://github.com/ynput/OpenPype/pull/5704">#5704</a></summary>
|
||||
|
||||
When updating `Image` containers the code is trying to set the name of the node. This results in a warning message from Nuke shown below;Suggesting to not change the node name when updating.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>UIDefLabel can be unique <a href="https://github.com/ynput/OpenPype/pull/5827">#5827</a></summary>
|
||||
|
||||
`UILabelDef` have implemented comparison and uniqueness.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Skip kitsu module when creating ayon addons <a href="https://github.com/ynput/OpenPype/pull/5828">#5828</a></summary>
|
||||
|
||||
Create AYON packages is skipping kitsu module in creation of modules/addons and kitsu module is not loaded from modules on start. The addon already has it's repository https://github.com/ynput/ayon-kitsu.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: Collect Rendered Files only collecting first instance <a href="https://github.com/ynput/OpenPype/pull/5832">#5832</a></summary>
|
||||
|
||||
Collect all instances from the metadata file - don't return on first instance iteration.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: set frame range for the created composite ROP <a href="https://github.com/ynput/OpenPype/pull/5833">#5833</a></summary>
|
||||
|
||||
Quick bug fix for created composite ROP, set its frame range to the frame range of the playbar.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix registering launcher actions from OpenPypeModules <a href="https://github.com/ynput/OpenPype/pull/5843">#5843</a></summary>
|
||||
|
||||
Fix typo `actions_dir` -> `path` to fix register launcher actions fromm OpenPypeModule
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix in houdini shelves manager and beautify settings <a href="https://github.com/ynput/OpenPype/pull/5844">#5844</a></summary>
|
||||
|
||||
This PR fixes the problem in this PR https://github.com/ynput/OpenPype/issues/5457 by using the right function to load a pre-made houdini `.shelf` fileAlso, it beautifies houdini shelves settings to provide better guidance for users which helps with other issue https://github.com/ynput/OpenPype/issues/5458 , Rather adding default shelf and set names, I'll educate users how to use the tool correctly.Users now are able to select between the two options.| OpenPype | Ayon || -- | -- || | |
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Fix missing Grease Pencils in review <a href="https://github.com/ynput/OpenPype/pull/5848">#5848</a></summary>
|
||||
|
||||
Fix Grease Pencil missing in review when isolating objects.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Fix Render Settings in Ayon <a href="https://github.com/ynput/OpenPype/pull/5849">#5849</a></summary>
|
||||
|
||||
Fix Render Settings in Ayon for Blender.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: houdini tab menu working as expected <a href="https://github.com/ynput/OpenPype/pull/5850">#5850</a></summary>
|
||||
|
||||
This PR:Tab menu name changes to Ayon when using ayon get_network_categories is checked in all creator plugins. | Product | Network Category | | -- | -- | | Alembic camera | rop, obj | | Arnold Ass | rop | | Arnold ROP | rop | | Bgeo | rop, sop | | composite sequence | cop2, rop | | hda | obj | | Karma ROP | rop | | Mantra ROP | rop | | ABC | rop, sop | | RS proxy | rop, sop| | RS ROP | rop | | Review | rop | | Static mesh | rop, obj, sop | | USD | lop, rop | | USD Render | rop | | VDB | rop, obj, sop | | V Ray | rop |
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bigfix: Houdini skip frame_range_validator if node has no 'trange' parameter <a href="https://github.com/ynput/OpenPype/pull/5851">#5851</a></summary>
|
||||
|
||||
I faced a bug when publishing HDA instance as it has no `trange` parameter. As this PR title says : skip frame_range_validator if node has no 'trange' parameter
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Bugfix: houdini image sequence loading and missing frames <a href="https://github.com/ynput/OpenPype/pull/5852">#5852</a></summary>
|
||||
|
||||
I made this PR in to fix issues mentioned here https://github.com/ynput/OpenPype/pull/5833#issuecomment-1789207727in short:
|
||||
- image load doesn't work
|
||||
- publisher only publish one frame
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Nuke: loaders' containers updating as nodes <a href="https://github.com/ynput/OpenPype/pull/5854">#5854</a></summary>
|
||||
|
||||
Nuke loaded containers are updating correctly even they have been duplicating of originally loaded nodes. This had previously been removed duplicated nodes.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>deadline: settings are not blocking extension input <a href="https://github.com/ynput/OpenPype/pull/5864">#5864</a></summary>
|
||||
|
||||
Settings are not blocking user input.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Blender: Fix loading of blend layouts <a href="https://github.com/ynput/OpenPype/pull/5866">#5866</a></summary>
|
||||
|
||||
Fix a problem with loading blend layouts.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON: Launcher refresh issues <a href="https://github.com/ynput/OpenPype/pull/5867">#5867</a></summary>
|
||||
|
||||
Fixed refresh of projects issue in launcher tool. And renamed Qt models to contain `Qt` in their name (it was really hard to find out where were used). It is not possible to click on disabled item in launcher's projects view.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Fix the Wrong key words for tycache workfile template settings in AYON <a href="https://github.com/ynput/OpenPype/pull/5870">#5870</a></summary>
|
||||
|
||||
Fix the wrong key words for the tycache workfile template settings in AYON(i.e. Instead of families, product_types should be used)
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>AYON tools: Handle empty icon definition <a href="https://github.com/ynput/OpenPype/pull/5876">#5876</a></summary>
|
||||
|
||||
Ignore if passed icon definition is `None`.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
### **🔀 Refactored code**
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Houdini: Remove on instance toggled callback <a href="https://github.com/ynput/OpenPype/pull/5860">#5860</a></summary>
|
||||
|
||||
Remove on instance toggled callback which isn't relevant to the new publisher
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Chore: Remove unused `instanceToggled` callbacks <a href="https://github.com/ynput/OpenPype/pull/5862">#5862</a></summary>
|
||||
|
||||
The `instanceToggled` callbacks should be irrelevant for new publisher.
|
||||
|
||||
|
||||
___
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.17.4](https://github.com/ynput/OpenPype/tree/3.17.4)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -282,6 +282,9 @@ def run(script):
|
|||
"--app_variant",
|
||||
help="Provide specific app variant for test, empty for latest",
|
||||
default=None)
|
||||
@click.option("--app_group",
|
||||
help="Provide specific app group for test, empty for default",
|
||||
default=None)
|
||||
@click.option("-t",
|
||||
"--timeout",
|
||||
help="Provide specific timeout value for test case",
|
||||
|
|
@ -293,12 +296,15 @@ def run(script):
|
|||
@click.option("--mongo_url",
|
||||
help="MongoDB for testing.",
|
||||
default=None)
|
||||
@click.option("--dump_databases",
|
||||
help="Dump all databases to data folder.",
|
||||
default=None)
|
||||
def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
|
||||
timeout, setup_only, mongo_url):
|
||||
timeout, setup_only, mongo_url, app_group, dump_databases):
|
||||
"""Run all automatic tests after proper initialization via start.py"""
|
||||
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
|
||||
persist, app_variant, timeout, setup_only,
|
||||
mongo_url)
|
||||
mongo_url, app_group, dump_databases)
|
||||
|
||||
|
||||
@main.command(help="DEPRECATED - run sync server")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from .mongo import (
|
||||
OpenPypeMongoConnection,
|
||||
)
|
||||
from .server.utils import get_ayon_server_api_connection
|
||||
|
||||
from .entities import (
|
||||
get_projects,
|
||||
|
|
@ -43,6 +44,8 @@ from .entities import (
|
|||
get_thumbnail_id_from_source,
|
||||
|
||||
get_workfile_info,
|
||||
|
||||
get_asset_name_identifier,
|
||||
)
|
||||
|
||||
from .entity_links import (
|
||||
|
|
@ -59,6 +62,8 @@ from .operations import (
|
|||
__all__ = (
|
||||
"OpenPypeMongoConnection",
|
||||
|
||||
"get_ayon_server_api_connection",
|
||||
|
||||
"get_projects",
|
||||
"get_project",
|
||||
"get_whole_project",
|
||||
|
|
@ -105,4 +110,6 @@ __all__ = (
|
|||
"get_linked_representation_id",
|
||||
|
||||
"create_project",
|
||||
|
||||
"get_asset_name_identifier",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,3 +4,22 @@ if not AYON_SERVER_ENABLED:
|
|||
from .mongo.entities import *
|
||||
else:
|
||||
from .server.entities import *
|
||||
|
||||
|
||||
def get_asset_name_identifier(asset_doc):
|
||||
"""Get asset name identifier by asset document.
|
||||
|
||||
This function is added because of AYON implementation where name
|
||||
identifier is not just a name but full path.
|
||||
|
||||
Asset document must have "name" key, and "data.parents" when in AYON mode.
|
||||
|
||||
Args:
|
||||
asset_doc (dict[str, Any]): Asset document.
|
||||
"""
|
||||
|
||||
if not AYON_SERVER_ENABLED:
|
||||
return asset_doc["name"]
|
||||
parents = list(asset_doc["data"]["parents"])
|
||||
parents.append(asset_doc["name"])
|
||||
return "/" + "/".join(parents)
|
||||
|
|
|
|||
|
|
@ -138,16 +138,22 @@ def _template_replacements_to_v3(template):
|
|||
)
|
||||
|
||||
|
||||
def _convert_template_item(template):
|
||||
# Others won't have 'directory'
|
||||
if "directory" not in template:
|
||||
return
|
||||
folder = _template_replacements_to_v3(template.pop("directory"))
|
||||
template["folder"] = folder
|
||||
template["file"] = _template_replacements_to_v3(template["file"])
|
||||
template["path"] = "/".join(
|
||||
(folder, template["file"])
|
||||
)
|
||||
def _convert_template_item(template_item):
|
||||
for key, value in tuple(template_item.items()):
|
||||
template_item[key] = _template_replacements_to_v3(value)
|
||||
|
||||
# Change 'directory' to 'folder'
|
||||
if "directory" in template_item:
|
||||
template_item["folder"] = template_item.pop("directory")
|
||||
|
||||
if (
|
||||
"path" not in template_item
|
||||
and "file" in template_item
|
||||
and "folder" in template_item
|
||||
):
|
||||
template_item["path"] = "/".join(
|
||||
(template_item["folder"], template_item["file"])
|
||||
)
|
||||
|
||||
|
||||
def _fill_template_category(templates, cat_templates, cat_key):
|
||||
|
|
@ -212,10 +218,27 @@ def convert_v4_project_to_v3(project):
|
|||
_convert_template_item(template)
|
||||
new_others_templates[name] = template
|
||||
|
||||
staging_templates = templates.pop("staging", None)
|
||||
# Key 'staging_directories' is legacy key that changed
|
||||
# to 'staging_dir'
|
||||
_legacy_staging_templates = templates.pop("staging_directories", None)
|
||||
if staging_templates is None:
|
||||
staging_templates = _legacy_staging_templates
|
||||
|
||||
if staging_templates is None:
|
||||
staging_templates = {}
|
||||
|
||||
# Prefix all staging template names with 'staging_' prefix
|
||||
# and add them to 'others'
|
||||
for name, template in staging_templates.items():
|
||||
_convert_template_item(template)
|
||||
new_name = "staging_{}".format(name)
|
||||
new_others_templates[new_name] = template
|
||||
|
||||
for key in (
|
||||
"work",
|
||||
"publish",
|
||||
"hero"
|
||||
"hero",
|
||||
):
|
||||
cat_templates = templates.pop(key)
|
||||
_fill_template_category(templates, cat_templates, key)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import collections
|
||||
|
||||
from ayon_api import get_server_api_connection
|
||||
|
||||
from openpype.client.mongo.operations import CURRENT_THUMBNAIL_SCHEMA
|
||||
|
||||
from .utils import get_ayon_server_api_connection
|
||||
from .openpype_comp import get_folders_with_tasks
|
||||
from .conversion_utils import (
|
||||
project_fields_v3_to_v4,
|
||||
|
|
@ -37,7 +36,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
|
|||
elif inactive:
|
||||
active = False
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = project_fields_v3_to_v4(fields, con)
|
||||
for project in con.get_projects(active, library, fields=fields):
|
||||
yield convert_v4_project_to_v3(project)
|
||||
|
|
@ -45,7 +44,7 @@ def get_projects(active=True, inactive=False, library=None, fields=None):
|
|||
|
||||
def get_project(project_name, active=True, inactive=False, fields=None):
|
||||
# Skip if both are disabled
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = project_fields_v3_to_v4(fields, con)
|
||||
return convert_v4_project_to_v3(
|
||||
con.get_project(project_name, fields=fields)
|
||||
|
|
@ -66,7 +65,7 @@ def _get_subsets(
|
|||
fields=None
|
||||
):
|
||||
# Convert fields and add minimum required fields
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = subset_fields_v3_to_v4(fields, con)
|
||||
if fields is not None:
|
||||
for key in (
|
||||
|
|
@ -81,8 +80,8 @@ def _get_subsets(
|
|||
|
||||
for subset in con.get_products(
|
||||
project_name,
|
||||
subset_ids,
|
||||
subset_names,
|
||||
product_ids=subset_ids,
|
||||
product_names=subset_names,
|
||||
folder_ids=folder_ids,
|
||||
names_by_folder_ids=names_by_folder_ids,
|
||||
active=active,
|
||||
|
|
@ -102,7 +101,7 @@ def _get_versions(
|
|||
active=None,
|
||||
fields=None
|
||||
):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
|
||||
fields = version_fields_v3_to_v4(fields, con)
|
||||
|
||||
|
|
@ -114,23 +113,23 @@ def _get_versions(
|
|||
|
||||
queried_versions = con.get_versions(
|
||||
project_name,
|
||||
version_ids,
|
||||
subset_ids,
|
||||
versions,
|
||||
hero,
|
||||
standard,
|
||||
latest,
|
||||
version_ids=version_ids,
|
||||
product_ids=subset_ids,
|
||||
versions=versions,
|
||||
hero=hero,
|
||||
standard=standard,
|
||||
latest=latest,
|
||||
active=active,
|
||||
fields=fields
|
||||
)
|
||||
|
||||
versions = []
|
||||
version_entities = []
|
||||
hero_versions = []
|
||||
for version in queried_versions:
|
||||
if version["version"] < 0:
|
||||
hero_versions.append(version)
|
||||
else:
|
||||
versions.append(convert_v4_version_to_v3(version))
|
||||
version_entities.append(convert_v4_version_to_v3(version))
|
||||
|
||||
if hero_versions:
|
||||
subset_ids = set()
|
||||
|
|
@ -160,9 +159,9 @@ def _get_versions(
|
|||
break
|
||||
conv_hero = convert_v4_version_to_v3(hero_version)
|
||||
conv_hero["version_id"] = version_id
|
||||
versions.append(conv_hero)
|
||||
version_entities.append(conv_hero)
|
||||
|
||||
return versions
|
||||
return version_entities
|
||||
|
||||
|
||||
def get_asset_by_id(project_name, asset_id, fields=None):
|
||||
|
|
@ -183,6 +182,19 @@ def get_asset_by_name(project_name, asset_name, fields=None):
|
|||
return None
|
||||
|
||||
|
||||
def _folders_query(project_name, con, fields, **kwargs):
|
||||
if fields is None or "tasks" in fields:
|
||||
folders = get_folders_with_tasks(
|
||||
con, project_name, fields=fields, **kwargs
|
||||
)
|
||||
|
||||
else:
|
||||
folders = con.get_folders(project_name, fields=fields, **kwargs)
|
||||
|
||||
for folder in folders:
|
||||
yield folder
|
||||
|
||||
|
||||
def get_assets(
|
||||
project_name,
|
||||
asset_ids=None,
|
||||
|
|
@ -198,24 +210,43 @@ def get_assets(
|
|||
if archived:
|
||||
active = None
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = folder_fields_v3_to_v4(fields, con)
|
||||
kwargs = dict(
|
||||
folder_ids=asset_ids,
|
||||
folder_names=asset_names,
|
||||
parent_ids=parent_ids,
|
||||
active=active,
|
||||
fields=fields
|
||||
)
|
||||
if not asset_names:
|
||||
for folder in _folders_query(project_name, con, fields, **kwargs):
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
return
|
||||
|
||||
if fields is None or "tasks" in fields:
|
||||
folders = get_folders_with_tasks(con, project_name, **kwargs)
|
||||
new_asset_names = set()
|
||||
folder_paths = set()
|
||||
for name in asset_names:
|
||||
if "/" in name:
|
||||
folder_paths.add(name)
|
||||
else:
|
||||
new_asset_names.add(name)
|
||||
|
||||
else:
|
||||
folders = con.get_folders(project_name, **kwargs)
|
||||
yielded_ids = set()
|
||||
if folder_paths:
|
||||
for folder in _folders_query(
|
||||
project_name, con, fields, folder_paths=folder_paths, **kwargs
|
||||
):
|
||||
yielded_ids.add(folder["id"])
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
|
||||
for folder in folders:
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
if not new_asset_names:
|
||||
return
|
||||
|
||||
for folder in _folders_query(
|
||||
project_name, con, fields, folder_names=new_asset_names, **kwargs
|
||||
):
|
||||
if folder["id"] not in yielded_ids:
|
||||
yielded_ids.add(folder["id"])
|
||||
yield convert_v4_folder_to_v3(folder, project_name)
|
||||
|
||||
|
||||
def get_archived_assets(
|
||||
|
|
@ -236,7 +267,7 @@ def get_archived_assets(
|
|||
|
||||
|
||||
def get_asset_ids_with_subsets(project_name, asset_ids=None):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.get_folder_ids_with_products(project_name, asset_ids)
|
||||
|
||||
|
||||
|
|
@ -282,7 +313,7 @@ def get_subsets(
|
|||
|
||||
|
||||
def get_subset_families(project_name, subset_ids=None):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.get_product_type_names(project_name, subset_ids)
|
||||
|
||||
|
||||
|
|
@ -430,7 +461,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
|
|||
if not version_id:
|
||||
return []
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
version_links = con.get_version_links(
|
||||
project_name, version_id, link_direction="out")
|
||||
|
||||
|
|
@ -446,7 +477,7 @@ def get_output_link_versions(project_name, version_id, fields=None):
|
|||
|
||||
|
||||
def version_is_latest(project_name, version_id):
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.version_is_latest(project_name, version_id)
|
||||
|
||||
|
||||
|
|
@ -501,18 +532,18 @@ def get_representations(
|
|||
else:
|
||||
active = None
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
fields = representation_fields_v3_to_v4(fields, con)
|
||||
if fields and active is not None:
|
||||
fields.add("active")
|
||||
|
||||
representations = con.get_representations(
|
||||
project_name,
|
||||
representation_ids,
|
||||
representation_names,
|
||||
version_ids,
|
||||
names_by_version_ids,
|
||||
active,
|
||||
representation_ids=representation_ids,
|
||||
representation_names=representation_names,
|
||||
version_ids=version_ids,
|
||||
names_by_version_ids=names_by_version_ids,
|
||||
active=active,
|
||||
fields=fields
|
||||
)
|
||||
for representation in representations:
|
||||
|
|
@ -535,7 +566,7 @@ def get_representations_parents(project_name, representations):
|
|||
repre["_id"]
|
||||
for repre in representations
|
||||
}
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
parents_by_repre_id = con.get_representations_parents(project_name,
|
||||
repre_ids)
|
||||
folder_ids = set()
|
||||
|
|
@ -677,7 +708,7 @@ def get_workfile_info(
|
|||
if not asset_id or not task_name or not filename:
|
||||
return None
|
||||
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
task = con.get_task_by_name(
|
||||
project_name, asset_id, task_name, fields=["id", "name", "folderId"]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import ayon_api
|
||||
from ayon_api import get_folder_links, get_versions_links
|
||||
|
||||
from .utils import get_ayon_server_api_connection
|
||||
from .entities import get_assets, get_representation_by_id
|
||||
|
||||
|
||||
|
|
@ -28,7 +26,8 @@ def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None):
|
|||
if not asset_id:
|
||||
asset_id = asset_doc["_id"]
|
||||
|
||||
links = get_folder_links(project_name, asset_id, link_direction="in")
|
||||
con = get_ayon_server_api_connection()
|
||||
links = con.get_folder_links(project_name, asset_id, link_direction="in")
|
||||
return [
|
||||
link["entityId"]
|
||||
for link in links
|
||||
|
|
@ -115,6 +114,7 @@ def get_linked_representation_id(
|
|||
if link_type:
|
||||
link_types = [link_type]
|
||||
|
||||
con = get_ayon_server_api_connection()
|
||||
# Store already found version ids to avoid recursion, and also to store
|
||||
# output -> Don't forget to remove 'version_id' at the end!!!
|
||||
linked_version_ids = {version_id}
|
||||
|
|
@ -124,7 +124,7 @@ def get_linked_representation_id(
|
|||
if not versions_to_check:
|
||||
break
|
||||
|
||||
links = get_versions_links(
|
||||
links = con.get_versions_links(
|
||||
project_name,
|
||||
versions_to_check,
|
||||
link_types=link_types,
|
||||
|
|
@ -145,8 +145,8 @@ def get_linked_representation_id(
|
|||
linked_version_ids.remove(version_id)
|
||||
if not linked_version_ids:
|
||||
return []
|
||||
|
||||
representations = ayon_api.get_representations(
|
||||
con = get_ayon_server_api_connection()
|
||||
representations = con.get_representations(
|
||||
project_name,
|
||||
version_ids=linked_version_ids,
|
||||
fields=["id"])
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import collections
|
||||
import json
|
||||
|
||||
import six
|
||||
from ayon_api.graphql import GraphQlQuery, FIELD_VALUE, fields_to_dict
|
||||
|
||||
from .constants import DEFAULT_FOLDER_FIELDS
|
||||
|
|
@ -84,12 +87,12 @@ def get_folders_with_tasks(
|
|||
for folder. All possible folder fields are returned if 'None'
|
||||
is passed.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Queried folder entities.
|
||||
Yields:
|
||||
Dict[str, Any]: Queried folder entities.
|
||||
"""
|
||||
|
||||
if not project_name:
|
||||
return []
|
||||
return
|
||||
|
||||
filters = {
|
||||
"projectName": project_name
|
||||
|
|
@ -97,25 +100,25 @@ def get_folders_with_tasks(
|
|||
if folder_ids is not None:
|
||||
folder_ids = set(folder_ids)
|
||||
if not folder_ids:
|
||||
return []
|
||||
return
|
||||
filters["folderIds"] = list(folder_ids)
|
||||
|
||||
if folder_paths is not None:
|
||||
folder_paths = set(folder_paths)
|
||||
if not folder_paths:
|
||||
return []
|
||||
return
|
||||
filters["folderPaths"] = list(folder_paths)
|
||||
|
||||
if folder_names is not None:
|
||||
folder_names = set(folder_names)
|
||||
if not folder_names:
|
||||
return []
|
||||
return
|
||||
filters["folderNames"] = list(folder_names)
|
||||
|
||||
if parent_ids is not None:
|
||||
parent_ids = set(parent_ids)
|
||||
if not parent_ids:
|
||||
return []
|
||||
return
|
||||
if None in parent_ids:
|
||||
# Replace 'None' with '"root"' which is used during GraphQl
|
||||
# query for parent ids filter for folders without folder
|
||||
|
|
@ -147,10 +150,10 @@ def get_folders_with_tasks(
|
|||
|
||||
parsed_data = query.query(con)
|
||||
folders = parsed_data["project"]["folders"]
|
||||
if active is None:
|
||||
return folders
|
||||
return [
|
||||
folder
|
||||
for folder in folders
|
||||
if folder["active"] is active
|
||||
]
|
||||
for folder in folders:
|
||||
if active is not None and folder["active"] is not active:
|
||||
continue
|
||||
folder_data = folder.get("data")
|
||||
if isinstance(folder_data, six.string_types):
|
||||
folder["data"] = json.loads(folder_data)
|
||||
yield folder
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import uuid
|
|||
import datetime
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from ayon_api import get_server_api_connection
|
||||
|
||||
from openpype.client.operations_base import (
|
||||
REMOVED_VALUE,
|
||||
|
|
@ -41,7 +40,7 @@ from .conversion_utils import (
|
|||
convert_update_representation_to_v4,
|
||||
convert_update_workfile_info_to_v4,
|
||||
)
|
||||
from .utils import create_entity_id
|
||||
from .utils import create_entity_id, get_ayon_server_api_connection
|
||||
|
||||
|
||||
def _create_or_convert_to_id(entity_id=None):
|
||||
|
|
@ -680,7 +679,7 @@ class OperationsSession(BaseOperationsSession):
|
|||
def __init__(self, con=None, *args, **kwargs):
|
||||
super(OperationsSession, self).__init__(*args, **kwargs)
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
self._con = con
|
||||
self._project_cache = {}
|
||||
self._nested_operations = collections.defaultdict(list)
|
||||
|
|
@ -858,7 +857,7 @@ def create_project(
|
|||
"""
|
||||
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
|
||||
return con.create_project(
|
||||
project_name,
|
||||
|
|
@ -870,12 +869,12 @@ def create_project(
|
|||
|
||||
def delete_project(project_name, con=None):
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
|
||||
return con.delete_project(project_name)
|
||||
|
||||
|
||||
def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None):
|
||||
if con is None:
|
||||
con = get_server_api_connection()
|
||||
con = get_ayon_server_api_connection()
|
||||
return con.create_thumbnail(project_name, src_filepath, thumbnail_id)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,33 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
import ayon_api
|
||||
|
||||
from openpype.client.operations_base import REMOVED_VALUE
|
||||
|
||||
|
||||
class _GlobalCache:
|
||||
initialized = False
|
||||
|
||||
|
||||
def get_ayon_server_api_connection():
|
||||
if _GlobalCache.initialized:
|
||||
con = ayon_api.get_server_api_connection()
|
||||
else:
|
||||
from openpype.lib.local_settings import get_local_site_id
|
||||
|
||||
_GlobalCache.initialized = True
|
||||
site_id = get_local_site_id()
|
||||
version = os.getenv("AYON_VERSION")
|
||||
if ayon_api.is_connection_created():
|
||||
con = ayon_api.get_server_api_connection()
|
||||
con.set_site_id(site_id)
|
||||
con.set_client_version(version)
|
||||
else:
|
||||
con = ayon_api.create_connection(site_id, version)
|
||||
return con
|
||||
|
||||
|
||||
def create_entity_id():
|
||||
return uuid.uuid1().hex
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
"tvpaint",
|
||||
"substancepainter",
|
||||
"aftereffects",
|
||||
"wrap"
|
||||
}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ class CopyTemplateWorkfile(PreLaunchHook):
|
|||
|
||||
# Before `AddLastWorkfileToLaunchArgs`
|
||||
order = 0
|
||||
app_groups = {"blender", "photoshop", "tvpaint", "aftereffects"}
|
||||
app_groups = {"blender", "photoshop", "tvpaint", "aftereffects",
|
||||
"wrap"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ class HostBase(object):
|
|||
if project_name:
|
||||
items.append(project_name)
|
||||
if asset_name:
|
||||
items.append(asset_name)
|
||||
items.append(asset_name.lstrip("/"))
|
||||
if task_name:
|
||||
items.append(task_name)
|
||||
if items:
|
||||
|
|
|
|||
|
|
@ -74,11 +74,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
|||
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
log.info(PUBLISH_PATH)
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
register_event_callback("application.launched", application_launch)
|
||||
|
||||
|
|
@ -186,11 +181,6 @@ def application_launch():
|
|||
check_inventory()
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle layer visibility on instance toggles."""
|
||||
instance[0].Visible = new_value
|
||||
|
||||
|
||||
def ls():
|
||||
"""Yields containers from active AfterEffects document.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,16 +56,15 @@ class RenderCreator(Creator):
|
|||
use_composition_name = (pre_create_data.get("use_composition_name") or
|
||||
len(comps) > 1)
|
||||
for comp in comps:
|
||||
composition_name = re.sub(
|
||||
"[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
|
||||
"",
|
||||
comp.name
|
||||
)
|
||||
if use_composition_name:
|
||||
if "{composition}" not in subset_name_from_ui.lower():
|
||||
subset_name_from_ui += "{Composition}"
|
||||
|
||||
composition_name = re.sub(
|
||||
"[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
|
||||
"",
|
||||
comp.name
|
||||
)
|
||||
|
||||
dynamic_fill = prepare_template_data({"composition":
|
||||
composition_name})
|
||||
subset_name = subset_name_from_ui.format(**dynamic_fill)
|
||||
|
|
@ -81,6 +80,8 @@ class RenderCreator(Creator):
|
|||
inst.subset_name))
|
||||
|
||||
data["members"] = [comp.id]
|
||||
data["orig_comp_name"] = composition_name
|
||||
|
||||
new_instance = CreatedInstance(self.family, subset_name, data,
|
||||
self)
|
||||
if "farm" in pre_create_data:
|
||||
|
|
@ -88,7 +89,7 @@ class RenderCreator(Creator):
|
|||
new_instance.creator_attributes["farm"] = use_farm
|
||||
|
||||
review = pre_create_data["mark_for_review"]
|
||||
new_instance.creator_attributes["mark_for_review"] = review
|
||||
new_instance. creator_attributes["mark_for_review"] = review
|
||||
|
||||
api.get_stub().imprint(new_instance.id,
|
||||
new_instance.data_to_store())
|
||||
|
|
@ -150,16 +151,18 @@ class RenderCreator(Creator):
|
|||
subset_change.new_value)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
"""Removes metadata and renames to original comp name if available."""
|
||||
for instance in instances:
|
||||
self._remove_instance_from_context(instance)
|
||||
self.host.remove_instance(instance)
|
||||
|
||||
subset = instance.data["subset"]
|
||||
comp_id = instance.data["members"][0]
|
||||
comp = api.get_stub().get_item(comp_id)
|
||||
orig_comp_name = instance.data.get("orig_comp_name")
|
||||
if comp:
|
||||
new_comp_name = comp.name.replace(subset, '')
|
||||
if not new_comp_name:
|
||||
if orig_comp_name:
|
||||
new_comp_name = orig_comp_name
|
||||
else:
|
||||
new_comp_name = "dummyCompName"
|
||||
api.get_stub().rename_item(comp_id,
|
||||
new_comp_name)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from openpype import AYON_SERVER_ENABLED
|
||||
import openpype.hosts.aftereffects.api as api
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
|
|
@ -43,6 +44,14 @@ class AEWorkfileCreator(AutoCreator):
|
|||
task_name = context.get_current_task_name()
|
||||
host_name = context.host_name
|
||||
|
||||
existing_asset_name = None
|
||||
if existing_instance is not None:
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_asset_name = existing_instance.get("folderPath")
|
||||
|
||||
if existing_asset_name is None:
|
||||
existing_asset_name = existing_instance["asset"]
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
|
|
@ -50,10 +59,13 @@ class AEWorkfileCreator(AutoCreator):
|
|||
project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name, None
|
||||
|
|
@ -68,7 +80,7 @@ class AEWorkfileCreator(AutoCreator):
|
|||
new_instance.data_to_store())
|
||||
|
||||
elif (
|
||||
existing_instance["asset"] != asset_name
|
||||
existing_asset_name != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
|
@ -76,6 +88,10 @@ class AEWorkfileCreator(AutoCreator):
|
|||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
existing_instance["asset"] = asset_name
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
||||
|
||||
|
|
@ -48,9 +50,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
asset_entity = context.data["assetEntity"]
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_entity)
|
||||
|
||||
instance_data = {
|
||||
"active": True,
|
||||
"asset": asset_entity["name"],
|
||||
"asset": asset_name,
|
||||
"task": task,
|
||||
"frameStart": context.data['frameStart'],
|
||||
"frameEnd": context.data['frameEnd'],
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from .pipeline import (
|
|||
ls,
|
||||
publish,
|
||||
containerise,
|
||||
BlenderHost,
|
||||
)
|
||||
|
||||
from .plugin import (
|
||||
|
|
@ -47,6 +48,7 @@ __all__ = [
|
|||
"ls",
|
||||
"publish",
|
||||
"containerise",
|
||||
"BlenderHost",
|
||||
|
||||
"Creator",
|
||||
"Loader",
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ def imprint(node: bpy.types.bpy_struct_meta_idprop, data: Dict):
|
|||
# Support values evaluated at imprint
|
||||
value = value()
|
||||
|
||||
if not isinstance(value, (int, float, bool, str, list)):
|
||||
if not isinstance(value, (int, float, bool, str, list, dict)):
|
||||
raise TypeError(f"Unsupported type: {type(value)}")
|
||||
|
||||
imprint_data[key] = value
|
||||
|
|
@ -266,9 +266,59 @@ def read(node: bpy.types.bpy_struct_meta_idprop):
|
|||
return data
|
||||
|
||||
|
||||
def get_selection() -> List[bpy.types.Object]:
|
||||
"""Return the selected objects from the current scene."""
|
||||
return [obj for obj in bpy.context.scene.objects if obj.select_get()]
|
||||
def get_selected_collections():
|
||||
"""
|
||||
Returns a list of the currently selected collections in the outliner.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the outliner cannot be found in the main Blender
|
||||
window.
|
||||
|
||||
Returns:
|
||||
list: A list of `bpy.types.Collection` objects that are currently
|
||||
selected in the outliner.
|
||||
"""
|
||||
window = bpy.context.window or bpy.context.window_manager.windows[0]
|
||||
|
||||
try:
|
||||
area = next(
|
||||
area for area in window.screen.areas
|
||||
if area.type == 'OUTLINER')
|
||||
region = next(
|
||||
region for region in area.regions
|
||||
if region.type == 'WINDOW')
|
||||
except StopIteration as e:
|
||||
raise RuntimeError("Could not find outliner. An outliner space "
|
||||
"must be in the main Blender window.") from e
|
||||
|
||||
with bpy.context.temp_override(
|
||||
window=window,
|
||||
area=area,
|
||||
region=region,
|
||||
screen=window.screen
|
||||
):
|
||||
ids = bpy.context.selected_ids
|
||||
|
||||
return [id for id in ids if isinstance(id, bpy.types.Collection)]
|
||||
|
||||
|
||||
def get_selection(include_collections: bool = False) -> List[bpy.types.Object]:
|
||||
"""
|
||||
Returns a list of selected objects in the current Blender scene.
|
||||
|
||||
Args:
|
||||
include_collections (bool, optional): Whether to include selected
|
||||
collections in the result. Defaults to False.
|
||||
|
||||
Returns:
|
||||
List[bpy.types.Object]: A list of selected objects.
|
||||
"""
|
||||
selection = [obj for obj in bpy.context.scene.objects if obj.select_get()]
|
||||
|
||||
if include_collections:
|
||||
selection.extend(get_selected_collections())
|
||||
|
||||
return selection
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ PREVIEW_COLLECTIONS: Dict = dict()
|
|||
TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1
|
||||
|
||||
|
||||
def execute_function_in_main_thread(f):
|
||||
"""Decorator to move a function call into main thread items"""
|
||||
def wrapper(*args, **kwargs):
|
||||
mti = MainThreadItem(f, *args, **kwargs)
|
||||
execute_in_main_thread(mti)
|
||||
return wrapper
|
||||
|
||||
|
||||
class BlenderApplication(QtWidgets.QApplication):
|
||||
_instance = None
|
||||
blender_windows = {}
|
||||
|
|
@ -238,8 +246,24 @@ class LaunchQtApp(bpy.types.Operator):
|
|||
|
||||
self.before_window_show()
|
||||
|
||||
def pull_to_front(window):
|
||||
"""Pull window forward to screen.
|
||||
|
||||
If Window is minimized this will un-minimize, then it can be raised
|
||||
and activated to the front.
|
||||
"""
|
||||
window.setWindowState(
|
||||
(window.windowState() & ~QtCore.Qt.WindowMinimized) |
|
||||
QtCore.Qt.WindowActive
|
||||
)
|
||||
window.raise_()
|
||||
window.activateWindow()
|
||||
|
||||
if isinstance(self._window, ModuleType):
|
||||
self._window.show()
|
||||
pull_to_front(self._window)
|
||||
|
||||
# Pull window to the front
|
||||
window = None
|
||||
if hasattr(self._window, "window"):
|
||||
window = self._window.window
|
||||
|
|
@ -254,6 +278,7 @@ class LaunchQtApp(bpy.types.Operator):
|
|||
on_top_flags = origin_flags | QtCore.Qt.WindowStaysOnTopHint
|
||||
self._window.setWindowFlags(on_top_flags)
|
||||
self._window.show()
|
||||
pull_to_front(self._window)
|
||||
|
||||
# if on_top_flags != origin_flags:
|
||||
# self._window.setWindowFlags(origin_flags)
|
||||
|
|
@ -275,6 +300,10 @@ class LaunchCreator(LaunchQtApp):
|
|||
def before_window_show(self):
|
||||
self._window.refresh()
|
||||
|
||||
def execute(self, context):
|
||||
host_tools.show_publisher(tab="create")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class LaunchLoader(LaunchQtApp):
|
||||
"""Launch Avalon Loader."""
|
||||
|
|
@ -299,7 +328,7 @@ class LaunchPublisher(LaunchQtApp):
|
|||
bl_label = "Publish..."
|
||||
|
||||
def execute(self, context):
|
||||
host_tools.show_publish()
|
||||
host_tools.show_publisher(tab="publish")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
|
@ -416,7 +445,6 @@ class TOPBAR_MT_avalon(bpy.types.Menu):
|
|||
layout.operator(SetResolution.bl_idname, text="Set Resolution")
|
||||
layout.separator()
|
||||
layout.operator(LaunchWorkFiles.bl_idname, text="Work Files...")
|
||||
# TODO (jasper): maybe add 'Reload Pipeline'
|
||||
|
||||
|
||||
def draw_avalon_menu(self, context):
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ from . import ops
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.host import (
|
||||
HostBase,
|
||||
IWorkfileHost,
|
||||
IPublishHost,
|
||||
ILoadHost
|
||||
)
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
schema,
|
||||
|
|
@ -29,6 +35,14 @@ from openpype.lib import (
|
|||
)
|
||||
import openpype.hosts.blender
|
||||
from openpype.settings import get_project_settings
|
||||
from .workio import (
|
||||
open_file,
|
||||
save_file,
|
||||
current_file,
|
||||
has_unsaved_changes,
|
||||
file_extensions,
|
||||
work_root,
|
||||
)
|
||||
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))
|
||||
|
|
@ -47,6 +61,101 @@ IS_HEADLESS = bpy.app.background
|
|||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class BlenderHost(HostBase, IWorkfileHost, IPublishHost, ILoadHost):
|
||||
name = "blender"
|
||||
|
||||
def install(self):
|
||||
"""Override install method from HostBase.
|
||||
Install Blender host functionality."""
|
||||
install()
|
||||
|
||||
def get_containers(self) -> Iterator:
|
||||
"""List containers from active Blender scene."""
|
||||
return ls()
|
||||
|
||||
def get_workfile_extensions(self) -> List[str]:
|
||||
"""Override get_workfile_extensions method from IWorkfileHost.
|
||||
Get workfile possible extensions.
|
||||
|
||||
Returns:
|
||||
List[str]: Workfile extensions.
|
||||
"""
|
||||
return file_extensions()
|
||||
|
||||
def save_workfile(self, dst_path: str = None):
|
||||
"""Override save_workfile method from IWorkfileHost.
|
||||
Save currently opened workfile.
|
||||
|
||||
Args:
|
||||
dst_path (str): Where the current scene should be saved. Or use
|
||||
current path if `None` is passed.
|
||||
"""
|
||||
save_file(dst_path if dst_path else bpy.data.filepath)
|
||||
|
||||
def open_workfile(self, filepath: str):
|
||||
"""Override open_workfile method from IWorkfileHost.
|
||||
Open workfile at specified filepath in the host.
|
||||
|
||||
Args:
|
||||
filepath (str): Path to workfile.
|
||||
"""
|
||||
open_file(filepath)
|
||||
|
||||
def get_current_workfile(self) -> str:
|
||||
"""Override get_current_workfile method from IWorkfileHost.
|
||||
Retrieve currently opened workfile path.
|
||||
|
||||
Returns:
|
||||
str: Path to currently opened workfile.
|
||||
"""
|
||||
return current_file()
|
||||
|
||||
def workfile_has_unsaved_changes(self) -> bool:
|
||||
"""Override wokfile_has_unsaved_changes method from IWorkfileHost.
|
||||
Returns True if opened workfile has no unsaved changes.
|
||||
|
||||
Returns:
|
||||
bool: True if scene is saved and False if it has unsaved
|
||||
modifications.
|
||||
"""
|
||||
return has_unsaved_changes()
|
||||
|
||||
def work_root(self, session) -> str:
|
||||
"""Override work_root method from IWorkfileHost.
|
||||
Modify workdir per host.
|
||||
|
||||
Args:
|
||||
session (dict): Session context data.
|
||||
|
||||
Returns:
|
||||
str: Path to new workdir.
|
||||
"""
|
||||
return work_root(session)
|
||||
|
||||
def get_context_data(self) -> dict:
|
||||
"""Override abstract method from IPublishHost.
|
||||
Get global data related to creation-publishing from workfile.
|
||||
|
||||
Returns:
|
||||
dict: Context data stored using 'update_context_data'.
|
||||
"""
|
||||
property = bpy.context.scene.get(AVALON_PROPERTY)
|
||||
if property:
|
||||
return property.to_dict()
|
||||
return {}
|
||||
|
||||
def update_context_data(self, data: dict, changes: dict):
|
||||
"""Override abstract method from IPublishHost.
|
||||
Store global context data to workfile.
|
||||
|
||||
Args:
|
||||
data (dict): New data as are.
|
||||
changes (dict): Only data that has been changed. Each value has
|
||||
tuple with '(<old>, <new>)' value.
|
||||
"""
|
||||
bpy.context.scene[AVALON_PROPERTY] = data
|
||||
|
||||
|
||||
def pype_excepthook_handler(*args):
|
||||
traceback.print_exception(*args)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,34 @@
|
|||
"""Shared functionality for pipeline plugins for Blender."""
|
||||
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
Creator,
|
||||
CreatedInstance,
|
||||
LoaderPlugin,
|
||||
)
|
||||
from .pipeline import AVALON_CONTAINERS
|
||||
from openpype.lib import BoolDef
|
||||
|
||||
from .pipeline import (
|
||||
AVALON_CONTAINERS,
|
||||
AVALON_INSTANCES,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
from .ops import (
|
||||
MainThreadItem,
|
||||
execute_in_main_thread
|
||||
)
|
||||
from .lib import (
|
||||
imprint,
|
||||
get_selection
|
||||
)
|
||||
from .lib import imprint
|
||||
|
||||
VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
|
||||
|
||||
|
||||
def asset_name(
|
||||
def prepare_scene_name(
|
||||
asset: str, subset: str, namespace: Optional[str] = None
|
||||
) -> str:
|
||||
"""Return a consistent name for an asset."""
|
||||
|
|
@ -40,9 +46,16 @@ def get_unique_number(
|
|||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
return "01"
|
||||
asset_groups = avalon_container.all_objects
|
||||
|
||||
container_names = [c.name for c in asset_groups if c.type == 'EMPTY']
|
||||
# Check the names of both object and collection containers
|
||||
obj_asset_groups = avalon_container.objects
|
||||
obj_group_names = {
|
||||
c.name for c in obj_asset_groups
|
||||
if c.type == 'EMPTY' and c.get(AVALON_PROPERTY)}
|
||||
coll_asset_groups = avalon_container.children
|
||||
coll_group_names = {
|
||||
c.name for c in coll_asset_groups
|
||||
if c.get(AVALON_PROPERTY)}
|
||||
container_names = obj_group_names.union(coll_group_names)
|
||||
count = 1
|
||||
name = f"{asset}_{count:0>2}_{subset}"
|
||||
while name in container_names:
|
||||
|
|
@ -134,20 +147,224 @@ def deselect_all():
|
|||
bpy.context.view_layer.objects.active = active
|
||||
|
||||
|
||||
class Creator(LegacyCreator):
|
||||
"""Base class for Creator plug-ins."""
|
||||
class BaseCreator(Creator):
|
||||
"""Base class for Blender Creator plug-ins."""
|
||||
defaults = ['Main']
|
||||
|
||||
def process(self):
|
||||
collection = bpy.data.collections.new(name=self.data["subset"])
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
imprint(collection, self.data)
|
||||
create_as_asset_group = False
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in get_selection():
|
||||
collection.objects.link(obj)
|
||||
@staticmethod
|
||||
def cache_subsets(shared_data):
|
||||
"""Cache instances for Creators shared data.
|
||||
|
||||
return collection
|
||||
Create `blender_cached_subsets` key when needed in shared data and
|
||||
fill it with all collected instances from the scene under its
|
||||
respective creator identifiers.
|
||||
|
||||
If legacy instances are detected in the scene, create
|
||||
`blender_cached_legacy_subsets` key and fill it with
|
||||
all legacy subsets from this family as a value. # key or value?
|
||||
|
||||
Args:
|
||||
shared_data(Dict[str, Any]): Shared data.
|
||||
|
||||
Return:
|
||||
Dict[str, Any]: Shared data with cached subsets.
|
||||
"""
|
||||
if not shared_data.get('blender_cached_subsets'):
|
||||
cache = {}
|
||||
cache_legacy = {}
|
||||
|
||||
avalon_instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
avalon_instance_objs = (
|
||||
avalon_instances.objects if avalon_instances else []
|
||||
)
|
||||
|
||||
for obj_or_col in itertools.chain(
|
||||
avalon_instance_objs,
|
||||
bpy.data.collections
|
||||
):
|
||||
avalon_prop = obj_or_col.get(AVALON_PROPERTY, {})
|
||||
if not avalon_prop:
|
||||
continue
|
||||
|
||||
if avalon_prop.get('id') != 'pyblish.avalon.instance':
|
||||
continue
|
||||
|
||||
creator_id = avalon_prop.get('creator_identifier')
|
||||
if creator_id:
|
||||
# Creator instance
|
||||
cache.setdefault(creator_id, []).append(obj_or_col)
|
||||
else:
|
||||
family = avalon_prop.get('family')
|
||||
if family:
|
||||
# Legacy creator instance
|
||||
cache_legacy.setdefault(family, []).append(obj_or_col)
|
||||
|
||||
shared_data["blender_cached_subsets"] = cache
|
||||
shared_data["blender_cached_legacy_subsets"] = cache_legacy
|
||||
|
||||
return shared_data
|
||||
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
"""Override abstract method from Creator.
|
||||
Create new instance and store it.
|
||||
|
||||
Args:
|
||||
subset_name(str): Subset name of created instance.
|
||||
instance_data(dict): Instance base data.
|
||||
pre_create_data(dict): Data based on pre creation attributes.
|
||||
Those may affect how creator works.
|
||||
"""
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create asset group
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = instance_data["folderPath"]
|
||||
else:
|
||||
asset_name = instance_data["asset"]
|
||||
|
||||
name = prepare_scene_name(asset_name, subset_name)
|
||||
if self.create_as_asset_group:
|
||||
# Create instance as empty
|
||||
instance_node = bpy.data.objects.new(name=name, object_data=None)
|
||||
instance_node.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(instance_node)
|
||||
else:
|
||||
# Create instance collection
|
||||
instance_node = bpy.data.collections.new(name=name)
|
||||
instances.children.link(instance_node)
|
||||
|
||||
self.set_instance_data(subset_name, instance_data)
|
||||
|
||||
instance = CreatedInstance(
|
||||
self.family, subset_name, instance_data, self
|
||||
)
|
||||
instance.transient_data["instance_node"] = instance_node
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
imprint(instance_node, instance_data)
|
||||
|
||||
return instance_node
|
||||
|
||||
def collect_instances(self):
|
||||
"""Override abstract method from BaseCreator.
|
||||
Collect existing instances related to this creator plugin."""
|
||||
|
||||
# Cache subsets in shared data
|
||||
self.cache_subsets(self.collection_shared_data)
|
||||
|
||||
# Get cached subsets
|
||||
cached_subsets = self.collection_shared_data.get(
|
||||
"blender_cached_subsets"
|
||||
)
|
||||
if not cached_subsets:
|
||||
return
|
||||
|
||||
# Process only instances that were created by this creator
|
||||
for instance_node in cached_subsets.get(self.identifier, []):
|
||||
property = instance_node.get(AVALON_PROPERTY)
|
||||
# Create instance object from existing data
|
||||
instance = CreatedInstance.from_existing(
|
||||
instance_data=property.to_dict(),
|
||||
creator=self
|
||||
)
|
||||
instance.transient_data["instance_node"] = instance_node
|
||||
|
||||
# Add instance to create context
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
"""Override abstract method from BaseCreator.
|
||||
Store changes of existing instances so they can be recollected.
|
||||
|
||||
Args:
|
||||
update_list(List[UpdateData]): Changed instances
|
||||
and their changes, as a list of tuples.
|
||||
"""
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name_key = "folderPath"
|
||||
else:
|
||||
asset_name_key = "asset"
|
||||
|
||||
for created_instance, changes in update_list:
|
||||
data = created_instance.data_to_store()
|
||||
node = created_instance.transient_data["instance_node"]
|
||||
if not node:
|
||||
# We can't update if we don't know the node
|
||||
self.log.error(
|
||||
f"Unable to update instance {created_instance} "
|
||||
f"without instance node."
|
||||
)
|
||||
return
|
||||
|
||||
# Rename the instance node in the scene if subset or asset changed
|
||||
if (
|
||||
"subset" in changes.changed_keys
|
||||
or asset_name_key in changes.changed_keys
|
||||
):
|
||||
asset_name = data[asset_name_key]
|
||||
name = prepare_scene_name(
|
||||
asset=asset_name, subset=data["subset"]
|
||||
)
|
||||
node.name = name
|
||||
|
||||
imprint(node, data)
|
||||
|
||||
def remove_instances(self, instances: List[CreatedInstance]):
|
||||
|
||||
for instance in instances:
|
||||
node = instance.transient_data["instance_node"]
|
||||
|
||||
if isinstance(node, bpy.types.Collection):
|
||||
for children in node.children_recursive:
|
||||
if isinstance(children, bpy.types.Collection):
|
||||
bpy.data.collections.remove(children)
|
||||
else:
|
||||
bpy.data.objects.remove(children)
|
||||
|
||||
bpy.data.collections.remove(node)
|
||||
elif isinstance(node, bpy.types.Object):
|
||||
bpy.data.objects.remove(node)
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def set_instance_data(
|
||||
self,
|
||||
subset_name: str,
|
||||
instance_data: dict
|
||||
):
|
||||
"""Fill instance data with required items.
|
||||
|
||||
Args:
|
||||
subset_name(str): Subset name of created instance.
|
||||
instance_data(dict): Instance base data.
|
||||
instance_node(bpy.types.ID): Instance node in blender scene.
|
||||
"""
|
||||
if not instance_data:
|
||||
instance_data = {}
|
||||
|
||||
instance_data.update(
|
||||
{
|
||||
"id": "pyblish.avalon.instance",
|
||||
"creator_identifier": self.identifier,
|
||||
"subset": subset_name,
|
||||
}
|
||||
)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
return [
|
||||
BoolDef("use_selection",
|
||||
label="Use selection",
|
||||
default=True)
|
||||
]
|
||||
|
||||
|
||||
class Loader(LoaderPlugin):
|
||||
|
|
@ -241,7 +458,7 @@ class AssetLoader(LoaderPlugin):
|
|||
namespace: Use pre-defined namespace
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
# TODO (jasper): make it possible to add the asset several times by
|
||||
# TODO: make it possible to add the asset several times by
|
||||
# just re-using the collection
|
||||
filepath = self.filepath_from_context(context)
|
||||
assert Path(filepath).exists(), f"{filepath} doesn't exist."
|
||||
|
|
@ -252,7 +469,7 @@ class AssetLoader(LoaderPlugin):
|
|||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
name = name or asset_name(
|
||||
name = name or prepare_scene_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
|
|
@ -281,7 +498,9 @@ class AssetLoader(LoaderPlugin):
|
|||
|
||||
# asset = context["asset"]["name"]
|
||||
# subset = context["subset"]["name"]
|
||||
# instance_name = asset_name(asset, subset, unique_number) + '_CON'
|
||||
# instance_name = prepare_scene_name(
|
||||
# asset, subset, unique_number
|
||||
# ) + '_CON'
|
||||
|
||||
# return self._get_instance_collection(instance_name, nodes)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ def get_render_product(output_path, name, aov_sep):
|
|||
instance (pyblish.api.Instance): The instance to publish.
|
||||
ext (str): The image format to render.
|
||||
"""
|
||||
filepath = os.path.join(output_path, name)
|
||||
filepath = output_path / name.lstrip("/")
|
||||
render_product = f"{filepath}{aov_sep}beauty.####"
|
||||
render_product = render_product.replace("\\", "/")
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer):
|
|||
return []
|
||||
|
||||
output.file_slots.clear()
|
||||
output.base_path = output_path
|
||||
output.base_path = str(output_path)
|
||||
|
||||
aov_file_products = []
|
||||
|
||||
|
|
@ -191,8 +191,9 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer):
|
|||
|
||||
output.file_slots.new(filepath)
|
||||
|
||||
aov_file_products.append(
|
||||
(render_pass.name, os.path.join(output_path, filepath)))
|
||||
filename = str(output_path / filepath.lstrip("/"))
|
||||
|
||||
aov_file_products.append((render_pass.name, filename))
|
||||
|
||||
node_input = output.inputs[-1]
|
||||
|
||||
|
|
@ -214,12 +215,11 @@ def imprint_render_settings(node, data):
|
|||
def prepare_rendering(asset_group):
|
||||
name = asset_group.name
|
||||
|
||||
filepath = bpy.data.filepath
|
||||
filepath = Path(bpy.data.filepath)
|
||||
assert filepath, "Workfile not saved. Please save the file first."
|
||||
|
||||
file_path = os.path.dirname(filepath)
|
||||
file_name = os.path.basename(filepath)
|
||||
file_name, _ = os.path.splitext(file_name)
|
||||
dirpath = filepath.parent
|
||||
file_name = Path(filepath.name).stem
|
||||
|
||||
project = get_current_project_name()
|
||||
settings = get_project_settings(project)
|
||||
|
|
@ -232,7 +232,7 @@ def prepare_rendering(asset_group):
|
|||
set_render_format(ext, multilayer)
|
||||
aov_list, custom_passes = set_render_passes(settings)
|
||||
|
||||
output_path = os.path.join(file_path, render_folder, file_name)
|
||||
output_path = Path.joinpath(dirpath, render_folder, file_name)
|
||||
|
||||
render_product = get_render_product(output_path, name, aov_sep)
|
||||
aov_file_product = set_node_tree(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from openpype.pipeline import install_host
|
||||
from openpype.hosts.blender import api
|
||||
from openpype.hosts.blender.api import BlenderHost
|
||||
|
||||
|
||||
def register():
|
||||
install_host(api)
|
||||
install_host(BlenderHost())
|
||||
|
||||
|
||||
def unregister():
|
||||
|
|
|
|||
78
openpype/hosts/blender/plugins/create/convert_legacy.py
Normal file
78
openpype/hosts/blender/plugins/create/convert_legacy.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Converter for legacy Houdini subsets."""
|
||||
from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin
|
||||
from openpype.hosts.blender.api.lib import imprint
|
||||
|
||||
|
||||
class BlenderLegacyConvertor(SubsetConvertorPlugin):
|
||||
"""Find and convert any legacy subsets in the scene.
|
||||
|
||||
This Converter will find all legacy subsets in the scene and will
|
||||
transform them to the current system. Since the old subsets doesn't
|
||||
retain any information about their original creators, the only mapping
|
||||
we can do is based on their families.
|
||||
|
||||
Its limitation is that you can have multiple creators creating subset
|
||||
of the same family and there is no way to handle it. This code should
|
||||
nevertheless cover all creators that came with OpenPype.
|
||||
|
||||
"""
|
||||
identifier = "io.openpype.creators.blender.legacy"
|
||||
family_to_id = {
|
||||
"action": "io.openpype.creators.blender.action",
|
||||
"camera": "io.openpype.creators.blender.camera",
|
||||
"animation": "io.openpype.creators.blender.animation",
|
||||
"blendScene": "io.openpype.creators.blender.blendscene",
|
||||
"layout": "io.openpype.creators.blender.layout",
|
||||
"model": "io.openpype.creators.blender.model",
|
||||
"pointcache": "io.openpype.creators.blender.pointcache",
|
||||
"render": "io.openpype.creators.blender.render",
|
||||
"review": "io.openpype.creators.blender.review",
|
||||
"rig": "io.openpype.creators.blender.rig",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BlenderLegacyConvertor, self).__init__(*args, **kwargs)
|
||||
self.legacy_subsets = {}
|
||||
|
||||
def find_instances(self):
|
||||
"""Find legacy subsets in the scene.
|
||||
|
||||
Legacy subsets are the ones that doesn't have `creator_identifier`
|
||||
parameter on them.
|
||||
|
||||
This is using cached entries done in
|
||||
:py:meth:`~BaseCreator.cache_subsets()`
|
||||
|
||||
"""
|
||||
self.legacy_subsets = self.collection_shared_data.get(
|
||||
"blender_cached_legacy_subsets")
|
||||
if not self.legacy_subsets:
|
||||
return
|
||||
self.add_convertor_item(
|
||||
"Found {} incompatible subset{}".format(
|
||||
len(self.legacy_subsets),
|
||||
"s" if len(self.legacy_subsets) > 1 else ""
|
||||
)
|
||||
)
|
||||
|
||||
def convert(self):
|
||||
"""Convert all legacy subsets to current.
|
||||
|
||||
It is enough to add `creator_identifier` and `instance_node`.
|
||||
|
||||
"""
|
||||
if not self.legacy_subsets:
|
||||
return
|
||||
|
||||
for family, instance_nodes in self.legacy_subsets.items():
|
||||
if family in self.family_to_id:
|
||||
for instance_node in instance_nodes:
|
||||
creator_identifier = self.family_to_id[family]
|
||||
self.log.info(
|
||||
"Converting {} to {}".format(instance_node.name,
|
||||
creator_identifier)
|
||||
)
|
||||
imprint(instance_node, data={
|
||||
"creator_identifier": creator_identifier
|
||||
})
|
||||
|
|
@ -2,30 +2,29 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from openpype.hosts.blender.api import lib
|
||||
from openpype.hosts.blender.api import lib, plugin
|
||||
|
||||
|
||||
class CreateAction(openpype.hosts.blender.api.plugin.Creator):
|
||||
"""Action output for character rigs"""
|
||||
class CreateAction(plugin.BaseCreator):
|
||||
"""Action output for character rigs."""
|
||||
|
||||
name = "actionMain"
|
||||
identifier = "io.openpype.creators.blender.action"
|
||||
label = "Action"
|
||||
family = "action"
|
||||
icon = "male"
|
||||
|
||||
def process(self):
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
collection = bpy.data.collections.new(name=name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(collection, self.data)
|
||||
# Get instance name
|
||||
name = plugin.prepare_scene_name(instance_data["asset"], subset_name)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
for obj in lib.get_selection():
|
||||
if (obj.animation_data is not None
|
||||
and obj.animation_data.action is not None):
|
||||
|
|
|
|||
|
|
@ -1,51 +1,32 @@
|
|||
"""Create an animation asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateAnimation(plugin.Creator):
|
||||
"""Animation output for character rigs"""
|
||||
class CreateAnimation(plugin.BaseCreator):
|
||||
"""Animation output for character rigs."""
|
||||
|
||||
name = "animationMain"
|
||||
identifier = "io.openpype.creators.blender.animation"
|
||||
label = "Animation"
|
||||
family = "animation"
|
||||
icon = "male"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
# name = self.name
|
||||
# if not name:
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
# asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
# asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
asset_group.objects.link(obj)
|
||||
elif (self.options or {}).get("asset_group"):
|
||||
obj = (self.options or {}).get("asset_group")
|
||||
asset_group.objects.link(obj)
|
||||
collection.objects.link(obj)
|
||||
elif pre_create_data.get("asset_group"):
|
||||
# Use for Load Blend automated creation of animation instances
|
||||
# upon loading rig files
|
||||
obj = pre_create_data.get("asset_group")
|
||||
collection.objects.link(obj)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -2,50 +2,33 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateBlendScene(plugin.Creator):
|
||||
"""Generic group of assets"""
|
||||
class CreateBlendScene(plugin.BaseCreator):
|
||||
"""Generic group of assets."""
|
||||
|
||||
name = "blendScene"
|
||||
identifier = "io.openpype.creators.blender.blendscene"
|
||||
label = "Blender Scene"
|
||||
family = "blendScene"
|
||||
icon = "cubes"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
maintain_selection = False
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
instance_node = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
if pre_create_data.get("use_selection"):
|
||||
selection = lib.get_selection(include_collections=True)
|
||||
for data in selection:
|
||||
if isinstance(data, bpy.types.Collection):
|
||||
instance_node.children.link(data)
|
||||
elif isinstance(data, bpy.types.Object):
|
||||
instance_node.objects.link(data)
|
||||
|
||||
return asset_group
|
||||
return instance_node
|
||||
|
|
|
|||
|
|
@ -2,62 +2,41 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreateCamera(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
class CreateCamera(plugin.BaseCreator):
|
||||
"""Polygonal static geometry."""
|
||||
|
||||
name = "cameraMain"
|
||||
identifier = "io.openpype.creators.blender.camera"
|
||||
label = "Camera"
|
||||
family = "camera"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
print(f"self.data: {self.data}")
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
if pre_create_data.get("use_selection"):
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
else:
|
||||
plugin.deselect_all()
|
||||
camera = bpy.data.cameras.new(subset)
|
||||
camera_obj = bpy.data.objects.new(subset, camera)
|
||||
camera = bpy.data.cameras.new(subset_name)
|
||||
camera_obj = bpy.data.objects.new(subset_name, camera)
|
||||
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
instances.objects.link(camera_obj)
|
||||
|
||||
camera_obj.select_set(True)
|
||||
asset_group.select_set(True)
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
camera_obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -2,50 +2,31 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateLayout(plugin.Creator):
|
||||
"""Layout output for character rigs"""
|
||||
class CreateLayout(plugin.BaseCreator):
|
||||
"""Layout output for character rigs."""
|
||||
|
||||
name = "layoutMain"
|
||||
identifier = "io.openpype.creators.blender.layout"
|
||||
label = "Layout"
|
||||
family = "layout"
|
||||
icon = "cubes"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -2,50 +2,30 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateModel(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
class CreateModel(plugin.BaseCreator):
|
||||
"""Polygonal static geometry."""
|
||||
|
||||
name = "modelMain"
|
||||
identifier = "io.openpype.creators.blender.model"
|
||||
label = "Model"
|
||||
family = "model"
|
||||
icon = "cube"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -1,51 +1,29 @@
|
|||
"""Create a pointcache asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreatePointcache(plugin.Creator):
|
||||
"""Polygonal static geometry"""
|
||||
class CreatePointcache(plugin.BaseCreator):
|
||||
"""Polygonal static geometry."""
|
||||
|
||||
name = "pointcacheMain"
|
||||
identifier = "io.openpype.creators.blender.pointcache"
|
||||
label = "Point Cache"
|
||||
family = "pointcache"
|
||||
icon = "gears"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
if pre_create_data.get("use_selection"):
|
||||
objects = lib.get_selection()
|
||||
for obj in objects:
|
||||
collection.objects.link(obj)
|
||||
if obj.type == 'EMPTY':
|
||||
objects.extend(obj.children)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -1,42 +1,31 @@
|
|||
"""Create render."""
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.render_lib import prepare_rendering
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
|
||||
|
||||
class CreateRenderlayer(plugin.Creator):
|
||||
"""Single baked camera"""
|
||||
class CreateRenderlayer(plugin.BaseCreator):
|
||||
"""Single baked camera."""
|
||||
|
||||
name = "renderingMain"
|
||||
identifier = "io.openpype.creators.blender.render"
|
||||
label = "Render"
|
||||
family = "render"
|
||||
icon = "eye"
|
||||
|
||||
def process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
try:
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
prepare_rendering(asset_group)
|
||||
prepare_rendering(collection)
|
||||
except Exception:
|
||||
# Remove the instance if there was an error
|
||||
bpy.data.collections.remove(asset_group)
|
||||
bpy.data.collections.remove(collection)
|
||||
raise
|
||||
|
||||
# TODO: this is undesiderable, but it's the only way to be sure that
|
||||
|
|
@ -50,4 +39,4 @@ class CreateRenderlayer(plugin.Creator):
|
|||
# now it is to force the file to be saved.
|
||||
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -1,47 +1,27 @@
|
|||
"""Create review."""
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateReview(plugin.Creator):
|
||||
"""Single baked camera"""
|
||||
class CreateReview(plugin.BaseCreator):
|
||||
"""Single baked camera."""
|
||||
|
||||
name = "reviewDefault"
|
||||
identifier = "io.openpype.creators.blender.review"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
# Run parent create method
|
||||
collection = super().create(
|
||||
subset_name, instance_data, pre_create_data
|
||||
)
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
asset_group.objects.link(obj)
|
||||
elif (self.options or {}).get("asset_group"):
|
||||
obj = (self.options or {}).get("asset_group")
|
||||
asset_group.objects.link(obj)
|
||||
collection.objects.link(obj)
|
||||
|
||||
return asset_group
|
||||
return collection
|
||||
|
|
|
|||
|
|
@ -2,50 +2,30 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import get_current_task_name
|
||||
from openpype.hosts.blender.api import plugin, lib, ops
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin, lib
|
||||
|
||||
|
||||
class CreateRig(plugin.Creator):
|
||||
"""Artist-friendly rig with controls to direct motion"""
|
||||
class CreateRig(plugin.BaseCreator):
|
||||
"""Artist-friendly rig with controls to direct motion."""
|
||||
|
||||
name = "rigMain"
|
||||
identifier = "io.openpype.creators.blender.rig"
|
||||
label = "Rig"
|
||||
family = "rig"
|
||||
icon = "wheelchair"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
create_as_asset_group = True
|
||||
|
||||
def _process(self):
|
||||
# Get Instance Container or create it if it does not exist
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if not instances:
|
||||
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
|
||||
bpy.context.scene.collection.children.link(instances)
|
||||
|
||||
# Create instance object
|
||||
asset = self.data["asset"]
|
||||
subset = self.data["subset"]
|
||||
name = plugin.asset_name(asset, subset)
|
||||
asset_group = bpy.data.objects.new(name=name, object_data=None)
|
||||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
instances.objects.link(asset_group)
|
||||
self.data['task'] = get_current_task_name()
|
||||
lib.imprint(asset_group, self.data)
|
||||
def create(
|
||||
self, subset_name: str, instance_data: dict, pre_create_data: dict
|
||||
):
|
||||
asset_group = super().create(subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
# Add selected objects to instance
|
||||
if (self.options or {}).get("useSelection"):
|
||||
if pre_create_data.get("use_selection"):
|
||||
bpy.context.view_layer.objects.active = asset_group
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
if obj.parent in selected:
|
||||
obj.select_set(False)
|
||||
continue
|
||||
selected.append(asset_group)
|
||||
bpy.ops.object.parent_set(keep_transform=True)
|
||||
for obj in lib.get_selection():
|
||||
obj.parent = asset_group
|
||||
|
||||
return asset_group
|
||||
|
|
|
|||
121
openpype/hosts/blender/plugins/create/create_workfile.py
Normal file
121
openpype/hosts/blender/plugins/create/create_workfile.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import bpy
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline import CreatedInstance, AutoCreator
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.hosts.blender.api.plugin import BaseCreator
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_PROPERTY,
|
||||
AVALON_CONTAINERS
|
||||
)
|
||||
|
||||
|
||||
class CreateWorkfile(BaseCreator, AutoCreator):
|
||||
"""Workfile auto-creator.
|
||||
|
||||
The workfile instance stores its data on the `AVALON_CONTAINERS` collection
|
||||
as custom attributes, because unlike other instances it doesn't have an
|
||||
instance node of its own.
|
||||
|
||||
"""
|
||||
identifier = "io.openpype.creators.blender.workfile"
|
||||
label = "Workfile"
|
||||
family = "workfile"
|
||||
icon = "fa5.file"
|
||||
|
||||
def create(self):
|
||||
"""Create workfile instances."""
|
||||
existing_instance = next(
|
||||
(
|
||||
instance for instance in self.create_context.instances
|
||||
if instance.creator_identifier == self.identifier
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
project_name = self.project_name
|
||||
asset_name = self.create_context.get_current_asset_name()
|
||||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.create_context.host_name
|
||||
|
||||
existing_asset_name = None
|
||||
if existing_instance is not None:
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_asset_name = existing_instance.get("folderPath")
|
||||
|
||||
if existing_asset_name is None:
|
||||
existing_asset_name = existing_instance["asset"]
|
||||
|
||||
if not existing_instance:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
task_name, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"task": task_name,
|
||||
"variant": task_name,
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
data.update(
|
||||
self.get_dynamic_data(
|
||||
task_name,
|
||||
task_name,
|
||||
asset_doc,
|
||||
project_name,
|
||||
host_name,
|
||||
existing_instance,
|
||||
)
|
||||
)
|
||||
self.log.info("Auto-creating workfile instance...")
|
||||
current_instance = CreatedInstance(
|
||||
self.family, subset_name, data, self
|
||||
)
|
||||
instance_node = bpy.data.collections.get(AVALON_CONTAINERS, {})
|
||||
current_instance.transient_data["instance_node"] = instance_node
|
||||
self._add_instance_to_context(current_instance)
|
||||
elif (
|
||||
existing_asset_name != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
# Update instance context if it's different
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
task_name, task_name, asset_doc, project_name, host_name
|
||||
)
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
||||
def collect_instances(self):
|
||||
|
||||
instance_node = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not instance_node:
|
||||
return
|
||||
|
||||
property = instance_node.get(AVALON_PROPERTY)
|
||||
if not property:
|
||||
return
|
||||
|
||||
# Create instance object from existing data
|
||||
instance = CreatedInstance.from_existing(
|
||||
instance_data=property.to_dict(),
|
||||
creator=self
|
||||
)
|
||||
instance.transient_data["instance_node"] = instance_node
|
||||
|
||||
# Add instance to create context
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
for instance in instances:
|
||||
node = instance.transient_data["instance_node"]
|
||||
del node[AVALON_PROPERTY]
|
||||
|
||||
self._remove_instance_from_context(instance)
|
||||
|
|
@ -7,7 +7,7 @@ def append_workfile(context, fname, do_import):
|
|||
asset = context['asset']['name']
|
||||
subset = context['subset']['name']
|
||||
|
||||
group_name = plugin.asset_name(asset, subset)
|
||||
group_name = plugin.prepare_scene_name(asset, subset)
|
||||
|
||||
# We need to preserve the original names of the scenes, otherwise,
|
||||
# if there are duplicate names in the current workfile, the imported
|
||||
|
|
|
|||
|
|
@ -137,9 +137,9 @@ class CacheModelLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
containers = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import Dict, List, Optional
|
|||
|
||||
import bpy
|
||||
from openpype.pipeline import get_representation_path
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
containerise_existing,
|
||||
AVALON_PROPERTY,
|
||||
|
|
@ -16,7 +16,7 @@ from openpype.hosts.blender.api.pipeline import (
|
|||
logger = logging.getLogger("openpype").getChild("blender").getChild("load_action")
|
||||
|
||||
|
||||
class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
||||
class BlendActionLoader(plugin.AssetLoader):
|
||||
"""Load action from a .blend file.
|
||||
|
||||
Warning:
|
||||
|
|
@ -46,8 +46,8 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
libpath = self.filepath_from_context(context)
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
container_name = openpype.hosts.blender.api.plugin.asset_name(
|
||||
lib_container = plugin.prepare_scene_name(asset, subset)
|
||||
container_name = plugin.prepare_scene_name(
|
||||
asset, subset, namespace
|
||||
)
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ class BlendActionLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
assert libpath.is_file(), (
|
||||
f"The file doesn't exist: {libpath}"
|
||||
)
|
||||
assert extension in openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
|
||||
assert extension in plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ class AudioLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ from pathlib import Path
|
|||
import bpy
|
||||
|
||||
from openpype.pipeline import (
|
||||
legacy_create,
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
registered_host
|
||||
)
|
||||
from openpype.pipeline.create import get_legacy_creator_by_name
|
||||
from openpype.pipeline.create import CreateContext
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.lib import imprint
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
|
|
@ -20,7 +20,7 @@ from openpype.hosts.blender.api.pipeline import (
|
|||
class BlendLoader(plugin.AssetLoader):
|
||||
"""Load assets from a .blend file."""
|
||||
|
||||
families = ["model", "rig", "layout", "camera", "blendScene"]
|
||||
families = ["model", "rig", "layout", "camera"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Append Blend"
|
||||
|
|
@ -32,7 +32,7 @@ class BlendLoader(plugin.AssetLoader):
|
|||
empties = [obj for obj in objects if obj.type == 'EMPTY']
|
||||
|
||||
for empty in empties:
|
||||
if empty.get(AVALON_PROPERTY):
|
||||
if empty.get(AVALON_PROPERTY) and empty.parent is None:
|
||||
return empty
|
||||
|
||||
return None
|
||||
|
|
@ -57,19 +57,21 @@ class BlendLoader(plugin.AssetLoader):
|
|||
obj.get(AVALON_PROPERTY).get('family') == 'rig'
|
||||
)
|
||||
]
|
||||
if not rigs:
|
||||
return
|
||||
|
||||
# Create animation instances for each rig
|
||||
creator_identifier = "io.openpype.creators.blender.animation"
|
||||
host = registered_host()
|
||||
create_context = CreateContext(host)
|
||||
|
||||
for rig in rigs:
|
||||
creator_plugin = get_legacy_creator_by_name("CreateAnimation")
|
||||
legacy_create(
|
||||
creator_plugin,
|
||||
name=rig.name.split(':')[-1] + "_animation",
|
||||
asset=asset,
|
||||
options={
|
||||
"useSelection": False,
|
||||
create_context.create(
|
||||
creator_identifier=creator_identifier,
|
||||
variant=rig.name.split(':')[-1],
|
||||
pre_create_data={
|
||||
"use_selection": False,
|
||||
"asset_group": rig
|
||||
},
|
||||
data={
|
||||
"dependencies": representation
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -100,6 +102,7 @@ class BlendLoader(plugin.AssetLoader):
|
|||
|
||||
# Link all the container children to the collection
|
||||
for obj in container.children_recursive:
|
||||
print(obj)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
|
||||
# Remove the library from the blend file
|
||||
|
|
@ -130,9 +133,9 @@ class BlendLoader(plugin.AssetLoader):
|
|||
|
||||
representation = str(context["representation"]["_id"])
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
221
openpype/hosts/blender/plugins/load/load_blendscene.py
Normal file
221
openpype/hosts/blender/plugins/load/load_blendscene.py
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
from typing import Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from openpype.pipeline import (
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.lib import imprint
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_CONTAINERS,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
|
||||
|
||||
class BlendSceneLoader(plugin.AssetLoader):
|
||||
"""Load assets from a .blend file."""
|
||||
|
||||
families = ["blendScene"]
|
||||
representations = ["blend"]
|
||||
|
||||
label = "Append Blend"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
@staticmethod
|
||||
def _get_asset_container(collections):
|
||||
for coll in collections:
|
||||
parents = [c for c in collections if c.user_of_id(coll)]
|
||||
if coll.get(AVALON_PROPERTY) and not parents:
|
||||
return coll
|
||||
|
||||
return None
|
||||
|
||||
def _process_data(self, libpath, group_name, family):
|
||||
# Append all the data from the .blend file
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=False, relative=False
|
||||
) as (data_from, data_to):
|
||||
for attr in dir(data_to):
|
||||
setattr(data_to, attr, getattr(data_from, attr))
|
||||
|
||||
members = []
|
||||
|
||||
# Rename the object to add the asset name
|
||||
for attr in dir(data_to):
|
||||
for data in getattr(data_to, attr):
|
||||
data.name = f"{group_name}:{data.name}"
|
||||
members.append(data)
|
||||
|
||||
container = self._get_asset_container(
|
||||
data_to.collections)
|
||||
assert container, "No asset group found"
|
||||
|
||||
container.name = group_name
|
||||
|
||||
# Link the group to the scene
|
||||
bpy.context.scene.collection.children.link(container)
|
||||
|
||||
# Remove the library from the blend file
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
return container, members
|
||||
|
||||
def process_asset(
|
||||
self, context: dict, name: str, namespace: Optional[str] = None,
|
||||
options: Optional[Dict] = None
|
||||
) -> Optional[List]:
|
||||
"""
|
||||
Arguments:
|
||||
name: Use pre-defined name
|
||||
namespace: Use pre-defined namespace
|
||||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
libpath = self.filepath_from_context(context)
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
try:
|
||||
family = context["representation"]["context"]["family"]
|
||||
except ValueError:
|
||||
family = "model"
|
||||
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
if not avalon_container:
|
||||
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
|
||||
bpy.context.scene.collection.children.link(avalon_container)
|
||||
|
||||
container, members = self._process_data(libpath, group_name, family)
|
||||
|
||||
avalon_container.children.link(container)
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or '',
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"libpath": libpath,
|
||||
"asset_name": asset_name,
|
||||
"parent": str(context["representation"]["parent"]),
|
||||
"family": context["representation"]["context"]["family"],
|
||||
"objectName": group_name,
|
||||
"members": members,
|
||||
}
|
||||
|
||||
container[AVALON_PROPERTY] = data
|
||||
|
||||
objects = [
|
||||
obj for obj in bpy.data.objects
|
||||
if obj.name.startswith(f"{group_name}:")
|
||||
]
|
||||
|
||||
self[:] = objects
|
||||
return objects
|
||||
|
||||
def exec_update(self, container: Dict, representation: Dict):
|
||||
"""
|
||||
Update the loaded asset.
|
||||
"""
|
||||
group_name = container["objectName"]
|
||||
asset_group = bpy.data.collections.get(group_name)
|
||||
libpath = Path(get_representation_path(representation)).as_posix()
|
||||
|
||||
assert asset_group, (
|
||||
f"The asset is not loaded: {container['objectName']}"
|
||||
)
|
||||
|
||||
# Get the parents of the members of the asset group, so we can
|
||||
# re-link them after the update.
|
||||
# Also gets the transform for each object to reapply after the update.
|
||||
collection_parents = {}
|
||||
member_transforms = {}
|
||||
members = asset_group.get(AVALON_PROPERTY).get("members", [])
|
||||
loaded_collections = {c for c in bpy.data.collections if c in members}
|
||||
loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))
|
||||
for member in members:
|
||||
if isinstance(member, bpy.types.Object):
|
||||
member_parents = set(member.users_collection)
|
||||
member_transforms[member.name] = member.matrix_basis.copy()
|
||||
elif isinstance(member, bpy.types.Collection):
|
||||
member_parents = {
|
||||
c for c in bpy.data.collections if c.user_of_id(member)}
|
||||
else:
|
||||
continue
|
||||
|
||||
member_parents = member_parents.difference(loaded_collections)
|
||||
if member_parents:
|
||||
collection_parents[member.name] = list(member_parents)
|
||||
|
||||
old_data = dict(asset_group.get(AVALON_PROPERTY))
|
||||
|
||||
self.exec_remove(container)
|
||||
|
||||
family = container["family"]
|
||||
asset_group, members = self._process_data(libpath, group_name, family)
|
||||
|
||||
for member in members:
|
||||
if member.name in collection_parents:
|
||||
for parent in collection_parents[member.name]:
|
||||
if isinstance(member, bpy.types.Object):
|
||||
parent.objects.link(member)
|
||||
elif isinstance(member, bpy.types.Collection):
|
||||
parent.children.link(member)
|
||||
if member.name in member_transforms and isinstance(
|
||||
member, bpy.types.Object
|
||||
):
|
||||
member.matrix_basis = member_transforms[member.name]
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
avalon_container.children.link(asset_group)
|
||||
|
||||
# Restore the old data, but reset members, as they don't exist anymore
|
||||
# This avoids a crash, because the memory addresses of those members
|
||||
# are not valid anymore
|
||||
old_data["members"] = []
|
||||
asset_group[AVALON_PROPERTY] = old_data
|
||||
|
||||
new_data = {
|
||||
"libpath": libpath,
|
||||
"representation": str(representation["_id"]),
|
||||
"parent": str(representation["parent"]),
|
||||
"members": members,
|
||||
}
|
||||
|
||||
imprint(asset_group, new_data)
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""
|
||||
Remove an existing container from a Blender scene.
|
||||
"""
|
||||
group_name = container["objectName"]
|
||||
asset_group = bpy.data.collections.get(group_name)
|
||||
|
||||
members = set(asset_group.get(AVALON_PROPERTY).get("members", []))
|
||||
|
||||
if members:
|
||||
for attr_name in dir(bpy.data):
|
||||
attr = getattr(bpy.data, attr_name)
|
||||
if not isinstance(attr, bpy.types.bpy_prop_collection):
|
||||
continue
|
||||
|
||||
# ensure to make a list copy because we
|
||||
# we remove members as we iterate
|
||||
for data in list(attr):
|
||||
if data not in members or data == asset_group:
|
||||
continue
|
||||
|
||||
attr.remove(data)
|
||||
|
||||
bpy.data.collections.remove(asset_group)
|
||||
|
|
@ -87,9 +87,9 @@ class AbcCameraLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -90,9 +90,9 @@ class FbxCameraLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -134,9 +134,9 @@ class FbxModelLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
# raise ValueError("Creator plugin \"CreateCamera\" was "
|
||||
# "not found.")
|
||||
|
||||
# TODO: Refactor legacy create usage to new style creators
|
||||
# legacy_create(
|
||||
# creator_plugin,
|
||||
# name="camera",
|
||||
|
|
@ -148,9 +149,9 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
asset_name = plugin.prepare_scene_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
group_name = plugin.asset_name(asset, subset, unique_number)
|
||||
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
|
||||
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
|
||||
|
|
|
|||
|
|
@ -96,14 +96,14 @@ class BlendLookLoader(plugin.AssetLoader):
|
|||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
|
||||
lib_container = plugin.asset_name(
|
||||
lib_container = plugin.prepare_scene_name(
|
||||
asset, subset
|
||||
)
|
||||
unique_number = plugin.get_unique_number(
|
||||
asset, subset
|
||||
)
|
||||
namespace = namespace or f"{asset}_{unique_number}"
|
||||
container_name = plugin.asset_name(
|
||||
container_name = plugin.prepare_scene_name(
|
||||
asset, subset, unique_number
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,72 +1,15 @@
|
|||
import os
|
||||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline import get_current_task_name, get_current_asset_name
|
||||
from openpype.hosts.blender.api import workio
|
||||
|
||||
|
||||
class SaveWorkfiledAction(pyblish.api.Action):
|
||||
"""Save Workfile."""
|
||||
label = "Save Workfile"
|
||||
on = "failed"
|
||||
icon = "save"
|
||||
|
||||
def process(self, context, plugin):
|
||||
bpy.ops.wm.avalon_workfiles()
|
||||
|
||||
|
||||
class CollectBlenderCurrentFile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context"""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
label = "Blender Current File"
|
||||
hosts = ["blender"]
|
||||
actions = [SaveWorkfiledAction]
|
||||
|
||||
def process(self, context):
|
||||
"""Inject the current working file"""
|
||||
current_file = workio.current_file()
|
||||
|
||||
context.data["currentFile"] = current_file
|
||||
|
||||
assert current_file, (
|
||||
"Current file is empty. Save the file before continuing."
|
||||
)
|
||||
|
||||
folder, file = os.path.split(current_file)
|
||||
filename, ext = os.path.splitext(file)
|
||||
|
||||
task = get_current_task_name()
|
||||
|
||||
data = {}
|
||||
|
||||
# create instance
|
||||
instance = context.create_instance(name=filename)
|
||||
subset = "workfile" + task.capitalize()
|
||||
|
||||
data.update({
|
||||
"subset": subset,
|
||||
"asset": get_current_asset_name(),
|
||||
"label": subset,
|
||||
"publish": True,
|
||||
"family": "workfile",
|
||||
"families": ["workfile"],
|
||||
"setMembers": [current_file],
|
||||
"frameStart": bpy.context.scene.frame_start,
|
||||
"frameEnd": bpy.context.scene.frame_end,
|
||||
})
|
||||
|
||||
data["representations"] = [{
|
||||
"name": ext.lstrip("."),
|
||||
"ext": ext.lstrip("."),
|
||||
"files": file,
|
||||
"stagingDir": folder,
|
||||
}]
|
||||
|
||||
instance.data.update(data)
|
||||
|
||||
self.log.info("Collected instance: {}".format(file))
|
||||
self.log.info("Scene path: {}".format(current_file))
|
||||
self.log.info("staging Dir: {}".format(folder))
|
||||
self.log.info("subset: {}".format(subset))
|
||||
|
|
|
|||
43
openpype/hosts/blender/plugins/publish/collect_instance.py
Normal file
43
openpype/hosts/blender/plugins/publish/collect_instance.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import KnownPublishError
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
|
||||
"""Validator to verify that the instance is not empty"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model", "pointcache", "animation", "rig", "camera", "layout",
|
||||
"blendScene"]
|
||||
label = "Collect Instance"
|
||||
|
||||
def process(self, instance):
|
||||
instance_node = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# Collect members of the instance
|
||||
members = [instance_node]
|
||||
if isinstance(instance_node, bpy.types.Collection):
|
||||
members.extend(instance_node.objects)
|
||||
members.extend(instance_node.children)
|
||||
|
||||
# Special case for animation instances, include armatures
|
||||
if instance.data["family"] == "animation":
|
||||
for obj in instance_node.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
members.extend(
|
||||
child for child in obj.children
|
||||
if child.type == 'ARMATURE'
|
||||
)
|
||||
elif isinstance(instance_node, bpy.types.Object):
|
||||
members.extend(instance_node.children_recursive)
|
||||
else:
|
||||
raise KnownPublishError(
|
||||
f"Unsupported instance node type '{type(instance_node)}' "
|
||||
f"for instance '{instance}'"
|
||||
)
|
||||
|
||||
instance[:] = members
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import json
|
||||
from typing import Generator
|
||||
|
||||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_INSTANCES,
|
||||
AVALON_PROPERTY,
|
||||
)
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect the data of a model."""
|
||||
|
||||
hosts = ["blender"]
|
||||
label = "Collect Instances"
|
||||
order = pyblish.api.CollectorOrder
|
||||
|
||||
@staticmethod
|
||||
def get_asset_groups() -> Generator:
|
||||
"""Return all instances that are empty objects asset groups.
|
||||
"""
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
for obj in list(instances.objects) + list(instances.children):
|
||||
avalon_prop = obj.get(AVALON_PROPERTY) or {}
|
||||
if avalon_prop.get('id') == 'pyblish.avalon.instance':
|
||||
yield obj
|
||||
|
||||
@staticmethod
|
||||
def create_instance(context, group):
|
||||
avalon_prop = group[AVALON_PROPERTY]
|
||||
asset = avalon_prop['asset']
|
||||
family = avalon_prop['family']
|
||||
subset = avalon_prop['subset']
|
||||
task = avalon_prop['task']
|
||||
name = f"{asset}_{subset}"
|
||||
return context.create_instance(
|
||||
name=name,
|
||||
family=family,
|
||||
families=[family],
|
||||
subset=subset,
|
||||
asset=asset,
|
||||
task=task,
|
||||
)
|
||||
|
||||
def process(self, context):
|
||||
"""Collect the models from the current Blender scene."""
|
||||
asset_groups = self.get_asset_groups()
|
||||
|
||||
for group in asset_groups:
|
||||
instance = self.create_instance(context, group)
|
||||
members = []
|
||||
if isinstance(group, bpy.types.Collection):
|
||||
members = list(group.objects)
|
||||
family = instance.data["family"]
|
||||
if family == "animation":
|
||||
for obj in group.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
members.extend(
|
||||
child for child in obj.children
|
||||
if child.type == 'ARMATURE')
|
||||
else:
|
||||
members = group.children_recursive
|
||||
|
||||
members.append(group)
|
||||
instance[:] = members
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
|
|
@ -11,12 +11,12 @@ import pyblish.api
|
|||
|
||||
|
||||
class CollectBlenderRender(pyblish.api.InstancePlugin):
|
||||
"""Gather all publishable render layers from renderSetup."""
|
||||
"""Gather all publishable render instances."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
hosts = ["blender"]
|
||||
families = ["render"]
|
||||
label = "Collect Render Layers"
|
||||
label = "Collect Render"
|
||||
sync_workfile_version = False
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -73,12 +73,11 @@ class CollectBlenderRender(pyblish.api.InstancePlugin):
|
|||
def process(self, instance):
|
||||
context = instance.context
|
||||
|
||||
render_data = bpy.data.collections[str(instance)].get("render_data")
|
||||
instance_node = instance.data["transientData"]["instance_node"]
|
||||
render_data = instance_node.get("render_data")
|
||||
|
||||
assert render_data, "No render data found."
|
||||
|
||||
self.log.info(f"render_data: {dict(render_data)}")
|
||||
|
||||
render_product = render_data.get("render_product")
|
||||
aov_file_product = render_data.get("aov_file_product")
|
||||
ext = render_data.get("image_format")
|
||||
|
|
@ -100,7 +99,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin):
|
|||
expected_files = expected_beauty | expected_aovs
|
||||
|
||||
instance.data.update({
|
||||
"family": "render.farm",
|
||||
"families": ["render", "render.farm"],
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"frameStartHandle": frame_handle_start,
|
||||
|
|
@ -119,5 +118,3 @@ class CollectBlenderRender(pyblish.api.InstancePlugin):
|
|||
"colorspaceView": "ACES 1.0 SDR-video",
|
||||
"renderProducts": colorspace.ARenderProduct(),
|
||||
})
|
||||
|
||||
self.log.info(f"data: {instance.data}")
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
|
||||
self.log.debug(f"instance: {instance}")
|
||||
|
||||
datablock = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# get cameras
|
||||
cameras = [
|
||||
obj
|
||||
for obj in instance
|
||||
for obj in datablock.all_objects
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA"
|
||||
]
|
||||
|
||||
|
|
@ -31,11 +33,12 @@ class CollectReview(pyblish.api.InstancePlugin):
|
|||
|
||||
focal_length = cameras[0].data.lens
|
||||
|
||||
# get isolate objects list from meshes instance members .
|
||||
# get isolate objects list from meshes instance members.
|
||||
types = {"MESH", "GPENCIL"}
|
||||
isolate_objects = [
|
||||
obj
|
||||
for obj in instance
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == "MESH"
|
||||
if isinstance(obj, bpy.types.Object) and obj.type in types
|
||||
]
|
||||
|
||||
if not instance.data.get("remove"):
|
||||
|
|
|
|||
37
openpype/hosts/blender/plugins/publish/collect_workfile.py
Normal file
37
openpype/hosts/blender/plugins/publish/collect_workfile.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from pathlib import Path
|
||||
|
||||
from pyblish.api import InstancePlugin, CollectorOrder
|
||||
|
||||
|
||||
class CollectWorkfile(InstancePlugin):
|
||||
"""Inject workfile data into its instance."""
|
||||
|
||||
order = CollectorOrder
|
||||
label = "Collect Workfile"
|
||||
hosts = ["blender"]
|
||||
families = ["workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Process collector."""
|
||||
|
||||
context = instance.context
|
||||
filepath = Path(context.data["currentFile"])
|
||||
ext = filepath.suffix
|
||||
|
||||
instance.data.update(
|
||||
{
|
||||
"setMembers": [filepath.as_posix()],
|
||||
"frameStart": context.data.get("frameStart", 1),
|
||||
"frameEnd": context.data.get("frameEnd", 1),
|
||||
"handleStart": context.data.get("handleStart", 1),
|
||||
"handledEnd": context.data.get("handleEnd", 1),
|
||||
"representations": [
|
||||
{
|
||||
"name": ext.lstrip("."),
|
||||
"ext": ext.lstrip("."),
|
||||
"files": filepath.name,
|
||||
"stagingDir": filepath.parent,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
@ -4,10 +4,9 @@ import bpy
|
|||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractABC(publish.Extractor):
|
||||
class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract as ABC."""
|
||||
|
||||
label = "Extract ABC"
|
||||
|
|
@ -15,28 +14,32 @@ class ExtractABC(publish.Extractor):
|
|||
families = ["pointcache"]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
active = None
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
selected = []
|
||||
for obj in instance:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
# Set as active the asset group
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
active = obj
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=active, selected=selected)
|
||||
active=asset_group, selected=selected)
|
||||
|
||||
with bpy.context.temp_override(**context):
|
||||
# We export the abc
|
||||
|
|
@ -59,8 +62,8 @@ class ExtractABC(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
||||
|
||||
class ExtractModelABC(ExtractABC):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ from openpype.pipeline import publish
|
|||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class ExtractAnimationABC(publish.Extractor):
|
||||
class ExtractAnimationABC(
|
||||
publish.Extractor,
|
||||
publish.OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Extract as ABC."""
|
||||
|
||||
label = "Extract Animation ABC"
|
||||
|
|
@ -15,18 +18,25 @@ class ExtractAnimationABC(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.abc"
|
||||
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
asset_group = None
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
objects = []
|
||||
for obj in instance:
|
||||
|
|
@ -66,5 +76,5 @@ class ExtractAnimationABC(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import bpy
|
|||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractBlend(publish.Extractor):
|
||||
class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract a blend file."""
|
||||
|
||||
label = "Extract Blend"
|
||||
|
|
@ -14,30 +14,44 @@ class ExtractBlend(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.blend"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.blend"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
data_blocks = set()
|
||||
|
||||
for obj in instance:
|
||||
data_blocks.add(obj)
|
||||
for data in instance:
|
||||
data_blocks.add(data)
|
||||
# Pack used images in the blend files.
|
||||
if obj.type == 'MESH':
|
||||
for material_slot in obj.material_slots:
|
||||
mat = material_slot.material
|
||||
if mat and mat.use_nodes:
|
||||
tree = mat.node_tree
|
||||
if tree.type == 'SHADER':
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname == 'ShaderNodeTexImage':
|
||||
if node.image:
|
||||
node.image.pack()
|
||||
if not (
|
||||
isinstance(data, bpy.types.Object) and data.type == 'MESH'
|
||||
):
|
||||
continue
|
||||
for material_slot in data.material_slots:
|
||||
mat = material_slot.material
|
||||
if not (mat and mat.use_nodes):
|
||||
continue
|
||||
tree = mat.node_tree
|
||||
if tree.type != 'SHADER':
|
||||
continue
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname != 'ShaderNodeTexImage':
|
||||
continue
|
||||
# Check if image is not packed already
|
||||
# and pack it if not.
|
||||
if node.image and node.image.packed_file is None:
|
||||
node.image.pack()
|
||||
|
||||
bpy.data.libraries.write(filepath, data_blocks)
|
||||
|
||||
|
|
@ -52,5 +66,5 @@ class ExtractBlend(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import bpy
|
|||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractBlendAnimation(publish.Extractor):
|
||||
class ExtractBlendAnimation(
|
||||
publish.Extractor,
|
||||
publish.OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Extract a blend file."""
|
||||
|
||||
label = "Extract Blend"
|
||||
|
|
@ -14,14 +17,20 @@ class ExtractBlendAnimation(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.blend"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.blend"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
data_blocks = set()
|
||||
|
||||
|
|
@ -50,5 +59,5 @@ class ExtractBlendAnimation(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractCameraABC(publish.Extractor):
|
||||
class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract camera as ABC."""
|
||||
|
||||
label = "Extract Camera (ABC)"
|
||||
|
|
@ -16,22 +16,23 @@ class ExtractCameraABC(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.abc"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.abc"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
asset_group = None
|
||||
for obj in instance:
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
break
|
||||
assert asset_group, "No asset group found"
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# Need to cast to list because children is a tuple
|
||||
selected = list(asset_group.children)
|
||||
|
|
@ -64,5 +65,5 @@ class ExtractCameraABC(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from openpype.pipeline import publish
|
|||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class ExtractCamera(publish.Extractor):
|
||||
class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract as the camera as FBX."""
|
||||
|
||||
label = "Extract Camera (FBX)"
|
||||
|
|
@ -15,13 +15,19 @@ class ExtractCamera(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.fbx"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
|
|
@ -73,5 +79,5 @@ class ExtractCamera(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractFBX(publish.Extractor):
|
||||
class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract as FBX."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -16,24 +16,28 @@ class ExtractFBX(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = f"{instance.name}.fbx"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
filename = f"{instance_name}.fbx"
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
selected = []
|
||||
asset_group = None
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
selected = []
|
||||
for obj in instance:
|
||||
obj.select_set(True)
|
||||
selected.append(obj)
|
||||
if obj.get(AVALON_PROPERTY):
|
||||
asset_group = obj
|
||||
|
||||
context = plugin.create_blender_context(
|
||||
active=asset_group, selected=selected)
|
||||
|
|
@ -84,5 +88,5 @@ class ExtractFBX(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,41 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractAnimationFBX(publish.Extractor):
|
||||
def get_all_parents(obj):
|
||||
"""Get all recursive parents of object"""
|
||||
result = []
|
||||
while True:
|
||||
obj = obj.parent
|
||||
if not obj:
|
||||
break
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
def get_highest_root(objects):
|
||||
# Get the highest object that is also in the collection
|
||||
included_objects = {obj.name_full for obj in objects}
|
||||
num_parents_to_obj = {}
|
||||
for obj in objects:
|
||||
if isinstance(obj, bpy.types.Object):
|
||||
parents = get_all_parents(obj)
|
||||
# included parents
|
||||
parents = [parent for parent in parents if
|
||||
parent.name_full in included_objects]
|
||||
if not parents:
|
||||
# A node without parents must be a highest root
|
||||
return obj
|
||||
|
||||
num_parents_to_obj.setdefault(len(parents), obj)
|
||||
|
||||
minimum_parent = min(num_parents_to_obj)
|
||||
return num_parents_to_obj[minimum_parent]
|
||||
|
||||
|
||||
class ExtractAnimationFBX(
|
||||
publish.Extractor,
|
||||
publish.OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Extract as animation."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -19,23 +53,43 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
# The first collection object in the instance is taken, as there
|
||||
# should be only one that contains the asset group.
|
||||
collection = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection][0]
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
# Again, the first object in the collection is taken , as there
|
||||
# should be only the asset group in the collection.
|
||||
asset_group = collection.objects[0]
|
||||
# Get objects in this collection (but not in children collections)
|
||||
# and for those objects include the children hierarchy
|
||||
# TODO: Would it make more sense for the Collect Instance collector
|
||||
# to also always retrieve all the children?
|
||||
objects = set(asset_group.objects)
|
||||
|
||||
armature = [
|
||||
obj for obj in asset_group.children if obj.type == 'ARMATURE'][0]
|
||||
# From the direct children of the collection find the 'root' node
|
||||
# that we want to export - it is the 'highest' node in a hierarchy
|
||||
root = get_highest_root(objects)
|
||||
|
||||
for obj in list(objects):
|
||||
objects.update(obj.children_recursive)
|
||||
|
||||
# Find all armatures among the objects, assume to find only one
|
||||
armatures = [obj for obj in objects if obj.type == "ARMATURE"]
|
||||
if not armatures:
|
||||
raise RuntimeError(
|
||||
f"Unable to find ARMATURE in collection: "
|
||||
f"{asset_group.name}"
|
||||
)
|
||||
elif len(armatures) > 1:
|
||||
self.log.warning(
|
||||
"Found more than one ARMATURE, using "
|
||||
f"only first of: {armatures}"
|
||||
)
|
||||
armature = armatures[0]
|
||||
|
||||
object_action_pairs = []
|
||||
original_actions = []
|
||||
|
|
@ -44,9 +98,6 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
ending_frames = []
|
||||
|
||||
# For each armature, we make a copy of the current action
|
||||
curr_action = None
|
||||
copy_action = None
|
||||
|
||||
if armature.animation_data and armature.animation_data.action:
|
||||
curr_action = armature.animation_data.action
|
||||
copy_action = curr_action.copy()
|
||||
|
|
@ -56,12 +107,20 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
starting_frames.append(curr_frame_range[0])
|
||||
ending_frames.append(curr_frame_range[1])
|
||||
else:
|
||||
self.log.info("Object have no animation.")
|
||||
self.log.info(
|
||||
f"Armature '{armature.name}' has no animation, "
|
||||
f"skipping FBX animation extraction for {instance}."
|
||||
)
|
||||
return
|
||||
|
||||
asset_group_name = asset_group.name
|
||||
asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name")
|
||||
asset_name = asset_group.get(AVALON_PROPERTY).get("asset_name")
|
||||
if asset_name:
|
||||
# Rename for the export; this data is only present when loaded
|
||||
# from a JSON Layout (layout family)
|
||||
asset_group.name = asset_name
|
||||
|
||||
# Remove : from the armature name for the export
|
||||
armature_name = armature.name
|
||||
original_name = armature_name.split(':')[1]
|
||||
armature.name = original_name
|
||||
|
|
@ -84,13 +143,16 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
asset_group.select_set(True)
|
||||
root.select_set(True)
|
||||
armature.select_set(True)
|
||||
fbx_filename = f"{instance.name}_{armature.name}.fbx"
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
fbx_filename = f"{instance_name}_{armature.name}.fbx"
|
||||
filepath = os.path.join(stagingdir, fbx_filename)
|
||||
|
||||
override = plugin.create_blender_context(
|
||||
active=asset_group, selected=[asset_group, armature])
|
||||
active=root, selected=[root, armature])
|
||||
bpy.ops.export_scene.fbx(
|
||||
override,
|
||||
filepath=filepath,
|
||||
|
|
@ -104,7 +166,7 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
)
|
||||
armature.name = armature_name
|
||||
asset_group.name = asset_group_name
|
||||
asset_group.select_set(False)
|
||||
root.select_set(True)
|
||||
armature.select_set(False)
|
||||
|
||||
# We delete the baked action and set the original one back
|
||||
|
|
@ -119,7 +181,7 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
pair[1].user_clear()
|
||||
bpy.data.actions.remove(pair[1])
|
||||
|
||||
json_filename = f"{instance.name}.json"
|
||||
json_filename = f"{instance_name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
json_dict = {
|
||||
|
|
@ -158,5 +220,5 @@ class ExtractAnimationFBX(publish.Extractor):
|
|||
instance.data["representations"].append(fbx_representation)
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
self.log.debug("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ from openpype.hosts.blender.api import plugin
|
|||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
|
||||
|
||||
class ExtractLayout(publish.Extractor):
|
||||
class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""Extract a layout."""
|
||||
|
||||
label = "Extract Layout"
|
||||
label = "Extract Layout (JSON)"
|
||||
hosts = ["blender"]
|
||||
families = ["layout"]
|
||||
optional = True
|
||||
|
|
@ -45,7 +45,7 @@ class ExtractLayout(publish.Extractor):
|
|||
starting_frames.append(curr_frame_range[0])
|
||||
ending_frames.append(curr_frame_range[1])
|
||||
else:
|
||||
self.log.info("Object have no animation.")
|
||||
self.log.info("Object has no animation.")
|
||||
continue
|
||||
|
||||
asset_group_name = asset.name
|
||||
|
|
@ -113,11 +113,14 @@ class ExtractLayout(publish.Extractor):
|
|||
return None, n
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
self.log.debug("Performing extraction..")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
@ -125,13 +128,22 @@ class ExtractLayout(publish.Extractor):
|
|||
json_data = []
|
||||
fbx_files = []
|
||||
|
||||
asset_group = bpy.data.objects[str(instance)]
|
||||
asset_group = instance.data["transientData"]["instance_node"]
|
||||
|
||||
fbx_count = 0
|
||||
|
||||
project_name = instance.context.data["projectEntity"]["name"]
|
||||
for asset in asset_group.children:
|
||||
metadata = asset.get(AVALON_PROPERTY)
|
||||
if not metadata:
|
||||
# Avoid raising error directly if there's just invalid data
|
||||
# inside the instance; better to log it to the artist
|
||||
# TODO: This should actually be validated in a validator
|
||||
self.log.warning(
|
||||
f"Found content in layout that is not a loaded "
|
||||
f"asset, skipping: {asset.name_full}"
|
||||
)
|
||||
continue
|
||||
|
||||
version_id = metadata["parent"]
|
||||
family = metadata["family"]
|
||||
|
|
@ -212,7 +224,11 @@ class ExtractLayout(publish.Extractor):
|
|||
|
||||
json_data.append(json_element)
|
||||
|
||||
json_filename = "{}.json".format(instance.name)
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
instance_name = f"{asset_name}_{subset}"
|
||||
json_filename = f"{instance_name}.json"
|
||||
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
|
|
@ -245,5 +261,5 @@ class ExtractLayout(publish.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, json_representation)
|
||||
self.log.debug("Extracted instance '%s' to: %s",
|
||||
instance.name, json_representation)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype.hosts.blender.api import capture
|
|||
from openpype.hosts.blender.api.lib import maintained_time
|
||||
|
||||
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
|
||||
"""
|
||||
Extract viewport playblast.
|
||||
|
||||
|
|
@ -24,9 +24,8 @@ class ExtractPlayblast(publish.Extractor):
|
|||
order = pyblish.api.ExtractorOrder + 0.01
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
|
||||
self.log.info(instance.data)
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# get scene fps
|
||||
fps = instance.data.get("fps")
|
||||
|
|
@ -34,14 +33,14 @@ class ExtractPlayblast(publish.Extractor):
|
|||
fps = bpy.context.scene.render.fps
|
||||
instance.data["fps"] = fps
|
||||
|
||||
self.log.info(f"fps: {fps}")
|
||||
self.log.debug(f"fps: {fps}")
|
||||
|
||||
# If start and end frames cannot be determined,
|
||||
# get them from Blender timeline.
|
||||
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
|
||||
end = instance.data.get("frameEnd", bpy.context.scene.frame_end)
|
||||
|
||||
self.log.info(f"start: {start}, end: {end}")
|
||||
self.log.debug(f"start: {start}, end: {end}")
|
||||
assert end > start, "Invalid time range !"
|
||||
|
||||
# get cameras
|
||||
|
|
@ -52,10 +51,13 @@ class ExtractPlayblast(publish.Extractor):
|
|||
|
||||
# get output path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = instance.name
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
filename = f"{asset_name}_{subset}"
|
||||
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.info(f"Outputting images to {path}")
|
||||
self.log.debug(f"Outputting images to {path}")
|
||||
|
||||
project_settings = instance.context.data["project_settings"]["blender"]
|
||||
presets = project_settings["publish"]["ExtractPlayblast"]["presets"]
|
||||
|
|
@ -100,7 +102,7 @@ class ExtractPlayblast(publish.Extractor):
|
|||
|
||||
frame_collection = collections[0]
|
||||
|
||||
self.log.info(f"We found collection of interest {frame_collection}")
|
||||
self.log.debug(f"Found collection of interest {frame_collection}")
|
||||
|
||||
instance.data.setdefault("representations", [])
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,20 @@ class ExtractThumbnail(publish.Extractor):
|
|||
presets = {}
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
self.log.debug("Extracting capture..")
|
||||
|
||||
if instance.data.get("thumbnailSource"):
|
||||
self.log.debug("Thumbnail source found, skipping...")
|
||||
return
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = instance.name
|
||||
asset_name = instance.data["assetEntity"]["name"]
|
||||
subset = instance.data["subset"]
|
||||
filename = f"{asset_name}_{subset}"
|
||||
|
||||
path = os.path.join(stagingdir, filename)
|
||||
|
||||
self.log.info(f"Outputting images to {path}")
|
||||
self.log.debug(f"Outputting images to {path}")
|
||||
|
||||
camera = instance.data.get("review_camera", "AUTO")
|
||||
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
|
||||
|
|
@ -61,7 +68,7 @@ class ExtractThumbnail(publish.Extractor):
|
|||
|
||||
thumbnail = os.path.basename(self._fix_output_path(path))
|
||||
|
||||
self.log.info(f"thumbnail: {thumbnail}")
|
||||
self.log.debug(f"thumbnail: {thumbnail}")
|
||||
|
||||
instance.data.setdefault("representations", [])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline.publish import OptionalPyblishPluginMixin
|
||||
from openpype.hosts.blender.api.workio import save_file
|
||||
|
||||
|
||||
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
||||
class IncrementWorkfileVersion(
|
||||
pyblish.api.ContextPlugin,
|
||||
OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Increment current workfile version."""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 0.9
|
||||
|
|
@ -10,9 +14,11 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
optional = True
|
||||
hosts = ["blender"]
|
||||
families = ["animation", "model", "rig", "action", "layout", "blendScene",
|
||||
"pointcache", "render"]
|
||||
"pointcache", "render.farm"]
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
assert all(result["success"] for result in context.data["results"]), (
|
||||
"Publishing not successful so version is not increased.")
|
||||
|
|
@ -23,4 +29,4 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
|
||||
save_file(filepath, copy=False)
|
||||
|
||||
self.log.info('Incrementing script version')
|
||||
self.log.debug('Incrementing blender workfile version')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline.publish import OptionalPyblishPluginMixin
|
||||
|
||||
|
||||
class IntegrateAnimation(pyblish.api.InstancePlugin):
|
||||
class IntegrateAnimation(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Generate a JSON file for animation."""
|
||||
|
||||
label = "Integrate Animation"
|
||||
|
|
@ -13,7 +17,7 @@ class IntegrateAnimation(pyblish.api.InstancePlugin):
|
|||
families = ["setdress"]
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Integrate Animation")
|
||||
self.log.debug("Integrate Animation")
|
||||
|
||||
representation = instance.data.get('representations')[0]
|
||||
json_path = representation.get('publishedFiles')[0]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import bpy
|
|||
import pyblish.api
|
||||
|
||||
import openpype.hosts.blender.api.action
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
|
||||
|
||||
class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
|
||||
class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Camera must have a keyframe at frame 0.
|
||||
|
||||
Unreal shifts the first keyframe to frame 0. Forcing the camera to have
|
||||
|
|
@ -40,8 +45,12 @@ class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Camera must have a keyframe at frame 0: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Camera must have a keyframe at frame 0: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
|
|||
"""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
families = ["render.farm"]
|
||||
families = ["render"]
|
||||
hosts = ["blender"]
|
||||
label = "Validate Render Output for Deadline"
|
||||
optional = True
|
||||
|
|
@ -36,12 +36,12 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
|
|||
"Render output folder "
|
||||
"doesn't match the blender scene name! "
|
||||
"Use Repair action to "
|
||||
"fix the folder file path.."
|
||||
"fix the folder file path."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
container = bpy.data.collections[str(instance)]
|
||||
container = instance.data["transientData"]["instance_node"]
|
||||
prepare_rendering(container)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
|
||||
cls.log.debug("Reset the render output folder...")
|
||||
|
|
|
|||
|
|
@ -2,8 +2,24 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
class ValidateFileSaved(pyblish.api.InstancePlugin):
|
||||
|
||||
class SaveWorkfileAction(pyblish.api.Action):
|
||||
"""Save Workfile."""
|
||||
label = "Save Workfile"
|
||||
on = "failed"
|
||||
icon = "save"
|
||||
|
||||
def process(self, context, plugin):
|
||||
bpy.ops.wm.avalon_workfiles()
|
||||
|
||||
|
||||
class ValidateFileSaved(pyblish.api.ContextPlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate that the workfile has been saved."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
|
|
@ -11,10 +27,35 @@ class ValidateFileSaved(pyblish.api.InstancePlugin):
|
|||
label = "Validate File Saved"
|
||||
optional = False
|
||||
exclude_families = []
|
||||
actions = [SaveWorkfileAction]
|
||||
|
||||
def process(self, instance):
|
||||
if [ef for ef in self.exclude_families
|
||||
if instance.data["family"] in ef]:
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
return
|
||||
|
||||
if not context.data["currentFile"]:
|
||||
# File has not been saved at all and has no filename
|
||||
raise PublishValidationError(
|
||||
"Current file is empty. Save the file before continuing."
|
||||
)
|
||||
|
||||
# Do not validate workfile has unsaved changes if only instances
|
||||
# present of families that should be excluded
|
||||
families = {
|
||||
instance.data["family"] for instance in context
|
||||
# Consider only enabled instances
|
||||
if instance.data.get("publish", True)
|
||||
and instance.data.get("active", True)
|
||||
}
|
||||
|
||||
def is_excluded(family):
|
||||
return any(family in exclude_family
|
||||
for exclude_family in self.exclude_families)
|
||||
|
||||
if all(is_excluded(family) for family in families):
|
||||
self.log.debug("Only excluded families found, skipping workfile "
|
||||
"unsaved changes validation..")
|
||||
return
|
||||
|
||||
if bpy.data.is_dirty:
|
||||
raise RuntimeError("Workfile is not saved.")
|
||||
raise PublishValidationError("Workfile has unsaved changes.")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline.publish import PublishValidationError
|
||||
|
||||
|
||||
class ValidateInstanceEmpty(pyblish.api.InstancePlugin):
|
||||
"""Validator to verify that the instance is not empty"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
hosts = ["blender"]
|
||||
families = ["model", "pointcache", "rig", "camera" "layout", "blendScene"]
|
||||
label = "Validate Instance is not Empty"
|
||||
optional = False
|
||||
|
||||
def process(self, instance):
|
||||
# Members are collected by `collect_instance` so we only need to check
|
||||
# whether any member is included. The instance node will be included
|
||||
# as a member as well, hence we will check for at least 2 members
|
||||
if len(instance) < 2:
|
||||
raise PublishValidationError(f"Instance {instance.name} is empty.")
|
||||
|
|
@ -4,17 +4,24 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
||||
class ValidateMeshHasUvs(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Validate that the current mesh has UV's."""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
hosts = ["blender"]
|
||||
families = ["model"]
|
||||
label = "Mesh Has UV's"
|
||||
label = "Mesh Has UVs"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
optional = True
|
||||
|
||||
|
|
@ -49,8 +56,11 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
raise PublishValidationError(
|
||||
f"Meshes found in instance without valid UV's: {invalid}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
||||
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Ensure that meshes don't have a negative scale."""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
|
|
@ -27,8 +32,12 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Meshes found in instance with negative scale: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Meshes found in instance with negative scale: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import bpy
|
|||
import pyblish.api
|
||||
|
||||
import openpype.hosts.blender.api.action
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
|
||||
class ValidateNoColonsInName(pyblish.api.InstancePlugin):
|
||||
class ValidateNoColonsInName(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""There cannot be colons in names
|
||||
|
||||
Object or bone names cannot include colons. Other software do not
|
||||
|
|
@ -36,8 +41,12 @@ class ValidateNoColonsInName(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Objects found with colon in name: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Objects found with colon in name: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ from typing import List
|
|||
import bpy
|
||||
|
||||
import pyblish.api
|
||||
from openpype.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
|
||||
class ValidateObjectIsInObjectMode(
|
||||
pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin,
|
||||
):
|
||||
"""Validate that the objects in the instance are in Object Mode."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
|
|
@ -25,8 +32,12 @@ class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Object found in instance is not in Object Mode: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
f"Object found in instance is not in Object Mode: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,14 @@ import bpy
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin):
|
||||
|
||||
class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate that there is a camera set as active for rendering."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
|
|
@ -13,5 +19,8 @@ class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin):
|
|||
optional = False
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
if not bpy.context.scene.camera:
|
||||
raise RuntimeError("No camera is active for rendering.")
|
||||
raise PublishValidationError("No camera is active for rendering.")
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ import bpy
|
|||
import pyblish.api
|
||||
|
||||
import openpype.hosts.blender.api.action
|
||||
from openpype.pipeline.publish import ValidateContentsOrder
|
||||
from openpype.pipeline.publish import (
|
||||
ValidateContentsOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
|
||||
|
||||
class ValidateTransformZero(pyblish.api.InstancePlugin):
|
||||
class ValidateTransformZero(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Transforms can't have any values
|
||||
|
||||
To solve this issue, try freezing the transforms. So long
|
||||
|
|
@ -38,9 +43,13 @@ class ValidateTransformZero(pyblish.api.InstancePlugin):
|
|||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
"Object found in instance has not"
|
||||
f" transform to zero: {invalid}"
|
||||
names = ", ".join(obj.name for obj in invalid)
|
||||
raise PublishValidationError(
|
||||
"Objects found in instance which do not"
|
||||
f" have transform set to zero: {names}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
|
||||
|
||||
class CollectCelactionInstances(pyblish.api.ContextPlugin):
|
||||
""" Adds the celaction render instances """
|
||||
|
|
@ -17,8 +19,10 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
|
|||
asset_entity = context.data["assetEntity"]
|
||||
project_entity = context.data["projectEntity"]
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_entity)
|
||||
|
||||
shared_instance_data = {
|
||||
"asset": asset_entity["name"],
|
||||
"asset": asset_name,
|
||||
"frameStart": asset_entity["data"]["frameStart"],
|
||||
"frameEnd": asset_entity["data"]["frameEnd"],
|
||||
"handleStart": asset_entity["data"]["handleStart"],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
import openpype.hosts.flame.api as opfapi
|
||||
from openpype.hosts.flame.otio import flame_export
|
||||
from openpype.pipeline.create import get_subset_name
|
||||
|
|
@ -33,13 +34,15 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin):
|
|||
project_settings=context.data["project_settings"]
|
||||
)
|
||||
|
||||
asset_name = get_asset_name_identifier(asset_doc)
|
||||
|
||||
# adding otio timeline to context
|
||||
with opfapi.maintained_segment_selection(sequence) as selected_seg:
|
||||
otio_timeline = flame_export.create_otio_timeline(sequence)
|
||||
|
||||
instance_data = {
|
||||
"name": subset_name,
|
||||
"asset": asset_doc["name"],
|
||||
"asset": asset_name,
|
||||
"subset": subset_name,
|
||||
"family": "workfile",
|
||||
"families": []
|
||||
|
|
|
|||
|
|
@ -280,7 +280,11 @@ def get_current_comp():
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"):
|
||||
def comp_lock_and_undo_chunk(
|
||||
comp,
|
||||
undo_queue_name="Script CMD",
|
||||
keep_undo=True,
|
||||
):
|
||||
"""Lock comp and open an undo chunk during the context"""
|
||||
try:
|
||||
comp.Lock()
|
||||
|
|
@ -288,4 +292,4 @@ def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"):
|
|||
yield
|
||||
finally:
|
||||
comp.Unlock()
|
||||
comp.EndUndo()
|
||||
comp.EndUndo(keep_undo)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
|
@ -18,6 +19,10 @@ from openpype.resources import get_openpype_icon_filepath
|
|||
from .pipeline import FusionEventHandler
|
||||
from .pulse import FusionPulse
|
||||
|
||||
|
||||
MENU_LABEL = os.environ["AVALON_LABEL"]
|
||||
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self.menu = None
|
||||
|
||||
|
|
@ -26,7 +31,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(OpenPypeMenu, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setObjectName("OpenPypeMenu")
|
||||
self.setObjectName(f"{MENU_LABEL}Menu")
|
||||
|
||||
icon_path = get_openpype_icon_filepath()
|
||||
icon = QtGui.QIcon(icon_path)
|
||||
|
|
@ -41,7 +46,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
self.render_mode_widget = None
|
||||
self.setWindowTitle("OpenPype")
|
||||
self.setWindowTitle(MENU_LABEL)
|
||||
|
||||
asset_label = QtWidgets.QLabel("Context", self)
|
||||
asset_label.setStyleSheet(
|
||||
|
|
|
|||
60
openpype/hosts/fusion/deploy/ayon/Config/menu.fu
Normal file
60
openpype/hosts/fusion/deploy/ayon/Config/menu.fu
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
Action
|
||||
{
|
||||
ID = "AYON_Menu",
|
||||
Category = "AYON",
|
||||
Name = "AYON Menu",
|
||||
|
||||
Targets =
|
||||
{
|
||||
Composition =
|
||||
{
|
||||
Execute = _Lua [=[
|
||||
local scriptPath = app:MapPath("AYON:../MenuScripts/launch_menu.py")
|
||||
if bmd.fileexists(scriptPath) == false then
|
||||
print("[AYON Error] Can't run file: " .. scriptPath)
|
||||
else
|
||||
target:RunScript(scriptPath)
|
||||
end
|
||||
]=],
|
||||
},
|
||||
},
|
||||
},
|
||||
Action
|
||||
{
|
||||
ID = "AYON_Install_PySide2",
|
||||
Category = "AYON",
|
||||
Name = "Install PySide2",
|
||||
|
||||
Targets =
|
||||
{
|
||||
Composition =
|
||||
{
|
||||
Execute = _Lua [=[
|
||||
local scriptPath = app:MapPath("AYON:../MenuScripts/install_pyside2.py")
|
||||
if bmd.fileexists(scriptPath) == false then
|
||||
print("[AYON Error] Can't run file: " .. scriptPath)
|
||||
else
|
||||
target:RunScript(scriptPath)
|
||||
end
|
||||
]=],
|
||||
},
|
||||
},
|
||||
},
|
||||
Menus
|
||||
{
|
||||
Target = "ChildFrame",
|
||||
|
||||
Before "Help"
|
||||
{
|
||||
Sub "AYON"
|
||||
{
|
||||
"AYON_Menu{}",
|
||||
"_",
|
||||
Sub "Admin" {
|
||||
"AYON_Install_PySide2{}"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
19
openpype/hosts/fusion/deploy/ayon/fusion_shared.prefs
Normal file
19
openpype/hosts/fusion/deploy/ayon/fusion_shared.prefs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
Locked = true,
|
||||
Global = {
|
||||
Paths = {
|
||||
Map = {
|
||||
["AYON:"] = "$(OPENPYPE_FUSION)/deploy/ayon",
|
||||
["Config:"] = "UserPaths:Config;AYON:Config",
|
||||
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts",
|
||||
},
|
||||
},
|
||||
Script = {
|
||||
PythonVersion = 3,
|
||||
Python3Forced = true
|
||||
},
|
||||
UserInterface = {
|
||||
Language = "en_US"
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
Composition =
|
||||
{
|
||||
Execute = _Lua [=[
|
||||
local scriptPath = app:MapPath("OpenPype:MenuScripts/openpype_menu.py")
|
||||
local scriptPath = app:MapPath("OpenPype:../MenuScripts/launch_menu.py")
|
||||
if bmd.fileexists(scriptPath) == false then
|
||||
print("[OpenPype Error] Can't run file: " .. scriptPath)
|
||||
else
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
Composition =
|
||||
{
|
||||
Execute = _Lua [=[
|
||||
local scriptPath = app:MapPath("OpenPype:MenuScripts/install_pyside2.py")
|
||||
local scriptPath = app:MapPath("OpenPype:../MenuScripts/install_pyside2.py")
|
||||
if bmd.fileexists(scriptPath) == false then
|
||||
print("[OpenPype Error] Can't run file: " .. scriptPath)
|
||||
else
|
||||
|
|
@ -3,7 +3,7 @@ Locked = true,
|
|||
Global = {
|
||||
Paths = {
|
||||
Map = {
|
||||
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
|
||||
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy/openpype",
|
||||
["Config:"] = "UserPaths:Config;OpenPype:Config",
|
||||
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts",
|
||||
},
|
||||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import shutil
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.hosts.fusion import (
|
||||
FUSION_HOST_DIR,
|
||||
FUSION_VERSIONS_DICT,
|
||||
|
|
@ -161,6 +162,13 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook):
|
|||
# profile directory variables to customize Fusion
|
||||
# to define where it can read custom scripts and tools from
|
||||
master_prefs_variable = f"FUSION{profile_version}_MasterPrefs"
|
||||
master_prefs = Path(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs")
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
master_prefs = Path(
|
||||
FUSION_HOST_DIR, "deploy", "ayon", "fusion_shared.prefs")
|
||||
else:
|
||||
master_prefs = Path(
|
||||
FUSION_HOST_DIR, "deploy", "openpype", "fusion_shared.prefs")
|
||||
|
||||
self.log.info(f"Setting {master_prefs_variable}: {master_prefs}")
|
||||
self.launch_context.env[master_prefs_variable] = str(master_prefs)
|
||||
|
|
|
|||
|
|
@ -69,8 +69,6 @@ class CreateSaver(NewCreator):
|
|||
# TODO Is this needed?
|
||||
saver[file_format]["SaveAlpha"] = 1
|
||||
|
||||
self._imprint(saver, instance_data)
|
||||
|
||||
# Register the CreatedInstance
|
||||
instance = CreatedInstance(
|
||||
family=self.family,
|
||||
|
|
@ -78,6 +76,8 @@ class CreateSaver(NewCreator):
|
|||
data=instance_data,
|
||||
creator=self,
|
||||
)
|
||||
data = instance.data_to_store()
|
||||
self._imprint(saver, data)
|
||||
|
||||
# Insert the transient data
|
||||
instance.transient_data["tool"] = saver
|
||||
|
|
@ -149,9 +149,7 @@ class CreateSaver(NewCreator):
|
|||
|
||||
# get frame padding from anatomy templates
|
||||
anatomy = Anatomy()
|
||||
frame_padding = int(
|
||||
anatomy.templates["render"].get("frame_padding", 4)
|
||||
)
|
||||
frame_padding = anatomy.templates["frame_padding"]
|
||||
|
||||
# Subset change detected
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp
|
||||
)
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.pipeline import (
|
||||
AutoCreator,
|
||||
|
|
@ -68,6 +69,13 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.create_context.host_name
|
||||
|
||||
if existing_instance is None:
|
||||
existing_instance_asset = None
|
||||
elif AYON_SERVER_ENABLED:
|
||||
existing_instance_asset = existing_instance["folderPath"]
|
||||
else:
|
||||
existing_instance_asset = existing_instance["asset"]
|
||||
|
||||
if existing_instance is None:
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
subset_name = self.get_subset_name(
|
||||
|
|
@ -75,10 +83,13 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
project_name, host_name
|
||||
)
|
||||
data = {
|
||||
"asset": asset_name,
|
||||
"task": task_name,
|
||||
"variant": self.default_variant
|
||||
}
|
||||
if AYON_SERVER_ENABLED:
|
||||
data["folderPath"] = asset_name
|
||||
else:
|
||||
data["asset"] = asset_name
|
||||
data.update(self.get_dynamic_data(
|
||||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name, None
|
||||
|
|
@ -91,7 +102,7 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
self._add_instance_to_context(new_instance)
|
||||
|
||||
elif (
|
||||
existing_instance["asset"] != asset_name
|
||||
existing_instance_asset != asset_name
|
||||
or existing_instance["task"] != task_name
|
||||
):
|
||||
asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
|
|
@ -99,6 +110,9 @@ class FusionWorkfileCreator(AutoCreator):
|
|||
self.default_variant, task_name, asset_doc,
|
||||
project_name, host_name
|
||||
)
|
||||
existing_instance["asset"] = asset_name
|
||||
if AYON_SERVER_ENABLED:
|
||||
existing_instance["folderPath"] = asset_name
|
||||
else:
|
||||
existing_instance["asset"] = asset_name
|
||||
existing_instance["task"] = task_name
|
||||
existing_instance["subset"] = subset_name
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin):
|
|||
families = ["animation",
|
||||
"camera",
|
||||
"imagesequence",
|
||||
"render",
|
||||
"yeticache",
|
||||
"pointcache",
|
||||
"render"]
|
||||
|
|
@ -46,6 +47,7 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin):
|
|||
families = ["animation",
|
||||
"camera",
|
||||
"imagesequence",
|
||||
"render",
|
||||
"yeticache",
|
||||
"pointcache",
|
||||
"render"]
|
||||
|
|
|
|||
87
openpype/hosts/fusion/plugins/load/load_usd.py
Normal file
87
openpype/hosts/fusion/plugins/load/load_usd.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from openpype.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
from openpype.hosts.fusion.api.lib import get_fusion_module
|
||||
|
||||
|
||||
class FusionLoadUSD(load.LoaderPlugin):
|
||||
"""Load USD into Fusion
|
||||
|
||||
Support for USD was added since Fusion 18.5
|
||||
"""
|
||||
|
||||
families = ["*"]
|
||||
representations = ["*"]
|
||||
extensions = {"usd", "usda", "usdz"}
|
||||
|
||||
label = "Load USD"
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
tool_type = "uLoader"
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, project_settings, system_settings):
|
||||
super(FusionLoadUSD, cls).apply_settings(project_settings,
|
||||
system_settings)
|
||||
if cls.enabled:
|
||||
# Enable only in Fusion 18.5+
|
||||
fusion = get_fusion_module()
|
||||
version = fusion.GetVersion()
|
||||
major = version[1]
|
||||
minor = version[2]
|
||||
is_usd_supported = (major, minor) >= (18, 5)
|
||||
cls.enabled = is_usd_supported
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
# Fallback to asset name when namespace is None
|
||||
if namespace is None:
|
||||
namespace = context['asset']['name']
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp, "Create tool"):
|
||||
|
||||
path = self.fname
|
||||
|
||||
args = (-32768, -32768)
|
||||
tool = comp.AddTool(self.tool_type, *args)
|
||||
tool["Filename"] = path
|
||||
|
||||
imprint_container(tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def update(self, container, representation):
|
||||
|
||||
tool = container["_tool"]
|
||||
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
|
||||
comp = tool.Comp()
|
||||
|
||||
path = get_representation_path(representation)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update tool"):
|
||||
tool["Filename"] = path
|
||||
|
||||
# Update the imprinted representation
|
||||
tool.SetData("avalon.representation", str(representation["_id"]))
|
||||
|
||||
def remove(self, container):
|
||||
tool = container["_tool"]
|
||||
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
|
||||
comp = tool.Comp()
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Remove tool"):
|
||||
tool.Delete()
|
||||
|
|
@ -25,20 +25,24 @@ def enabled_savers(comp, savers):
|
|||
"""
|
||||
passthrough_key = "TOOLB_PassThrough"
|
||||
original_states = {}
|
||||
enabled_save_names = {saver.Name for saver in savers}
|
||||
enabled_saver_names = {saver.Name for saver in savers}
|
||||
|
||||
all_savers = comp.GetToolList(False, "Saver").values()
|
||||
savers_by_name = {saver.Name: saver for saver in all_savers}
|
||||
|
||||
try:
|
||||
all_savers = comp.GetToolList(False, "Saver").values()
|
||||
for saver in all_savers:
|
||||
original_state = saver.GetAttrs()[passthrough_key]
|
||||
original_states[saver] = original_state
|
||||
original_states[saver.Name] = original_state
|
||||
|
||||
# The passthrough state we want to set (passthrough != enabled)
|
||||
state = saver.Name not in enabled_save_names
|
||||
state = saver.Name not in enabled_saver_names
|
||||
if state != original_state:
|
||||
saver.SetAttrs({passthrough_key: state})
|
||||
yield
|
||||
finally:
|
||||
for saver, original_state in original_states.items():
|
||||
for saver_name, original_state in original_states.items():
|
||||
saver = savers_by_name[saver_name]
|
||||
saver.SetAttrs({"TOOLB_PassThrough": original_state})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin,
|
||||
)
|
||||
|
||||
from openpype.hosts.fusion.api.action import SelectInvalidAction
|
||||
from openpype.hosts.fusion.api import comp_lock_and_undo_chunk
|
||||
|
||||
|
||||
def get_tool_resolution(tool, frame):
|
||||
"""Return the 2D input resolution to a Fusion tool
|
||||
|
||||
If the current tool hasn't been rendered its input resolution
|
||||
hasn't been saved. To combat this, add an expression in
|
||||
the comments field to read the resolution
|
||||
|
||||
Args
|
||||
tool (Fusion Tool): The tool to query input resolution
|
||||
frame (int): The frame to query the resolution on.
|
||||
|
||||
Returns:
|
||||
tuple: width, height as 2-tuple of integers
|
||||
|
||||
"""
|
||||
comp = tool.Composition
|
||||
|
||||
# False undo removes the undo-stack from the undo list
|
||||
with comp_lock_and_undo_chunk(comp, "Read resolution", False):
|
||||
# Save old comment
|
||||
old_comment = ""
|
||||
has_expression = False
|
||||
if tool["Comments"][frame] != "":
|
||||
if tool["Comments"].GetExpression() is not None:
|
||||
has_expression = True
|
||||
old_comment = tool["Comments"].GetExpression()
|
||||
tool["Comments"].SetExpression(None)
|
||||
else:
|
||||
old_comment = tool["Comments"][frame]
|
||||
tool["Comments"][frame] = ""
|
||||
|
||||
# Get input width
|
||||
tool["Comments"].SetExpression("self.Input.OriginalWidth")
|
||||
width = int(tool["Comments"][frame])
|
||||
|
||||
# Get input height
|
||||
tool["Comments"].SetExpression("self.Input.OriginalHeight")
|
||||
height = int(tool["Comments"][frame])
|
||||
|
||||
# Reset old comment
|
||||
tool["Comments"].SetExpression(None)
|
||||
if has_expression:
|
||||
tool["Comments"].SetExpression(old_comment)
|
||||
else:
|
||||
tool["Comments"][frame] = old_comment
|
||||
|
||||
return width, height
|
||||
|
||||
|
||||
class ValidateSaverResolution(
|
||||
pyblish.api.InstancePlugin, OptionalPyblishPluginMixin
|
||||
):
|
||||
"""Validate that the saver input resolution matches the asset resolution"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
label = "Validate Asset Resolution"
|
||||
families = ["render"]
|
||||
hosts = ["fusion"]
|
||||
optional = True
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
resolution = self.get_resolution(instance)
|
||||
expected_resolution = self.get_expected_resolution(instance)
|
||||
if resolution != expected_resolution:
|
||||
raise PublishValidationError(
|
||||
"The input's resolution does not match "
|
||||
"the asset's resolution {}x{}.\n\n"
|
||||
"The input's resolution is {}x{}.".format(
|
||||
expected_resolution[0], expected_resolution[1],
|
||||
resolution[0], resolution[1]
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
resolution = cls.get_resolution(instance)
|
||||
expected_resolution = cls.get_expected_resolution(instance)
|
||||
if resolution != expected_resolution:
|
||||
saver = instance.data["tool"]
|
||||
return [saver]
|
||||
|
||||
@classmethod
|
||||
def get_resolution(cls, instance):
|
||||
saver = instance.data["tool"]
|
||||
first_frame = instance.data["frameStartHandle"]
|
||||
return get_tool_resolution(saver, frame=first_frame)
|
||||
|
||||
@classmethod
|
||||
def get_expected_resolution(cls, instance):
|
||||
data = instance.data["assetEntity"]["data"]
|
||||
return data["resolutionWidth"], data["resolutionHeight"]
|
||||
|
|
@ -13,7 +13,7 @@ var LD_OPENHARMONY_PATH = System.getenv('LIB_OPENHARMONY_PATH');
|
|||
LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH + '/openHarmony.js';
|
||||
LD_OPENHARMONY_PATH = LD_OPENHARMONY_PATH.replace(/\\/g, "/");
|
||||
include(LD_OPENHARMONY_PATH);
|
||||
this.__proto__['$'] = $;
|
||||
//this.__proto__['$'] = $;
|
||||
|
||||
function Client() {
|
||||
var self = this;
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ class ExtractRender(pyblish.api.InstancePlugin):
|
|||
|
||||
args = [application_path, "-batch",
|
||||
"-frames", str(frame_start), str(frame_end),
|
||||
"-scene", scene_path]
|
||||
self.log.info(f"running [ {application_path} {' '.join(args)}")
|
||||
scene_path]
|
||||
self.log.info(f"running: {' '.join(args)}")
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
|
|
|
|||
|
|
@ -95,18 +95,18 @@ def menu_install():
|
|||
|
||||
menu.addSeparator()
|
||||
|
||||
publish_action = menu.addAction("Publish...")
|
||||
publish_action.setIcon(QtGui.QIcon("icons:Output.png"))
|
||||
publish_action.triggered.connect(
|
||||
lambda *args: publish(hiero.ui.mainWindow())
|
||||
)
|
||||
|
||||
creator_action = menu.addAction("Create...")
|
||||
creator_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
|
||||
creator_action.triggered.connect(
|
||||
lambda: host_tools.show_creator(parent=main_window)
|
||||
)
|
||||
|
||||
publish_action = menu.addAction("Publish...")
|
||||
publish_action.setIcon(QtGui.QIcon("icons:Output.png"))
|
||||
publish_action.triggered.connect(
|
||||
lambda *args: publish(hiero.ui.mainWindow())
|
||||
)
|
||||
|
||||
loader_action = menu.addAction("Load...")
|
||||
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
|
||||
loader_action.triggered.connect(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import qargparse
|
|||
from openpype.settings import get_current_project_settings
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import LoaderPlugin, LegacyCreator
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
from openpype.pipeline.load import get_representation_path_from_context
|
||||
from . import lib
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ def load_stylesheet():
|
|||
class CreatorWidget(QtWidgets.QDialog):
|
||||
|
||||
# output items
|
||||
items = dict()
|
||||
items = {}
|
||||
|
||||
def __init__(self, name, info, ui_inputs, parent=None):
|
||||
super(CreatorWidget, self).__init__(parent)
|
||||
|
|
@ -494,9 +493,8 @@ class ClipLoader:
|
|||
joint `data` key with asset.data dict into the representation
|
||||
|
||||
"""
|
||||
asset_name = self.context["representation"]["context"]["asset"]
|
||||
asset_doc = get_current_project_asset(asset_name)
|
||||
log.debug("__ asset_doc: {}".format(pformat(asset_doc)))
|
||||
|
||||
asset_doc = self.context["asset"]
|
||||
self.data["assetData"] = asset_doc["data"]
|
||||
|
||||
def _make_track_item(self, source_bin_item, audio=False):
|
||||
|
|
@ -644,8 +642,8 @@ class PublishClip:
|
|||
Returns:
|
||||
hiero.core.TrackItem: hiero track item object with pype tag
|
||||
"""
|
||||
vertical_clip_match = dict()
|
||||
tag_data = dict()
|
||||
vertical_clip_match = {}
|
||||
tag_data = {}
|
||||
types = {
|
||||
"shot": "shot",
|
||||
"folder": "folder",
|
||||
|
|
@ -707,9 +705,10 @@ class PublishClip:
|
|||
self._create_parents()
|
||||
|
||||
def convert(self):
|
||||
|
||||
# solve track item data and add them to tag data
|
||||
self._convert_to_tag_data()
|
||||
tag_hierarchy_data = self._convert_to_tag_data()
|
||||
|
||||
self.tag_data.update(tag_hierarchy_data)
|
||||
|
||||
# if track name is in review track name and also if driving track name
|
||||
# is not in review track name: skip tag creation
|
||||
|
|
@ -723,16 +722,23 @@ class PublishClip:
|
|||
if self.rename:
|
||||
# rename track item
|
||||
self.track_item.setName(new_name)
|
||||
self.tag_data["asset"] = new_name
|
||||
self.tag_data["asset_name"] = new_name
|
||||
else:
|
||||
self.tag_data["asset"] = self.ti_name
|
||||
self.tag_data["asset_name"] = self.ti_name
|
||||
self.tag_data["hierarchyData"]["shot"] = self.ti_name
|
||||
|
||||
# AYON unique identifier
|
||||
folder_path = "/{}/{}".format(
|
||||
tag_hierarchy_data["hierarchy"],
|
||||
self.tag_data["asset_name"]
|
||||
)
|
||||
self.tag_data["folderPath"] = folder_path
|
||||
if self.tag_data["heroTrack"] and self.review_layer:
|
||||
self.tag_data.update({"reviewTrack": self.review_layer})
|
||||
else:
|
||||
self.tag_data.update({"reviewTrack": None})
|
||||
|
||||
# TODO: remove debug print
|
||||
log.debug("___ self.tag_data: {}".format(
|
||||
pformat(self.tag_data)
|
||||
))
|
||||
|
|
@ -891,7 +897,7 @@ class PublishClip:
|
|||
tag_hierarchy_data = hero_data
|
||||
|
||||
# add data to return data dict
|
||||
self.tag_data.update(tag_hierarchy_data)
|
||||
return tag_hierarchy_data
|
||||
|
||||
def _solve_tag_hierarchy_data(self, hierarchy_formatting_data):
|
||||
""" Solve tag data from hierarchy data and templates. """
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import json
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_asset_name_identifier
|
||||
|
||||
|
||||
class CollectFrameTagInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect frames from tags.
|
||||
|
|
@ -99,6 +101,9 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
# first collect all available subset tag frames
|
||||
subset_data = {}
|
||||
context_asset_doc = context.data["assetEntity"]
|
||||
context_asset_name = get_asset_name_identifier(context_asset_doc)
|
||||
|
||||
for tag_data in sequence_tags:
|
||||
frame = int(tag_data["start"])
|
||||
|
||||
|
|
@ -115,7 +120,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin):
|
|||
subset_data[subset] = {
|
||||
"frames": [frame],
|
||||
"format": tag_data["format"],
|
||||
"asset": context.data["assetEntity"]["name"]
|
||||
"asset": context_asset_name
|
||||
}
|
||||
return subset_data
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import pyblish
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.pipeline.editorial import is_overlapping_otio_ranges
|
||||
|
||||
from openpype.hosts.hiero import api as phiero
|
||||
from openpype.hosts.hiero.api.otio import hiero_export
|
||||
import hiero
|
||||
|
||||
import hiero
|
||||
# # developer reload modules
|
||||
from pprint import pformat
|
||||
|
||||
|
|
@ -80,25 +83,24 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
if k not in ("id", "applieswhole", "label")
|
||||
})
|
||||
|
||||
asset = tag_data["asset"]
|
||||
asset, asset_name = self._get_asset_data(tag_data)
|
||||
|
||||
subset = tag_data["subset"]
|
||||
|
||||
# insert family into families
|
||||
family = tag_data["family"]
|
||||
families = [str(f) for f in tag_data["families"]]
|
||||
families.insert(0, str(family))
|
||||
|
||||
# form label
|
||||
label = asset
|
||||
if asset != clip_name:
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({})".format(clip_name)
|
||||
label += " {}".format(subset)
|
||||
label += " {}".format("[" + ", ".join(families) + "]")
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"label": label,
|
||||
"asset": asset,
|
||||
"asset_name": asset_name,
|
||||
"item": track_item,
|
||||
"families": families,
|
||||
"publish": tag_data["publish"],
|
||||
|
|
@ -176,9 +178,9 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
})
|
||||
|
||||
def create_shot_instance(self, context, **data):
|
||||
subset = "shotMain"
|
||||
master_layer = data.get("heroTrack")
|
||||
hierarchy_data = data.get("hierarchyData")
|
||||
asset = data.get("asset")
|
||||
item = data.get("item")
|
||||
clip_name = item.name()
|
||||
|
||||
|
|
@ -189,23 +191,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
return
|
||||
|
||||
asset = data["asset"]
|
||||
subset = "shotMain"
|
||||
asset_name = data["asset_name"]
|
||||
|
||||
# insert family into families
|
||||
family = "shot"
|
||||
|
||||
# form label
|
||||
label = asset
|
||||
if asset != clip_name:
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({}) ".format(clip_name)
|
||||
label += " {}".format(subset)
|
||||
label += " [{}]".format(family)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"label": label,
|
||||
"subset": subset,
|
||||
"asset": asset,
|
||||
"family": family,
|
||||
"families": []
|
||||
})
|
||||
|
|
@ -215,7 +215,33 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
self.log.debug(
|
||||
"_ instance.data: {}".format(pformat(instance.data)))
|
||||
|
||||
def _get_asset_data(self, data):
|
||||
folder_path = data.pop("folderPath", None)
|
||||
|
||||
if data.get("asset_name"):
|
||||
asset_name = data["asset_name"]
|
||||
else:
|
||||
asset_name = data["asset"]
|
||||
|
||||
# backward compatibility for clip tags
|
||||
# which are missing folderPath key
|
||||
# TODO remove this in future versions
|
||||
if not folder_path:
|
||||
hierarchy_path = data["hierarchy"]
|
||||
folder_path = "/{}/{}".format(
|
||||
hierarchy_path,
|
||||
asset_name
|
||||
)
|
||||
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset = folder_path
|
||||
else:
|
||||
asset = asset_name
|
||||
|
||||
return asset, asset_name
|
||||
|
||||
def create_audio_instance(self, context, **data):
|
||||
subset = "audioMain"
|
||||
master_layer = data.get("heroTrack")
|
||||
|
||||
if not master_layer:
|
||||
|
|
@ -230,23 +256,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
|
|||
return
|
||||
|
||||
asset = data["asset"]
|
||||
subset = "audioMain"
|
||||
asset_name = data["asset_name"]
|
||||
|
||||
# insert family into families
|
||||
family = "audio"
|
||||
|
||||
# form label
|
||||
label = asset
|
||||
if asset != clip_name:
|
||||
label = "{} -".format(asset)
|
||||
if asset_name != clip_name:
|
||||
label += " ({}) ".format(clip_name)
|
||||
label += " {}".format(subset)
|
||||
label += " [{}]".format(family)
|
||||
|
||||
data.update({
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"label": label,
|
||||
"subset": subset,
|
||||
"asset": asset,
|
||||
"family": family,
|
||||
"families": ["clip"]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from qtpy.QtGui import QPixmap
|
|||
|
||||
import hiero.ui
|
||||
|
||||
from openpype import AYON_SERVER_ENABLED
|
||||
from openpype.hosts.hiero.api.otio import hiero_export
|
||||
|
||||
|
||||
|
|
@ -17,9 +18,11 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.CollectorOrder - 0.491
|
||||
|
||||
def process(self, context):
|
||||
|
||||
asset = context.data["asset"]
|
||||
subset = "workfile"
|
||||
asset_name = asset
|
||||
if AYON_SERVER_ENABLED:
|
||||
asset_name = asset_name.split("/")[-1]
|
||||
|
||||
active_timeline = hiero.ui.activeSequence()
|
||||
project = active_timeline.project()
|
||||
fps = active_timeline.framerate().toFloat()
|
||||
|
|
@ -27,7 +30,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
# adding otio timeline to context
|
||||
otio_timeline = hiero_export.create_otio_timeline()
|
||||
|
||||
# get workfile thumnail paths
|
||||
# get workfile thumbnail paths
|
||||
tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
thumbnail_name = "workfile_thumbnail.png"
|
||||
thumbnail_path = os.path.join(tmp_staging, thumbnail_name)
|
||||
|
|
@ -49,8 +52,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
}
|
||||
|
||||
# get workfile paths
|
||||
curent_file = project.path()
|
||||
staging_dir, base_name = os.path.split(curent_file)
|
||||
current_file = project.path()
|
||||
staging_dir, base_name = os.path.split(current_file)
|
||||
|
||||
# creating workfile representation
|
||||
workfile_representation = {
|
||||
|
|
@ -59,13 +62,16 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
'files': base_name,
|
||||
"stagingDir": staging_dir,
|
||||
}
|
||||
|
||||
family = "workfile"
|
||||
instance_data = {
|
||||
"name": "{}_{}".format(asset, subset),
|
||||
"asset": asset,
|
||||
"subset": "{}{}".format(asset, subset.capitalize()),
|
||||
"label": "{} - {}Main".format(
|
||||
asset, family),
|
||||
"name": "{}_{}".format(asset_name, family),
|
||||
"asset": context.data["asset"],
|
||||
# TODO use 'get_subset_name'
|
||||
"subset": "{}{}Main".format(asset_name, family.capitalize()),
|
||||
"item": project,
|
||||
"family": "workfile",
|
||||
"family": family,
|
||||
"families": [],
|
||||
"representations": [workfile_representation, thumb_representation]
|
||||
}
|
||||
|
|
@ -78,7 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
|||
"activeProject": project,
|
||||
"activeTimeline": active_timeline,
|
||||
"otioTimeline": otio_timeline,
|
||||
"currentFile": curent_file,
|
||||
"currentFile": current_file,
|
||||
"colorspace": self.get_colorspace(project),
|
||||
"fps": fps
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue