mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge remote-tracking branch 'upstream/develop' into fix3573
This commit is contained in:
commit
f648e669de
101 changed files with 2759 additions and 1861 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -107,3 +107,6 @@ website/.docusaurus
|
|||
mypy.ini
|
||||
|
||||
tools/run_eventserver.*
|
||||
|
||||
# Developer tools
|
||||
tools/dev_*
|
||||
|
|
|
|||
72
CHANGELOG.md
72
CHANGELOG.md
|
|
@ -1,29 +1,50 @@
|
|||
# Changelog
|
||||
|
||||
## [3.14.2-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.14.3-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.1...HEAD)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Publisher: Add new publisher to host tools [\#3833](https://github.com/pypeclub/OpenPype/pull/3833)
|
||||
- Maya: Workspace mel loaded from settings [\#3790](https://github.com/pypeclub/OpenPype/pull/3790)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Ftrack: Url validation does not require ftrackapp [\#3834](https://github.com/pypeclub/OpenPype/pull/3834)
|
||||
- Maya+Ftrack: Change typo in family name `mayaascii` -\> `mayaAscii` [\#3820](https://github.com/pypeclub/OpenPype/pull/3820)
|
||||
|
||||
## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.2-nightly.5...3.14.2)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- Nuke: Build workfile by template [\#3763](https://github.com/pypeclub/OpenPype/pull/3763)
|
||||
- Houdini: Publishing workfiles [\#3697](https://github.com/pypeclub/OpenPype/pull/3697)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Flame: Adding Creator's retimed shot and handles switch [\#3826](https://github.com/pypeclub/OpenPype/pull/3826)
|
||||
- Flame: OpenPype submenu to batch and media manager [\#3825](https://github.com/pypeclub/OpenPype/pull/3825)
|
||||
- General: Better pixmap scaling [\#3809](https://github.com/pypeclub/OpenPype/pull/3809)
|
||||
- Photoshop: attempt to speed up ExtractImage [\#3793](https://github.com/pypeclub/OpenPype/pull/3793)
|
||||
- SyncServer: Added cli commands for sync server [\#3765](https://github.com/pypeclub/OpenPype/pull/3765)
|
||||
- Blender: Publisher collect workfile representation [\#3670](https://github.com/pypeclub/OpenPype/pull/3670)
|
||||
- Maya: move set render settings menu entry [\#3669](https://github.com/pypeclub/OpenPype/pull/3669)
|
||||
- Scene Inventory: Maya add actions to select from or to scene [\#3659](https://github.com/pypeclub/OpenPype/pull/3659)
|
||||
- Kitsu: Drop 'entities root' setting. [\#3739](https://github.com/pypeclub/OpenPype/pull/3739)
|
||||
- git: update gitignore [\#3722](https://github.com/pypeclub/OpenPype/pull/3722)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- General: Fix Pattern access in client code [\#3828](https://github.com/pypeclub/OpenPype/pull/3828)
|
||||
- Launcher: Skip opening last work file works for groups [\#3822](https://github.com/pypeclub/OpenPype/pull/3822)
|
||||
- Maya: Publishing data key change [\#3811](https://github.com/pypeclub/OpenPype/pull/3811)
|
||||
- Igniter: Fix status handling when version is already installed [\#3804](https://github.com/pypeclub/OpenPype/pull/3804)
|
||||
- Resolve: Addon import is Python 2 compatible [\#3798](https://github.com/pypeclub/OpenPype/pull/3798)
|
||||
- Hiero: retimed clip publishing is working [\#3792](https://github.com/pypeclub/OpenPype/pull/3792)
|
||||
- nuke: validate write node is not failing due wrong type [\#3780](https://github.com/pypeclub/OpenPype/pull/3780)
|
||||
- Fix - changed format of version string in pyproject.toml [\#3777](https://github.com/pypeclub/OpenPype/pull/3777)
|
||||
- Ftrack status fix typo prgoress -\> progress [\#3761](https://github.com/pypeclub/OpenPype/pull/3761)
|
||||
- Fix version resolution [\#3757](https://github.com/pypeclub/OpenPype/pull/3757)
|
||||
- Maya: `containerise` dont skip empty values [\#3674](https://github.com/pypeclub/OpenPype/pull/3674)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
|
|
@ -33,17 +54,21 @@
|
|||
- General: Remove unused teshost [\#3773](https://github.com/pypeclub/OpenPype/pull/3773)
|
||||
- General: Copied 'Extractor' plugin to publish pipeline [\#3771](https://github.com/pypeclub/OpenPype/pull/3771)
|
||||
- General: Move queries of asset and representation links [\#3770](https://github.com/pypeclub/OpenPype/pull/3770)
|
||||
- General: Move create project folders to pipeline [\#3768](https://github.com/pypeclub/OpenPype/pull/3768)
|
||||
- General: Create project function moved to client code [\#3766](https://github.com/pypeclub/OpenPype/pull/3766)
|
||||
- General: Move delivery logic to pipeline [\#3751](https://github.com/pypeclub/OpenPype/pull/3751)
|
||||
- Maya: Refactor submit deadline to use AbstractSubmitDeadline [\#3759](https://github.com/pypeclub/OpenPype/pull/3759)
|
||||
- General: Change publish template settings location [\#3755](https://github.com/pypeclub/OpenPype/pull/3755)
|
||||
- General: Move hostdirname functionality into host [\#3749](https://github.com/pypeclub/OpenPype/pull/3749)
|
||||
- General: Move publish utils to pipeline [\#3745](https://github.com/pypeclub/OpenPype/pull/3745)
|
||||
- Houdini: Define houdini as addon [\#3735](https://github.com/pypeclub/OpenPype/pull/3735)
|
||||
- Fusion: Defined fusion as addon [\#3733](https://github.com/pypeclub/OpenPype/pull/3733)
|
||||
- Flame: Defined flame as addon [\#3732](https://github.com/pypeclub/OpenPype/pull/3732)
|
||||
- Resolve: Define resolve as addon [\#3727](https://github.com/pypeclub/OpenPype/pull/3727)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Standalone Publisher: Ignore empty labels, then still use name like other asset models [\#3779](https://github.com/pypeclub/OpenPype/pull/3779)
|
||||
- Kitsu - sync\_all\_project - add list ignore\_projects [\#3776](https://github.com/pypeclub/OpenPype/pull/3776)
|
||||
|
||||
## [3.14.1](https://github.com/pypeclub/OpenPype/tree/3.14.1) (2022-08-30)
|
||||
|
||||
|
|
@ -52,12 +77,6 @@
|
|||
### 📖 Documentation
|
||||
|
||||
- Documentation: Few updates [\#3698](https://github.com/pypeclub/OpenPype/pull/3698)
|
||||
- Documentation: Settings development [\#3660](https://github.com/pypeclub/OpenPype/pull/3660)
|
||||
|
||||
**🆕 New features**
|
||||
|
||||
- Webpublisher:change create flatten image into tri state [\#3678](https://github.com/pypeclub/OpenPype/pull/3678)
|
||||
- Blender: validators code correction with settings and defaults [\#3662](https://github.com/pypeclub/OpenPype/pull/3662)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
|
|
@ -66,9 +85,6 @@
|
|||
- General: Added helper getters to modules manager [\#3712](https://github.com/pypeclub/OpenPype/pull/3712)
|
||||
- Unreal: Define unreal as module and use host class [\#3701](https://github.com/pypeclub/OpenPype/pull/3701)
|
||||
- Settings: Lock settings UI session [\#3700](https://github.com/pypeclub/OpenPype/pull/3700)
|
||||
- General: Benevolent context label collector [\#3686](https://github.com/pypeclub/OpenPype/pull/3686)
|
||||
- Ftrack: Store ftrack entities on hierarchy integration to instances [\#3677](https://github.com/pypeclub/OpenPype/pull/3677)
|
||||
- Blender: ops refresh manager after process events [\#3663](https://github.com/pypeclub/OpenPype/pull/3663)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
|
|
@ -82,11 +98,10 @@
|
|||
- Settings: Fix project overrides save [\#3708](https://github.com/pypeclub/OpenPype/pull/3708)
|
||||
- Workfiles tool: Fix published workfile filtering [\#3704](https://github.com/pypeclub/OpenPype/pull/3704)
|
||||
- PS, AE: Provide default variant value for workfile subset [\#3703](https://github.com/pypeclub/OpenPype/pull/3703)
|
||||
- Flame: retime is working on clip publishing [\#3684](https://github.com/pypeclub/OpenPype/pull/3684)
|
||||
- Webpublisher: added check for empty context [\#3682](https://github.com/pypeclub/OpenPype/pull/3682)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- General: Move delivery logic to pipeline [\#3751](https://github.com/pypeclub/OpenPype/pull/3751)
|
||||
- General: Host addons cleanup [\#3744](https://github.com/pypeclub/OpenPype/pull/3744)
|
||||
- Webpublisher: Webpublisher is used as addon [\#3740](https://github.com/pypeclub/OpenPype/pull/3740)
|
||||
- Photoshop: Defined photoshop as addon [\#3736](https://github.com/pypeclub/OpenPype/pull/3736)
|
||||
|
|
@ -110,32 +125,11 @@
|
|||
|
||||
- Hiero: Define hiero as module [\#3717](https://github.com/pypeclub/OpenPype/pull/3717)
|
||||
- Deadline: better logging for DL webservice failures [\#3694](https://github.com/pypeclub/OpenPype/pull/3694)
|
||||
- Photoshop: resize saved images in ExtractReview for ffmpeg [\#3676](https://github.com/pypeclub/OpenPype/pull/3676)
|
||||
|
||||
## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.0-nightly.1...3.14.0)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Ftrack: Addiotional component metadata [\#3685](https://github.com/pypeclub/OpenPype/pull/3685)
|
||||
- Ftrack: Set task status on farm publishing [\#3680](https://github.com/pypeclub/OpenPype/pull/3680)
|
||||
- Ftrack: Set task status on task creation in integrate hierarchy [\#3675](https://github.com/pypeclub/OpenPype/pull/3675)
|
||||
- Maya: Disable rendering of all lights for render instances submitted through Deadline. [\#3661](https://github.com/pypeclub/OpenPype/pull/3661)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- General: Switch from hero version to versioned works [\#3691](https://github.com/pypeclub/OpenPype/pull/3691)
|
||||
- General: Fix finding of last version [\#3656](https://github.com/pypeclub/OpenPype/pull/3656)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- General: Use client projects getter [\#3673](https://github.com/pypeclub/OpenPype/pull/3673)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Deadline: Global job pre load is not Pype 2 compatible [\#3666](https://github.com/pypeclub/OpenPype/pull/3666)
|
||||
|
||||
## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.13.0-nightly.1...3.13.0)
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -41,7 +41,7 @@ It can be built and ran on all common platforms. We develop and test on the foll
|
|||
- **Linux**
|
||||
- **Ubuntu** 20.04 LTS
|
||||
- **Centos** 7
|
||||
- **Mac OSX**
|
||||
- **Mac OSX**
|
||||
- **10.15** Catalina
|
||||
- **11.1** Big Sur (using Rosetta2)
|
||||
|
||||
|
|
@ -287,6 +287,14 @@ To run tests, execute `.\tools\run_tests(.ps1|.sh)`.
|
|||
|
||||
**Note that it needs existing virtual environment.**
|
||||
|
||||
|
||||
Developer tools
|
||||
-------------
|
||||
|
||||
In case you wish to add your own tools to `.\tools` folder without git tracking, it is possible by adding it with `dev_*` suffix (example: `dev_clear_pyc(.ps1|.sh)`).
|
||||
|
||||
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ from bson.objectid import ObjectId
|
|||
|
||||
from .mongo import get_project_database, get_project_connection
|
||||
|
||||
PatternType = type(re.compile(""))
|
||||
|
||||
|
||||
def _prepare_fields(fields, required_fields=None):
|
||||
if not fields:
|
||||
|
|
@ -1054,11 +1056,11 @@ def _regex_filters(filters):
|
|||
for key, value in filters.items():
|
||||
regexes = []
|
||||
a_values = []
|
||||
if isinstance(value, re.Pattern):
|
||||
if isinstance(value, PatternType):
|
||||
regexes.append(value)
|
||||
elif isinstance(value, (list, tuple, set)):
|
||||
for item in value:
|
||||
if isinstance(item, re.Pattern):
|
||||
if isinstance(item, PatternType):
|
||||
regexes.append(item)
|
||||
else:
|
||||
a_values.append(item)
|
||||
|
|
@ -1194,7 +1196,7 @@ def get_representations(
|
|||
as filter. Filter ignored if 'None' is passed.
|
||||
version_ids (Iterable[str]): Subset ids used as parent filter. Filter
|
||||
ignored if 'None' is passed.
|
||||
context_filters (Dict[str, List[str, re.Pattern]]): Filter by
|
||||
context_filters (Dict[str, List[str, PatternType]]): Filter by
|
||||
representation context fields.
|
||||
names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering
|
||||
using version ids and list of names under the version.
|
||||
|
|
@ -1240,7 +1242,7 @@ def get_archived_representations(
|
|||
as filter. Filter ignored if 'None' is passed.
|
||||
version_ids (Iterable[str]): Subset ids used as parent filter. Filter
|
||||
ignored if 'None' is passed.
|
||||
context_filters (Dict[str, List[str, re.Pattern]]): Filter by
|
||||
context_filters (Dict[str, List[str, PatternType]]): Filter by
|
||||
representation context fields.
|
||||
names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering
|
||||
using version ids and list of names under the version.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ from .pipeline import (
|
|||
)
|
||||
from .menu import (
|
||||
FlameMenuProjectConnect,
|
||||
FlameMenuTimeline
|
||||
FlameMenuTimeline,
|
||||
FlameMenuUniversal
|
||||
)
|
||||
from .plugin import (
|
||||
Creator,
|
||||
|
|
@ -131,6 +132,7 @@ __all__ = [
|
|||
# menu
|
||||
"FlameMenuProjectConnect",
|
||||
"FlameMenuTimeline",
|
||||
"FlameMenuUniversal",
|
||||
|
||||
# plugin
|
||||
"Creator",
|
||||
|
|
|
|||
|
|
@ -201,3 +201,53 @@ class FlameMenuTimeline(_FlameMenuApp):
|
|||
if self.flame:
|
||||
self.flame.execute_shortcut('Rescan Python Hooks')
|
||||
self.log.info('Rescan Python Hooks')
|
||||
|
||||
|
||||
class FlameMenuUniversal(_FlameMenuApp):
|
||||
|
||||
# flameMenuProjectconnect app takes care of the preferences dialog as well
|
||||
|
||||
def __init__(self, framework):
|
||||
_FlameMenuApp.__init__(self, framework)
|
||||
|
||||
def __getattr__(self, name):
|
||||
def method(*args, **kwargs):
|
||||
project = self.dynamic_menu_data.get(name)
|
||||
if project:
|
||||
self.link_project(project)
|
||||
return method
|
||||
|
||||
def build_menu(self):
|
||||
if not self.flame:
|
||||
return []
|
||||
|
||||
menu = deepcopy(self.menu)
|
||||
|
||||
menu['actions'].append({
|
||||
"name": "Load...",
|
||||
"execute": lambda x: self.tools_helper.show_loader()
|
||||
})
|
||||
menu['actions'].append({
|
||||
"name": "Manage...",
|
||||
"execute": lambda x: self.tools_helper.show_scene_inventory()
|
||||
})
|
||||
menu['actions'].append({
|
||||
"name": "Library...",
|
||||
"execute": lambda x: self.tools_helper.show_library_loader()
|
||||
})
|
||||
return menu
|
||||
|
||||
def refresh(self, *args, **kwargs):
|
||||
self.rescan()
|
||||
|
||||
def rescan(self, *args, **kwargs):
|
||||
if not self.flame:
|
||||
try:
|
||||
import flame
|
||||
self.flame = flame
|
||||
except ImportError:
|
||||
self.flame = None
|
||||
|
||||
if self.flame:
|
||||
self.flame.execute_shortcut('Rescan Python Hooks')
|
||||
self.log.info('Rescan Python Hooks')
|
||||
|
|
|
|||
|
|
@ -361,6 +361,8 @@ class PublishableClip:
|
|||
index_from_segment_default = False
|
||||
use_shot_name_default = False
|
||||
include_handles_default = False
|
||||
retimed_handles_default = True
|
||||
retimed_framerange_default = True
|
||||
|
||||
def __init__(self, segment, **kwargs):
|
||||
self.rename_index = kwargs["rename_index"]
|
||||
|
|
@ -496,6 +498,14 @@ class PublishableClip:
|
|||
"audio", {}).get("value") or False
|
||||
self.include_handles = self.ui_inputs.get(
|
||||
"includeHandles", {}).get("value") or self.include_handles_default
|
||||
self.retimed_handles = (
|
||||
self.ui_inputs.get("retimedHandles", {}).get("value")
|
||||
or self.retimed_handles_default
|
||||
)
|
||||
self.retimed_framerange = (
|
||||
self.ui_inputs.get("retimedFramerange", {}).get("value")
|
||||
or self.retimed_framerange_default
|
||||
)
|
||||
|
||||
# build subset name from layer name
|
||||
if self.subset_name == "[ track name ]":
|
||||
|
|
|
|||
|
|
@ -276,6 +276,22 @@ class CreateShotClip(opfapi.Creator):
|
|||
"target": "tag",
|
||||
"toolTip": "By default handles are excluded", # noqa
|
||||
"order": 3
|
||||
},
|
||||
"retimedHandles": {
|
||||
"value": True,
|
||||
"type": "QCheckBox",
|
||||
"label": "Retimed handles",
|
||||
"target": "tag",
|
||||
"toolTip": "By default handles are retimed.", # noqa
|
||||
"order": 4
|
||||
},
|
||||
"retimedFramerange": {
|
||||
"value": True,
|
||||
"type": "QCheckBox",
|
||||
"label": "Retimed framerange",
|
||||
"target": "tag",
|
||||
"toolTip": "By default framerange is retimed.", # noqa
|
||||
"order": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,10 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
"fps": self.fps,
|
||||
"workfileFrameStart": workfile_start,
|
||||
"sourceFirstFrame": int(first_frame),
|
||||
"notRetimedHandles": (
|
||||
not marker_data.get("retimedHandles")),
|
||||
"notRetimedFramerange": (
|
||||
not marker_data.get("retimedFramerange")),
|
||||
"path": file_path,
|
||||
"flameAddTasks": self.add_tasks,
|
||||
"tasks": {
|
||||
|
|
|
|||
|
|
@ -90,26 +90,38 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
handle_end = instance.data["handleEnd"]
|
||||
handles = max(handle_start, handle_end)
|
||||
include_handles = instance.data.get("includeHandles")
|
||||
retimed_handles = instance.data.get("retimedHandles")
|
||||
|
||||
# get media source range with handles
|
||||
source_start_handles = instance.data["sourceStartH"]
|
||||
source_end_handles = instance.data["sourceEndH"]
|
||||
|
||||
# retime if needed
|
||||
if r_speed != 1.0:
|
||||
source_start_handles = (
|
||||
instance.data["sourceStart"] - r_handle_start)
|
||||
source_end_handles = (
|
||||
source_start_handles
|
||||
+ (r_source_dur - 1)
|
||||
+ r_handle_start
|
||||
+ r_handle_end
|
||||
)
|
||||
if retimed_handles:
|
||||
# handles are retimed
|
||||
source_start_handles = (
|
||||
instance.data["sourceStart"] - r_handle_start)
|
||||
source_end_handles = (
|
||||
source_start_handles
|
||||
+ (r_source_dur - 1)
|
||||
+ r_handle_start
|
||||
+ r_handle_end
|
||||
)
|
||||
else:
|
||||
# handles are not retimed
|
||||
source_end_handles = (
|
||||
source_start_handles
|
||||
+ (r_source_dur - 1)
|
||||
+ handle_start
|
||||
+ handle_end
|
||||
)
|
||||
|
||||
# get frame range with handles for representation range
|
||||
frame_start_handle = frame_start - handle_start
|
||||
repre_frame_start = frame_start_handle
|
||||
if include_handles:
|
||||
if r_speed == 1.0:
|
||||
if r_speed == 1.0 or not retimed_handles:
|
||||
frame_start_handle = frame_start
|
||||
else:
|
||||
frame_start_handle = (
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ def load_apps():
|
|||
opfapi.FlameMenuProjectConnect(opfapi.CTX.app_framework))
|
||||
opfapi.CTX.flame_apps.append(
|
||||
opfapi.FlameMenuTimeline(opfapi.CTX.app_framework))
|
||||
opfapi.CTX.flame_apps.append(
|
||||
opfapi.FlameMenuUniversal(opfapi.CTX.app_framework))
|
||||
opfapi.CTX.app_framework.log.info("Apps are loaded")
|
||||
|
||||
|
||||
|
|
@ -191,3 +193,27 @@ def get_timeline_custom_ui_actions():
|
|||
openpype_install()
|
||||
|
||||
return _build_app_menu("FlameMenuTimeline")
|
||||
|
||||
|
||||
def get_batch_custom_ui_actions():
|
||||
"""Hook to create submenu in batch
|
||||
|
||||
Returns:
|
||||
list: menu object
|
||||
"""
|
||||
# install openpype and the host
|
||||
openpype_install()
|
||||
|
||||
return _build_app_menu("FlameMenuUniversal")
|
||||
|
||||
|
||||
def get_media_panel_custom_ui_actions():
|
||||
"""Hook to create submenu in desktop
|
||||
|
||||
Returns:
|
||||
list: menu object
|
||||
"""
|
||||
# install openpype and the host
|
||||
openpype_install()
|
||||
|
||||
return _build_app_menu("FlameMenuUniversal")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
from .addon import (
|
||||
FusionAddon,
|
||||
FUSION_HOST_DIR,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"FusionAddon",
|
||||
"FUSION_HOST_DIR",
|
||||
)
|
||||
23
openpype/hosts/fusion/addon.py
Normal file
23
openpype/hosts/fusion/addon.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
|
||||
FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class FusionAddon(OpenPypeModule, IHostAddon):
|
||||
name = "fusion"
|
||||
host_name = "fusion"
|
||||
|
||||
def initialize(self, module_settings):
|
||||
self.enabled = True
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
if app.host_name != self.host_name:
|
||||
return []
|
||||
return [
|
||||
os.path.join(FUSION_HOST_DIR, "hooks")
|
||||
]
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".comp"]
|
||||
|
|
@ -18,12 +18,11 @@ from openpype.pipeline import (
|
|||
deregister_inventory_action_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
import openpype.hosts.fusion
|
||||
from openpype.hosts.fusion import FUSION_HOST_DIR
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.fusion.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
PLUGINS_DIR = os.path.join(FUSION_HOST_DIR, "plugins")
|
||||
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
from openpype.pipeline import HOST_WORKFILE_EXTENSIONS
|
||||
|
||||
from .pipeline import get_current_comp
|
||||
|
||||
|
||||
def file_extensions():
|
||||
return HOST_WORKFILE_EXTENSIONS["fusion"]
|
||||
return [".comp"]
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
|
|
|
|||
114
openpype/hosts/fusion/plugins/publish/collect_inputs.py
Normal file
114
openpype/hosts/fusion/plugins/publish/collect_inputs.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
from bson.objectid import ObjectId
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
|
||||
|
||||
def collect_input_containers(tools):
|
||||
"""Collect containers that contain any of the node in `nodes`.
|
||||
|
||||
This will return any loaded Avalon container that contains at least one of
|
||||
the nodes. As such, the Avalon container is an input for it. Or in short,
|
||||
there are member nodes of that container.
|
||||
|
||||
Returns:
|
||||
list: Input avalon containers
|
||||
|
||||
"""
|
||||
|
||||
# Lookup by node ids
|
||||
lookup = frozenset([tool.Name for tool in tools])
|
||||
|
||||
containers = []
|
||||
host = registered_host()
|
||||
for container in host.ls():
|
||||
|
||||
name = container["_tool"].Name
|
||||
|
||||
# We currently assume no "groups" as containers but just single tools
|
||||
# like a single "Loader" operator. As such we just check whether the
|
||||
# Loader is part of the processing queue.
|
||||
if name in lookup:
|
||||
containers.append(container)
|
||||
|
||||
return containers
|
||||
|
||||
|
||||
def iter_upstream(tool):
|
||||
"""Yields all upstream inputs for the current tool.
|
||||
|
||||
Yields:
|
||||
tool: The input tools.
|
||||
|
||||
"""
|
||||
|
||||
def get_connected_input_tools(tool):
|
||||
"""Helper function that returns connected input tools for a tool."""
|
||||
inputs = []
|
||||
|
||||
# Filter only to actual types that will have sensible upstream
|
||||
# connections. So we ignore just "Number" inputs as they can be
|
||||
# many to iterate, slowing things down quite a bit - and in practice
|
||||
# they don't have upstream connections.
|
||||
VALID_INPUT_TYPES = ['Image', 'Particles', 'Mask', 'DataType3D']
|
||||
for type_ in VALID_INPUT_TYPES:
|
||||
for input_ in tool.GetInputList(type_).values():
|
||||
output = input_.GetConnectedOutput()
|
||||
if output:
|
||||
input_tool = output.GetTool()
|
||||
inputs.append(input_tool)
|
||||
|
||||
return inputs
|
||||
|
||||
# Initialize process queue with the node's inputs itself
|
||||
queue = get_connected_input_tools(tool)
|
||||
|
||||
# We keep track of which node names we have processed so far, to ensure we
|
||||
# don't process the same hierarchy again. We are not pushing the tool
|
||||
# itself into the set as that doesn't correctly recognize the same tool.
|
||||
# Since tool names are unique in a comp in Fusion we rely on that.
|
||||
collected = set(tool.Name for tool in queue)
|
||||
|
||||
# Traverse upstream references for all nodes and yield them as we
|
||||
# process the queue.
|
||||
while queue:
|
||||
upstream_tool = queue.pop()
|
||||
yield upstream_tool
|
||||
|
||||
# Find upstream tools that are not collected yet.
|
||||
upstream_inputs = get_connected_input_tools(upstream_tool)
|
||||
upstream_inputs = [t for t in upstream_inputs if
|
||||
t.Name not in collected]
|
||||
|
||||
queue.extend(upstream_inputs)
|
||||
collected.update(tool.Name for tool in upstream_inputs)
|
||||
|
||||
|
||||
class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
||||
"""Collect source input containers used for this publish.
|
||||
|
||||
This will include `inputs` data of which loaded publishes were used in the
|
||||
generation of this publish. This leaves an upstream trace to what was used
|
||||
as input.
|
||||
|
||||
"""
|
||||
|
||||
label = "Collect Inputs"
|
||||
order = pyblish.api.CollectorOrder + 0.2
|
||||
hosts = ["fusion"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# Get all upstream and include itself
|
||||
tool = instance[0]
|
||||
nodes = list(iter_upstream(tool))
|
||||
nodes.append(tool)
|
||||
|
||||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(nodes)
|
||||
|
||||
inputs = [ObjectId(c["representation"]) for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
|
|
@ -14,7 +14,7 @@ from openpype.pipeline import (
|
|||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.hosts.houdini import HOUDINI_HOST_DIR
|
||||
from openpype.hosts.houdini.api import lib
|
||||
from openpype.hosts.houdini.api import lib, shelves
|
||||
|
||||
from openpype.lib import (
|
||||
register_event_callback,
|
||||
|
|
@ -73,6 +73,7 @@ def install():
|
|||
# so it initializes into the correct scene FPS, Frame Range, etc.
|
||||
# todo: make sure this doesn't trigger when opening with last workfile
|
||||
_set_context_settings()
|
||||
shelves.generate_shelves()
|
||||
|
||||
|
||||
def uninstall():
|
||||
|
|
|
|||
204
openpype/hosts/houdini/api/shelves.py
Normal file
204
openpype/hosts/houdini/api/shelves.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import os
|
||||
import logging
|
||||
import platform
|
||||
import six
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
import hou
|
||||
|
||||
log = logging.getLogger("openpype.hosts.houdini.shelves")
|
||||
|
||||
if six.PY2:
|
||||
FileNotFoundError = IOError
|
||||
|
||||
|
||||
def generate_shelves():
|
||||
"""This function generates complete shelves from shelf set to tools
|
||||
in Houdini from openpype project settings houdini shelf definition.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: Raised when the shelf set filepath does not exist
|
||||
"""
|
||||
current_os = platform.system().lower()
|
||||
|
||||
# load configuration of houdini shelves
|
||||
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
|
||||
shelves_set_config = project_settings["houdini"]["shelves"]
|
||||
|
||||
if not shelves_set_config:
|
||||
log.debug(
|
||||
"No custom shelves found in project settings."
|
||||
)
|
||||
return
|
||||
|
||||
for shelf_set_config in shelves_set_config:
|
||||
shelf_set_filepath = shelf_set_config.get('shelf_set_source_path')
|
||||
|
||||
if shelf_set_filepath[current_os]:
|
||||
if not os.path.isfile(shelf_set_filepath[current_os]):
|
||||
raise FileNotFoundError(
|
||||
"This path doesn't exist - {}".format(
|
||||
shelf_set_filepath[current_os]
|
||||
)
|
||||
)
|
||||
|
||||
hou.shelves.newShelfSet(file_path=shelf_set_filepath[current_os])
|
||||
continue
|
||||
|
||||
shelf_set_name = shelf_set_config.get('shelf_set_name')
|
||||
if not shelf_set_name:
|
||||
log.warning(
|
||||
"No name found in shelf set definition."
|
||||
)
|
||||
return
|
||||
|
||||
shelf_set = get_or_create_shelf_set(shelf_set_name)
|
||||
|
||||
shelves_definition = shelf_set_config.get('shelf_definition')
|
||||
|
||||
if not shelves_definition:
|
||||
log.debug(
|
||||
"No shelf definition found for shelf set named '{}'".format(
|
||||
shelf_set_name
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
for shelf_definition in shelves_definition:
|
||||
shelf_name = shelf_definition.get('shelf_name')
|
||||
if not shelf_name:
|
||||
log.warning(
|
||||
"No name found in shelf definition."
|
||||
)
|
||||
return
|
||||
|
||||
shelf = get_or_create_shelf(shelf_name)
|
||||
|
||||
if not shelf_definition.get('tools_list'):
|
||||
log.debug(
|
||||
"No tool definition found for shelf named {}".format(
|
||||
shelf_name
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
mandatory_attributes = {'name', 'script'}
|
||||
for tool_definition in shelf_definition.get('tools_list'):
|
||||
# We verify that the name and script attibutes of the tool
|
||||
# are set
|
||||
if not all(
|
||||
tool_definition[key] for key in mandatory_attributes
|
||||
):
|
||||
log.warning(
|
||||
"You need to specify at least the name and \
|
||||
the script path of the tool.")
|
||||
continue
|
||||
|
||||
tool = get_or_create_tool(tool_definition, shelf)
|
||||
|
||||
if not tool:
|
||||
return
|
||||
|
||||
# Add the tool to the shelf if not already in it
|
||||
if tool not in shelf.tools():
|
||||
shelf.setTools(list(shelf.tools()) + [tool])
|
||||
|
||||
# Add the shelf in the shelf set if not already in it
|
||||
if shelf not in shelf_set.shelves():
|
||||
shelf_set.setShelves(shelf_set.shelves() + (shelf,))
|
||||
|
||||
|
||||
def get_or_create_shelf_set(shelf_set_label):
|
||||
"""This function verifies if the shelf set label exists. If not,
|
||||
creates a new shelf set.
|
||||
|
||||
Arguments:
|
||||
shelf_set_label (str): The label of the shelf set
|
||||
|
||||
Returns:
|
||||
hou.ShelfSet: The shelf set existing or the new one
|
||||
"""
|
||||
all_shelves_sets = hou.shelves.shelfSets().values()
|
||||
|
||||
shelf_sets = [
|
||||
shelf for shelf in all_shelves_sets if shelf.label() == shelf_set_label
|
||||
]
|
||||
|
||||
if shelf_sets:
|
||||
return shelf_sets[0]
|
||||
|
||||
shelf_set_name = shelf_set_label.replace(' ', '_').lower()
|
||||
new_shelf_set = hou.shelves.newShelfSet(
|
||||
name=shelf_set_name,
|
||||
label=shelf_set_label
|
||||
)
|
||||
return new_shelf_set
|
||||
|
||||
|
||||
def get_or_create_shelf(shelf_label):
|
||||
"""This function verifies if the shelf label exists. If not, creates
|
||||
a new shelf.
|
||||
|
||||
Arguments:
|
||||
shelf_label (str): The label of the shelf
|
||||
|
||||
Returns:
|
||||
hou.Shelf: The shelf existing or the new one
|
||||
"""
|
||||
all_shelves = hou.shelves.shelves().values()
|
||||
|
||||
shelf = [s for s in all_shelves if s.label() == shelf_label]
|
||||
|
||||
if shelf:
|
||||
return shelf[0]
|
||||
|
||||
shelf_name = shelf_label.replace(' ', '_').lower()
|
||||
new_shelf = hou.shelves.newShelf(
|
||||
name=shelf_name,
|
||||
label=shelf_label
|
||||
)
|
||||
return new_shelf
|
||||
|
||||
|
||||
def get_or_create_tool(tool_definition, shelf):
|
||||
"""This function verifies if the tool exists and updates it. If not, creates
|
||||
a new one.
|
||||
|
||||
Arguments:
|
||||
tool_definition (dict): Dict with label, script, icon and help
|
||||
shelf (hou.Shelf): The parent shelf of the tool
|
||||
|
||||
Returns:
|
||||
hou.Tool: The tool updated or the new one
|
||||
"""
|
||||
existing_tools = shelf.tools()
|
||||
tool_label = tool_definition.get('label')
|
||||
|
||||
existing_tool = [
|
||||
tool for tool in existing_tools if tool.label() == tool_label
|
||||
]
|
||||
|
||||
if existing_tool:
|
||||
tool_definition.pop('name', None)
|
||||
tool_definition.pop('label', None)
|
||||
existing_tool[0].setData(**tool_definition)
|
||||
return existing_tool[0]
|
||||
|
||||
tool_name = tool_label.replace(' ', '_').lower()
|
||||
|
||||
if not os.path.exists(tool_definition['script']):
|
||||
log.warning(
|
||||
"This path doesn't exist - {}".format(
|
||||
tool_definition['script']
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
with open(tool_definition['script']) as f:
|
||||
script = f.read()
|
||||
tool_definition.update({'script': script})
|
||||
|
||||
new_tool = hou.shelves.newTool(name=tool_name, **tool_definition)
|
||||
|
||||
return new_tool
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
from bson.objectid import ObjectId
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
|
|
@ -115,7 +117,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
|||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(nodes)
|
||||
|
||||
inputs = [c["representation"] for c in containers]
|
||||
instance.data["inputs"] = inputs
|
||||
inputs = [ObjectId(c["representation"]) for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import pyblish.api
|
|||
|
||||
from openpype.lib import version_up
|
||||
from openpype.pipeline import registered_host
|
||||
from openpype.pipeline.publish import get_errored_plugins_from_context
|
||||
|
||||
|
||||
class IncrementCurrentFile(pyblish.api.InstancePlugin):
|
||||
class IncrementCurrentFile(pyblish.api.ContextPlugin):
|
||||
"""Increment the current file.
|
||||
|
||||
Saves the current scene with an increased version number.
|
||||
|
|
@ -15,30 +14,10 @@ class IncrementCurrentFile(pyblish.api.InstancePlugin):
|
|||
label = "Increment current file"
|
||||
order = pyblish.api.IntegratorOrder + 9.0
|
||||
hosts = ["houdini"]
|
||||
families = ["colorbleed.usdrender", "redshift_rop"]
|
||||
targets = ["local"]
|
||||
families = ["workfile"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# This should be a ContextPlugin, but this is a workaround
|
||||
# for a bug in pyblish to run once for a family: issue #250
|
||||
context = instance.context
|
||||
key = "__hasRun{}".format(self.__class__.__name__)
|
||||
if context.data.get(key, False):
|
||||
return
|
||||
else:
|
||||
context.data[key] = True
|
||||
|
||||
context = instance.context
|
||||
errored_plugins = get_errored_plugins_from_context(context)
|
||||
if any(
|
||||
plugin.__name__ == "HoudiniSubmitPublishDeadline"
|
||||
for plugin in errored_plugins
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Skipping incrementing current file because "
|
||||
"submission to deadline failed."
|
||||
)
|
||||
def process(self, context):
|
||||
|
||||
# Filename must not have changed since collecting
|
||||
host = registered_host()
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import pyblish.api
|
||||
|
||||
import hou
|
||||
from openpype.lib import version_up
|
||||
from openpype.pipeline.publish import get_errored_plugins_from_context
|
||||
|
||||
|
||||
class IncrementCurrentFileDeadline(pyblish.api.ContextPlugin):
|
||||
"""Increment the current file.
|
||||
|
||||
Saves the current scene with an increased version number.
|
||||
|
||||
"""
|
||||
|
||||
label = "Increment current file"
|
||||
order = pyblish.api.IntegratorOrder + 9.0
|
||||
hosts = ["houdini"]
|
||||
targets = ["deadline"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
errored_plugins = get_errored_plugins_from_context(context)
|
||||
if any(
|
||||
plugin.__name__ == "HoudiniSubmitPublishDeadline"
|
||||
for plugin in errored_plugins
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Skipping incrementing current file because "
|
||||
"submission to deadline failed."
|
||||
)
|
||||
|
||||
current_filepath = context.data["currentFile"]
|
||||
new_filepath = version_up(current_filepath)
|
||||
|
||||
hou.hipFile.save(file_name=new_filepath, save_to_recent_files=True)
|
||||
|
|
@ -2483,7 +2483,7 @@ def load_capture_preset(data=None):
|
|||
# DISPLAY OPTIONS
|
||||
id = 'Display Options'
|
||||
disp_options = {}
|
||||
for key in preset['Display Options']:
|
||||
for key in preset[id]:
|
||||
if key.startswith('background'):
|
||||
disp_options[key] = preset['Display Options'][key]
|
||||
if len(disp_options[key]) == 4:
|
||||
|
|
|
|||
|
|
@ -348,3 +348,71 @@ def get_attr_overrides(node_attr, layer,
|
|||
break
|
||||
|
||||
return reversed(plug_overrides)
|
||||
|
||||
|
||||
def get_shader_in_layer(node, layer):
|
||||
"""Return the assigned shader in a renderlayer without switching layers.
|
||||
|
||||
This has been developed and tested for Legacy Renderlayers and *not* for
|
||||
Render Setup.
|
||||
|
||||
Note: This will also return the shader for any face assignments, however
|
||||
it will *not* return the components they are assigned to. This could
|
||||
be implemented, but since Maya's renderlayers are famous for breaking
|
||||
with face assignments there has been no need for this function to
|
||||
support that.
|
||||
|
||||
Returns:
|
||||
list: The list of assigned shaders in the given layer.
|
||||
|
||||
"""
|
||||
|
||||
def _get_connected_shader(plug):
|
||||
"""Return current shader"""
|
||||
return cmds.listConnections(plug,
|
||||
source=False,
|
||||
destination=True,
|
||||
plugs=False,
|
||||
connections=False,
|
||||
type="shadingEngine") or []
|
||||
|
||||
# We check the instObjGroups (shader connection) for layer overrides.
|
||||
plug = node + ".instObjGroups"
|
||||
|
||||
# Ignore complex query if we're in the layer anyway (optimization)
|
||||
current_layer = cmds.editRenderLayerGlobals(query=True,
|
||||
currentRenderLayer=True)
|
||||
if layer == current_layer:
|
||||
return _get_connected_shader(plug)
|
||||
|
||||
connections = cmds.listConnections(plug,
|
||||
plugs=True,
|
||||
source=False,
|
||||
destination=True,
|
||||
type="renderLayer") or []
|
||||
connections = filter(lambda x: x.endswith(".outPlug"), connections)
|
||||
if not connections:
|
||||
# If no overrides anywhere on the shader, just get the current shader
|
||||
return _get_connected_shader(plug)
|
||||
|
||||
def _get_override(connections, layer):
|
||||
"""Return the overridden connection for that layer in connections"""
|
||||
# If there's an override on that layer, return that.
|
||||
for connection in connections:
|
||||
if (connection.startswith(layer + ".outAdjustments") and
|
||||
connection.endswith(".outPlug")):
|
||||
|
||||
# This is a shader override on that layer so get the shader
|
||||
# connected to .outValue of the .outAdjustment[i]
|
||||
out_adjustment = connection.rsplit(".", 1)[0]
|
||||
connection_attr = out_adjustment + ".outValue"
|
||||
override = cmds.listConnections(connection_attr) or []
|
||||
|
||||
return override
|
||||
|
||||
override_shader = _get_override(connections, layer)
|
||||
if override_shader is not None:
|
||||
return override_shader
|
||||
else:
|
||||
# Get the override for "defaultRenderLayer" (=masterLayer)
|
||||
return _get_override(connections, layer="defaultRenderLayer")
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ from openpype.pipeline import (
|
|||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.hosts.maya import MAYA_ROOT_DIR
|
||||
from openpype.hosts.maya.lib import copy_workspace_mel
|
||||
from openpype.hosts.maya.lib import create_workspace_mel
|
||||
|
||||
from . import menu, lib
|
||||
from .workio import (
|
||||
|
|
@ -63,7 +63,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
|
|||
self._op_events = {}
|
||||
|
||||
def install(self):
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
project_name = legacy_io.active_project()
|
||||
project_settings = get_project_settings(project_name)
|
||||
# process path mapping
|
||||
dirmap_processor = MayaDirmap("maya", project_name, project_settings)
|
||||
|
|
@ -533,7 +533,7 @@ def on_task_changed():
|
|||
lib.update_content_on_context_change()
|
||||
|
||||
msg = " project: {}\n asset: {}\n task:{}".format(
|
||||
legacy_io.Session["AVALON_PROJECT"],
|
||||
legacy_io.active_project(),
|
||||
legacy_io.Session["AVALON_ASSET"],
|
||||
legacy_io.Session["AVALON_TASK"]
|
||||
)
|
||||
|
|
@ -545,9 +545,10 @@ def on_task_changed():
|
|||
|
||||
|
||||
def before_workfile_save(event):
|
||||
project_name = legacy_io.active_project()
|
||||
workdir_path = event["workdir_path"]
|
||||
if workdir_path:
|
||||
copy_workspace_mel(workdir_path)
|
||||
create_workspace_mel(workdir_path, project_name)
|
||||
|
||||
|
||||
class MayaDirmap(HostDirmap):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from openpype.lib import PreLaunchHook
|
||||
from openpype.hosts.maya.lib import copy_workspace_mel
|
||||
from openpype.hosts.maya.lib import create_workspace_mel
|
||||
|
||||
|
||||
class PreCopyMel(PreLaunchHook):
|
||||
|
|
@ -10,9 +10,10 @@ class PreCopyMel(PreLaunchHook):
|
|||
app_groups = ["maya"]
|
||||
|
||||
def execute(self):
|
||||
project_name = self.launch_context.env.get("AVALON_PROJECT")
|
||||
workdir = self.launch_context.env.get("AVALON_WORKDIR")
|
||||
if not workdir:
|
||||
self.log.warning("BUG: Workdir is not filled.")
|
||||
return
|
||||
|
||||
copy_workspace_mel(workdir)
|
||||
create_workspace_mel(workdir, project_name)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
import os
|
||||
import shutil
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib import Logger
|
||||
|
||||
|
||||
def copy_workspace_mel(workdir):
|
||||
# Check that source mel exists
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
src_filepath = os.path.join(current_dir, "resources", "workspace.mel")
|
||||
if not os.path.exists(src_filepath):
|
||||
print("Source mel file does not exist. {}".format(src_filepath))
|
||||
return
|
||||
|
||||
# Skip if workspace.mel already exists
|
||||
def create_workspace_mel(workdir, project_name):
|
||||
dst_filepath = os.path.join(workdir, "workspace.mel")
|
||||
if os.path.exists(dst_filepath):
|
||||
return
|
||||
|
||||
# Create workdir if does not exists yet
|
||||
if not os.path.exists(workdir):
|
||||
os.makedirs(workdir)
|
||||
|
||||
# Copy file
|
||||
print("Copying workspace mel \"{}\" -> \"{}\"".format(
|
||||
src_filepath, dst_filepath
|
||||
))
|
||||
shutil.copy(src_filepath, dst_filepath)
|
||||
project_setting = get_project_settings(project_name)
|
||||
mel_script = project_setting["maya"].get("mel_workspace")
|
||||
|
||||
# Skip if mel script in settings is empty
|
||||
if not mel_script:
|
||||
log = Logger.get_logger("create_workspace_mel")
|
||||
log.debug("File 'workspace.mel' not created. Settings value is empty.")
|
||||
return
|
||||
|
||||
with open(dst_filepath, "w") as mel_file:
|
||||
mel_file.write(mel_script)
|
||||
|
|
|
|||
215
openpype/hosts/maya/plugins/publish/collect_inputs.py
Normal file
215
openpype/hosts/maya/plugins/publish/collect_inputs.py
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import copy
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from maya import cmds
|
||||
import maya.api.OpenMaya as om
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
from openpype.hosts.maya.api.lib import get_container_members
|
||||
from openpype.hosts.maya.api.lib_rendersetup import get_shader_in_layer
|
||||
|
||||
|
||||
def iter_history(nodes,
|
||||
filter=om.MFn.kInvalid,
|
||||
direction=om.MItDependencyGraph.kUpstream):
|
||||
"""Iterate unique upstream history for list of nodes.
|
||||
|
||||
This acts as a replacement to maya.cmds.listHistory.
|
||||
It's faster by about 2x-3x. It returns less than
|
||||
maya.cmds.listHistory as it excludes the input nodes
|
||||
from the output (unless an input node was history
|
||||
for another input node). It also excludes duplicates.
|
||||
|
||||
Args:
|
||||
nodes (list): Maya node names to start search from.
|
||||
filter (om.MFn.Type): Filter to only specific types.
|
||||
e.g. to dag nodes using om.MFn.kDagNode
|
||||
direction (om.MItDependencyGraph.Direction): Direction to traverse in.
|
||||
Defaults to upstream.
|
||||
|
||||
Yields:
|
||||
str: Node names in upstream history.
|
||||
|
||||
"""
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
sel = om.MSelectionList()
|
||||
for node in nodes:
|
||||
sel.add(node)
|
||||
|
||||
it = om.MItDependencyGraph(sel.getDependNode(0)) # init iterator
|
||||
handle = om.MObjectHandle
|
||||
|
||||
traversed = set()
|
||||
fn_dep = om.MFnDependencyNode()
|
||||
fn_dag = om.MFnDagNode()
|
||||
for i in range(sel.length()):
|
||||
|
||||
start_node = sel.getDependNode(i)
|
||||
start_node_hash = handle(start_node).hashCode()
|
||||
if start_node_hash in traversed:
|
||||
continue
|
||||
|
||||
it.resetTo(start_node,
|
||||
filter=filter,
|
||||
direction=direction)
|
||||
while not it.isDone():
|
||||
|
||||
node = it.currentNode()
|
||||
node_hash = handle(node).hashCode()
|
||||
|
||||
if node_hash in traversed:
|
||||
it.prune()
|
||||
it.next() # noqa: B305
|
||||
continue
|
||||
|
||||
traversed.add(node_hash)
|
||||
|
||||
if node.hasFn(om.MFn.kDagNode):
|
||||
fn_dag.setObject(node)
|
||||
yield fn_dag.fullPathName()
|
||||
else:
|
||||
fn_dep.setObject(node)
|
||||
yield fn_dep.name()
|
||||
|
||||
it.next() # noqa: B305
|
||||
|
||||
|
||||
def collect_input_containers(containers, nodes):
|
||||
"""Collect containers that contain any of the node in `nodes`.
|
||||
|
||||
This will return any loaded Avalon container that contains at least one of
|
||||
the nodes. As such, the Avalon container is an input for it. Or in short,
|
||||
there are member nodes of that container.
|
||||
|
||||
Returns:
|
||||
list: Input avalon containers
|
||||
|
||||
"""
|
||||
# Assume the containers have collected their cached '_members' data
|
||||
# in the collector.
|
||||
return [container for container in containers
|
||||
if any(node in container["_members"] for node in nodes)]
|
||||
|
||||
|
||||
class CollectUpstreamInputs(pyblish.api.InstancePlugin):
|
||||
"""Collect input source inputs for this publish.
|
||||
|
||||
This will include `inputs` data of which loaded publishes were used in the
|
||||
generation of this publish. This leaves an upstream trace to what was used
|
||||
as input.
|
||||
|
||||
"""
|
||||
|
||||
label = "Collect Inputs"
|
||||
order = pyblish.api.CollectorOrder + 0.34
|
||||
hosts = ["maya"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
# For large scenes the querying of "host.ls()" can be relatively slow
|
||||
# e.g. up to a second. Many instances calling it easily slows this
|
||||
# down. As such, we cache it so we trigger it only once.
|
||||
# todo: Instead of hidden cache make "CollectContainers" plug-in
|
||||
cache_key = "__cache_containers"
|
||||
scene_containers = instance.context.data.get(cache_key, None)
|
||||
if scene_containers is None:
|
||||
# Query the scenes' containers if there's no cache yet
|
||||
host = registered_host()
|
||||
scene_containers = list(host.ls())
|
||||
for container in scene_containers:
|
||||
# Embed the members into the container dictionary
|
||||
container_members = set(get_container_members(container))
|
||||
container["_members"] = container_members
|
||||
instance.context.data["__cache_containers"] = scene_containers
|
||||
|
||||
# Collect the relevant input containers for this instance
|
||||
if "renderlayer" in set(instance.data.get("families", [])):
|
||||
# Special behavior for renderlayers
|
||||
self.log.debug("Collecting renderlayer inputs....")
|
||||
containers = self._collect_renderlayer_inputs(scene_containers,
|
||||
instance)
|
||||
|
||||
else:
|
||||
# Basic behavior
|
||||
nodes = instance[:]
|
||||
|
||||
# Include any input connections of history with long names
|
||||
# For optimization purposes only trace upstream from shape nodes
|
||||
# looking for used dag nodes. This way having just a constraint
|
||||
# on a transform is also ignored which tended to give irrelevant
|
||||
# inputs for the majority of our use cases. We tend to care more
|
||||
# about geometry inputs.
|
||||
shapes = cmds.ls(nodes,
|
||||
type=("mesh", "nurbsSurface", "nurbsCurve"),
|
||||
noIntermediate=True)
|
||||
if shapes:
|
||||
history = list(iter_history(shapes, filter=om.MFn.kShape))
|
||||
history = cmds.ls(history, long=True)
|
||||
|
||||
# Include the transforms in the collected history as shapes
|
||||
# are excluded from containers
|
||||
transforms = cmds.listRelatives(cmds.ls(history, shapes=True),
|
||||
parent=True,
|
||||
fullPath=True,
|
||||
type="transform")
|
||||
if transforms:
|
||||
history.extend(transforms)
|
||||
|
||||
if history:
|
||||
nodes = list(set(nodes + history))
|
||||
|
||||
# Collect containers for the given set of nodes
|
||||
containers = collect_input_containers(scene_containers,
|
||||
nodes)
|
||||
|
||||
inputs = [ObjectId(c["representation"]) for c in containers]
|
||||
instance.data["inputRepresentations"] = inputs
|
||||
|
||||
self.log.info("Collected inputs: %s" % inputs)
|
||||
|
||||
def _collect_renderlayer_inputs(self, scene_containers, instance):
|
||||
"""Collects inputs from nodes in renderlayer, incl. shaders + camera"""
|
||||
|
||||
# Get the renderlayer
|
||||
renderlayer = instance.data.get("setMembers")
|
||||
|
||||
if renderlayer == "defaultRenderLayer":
|
||||
# Assume all loaded containers in the scene are inputs
|
||||
# for the masterlayer
|
||||
return copy.deepcopy(scene_containers)
|
||||
else:
|
||||
# Get the members of the layer
|
||||
members = cmds.editRenderLayerMembers(renderlayer,
|
||||
query=True,
|
||||
fullNames=True) or []
|
||||
|
||||
# In some cases invalid objects are returned from
|
||||
# `editRenderLayerMembers` so we filter them out
|
||||
members = cmds.ls(members, long=True)
|
||||
|
||||
# Include all children
|
||||
children = cmds.listRelatives(members,
|
||||
allDescendents=True,
|
||||
fullPath=True) or []
|
||||
members.extend(children)
|
||||
|
||||
# Include assigned shaders in renderlayer
|
||||
shapes = cmds.ls(members, shapes=True, long=True)
|
||||
shaders = set()
|
||||
for shape in shapes:
|
||||
shape_shaders = get_shader_in_layer(shape, layer=renderlayer)
|
||||
if not shape_shaders:
|
||||
continue
|
||||
shaders.update(shape_shaders)
|
||||
members.extend(shaders)
|
||||
|
||||
# Explicitly include the camera being rendered in renderlayer
|
||||
cameras = instance.data.get("cameras")
|
||||
members.extend(cameras)
|
||||
|
||||
containers = collect_input_containers(scene_containers, members)
|
||||
|
||||
return containers
|
||||
|
|
@ -293,6 +293,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
"source": filepath,
|
||||
"expectedFiles": full_exp_files,
|
||||
"publishRenderMetadataFolder": common_publish_meta_path,
|
||||
"renderProducts": layer_render_products,
|
||||
"resolutionWidth": lib.get_attr_in_layer(
|
||||
"defaultResolution.width", layer=layer_name
|
||||
),
|
||||
|
|
@ -359,7 +360,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
|
|||
instance.data["label"] = label
|
||||
instance.data["farm"] = True
|
||||
instance.data.update(data)
|
||||
self.log.debug("data: {}".format(json.dumps(data, indent=4)))
|
||||
|
||||
def parse_options(self, render_globals):
|
||||
"""Get all overrides with a value, skip those without.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import os
|
||||
|
||||
import openpype.api
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractAssStandin(openpype.api.Extractor):
|
||||
class ExtractAssStandin(publish.Extractor):
|
||||
"""Extract the content of the instance to a ass file
|
||||
|
||||
Things to pay attention to:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import os
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import extract_alembic
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ExtractAssembly(openpype.api.Extractor):
|
||||
class ExtractAssembly(publish.Extractor):
|
||||
"""Produce an alembic of just point positions and normals.
|
||||
|
||||
Positions and normals are preserved, but nothing more,
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import contextlib
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractAssProxy(openpype.api.Extractor):
|
||||
class ExtractAssProxy(publish.Extractor):
|
||||
"""Extract proxy model as Maya Ascii to use as arnold standin
|
||||
|
||||
|
||||
"""
|
||||
|
||||
order = openpype.api.Extractor.order + 0.2
|
||||
order = publish.Extractor.order + 0.2
|
||||
label = "Ass Proxy (Maya ASCII)"
|
||||
hosts = ["maya"]
|
||||
families = ["ass"]
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
class ExtractCameraAlembic(openpype.api.Extractor):
|
||||
class ExtractCameraAlembic(publish.Extractor):
|
||||
"""Extract a Camera as Alembic.
|
||||
|
||||
The cameras gets baked to world space by default. Only when the instance's
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import itertools
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ def unlock(plug):
|
|||
cmds.disconnectAttr(source, destination)
|
||||
|
||||
|
||||
class ExtractCameraMayaScene(openpype.api.Extractor):
|
||||
class ExtractCameraMayaScene(publish.Extractor):
|
||||
"""Extract a Camera as Maya Scene.
|
||||
|
||||
This will create a duplicate of the camera that will be baked *with*
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import os
|
|||
from maya import cmds # noqa
|
||||
import maya.mel as mel # noqa
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
from openpype.hosts.maya.api import fbx
|
||||
|
||||
|
||||
class ExtractFBX(openpype.api.Extractor):
|
||||
class ExtractFBX(publish.Extractor):
|
||||
"""Extract FBX from Maya.
|
||||
|
||||
This extracts reproducible FBX exports ignoring any of the
|
||||
|
|
|
|||
|
|
@ -5,13 +5,11 @@ import json
|
|||
from maya import cmds
|
||||
from maya.api import OpenMaya as om
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from openpype.pipeline import legacy_io
|
||||
import openpype.api
|
||||
from openpype.client import get_representation_by_id
|
||||
from openpype.pipeline import legacy_io, publish
|
||||
|
||||
|
||||
class ExtractLayout(openpype.api.Extractor):
|
||||
class ExtractLayout(publish.Extractor):
|
||||
"""Extract a layout."""
|
||||
|
||||
label = "Extract Layout"
|
||||
|
|
@ -30,6 +28,8 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
instance.data["representations"] = []
|
||||
|
||||
json_data = []
|
||||
# TODO representation queries can be refactored to be faster
|
||||
project_name = legacy_io.active_project()
|
||||
|
||||
for asset in cmds.sets(str(instance), query=True):
|
||||
# Find the container
|
||||
|
|
@ -43,11 +43,11 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
|
||||
representation_id = cmds.getAttr(f"{container}.representation")
|
||||
|
||||
representation = legacy_io.find_one(
|
||||
{
|
||||
"type": "representation",
|
||||
"_id": ObjectId(representation_id)
|
||||
}, projection={"parent": True, "context.family": True})
|
||||
representation = get_representation_by_id(
|
||||
project_name,
|
||||
representation_id,
|
||||
fields=["parent", "context.family"]
|
||||
)
|
||||
|
||||
self.log.info(representation)
|
||||
|
||||
|
|
@ -102,9 +102,10 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
for i in range(0, len(t_matrix_list), row_length):
|
||||
t_matrix.append(t_matrix_list[i:i + row_length])
|
||||
|
||||
json_element["transform_matrix"] = []
|
||||
for row in t_matrix:
|
||||
json_element["transform_matrix"].append(list(row))
|
||||
json_element["transform_matrix"] = [
|
||||
list(row)
|
||||
for row in t_matrix
|
||||
]
|
||||
|
||||
basis_list = [
|
||||
1, 0, 0, 0,
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ from maya import cmds # noqa
|
|||
|
||||
import pyblish.api
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.lib import source_hash
|
||||
from openpype.pipeline import legacy_io, publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
# Modes for transfer
|
||||
|
|
@ -161,7 +161,7 @@ def no_workspace_dir():
|
|||
os.rmdir(fake_workspace_dir)
|
||||
|
||||
|
||||
class ExtractLook(openpype.api.Extractor):
|
||||
class ExtractLook(publish.Extractor):
|
||||
"""Extract Look (Maya Scene + JSON)
|
||||
|
||||
Only extracts the sets (shadingEngines and alike) alongside a .json file
|
||||
|
|
@ -505,7 +505,7 @@ class ExtractLook(openpype.api.Extractor):
|
|||
args = []
|
||||
if do_maketx:
|
||||
args.append("maketx")
|
||||
texture_hash = openpype.api.source_hash(filepath, *args)
|
||||
texture_hash = source_hash(filepath, *args)
|
||||
|
||||
# If source has been published before with the same settings,
|
||||
# then don't reprocess but hardlink from the original
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
from openpype.pipeline import AVALON_CONTAINER_ID
|
||||
from openpype.pipeline import AVALON_CONTAINER_ID, publish
|
||||
|
||||
|
||||
class ExtractMayaSceneRaw(openpype.api.Extractor):
|
||||
class ExtractMayaSceneRaw(publish.Extractor):
|
||||
"""Extract as Maya Scene (raw).
|
||||
|
||||
This will preserve all references, construction history, etc.
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
class ExtractModel(openpype.api.Extractor):
|
||||
class ExtractModel(publish.Extractor):
|
||||
"""Extract as Model (Maya Scene).
|
||||
|
||||
Only extracts contents based on the original "setMembers" data to ensure
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractMultiverseLook(openpype.api.Extractor):
|
||||
class ExtractMultiverseLook(publish.Extractor):
|
||||
"""Extractor for Multiverse USD look data.
|
||||
|
||||
This will extract:
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import six
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractMultiverseUsd(openpype.api.Extractor):
|
||||
class ExtractMultiverseUsd(publish.Extractor):
|
||||
"""Extractor for Multiverse USD Asset data.
|
||||
|
||||
This will extract settings for a Multiverse Write Asset operation:
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractMultiverseUsdComposition(openpype.api.Extractor):
|
||||
class ExtractMultiverseUsdComposition(publish.Extractor):
|
||||
"""Extractor of Multiverse USD Composition data.
|
||||
|
||||
This will extract settings for a Multiverse Write Composition operation:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import os
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ExtractMultiverseUsdOverride(openpype.api.Extractor):
|
||||
class ExtractMultiverseUsdOverride(publish.Extractor):
|
||||
"""Extractor for Multiverse USD Override data.
|
||||
|
||||
This will extract settings for a Multiverse Write Override operation:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
import os
|
||||
import glob
|
||||
import contextlib
|
||||
|
||||
import clique
|
||||
import capture
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
import openpype.api
|
||||
|
||||
from maya import cmds
|
||||
import pymel.core as pm
|
||||
|
||||
|
||||
class ExtractPlayblast(openpype.api.Extractor):
|
||||
class ExtractPlayblast(publish.Extractor):
|
||||
"""Extract viewport playblast.
|
||||
|
||||
Takes review camera and creates review Quicktime video based on viewport
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
extract_alembic,
|
||||
suspended_refresh,
|
||||
|
|
@ -11,7 +11,7 @@ from openpype.hosts.maya.api.lib import (
|
|||
)
|
||||
|
||||
|
||||
class ExtractAlembic(openpype.api.Extractor):
|
||||
class ExtractAlembic(publish.Extractor):
|
||||
"""Produce an alembic of just point positions and normals.
|
||||
|
||||
Positions and normals, uvs, creases are preserved, but nothing more,
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractRedshiftProxy(openpype.api.Extractor):
|
||||
class ExtractRedshiftProxy(publish.Extractor):
|
||||
"""Extract the content of the instance to a redshift proxy file."""
|
||||
|
||||
label = "Redshift Proxy (.rs)"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import json
|
||||
import os
|
||||
import openpype.api
|
||||
import json
|
||||
|
||||
import maya.app.renderSetup.model.renderSetup as renderSetup
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractRenderSetup(openpype.api.Extractor):
|
||||
class ExtractRenderSetup(publish.Extractor):
|
||||
"""
|
||||
Produce renderSetup template file
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractRig(openpype.api.Extractor):
|
||||
class ExtractRig(publish.Extractor):
|
||||
"""Extract rig as Maya Scene."""
|
||||
|
||||
label = "Extract Rig (Maya Scene)"
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import glob
|
|||
|
||||
import capture
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
import openpype.api
|
||||
|
||||
from maya import cmds
|
||||
import pymel.core as pm
|
||||
|
||||
|
||||
class ExtractThumbnail(openpype.api.Extractor):
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""Extract viewport thumbnail.
|
||||
|
||||
Takes review camera and creates a thumbnail based on viewport
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from contextlib import contextmanager
|
|||
from maya import cmds # noqa
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import fbx
|
||||
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ def renamed(original_name, renamed_name):
|
|||
cmds.rename(renamed_name, original_name)
|
||||
|
||||
|
||||
class ExtractUnrealSkeletalMesh(openpype.api.Extractor):
|
||||
class ExtractUnrealSkeletalMesh(publish.Extractor):
|
||||
"""Extract Unreal Skeletal Mesh as FBX from Maya. """
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import os
|
|||
from maya import cmds # noqa
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
parent_nodes,
|
||||
maintained_selection
|
||||
|
|
@ -13,7 +14,7 @@ from openpype.hosts.maya.api.lib import (
|
|||
from openpype.hosts.maya.api import fbx
|
||||
|
||||
|
||||
class ExtractUnrealStaticMesh(openpype.api.Extractor):
|
||||
class ExtractUnrealStaticMesh(publish.Extractor):
|
||||
"""Extract Unreal Static Mesh as FBX from Maya. """
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
|
||||
class ExtractVRayProxy(openpype.api.Extractor):
|
||||
class ExtractVRayProxy(publish.Extractor):
|
||||
"""Extract the content of the instance to a vrmesh file
|
||||
|
||||
Things to pay attention to:
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.render_setup_tools import export_in_rs_layer
|
||||
from openpype.hosts.maya.api.lib import maintained_selection
|
||||
|
||||
from maya import cmds
|
||||
|
||||
|
||||
class ExtractVrayscene(openpype.api.Extractor):
|
||||
class ExtractVrayscene(publish.Extractor):
|
||||
"""Extractor for vrscene."""
|
||||
|
||||
label = "VRay Scene (.vrscene)"
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import os
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api.lib import (
|
||||
suspended_refresh,
|
||||
maintained_selection
|
||||
)
|
||||
|
||||
|
||||
class ExtractXgenCache(openpype.api.Extractor):
|
||||
class ExtractXgenCache(publish.Extractor):
|
||||
"""Produce an alembic of just xgen interactive groom
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import json
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractYetiCache(openpype.api.Extractor):
|
||||
class ExtractYetiCache(publish.Extractor):
|
||||
"""Producing Yeti cache files using scene time range.
|
||||
|
||||
This will extract Yeti cache file sequence and fur settings.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import contextlib
|
|||
|
||||
from maya import cmds
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ def yetigraph_attribute_values(assumed_destination, resources):
|
|||
pass
|
||||
|
||||
|
||||
class ExtractYetiRig(openpype.api.Extractor):
|
||||
class ExtractYetiRig(publish.Extractor):
|
||||
"""Extract the Yeti rig to a Maya Scene and write the Yeti rig data."""
|
||||
|
||||
label = "Extract Yeti Rig"
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
//Maya 2018 Project Definition
|
||||
|
||||
workspace -fr "shaders" "renderData/shaders";
|
||||
workspace -fr "alembicCache" "cache/alembic";
|
||||
workspace -fr "mayaAscii" "";
|
||||
workspace -fr "mayaBinary" "";
|
||||
workspace -fr "renderData" "renderData";
|
||||
workspace -fr "fileCache" "cache/nCache";
|
||||
workspace -fr "scene" "";
|
||||
workspace -fr "sourceImages" "sourceimages";
|
||||
workspace -fr "images" "renders";
|
||||
|
|
@ -154,7 +154,7 @@ def convert_value_by_type_name(value_type, value, logger=None):
|
|||
elif parts_len == 4:
|
||||
divisor = 2
|
||||
elif parts_len == 9:
|
||||
divisor == 3
|
||||
divisor = 3
|
||||
elif parts_len == 16:
|
||||
divisor = 4
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import os
|
|||
from abc import abstractmethod
|
||||
import platform
|
||||
import getpass
|
||||
from functools import partial
|
||||
from collections import OrderedDict
|
||||
|
||||
import six
|
||||
|
|
@ -66,6 +67,96 @@ def requests_get(*args, **kwargs):
|
|||
return requests.get(*args, **kwargs)
|
||||
|
||||
|
||||
class DeadlineKeyValueVar(dict):
|
||||
"""
|
||||
|
||||
Serializes dictionary key values as "{key}={value}" like Deadline uses
|
||||
for EnvironmentKeyValue.
|
||||
|
||||
As an example:
|
||||
EnvironmentKeyValue0="A_KEY=VALUE_A"
|
||||
EnvironmentKeyValue1="OTHER_KEY=VALUE_B"
|
||||
|
||||
The keys are serialized in alphabetical order (sorted).
|
||||
|
||||
Example:
|
||||
>>> var = DeadlineKeyValueVar("EnvironmentKeyValue")
|
||||
>>> var["my_var"] = "hello"
|
||||
>>> var["my_other_var"] = "hello2"
|
||||
>>> var.serialize()
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, key):
|
||||
super(DeadlineKeyValueVar, self).__init__()
|
||||
self.__key = key
|
||||
|
||||
def serialize(self):
|
||||
key = self.__key
|
||||
|
||||
# Allow custom location for index in serialized string
|
||||
if "{}" not in key:
|
||||
key = key + "{}"
|
||||
|
||||
return {
|
||||
key.format(index): "{}={}".format(var_key, var_value)
|
||||
for index, (var_key, var_value) in enumerate(sorted(self.items()))
|
||||
}
|
||||
|
||||
|
||||
class DeadlineIndexedVar(dict):
|
||||
"""
|
||||
|
||||
Allows to set and query values by integer indices:
|
||||
Query: var[1] or var.get(1)
|
||||
Set: var[1] = "my_value"
|
||||
Append: var += "value"
|
||||
|
||||
Note: Iterating the instance is not guarantueed to be the order of the
|
||||
indices. To do so iterate with `sorted()`
|
||||
|
||||
"""
|
||||
def __init__(self, key):
|
||||
super(DeadlineIndexedVar, self).__init__()
|
||||
self.__key = key
|
||||
|
||||
def serialize(self):
|
||||
key = self.__key
|
||||
|
||||
# Allow custom location for index in serialized string
|
||||
if "{}" not in key:
|
||||
key = key + "{}"
|
||||
|
||||
return {
|
||||
key.format(index): value for index, value in sorted(self.items())
|
||||
}
|
||||
|
||||
def next_available_index(self):
|
||||
# Add as first unused entry
|
||||
i = 0
|
||||
while i in self.keys():
|
||||
i += 1
|
||||
return i
|
||||
|
||||
def update(self, data):
|
||||
# Force the integer key check
|
||||
for key, value in data.items():
|
||||
self.__setitem__(key, value)
|
||||
|
||||
def __iadd__(self, other):
|
||||
index = self.next_available_index()
|
||||
self[index] = other
|
||||
return self
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if not isinstance(key, int):
|
||||
raise TypeError("Key must be an integer: {}".format(key))
|
||||
|
||||
if key < 0:
|
||||
raise ValueError("Negative index can't be set: {}".format(key))
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
|
||||
@attr.s
|
||||
class DeadlineJobInfo(object):
|
||||
"""Mapping of all Deadline *JobInfo* attributes.
|
||||
|
|
@ -218,24 +309,8 @@ class DeadlineJobInfo(object):
|
|||
|
||||
# Environment
|
||||
# ----------------------------------------------
|
||||
_environmentKeyValue = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def EnvironmentKeyValue(self): # noqa: N802
|
||||
"""Return all environment key values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'EnvironmentKeyValue0', 'key=value'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._environmentKeyValue):
|
||||
out["EnvironmentKeyValue{}".format(index)] = v
|
||||
return out
|
||||
|
||||
@EnvironmentKeyValue.setter
|
||||
def EnvironmentKeyValue(self, val): # noqa: N802
|
||||
self._environmentKeyValue.append(val)
|
||||
EnvironmentKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar,
|
||||
"EnvironmentKeyValue"))
|
||||
|
||||
IncludeEnvironment = attr.ib(default=None) # Default: false
|
||||
UseJobEnvironmentOnly = attr.ib(default=None) # Default: false
|
||||
|
|
@ -243,121 +318,29 @@ class DeadlineJobInfo(object):
|
|||
|
||||
# Job Extra Info
|
||||
# ----------------------------------------------
|
||||
_extraInfos = attr.ib(factory=list)
|
||||
_extraInfoKeyValues = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def ExtraInfo(self): # noqa: N802
|
||||
"""Return all ExtraInfo values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'ExtraInfo0': 'value'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._extraInfos):
|
||||
out["ExtraInfo{}".format(index)] = v
|
||||
return out
|
||||
|
||||
@ExtraInfo.setter
|
||||
def ExtraInfo(self, val): # noqa: N802
|
||||
self._extraInfos.append(val)
|
||||
|
||||
@property
|
||||
def ExtraInfoKeyValue(self): # noqa: N802
|
||||
"""Return all ExtraInfoKeyValue values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as {'ExtraInfoKeyValue0': 'key=value'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._extraInfoKeyValues):
|
||||
out["ExtraInfoKeyValue{}".format(index)] = v
|
||||
return out
|
||||
|
||||
@ExtraInfoKeyValue.setter
|
||||
def ExtraInfoKeyValue(self, val): # noqa: N802
|
||||
self._extraInfoKeyValues.append(val)
|
||||
ExtraInfo = attr.ib(factory=partial(DeadlineIndexedVar, "ExtraInfo"))
|
||||
ExtraInfoKeyValue = attr.ib(factory=partial(DeadlineKeyValueVar,
|
||||
"ExtraInfoKeyValue"))
|
||||
|
||||
# Task Extra Info Names
|
||||
# ----------------------------------------------
|
||||
OverrideTaskExtraInfoNames = attr.ib(default=None) # Default: false
|
||||
_taskExtraInfos = attr.ib(factory=list)
|
||||
|
||||
@property
|
||||
def TaskExtraInfoName(self): # noqa: N802
|
||||
"""Return all TaskExtraInfoName values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'TaskExtraInfoName0': 'value'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._taskExtraInfos):
|
||||
out["TaskExtraInfoName{}".format(index)] = v
|
||||
return out
|
||||
|
||||
@TaskExtraInfoName.setter
|
||||
def TaskExtraInfoName(self, val): # noqa: N802
|
||||
self._taskExtraInfos.append(val)
|
||||
TaskExtraInfoName = attr.ib(factory=partial(DeadlineIndexedVar,
|
||||
"TaskExtraInfoName"))
|
||||
|
||||
# Output
|
||||
# ----------------------------------------------
|
||||
_outputFilename = attr.ib(factory=list)
|
||||
_outputFilenameTile = attr.ib(factory=list)
|
||||
_outputDirectory = attr.ib(factory=list)
|
||||
OutputFilename = attr.ib(factory=partial(DeadlineIndexedVar,
|
||||
"OutputFilename"))
|
||||
OutputFilenameTile = attr.ib(factory=partial(DeadlineIndexedVar,
|
||||
"OutputFilename{}Tile"))
|
||||
OutputDirectory = attr.ib(factory=partial(DeadlineIndexedVar,
|
||||
"OutputDirectory"))
|
||||
|
||||
@property
|
||||
def OutputFilename(self): # noqa: N802
|
||||
"""Return all OutputFilename values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'OutputFilename0': 'filename'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputFilename):
|
||||
out["OutputFilename{}".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputFilename.setter
|
||||
def OutputFilename(self, val): # noqa: N802
|
||||
self._outputFilename.append(val)
|
||||
|
||||
@property
|
||||
def OutputFilenameTile(self): # noqa: N802
|
||||
"""Return all OutputFilename#Tile values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'OutputFilenme#Tile': 'tile'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputFilenameTile):
|
||||
out["OutputFilename{}Tile".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputFilenameTile.setter
|
||||
def OutputFilenameTile(self, val): # noqa: N802
|
||||
self._outputFilenameTile.append(val)
|
||||
|
||||
@property
|
||||
def OutputDirectory(self): # noqa: N802
|
||||
"""Return all OutputDirectory values formatted for Deadline.
|
||||
|
||||
Returns:
|
||||
dict: as `{'OutputDirectory0': 'dir'}`
|
||||
|
||||
"""
|
||||
out = {}
|
||||
for index, v in enumerate(self._outputDirectory):
|
||||
out["OutputDirectory{}".format(index)] = v
|
||||
return out
|
||||
|
||||
@OutputDirectory.setter
|
||||
def OutputDirectory(self, val): # noqa: N802
|
||||
self._outputDirectory.append(val)
|
||||
# Asset Dependency
|
||||
# ----------------------------------------------
|
||||
AssetDependency = attr.ib(factory=partial(DeadlineIndexedVar,
|
||||
"AssetDependency"))
|
||||
|
||||
# Tile Job
|
||||
# ----------------------------------------------
|
||||
|
|
@ -381,7 +364,7 @@ class DeadlineJobInfo(object):
|
|||
|
||||
"""
|
||||
def filter_data(a, v):
|
||||
if a.name.startswith("_"):
|
||||
if isinstance(v, (DeadlineIndexedVar, DeadlineKeyValueVar)):
|
||||
return False
|
||||
if v is None:
|
||||
return False
|
||||
|
|
@ -389,15 +372,27 @@ class DeadlineJobInfo(object):
|
|||
|
||||
serialized = attr.asdict(
|
||||
self, dict_factory=OrderedDict, filter=filter_data)
|
||||
serialized.update(self.EnvironmentKeyValue)
|
||||
serialized.update(self.ExtraInfo)
|
||||
serialized.update(self.ExtraInfoKeyValue)
|
||||
serialized.update(self.TaskExtraInfoName)
|
||||
serialized.update(self.OutputFilename)
|
||||
serialized.update(self.OutputFilenameTile)
|
||||
serialized.update(self.OutputDirectory)
|
||||
|
||||
# Custom serialize these attributes
|
||||
for attribute in [
|
||||
self.EnvironmentKeyValue,
|
||||
self.ExtraInfo,
|
||||
self.ExtraInfoKeyValue,
|
||||
self.TaskExtraInfoName,
|
||||
self.OutputFilename,
|
||||
self.OutputFilenameTile,
|
||||
self.OutputDirectory,
|
||||
self.AssetDependency
|
||||
]:
|
||||
serialized.update(attribute.serialize())
|
||||
|
||||
return serialized
|
||||
|
||||
def update(self, data):
|
||||
"""Update instance with data dict"""
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
|
||||
@six.add_metaclass(AbstractMetaInstancePlugin)
|
||||
class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
|
||||
|
|
@ -521,68 +516,72 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
published.
|
||||
|
||||
"""
|
||||
anatomy = self._instance.context.data['anatomy']
|
||||
file_path = None
|
||||
for i in self._instance.context:
|
||||
if "workfile" in i.data["families"] \
|
||||
or i.data["family"] == "workfile":
|
||||
# test if there is instance of workfile waiting
|
||||
# to be published.
|
||||
assert i.data["publish"] is True, (
|
||||
"Workfile (scene) must be published along")
|
||||
# determine published path from Anatomy.
|
||||
template_data = i.data.get("anatomyData")
|
||||
rep = i.data.get("representations")[0].get("ext")
|
||||
template_data["representation"] = rep
|
||||
template_data["ext"] = rep
|
||||
template_data["comment"] = None
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
file_path = os.path.normpath(template_filled)
|
||||
|
||||
self.log.info("Using published scene for render {}".format(
|
||||
file_path))
|
||||
instance = self._instance
|
||||
workfile_instance = self._get_workfile_instance(instance.context)
|
||||
if workfile_instance is None:
|
||||
return
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
self.log.error("published scene does not exist!")
|
||||
raise
|
||||
# determine published path from Anatomy.
|
||||
template_data = workfile_instance.data.get("anatomyData")
|
||||
rep = workfile_instance.data.get("representations")[0]
|
||||
template_data["representation"] = rep.get("name")
|
||||
template_data["ext"] = rep.get("ext")
|
||||
template_data["comment"] = None
|
||||
|
||||
if not replace_in_path:
|
||||
return file_path
|
||||
anatomy = instance.context.data['anatomy']
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["path"]
|
||||
file_path = os.path.normpath(template_filled)
|
||||
|
||||
# now we need to switch scene in expected files
|
||||
# because <scene> token will now point to published
|
||||
# scene file and that might differ from current one
|
||||
new_scene = os.path.splitext(
|
||||
os.path.basename(file_path))[0]
|
||||
orig_scene = os.path.splitext(
|
||||
os.path.basename(
|
||||
self._instance.context.data["currentFile"]))[0]
|
||||
exp = self._instance.data.get("expectedFiles")
|
||||
self.log.info("Using published scene for render {}".format(file_path))
|
||||
|
||||
if isinstance(exp[0], dict):
|
||||
# we have aovs and we need to iterate over them
|
||||
new_exp = {}
|
||||
for aov, files in exp[0].items():
|
||||
replaced_files = []
|
||||
for f in files:
|
||||
replaced_files.append(
|
||||
str(f).replace(orig_scene, new_scene)
|
||||
)
|
||||
new_exp[aov] = replaced_files
|
||||
# [] might be too much here, TODO
|
||||
self._instance.data["expectedFiles"] = [new_exp]
|
||||
else:
|
||||
new_exp = []
|
||||
for f in exp:
|
||||
new_exp.append(
|
||||
str(f).replace(orig_scene, new_scene)
|
||||
)
|
||||
self._instance.data["expectedFiles"] = new_exp
|
||||
if not os.path.exists(file_path):
|
||||
self.log.error("published scene does not exist!")
|
||||
raise
|
||||
|
||||
self.log.info("Scene name was switched {} -> {}".format(
|
||||
orig_scene, new_scene
|
||||
))
|
||||
if not replace_in_path:
|
||||
return file_path
|
||||
|
||||
# now we need to switch scene in expected files
|
||||
# because <scene> token will now point to published
|
||||
# scene file and that might differ from current one
|
||||
def _clean_name(path):
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
||||
|
||||
new_scene = _clean_name(file_path)
|
||||
orig_scene = _clean_name(instance.context.data["currentFile"])
|
||||
expected_files = instance.data.get("expectedFiles")
|
||||
|
||||
if isinstance(expected_files[0], dict):
|
||||
# we have aovs and we need to iterate over them
|
||||
new_exp = {}
|
||||
for aov, files in expected_files[0].items():
|
||||
replaced_files = []
|
||||
for f in files:
|
||||
replaced_files.append(
|
||||
str(f).replace(orig_scene, new_scene)
|
||||
)
|
||||
new_exp[aov] = replaced_files
|
||||
# [] might be too much here, TODO
|
||||
instance.data["expectedFiles"] = [new_exp]
|
||||
else:
|
||||
new_exp = []
|
||||
for f in expected_files:
|
||||
new_exp.append(
|
||||
str(f).replace(orig_scene, new_scene)
|
||||
)
|
||||
instance.data["expectedFiles"] = new_exp
|
||||
|
||||
metadata_folder = instance.data.get("publishRenderMetadataFolder")
|
||||
if metadata_folder:
|
||||
metadata_folder = metadata_folder.replace(orig_scene,
|
||||
new_scene)
|
||||
instance.data["publishRenderMetadataFolder"] = metadata_folder
|
||||
|
||||
self.log.info("Scene name was switched {} -> {}".format(
|
||||
orig_scene, new_scene
|
||||
))
|
||||
|
||||
return file_path
|
||||
|
||||
|
|
@ -645,3 +644,22 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
|
|||
self._instance.data["deadlineSubmissionJob"] = result
|
||||
|
||||
return result["_id"]
|
||||
|
||||
@staticmethod
|
||||
def _get_workfile_instance(context):
|
||||
"""Find workfile instance in context"""
|
||||
for i in context:
|
||||
|
||||
is_workfile = (
|
||||
"workfile" in i.data.get("families", []) or
|
||||
i.data["family"] == "workfile"
|
||||
)
|
||||
if not is_workfile:
|
||||
continue
|
||||
|
||||
# test if there is instance of workfile waiting
|
||||
# to be published.
|
||||
assert i.data["publish"] is True, (
|
||||
"Workfile (scene) must be published along")
|
||||
|
||||
return i
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin):
|
|||
|
||||
order = pyblish.api.CollectorOrder + 0.415
|
||||
label = "Deadline Webservice from the Instance"
|
||||
families = ["rendering"]
|
||||
families = ["rendering", "renderlayer"]
|
||||
|
||||
def process(self, instance):
|
||||
instance.data["deadlineUrl"] = self._collect_deadline_url(instance)
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ class AfterEffectsSubmitDeadline(
|
|||
dln_job_info.Group = self.group
|
||||
dln_job_info.Department = self.department
|
||||
dln_job_info.ChunkSize = self.chunk_size
|
||||
dln_job_info.OutputFilename = \
|
||||
dln_job_info.OutputFilename += \
|
||||
os.path.basename(self._instance.data["expectedFiles"][0])
|
||||
dln_job_info.OutputDirectory = \
|
||||
dln_job_info.OutputDirectory += \
|
||||
os.path.dirname(self._instance.data["expectedFiles"][0])
|
||||
dln_job_info.JobDelay = "00:00:00"
|
||||
|
||||
|
|
@ -92,13 +92,12 @@ class AfterEffectsSubmitDeadline(
|
|||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
for key in keys:
|
||||
val = environment.get(key)
|
||||
if val:
|
||||
dln_job_info.EnvironmentKeyValue = "{key}={value}".format(
|
||||
key=key,
|
||||
value=val)
|
||||
value = environment.get(key)
|
||||
if value:
|
||||
dln_job_info.EnvironmentKeyValue[key] = value
|
||||
|
||||
# to recognize job from PYPE for turning Event On/Off
|
||||
dln_job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1"
|
||||
dln_job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
|
||||
|
||||
return dln_job_info
|
||||
|
||||
|
|
|
|||
|
|
@ -284,14 +284,12 @@ class HarmonySubmitDeadline(
|
|||
environment = dict({key: os.environ[key] for key in keys
|
||||
if key in os.environ}, **legacy_io.Session)
|
||||
for key in keys:
|
||||
val = environment.get(key)
|
||||
if val:
|
||||
job_info.EnvironmentKeyValue = "{key}={value}".format(
|
||||
key=key,
|
||||
value=val)
|
||||
value = environment.get(key)
|
||||
if value:
|
||||
job_info.EnvironmentKeyValue[key] = value
|
||||
|
||||
# to recognize job from PYPE for turning Event On/Off
|
||||
job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1"
|
||||
job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1"
|
||||
|
||||
return job_info
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -778,7 +778,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
"resolutionHeight": data.get("resolutionHeight", 1080),
|
||||
"multipartExr": data.get("multipartExr", False),
|
||||
"jobBatchName": data.get("jobBatchName", ""),
|
||||
"useSequenceForReview": data.get("useSequenceForReview", True)
|
||||
"useSequenceForReview": data.get("useSequenceForReview", True),
|
||||
# map inputVersions `ObjectId` -> `str` so json supports it
|
||||
"inputVersions": list(map(str, data.get("inputVersions", [])))
|
||||
}
|
||||
|
||||
# skip locking version if we are creating v01
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ def convert_value_by_type_name(value_type, value):
|
|||
elif parts_len == 4:
|
||||
divisor = 2
|
||||
elif parts_len == 9:
|
||||
divisor == 3
|
||||
divisor = 3
|
||||
elif parts_len == 16:
|
||||
divisor = 4
|
||||
else:
|
||||
|
|
@ -453,7 +453,7 @@ class OpenPypeTileAssembler(DeadlinePlugin):
|
|||
# Swap to have input as foreground
|
||||
args.append("--swap")
|
||||
# Paste foreground to background
|
||||
args.append("--paste +{}+{}".format(pos_x, pos_y))
|
||||
args.append("--paste {x:+d}{y:+d}".format(x=pos_x, y=pos_y))
|
||||
|
||||
args.append("-o")
|
||||
args.append(output_path)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
from .ftrack_module import (
|
||||
FtrackModule,
|
||||
FTRACK_MODULE_DIR
|
||||
FTRACK_MODULE_DIR,
|
||||
|
||||
resolve_ftrack_url,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"FtrackModule",
|
||||
"FTRACK_MODULE_DIR"
|
||||
"FTRACK_MODULE_DIR",
|
||||
|
||||
"resolve_ftrack_url",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ import platform
|
|||
import click
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import (
|
||||
from openpype.modules.interfaces import (
|
||||
ITrayModule,
|
||||
IPluginPaths,
|
||||
ISettingsChangeListener
|
||||
)
|
||||
from openpype.settings import SaveWarningExc
|
||||
from openpype.lib import Logger
|
||||
|
||||
FTRACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
_URL_NOT_SET = object()
|
||||
|
||||
|
||||
class FtrackModule(
|
||||
|
|
@ -28,17 +30,8 @@ class FtrackModule(
|
|||
ftrack_settings = settings[self.name]
|
||||
|
||||
self.enabled = ftrack_settings["enabled"]
|
||||
# Add http schema
|
||||
ftrack_url = ftrack_settings["ftrack_server"].strip("/ ")
|
||||
if ftrack_url:
|
||||
if "http" not in ftrack_url:
|
||||
ftrack_url = "https://" + ftrack_url
|
||||
|
||||
# Check if "ftrack.app" is part os url
|
||||
if "ftrackapp.com" not in ftrack_url:
|
||||
ftrack_url = ftrack_url + ".ftrackapp.com"
|
||||
|
||||
self.ftrack_url = ftrack_url
|
||||
self._settings_ftrack_url = ftrack_settings["ftrack_server"]
|
||||
self._ftrack_url = _URL_NOT_SET
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
low_platform = platform.system().lower()
|
||||
|
|
@ -70,6 +63,16 @@ class FtrackModule(
|
|||
self.timers_manager_connector = None
|
||||
self._timers_manager_module = None
|
||||
|
||||
def get_ftrack_url(self):
|
||||
if self._ftrack_url is _URL_NOT_SET:
|
||||
self._ftrack_url = resolve_ftrack_url(
|
||||
self._settings_ftrack_url,
|
||||
logger=self.log
|
||||
)
|
||||
return self._ftrack_url
|
||||
|
||||
ftrack_url = property(get_ftrack_url)
|
||||
|
||||
def get_global_environments(self):
|
||||
"""Ftrack's global environments."""
|
||||
return {
|
||||
|
|
@ -479,6 +482,51 @@ class FtrackModule(
|
|||
click_group.add_command(cli_main)
|
||||
|
||||
|
||||
def _check_ftrack_url(url):
|
||||
import requests
|
||||
|
||||
try:
|
||||
result = requests.get(url, allow_redirects=False)
|
||||
except requests.exceptions.RequestException:
|
||||
return False
|
||||
|
||||
if (result.status_code != 200 or "FTRACK_VERSION" not in result.headers):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def resolve_ftrack_url(url, logger=None):
|
||||
"""Checks if Ftrack server is responding."""
|
||||
|
||||
if logger is None:
|
||||
logger = Logger.get_logger(__name__)
|
||||
|
||||
url = url.strip("/ ")
|
||||
if not url:
|
||||
logger.error("Ftrack URL is not set!")
|
||||
return None
|
||||
|
||||
if not url.startswith("http"):
|
||||
url = "https://" + url
|
||||
|
||||
ftrack_url = None
|
||||
if not url.endswith("ftrackapp.com"):
|
||||
ftrackapp_url = url + ".ftrackapp.com"
|
||||
if _check_ftrack_url(ftrackapp_url):
|
||||
ftrack_url = ftrackapp_url
|
||||
|
||||
if not ftrack_url and _check_ftrack_url(url):
|
||||
ftrack_url = url
|
||||
|
||||
if ftrack_url:
|
||||
logger.debug("Ftrack server \"{}\" is accessible.".format(ftrack_url))
|
||||
|
||||
else:
|
||||
logger.error("Ftrack server \"{}\" is not accessible!".format(url))
|
||||
|
||||
return ftrack_url
|
||||
|
||||
|
||||
@click.group(FtrackModule.name, help="Ftrack module related commands.")
|
||||
def cli_main():
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
from .ftrack_server import FtrackServer
|
||||
from .lib import check_ftrack_url
|
||||
|
||||
|
||||
__all__ = (
|
||||
"FtrackServer",
|
||||
"check_ftrack_url"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ from openpype.lib import (
|
|||
get_openpype_version,
|
||||
get_build_version,
|
||||
)
|
||||
from openpype_modules.ftrack import FTRACK_MODULE_DIR
|
||||
from openpype_modules.ftrack import (
|
||||
FTRACK_MODULE_DIR,
|
||||
resolve_ftrack_url,
|
||||
)
|
||||
from openpype_modules.ftrack.lib import credentials
|
||||
from openpype_modules.ftrack.ftrack_server.lib import check_ftrack_url
|
||||
from openpype_modules.ftrack.ftrack_server import socket_thread
|
||||
|
||||
|
||||
|
|
@ -114,7 +116,7 @@ def legacy_server(ftrack_url):
|
|||
|
||||
while True:
|
||||
if not ftrack_accessible:
|
||||
ftrack_accessible = check_ftrack_url(ftrack_url)
|
||||
ftrack_accessible = resolve_ftrack_url(ftrack_url)
|
||||
|
||||
# Run threads only if Ftrack is accessible
|
||||
if not ftrack_accessible and not printed_ftrack_error:
|
||||
|
|
@ -257,7 +259,7 @@ def main_loop(ftrack_url):
|
|||
while True:
|
||||
# Check if accessible Ftrack and Mongo url
|
||||
if not ftrack_accessible:
|
||||
ftrack_accessible = check_ftrack_url(ftrack_url)
|
||||
ftrack_accessible = resolve_ftrack_url(ftrack_url)
|
||||
|
||||
if not mongo_accessible:
|
||||
mongo_accessible = check_mongo_url(mongo_uri)
|
||||
|
|
@ -441,7 +443,7 @@ def run_event_server(
|
|||
os.environ["CLOCKIFY_API_KEY"] = clockify_api_key
|
||||
|
||||
# Check url regex and accessibility
|
||||
ftrack_url = check_ftrack_url(ftrack_url)
|
||||
ftrack_url = resolve_ftrack_url(ftrack_url)
|
||||
if not ftrack_url:
|
||||
print('Exiting! < Please enter Ftrack server url >')
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -26,45 +26,12 @@ except ImportError:
|
|||
from openpype_modules.ftrack.lib import get_ftrack_event_mongo_info
|
||||
|
||||
from openpype.client import OpenPypeMongoConnection
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
|
||||
TOPIC_STATUS_SERVER = "openpype.event.server.status"
|
||||
TOPIC_STATUS_SERVER_RESULT = "openpype.event.server.status.result"
|
||||
|
||||
|
||||
def check_ftrack_url(url, log_errors=True, logger=None):
|
||||
"""Checks if Ftrack server is responding"""
|
||||
if logger is None:
|
||||
logger = Logger.get_logger(__name__)
|
||||
|
||||
if not url:
|
||||
logger.error("Ftrack URL is not set!")
|
||||
return None
|
||||
|
||||
url = url.strip('/ ')
|
||||
|
||||
if 'http' not in url:
|
||||
if url.endswith('ftrackapp.com'):
|
||||
url = 'https://' + url
|
||||
else:
|
||||
url = 'https://{0}.ftrackapp.com'.format(url)
|
||||
try:
|
||||
result = requests.get(url, allow_redirects=False)
|
||||
except requests.exceptions.RequestException:
|
||||
if log_errors:
|
||||
logger.error("Entered Ftrack URL is not accesible!")
|
||||
return False
|
||||
|
||||
if (result.status_code != 200 or 'FTRACK_VERSION' not in result.headers):
|
||||
if log_errors:
|
||||
logger.error("Entered Ftrack URL is not accesible!")
|
||||
return False
|
||||
|
||||
logger.debug("Ftrack server {} is accessible.".format(url))
|
||||
|
||||
return url
|
||||
|
||||
|
||||
class SocketBaseEventHub(ftrack_api.event.hub.EventHub):
|
||||
|
||||
hearbeat_msg = b"hearbeat"
|
||||
|
|
|
|||
|
|
@ -19,11 +19,8 @@ from openpype.client.operations import (
|
|||
CURRENT_PROJECT_SCHEMA,
|
||||
CURRENT_PROJECT_CONFIG_SCHEMA,
|
||||
)
|
||||
from openpype.api import (
|
||||
Logger,
|
||||
get_anatomy_settings
|
||||
)
|
||||
from openpype.lib import ApplicationManager
|
||||
from openpype.settings import get_anatomy_settings
|
||||
from openpype.lib import ApplicationManager, Logger
|
||||
from openpype.pipeline import AvalonMongoDB, schema
|
||||
|
||||
from .constants import CUST_ATTR_ID_KEY, FPS_KEYS
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
family_mapping = {
|
||||
"camera": "cam",
|
||||
"look": "look",
|
||||
"mayaascii": "scene",
|
||||
"mayaAscii": "scene",
|
||||
"model": "geo",
|
||||
"rig": "rig",
|
||||
"setdress": "setdress",
|
||||
|
|
@ -74,11 +74,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
version_number = int(instance_version)
|
||||
|
||||
family = instance.data["family"]
|
||||
family_low = family.lower()
|
||||
|
||||
# Perform case-insensitive family mapping
|
||||
family_low = family.lower()
|
||||
asset_type = instance.data.get("ftrackFamily")
|
||||
if not asset_type and family_low in self.family_mapping:
|
||||
asset_type = self.family_mapping[family_low]
|
||||
if not asset_type:
|
||||
for map_family, map_value in self.family_mapping.items():
|
||||
if map_family.lower() == family_low:
|
||||
asset_type = map_value
|
||||
break
|
||||
|
||||
if not asset_type:
|
||||
asset_type = "upload"
|
||||
|
|
@ -86,15 +90,6 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin):
|
|||
self.log.debug(
|
||||
"Family: {}\nMapping: {}".format(family_low, self.family_mapping)
|
||||
)
|
||||
|
||||
# Ignore this instance if neither "ftrackFamily" or a family mapping is
|
||||
# found.
|
||||
if not asset_type:
|
||||
self.log.info((
|
||||
"Family \"{}\" does not match any asset type mapping"
|
||||
).format(family))
|
||||
return
|
||||
|
||||
status_name = self._get_asset_version_status_name(instance)
|
||||
|
||||
# Base of component item data
|
||||
|
|
|
|||
|
|
@ -6,22 +6,18 @@ import threading
|
|||
from Qt import QtCore, QtWidgets, QtGui
|
||||
|
||||
import ftrack_api
|
||||
from ..ftrack_server.lib import check_ftrack_url
|
||||
from ..ftrack_server import socket_thread
|
||||
from ..lib import credentials
|
||||
from ..ftrack_module import FTRACK_MODULE_DIR
|
||||
from . import login_dialog
|
||||
|
||||
from openpype import resources
|
||||
from openpype.lib import Logger
|
||||
|
||||
|
||||
log = Logger.get_logger("FtrackModule")
|
||||
from openpype_modules.ftrack import resolve_ftrack_url, FTRACK_MODULE_DIR
|
||||
from openpype_modules.ftrack.ftrack_server import socket_thread
|
||||
from openpype_modules.ftrack.lib import credentials
|
||||
from . import login_dialog
|
||||
|
||||
|
||||
class FtrackTrayWrapper:
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.log = Logger.get_logger(self.__class__.__name__)
|
||||
|
||||
self.thread_action_server = None
|
||||
self.thread_socket_server = None
|
||||
|
|
@ -62,19 +58,19 @@ class FtrackTrayWrapper:
|
|||
if validation:
|
||||
self.widget_login.set_credentials(ft_user, ft_api_key)
|
||||
self.module.set_credentials_to_env(ft_user, ft_api_key)
|
||||
log.info("Connected to Ftrack successfully")
|
||||
self.log.info("Connected to Ftrack successfully")
|
||||
self.on_login_change()
|
||||
|
||||
return validation
|
||||
|
||||
if not validation and ft_user and ft_api_key:
|
||||
log.warning(
|
||||
self.log.warning(
|
||||
"Current Ftrack credentials are not valid. {}: {} - {}".format(
|
||||
str(os.environ.get("FTRACK_SERVER")), ft_user, ft_api_key
|
||||
)
|
||||
)
|
||||
|
||||
log.info("Please sign in to Ftrack")
|
||||
self.log.info("Please sign in to Ftrack")
|
||||
self.bool_logged = False
|
||||
self.show_login_widget()
|
||||
self.set_menu_visibility()
|
||||
|
|
@ -104,7 +100,7 @@ class FtrackTrayWrapper:
|
|||
self.action_credentials.setIcon(self.icon_not_logged)
|
||||
self.action_credentials.setToolTip("Logged out")
|
||||
|
||||
log.info("Logged out of Ftrack")
|
||||
self.log.info("Logged out of Ftrack")
|
||||
self.bool_logged = False
|
||||
self.set_menu_visibility()
|
||||
|
||||
|
|
@ -126,10 +122,6 @@ class FtrackTrayWrapper:
|
|||
ftrack_url = self.module.ftrack_url
|
||||
os.environ["FTRACK_SERVER"] = ftrack_url
|
||||
|
||||
parent_file_path = os.path.dirname(
|
||||
os.path.dirname(os.path.realpath(__file__))
|
||||
)
|
||||
|
||||
min_fail_seconds = 5
|
||||
max_fail_count = 3
|
||||
wait_time_after_max_fail = 10
|
||||
|
|
@ -154,17 +146,19 @@ class FtrackTrayWrapper:
|
|||
# Main loop
|
||||
while True:
|
||||
if not self.bool_action_server_running:
|
||||
log.debug("Action server was pushed to stop.")
|
||||
self.log.debug("Action server was pushed to stop.")
|
||||
break
|
||||
|
||||
# Check if accessible Ftrack and Mongo url
|
||||
if not ftrack_accessible:
|
||||
ftrack_accessible = check_ftrack_url(ftrack_url)
|
||||
ftrack_accessible = resolve_ftrack_url(ftrack_url)
|
||||
|
||||
# Run threads only if Ftrack is accessible
|
||||
if not ftrack_accessible:
|
||||
if not printed_ftrack_error:
|
||||
log.warning("Can't access Ftrack {}".format(ftrack_url))
|
||||
self.log.warning(
|
||||
"Can't access Ftrack {}".format(ftrack_url)
|
||||
)
|
||||
|
||||
if self.thread_socket_server is not None:
|
||||
self.thread_socket_server.stop()
|
||||
|
|
@ -191,7 +185,7 @@ class FtrackTrayWrapper:
|
|||
self.set_menu_visibility()
|
||||
|
||||
elif failed_count == max_fail_count:
|
||||
log.warning((
|
||||
self.log.warning((
|
||||
"Action server failed {} times."
|
||||
" I'll try to run again {}s later"
|
||||
).format(
|
||||
|
|
@ -243,10 +237,10 @@ class FtrackTrayWrapper:
|
|||
self.thread_action_server.join()
|
||||
self.thread_action_server = None
|
||||
|
||||
log.info("Ftrack action server was forced to stop")
|
||||
self.log.info("Ftrack action server was forced to stop")
|
||||
|
||||
except Exception:
|
||||
log.warning(
|
||||
self.log.warning(
|
||||
"Error has happened during Killing action server",
|
||||
exc_info=True
|
||||
)
|
||||
|
|
@ -343,7 +337,7 @@ class FtrackTrayWrapper:
|
|||
self.thread_timer = None
|
||||
|
||||
except Exception as e:
|
||||
log.error("During Killing Timer event server: {0}".format(e))
|
||||
self.log.error("During Killing Timer event server: {0}".format(e))
|
||||
|
||||
def changed_user(self):
|
||||
self.stop_action_server()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ from .publish_plugins import (
|
|||
)
|
||||
|
||||
from .lib import (
|
||||
get_publish_template_name,
|
||||
|
||||
DiscoverResult,
|
||||
publish_plugins_discover,
|
||||
load_help_content_from_plugin,
|
||||
|
|
@ -62,6 +64,8 @@ __all__ = (
|
|||
|
||||
"Extractor",
|
||||
|
||||
"get_publish_template_name",
|
||||
|
||||
"DiscoverResult",
|
||||
"publish_plugins_discover",
|
||||
"load_help_content_from_plugin",
|
||||
|
|
|
|||
2
openpype/pipeline/publish/contants.py
Normal file
2
openpype/pipeline/publish/contants.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DEFAULT_PUBLISH_TEMPLATE = "publish"
|
||||
DEFAULT_HERO_PUBLISH_TEMPLATE = "hero"
|
||||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import sys
|
||||
import types
|
||||
import inspect
|
||||
import copy
|
||||
import tempfile
|
||||
import xml.etree.ElementTree
|
||||
|
||||
|
|
@ -9,8 +10,190 @@ import six
|
|||
import pyblish.plugin
|
||||
import pyblish.api
|
||||
|
||||
from openpype.lib import Logger
|
||||
from openpype.settings import get_project_settings, get_system_settings
|
||||
from openpype.lib import Logger, filter_profiles
|
||||
from openpype.settings import (
|
||||
get_project_settings,
|
||||
get_system_settings,
|
||||
)
|
||||
|
||||
from .contants import (
|
||||
DEFAULT_PUBLISH_TEMPLATE,
|
||||
DEFAULT_HERO_PUBLISH_TEMPLATE,
|
||||
)
|
||||
|
||||
|
||||
def get_template_name_profiles(
|
||||
project_name, project_settings=None, logger=None
|
||||
):
|
||||
"""Receive profiles for publish template keys.
|
||||
|
||||
At least one of arguments must be passed.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for templates.
|
||||
project_settings(Dic[str, Any]): Prepared project settings.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Publish template profiles.
|
||||
"""
|
||||
|
||||
if not project_name and not project_settings:
|
||||
raise ValueError((
|
||||
"Both project name and project settings are missing."
|
||||
" At least one must be entered."
|
||||
))
|
||||
|
||||
if not project_settings:
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
profiles = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["publish"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
if profiles:
|
||||
return copy.deepcopy(profiles)
|
||||
|
||||
# Use legacy approach for cases new settings are not filled yet for the
|
||||
# project
|
||||
legacy_profiles = (
|
||||
project_settings
|
||||
["global"]
|
||||
["publish"]
|
||||
["IntegrateAssetNew"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
if legacy_profiles:
|
||||
if not logger:
|
||||
logger = Logger.get_logger("get_template_name_profiles")
|
||||
|
||||
logger.warning((
|
||||
"Project \"{}\" is using legacy access to publish template."
|
||||
" It is recommended to move settings to new location"
|
||||
" 'project_settings/global/tools/publish/template_name_profiles'."
|
||||
).format(project_name))
|
||||
|
||||
# Replace "tasks" key with "task_names"
|
||||
profiles = []
|
||||
for profile in copy.deepcopy(legacy_profiles):
|
||||
profile["task_names"] = profile.pop("tasks", [])
|
||||
profiles.append(profile)
|
||||
return profiles
|
||||
|
||||
|
||||
def get_hero_template_name_profiles(
|
||||
project_name, project_settings=None, logger=None
|
||||
):
|
||||
"""Receive profiles for hero publish template keys.
|
||||
|
||||
At least one of arguments must be passed.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for templates.
|
||||
project_settings(Dic[str, Any]): Prepared project settings.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Publish template profiles.
|
||||
"""
|
||||
|
||||
if not project_name and not project_settings:
|
||||
raise ValueError((
|
||||
"Both project name and project settings are missing."
|
||||
" At least one must be entered."
|
||||
))
|
||||
|
||||
if not project_settings:
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
profiles = (
|
||||
project_settings
|
||||
["global"]
|
||||
["tools"]
|
||||
["publish"]
|
||||
["hero_template_name_profiles"]
|
||||
)
|
||||
if profiles:
|
||||
return copy.deepcopy(profiles)
|
||||
|
||||
# Use legacy approach for cases new settings are not filled yet for the
|
||||
# project
|
||||
legacy_profiles = copy.deepcopy(
|
||||
project_settings
|
||||
["global"]
|
||||
["publish"]
|
||||
["IntegrateHeroVersion"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
if legacy_profiles:
|
||||
if not logger:
|
||||
logger = Logger.get_logger("get_hero_template_name_profiles")
|
||||
|
||||
logger.warning((
|
||||
"Project \"{}\" is using legacy access to hero publish template."
|
||||
" It is recommended to move settings to new location"
|
||||
" 'project_settings/global/tools/publish/"
|
||||
"hero_template_name_profiles'."
|
||||
).format(project_name))
|
||||
return legacy_profiles
|
||||
|
||||
|
||||
def get_publish_template_name(
|
||||
project_name,
|
||||
host_name,
|
||||
family,
|
||||
task_name,
|
||||
task_type,
|
||||
project_settings=None,
|
||||
hero=False,
|
||||
logger=None
|
||||
):
|
||||
"""Get template name which should be used for passed context.
|
||||
|
||||
Publish templates are filtered by host name, family, task name and
|
||||
task type.
|
||||
|
||||
Default template which is used at if profiles are not available or profile
|
||||
has empty value is defined by 'DEFAULT_PUBLISH_TEMPLATE' constant.
|
||||
|
||||
Args:
|
||||
project_name (str): Name of project where to look for settings.
|
||||
host_name (str): Name of host integration.
|
||||
family (str): Family for which should be found template.
|
||||
task_name (str): Task name on which is intance working.
|
||||
task_type (str): Task type on which is intance working.
|
||||
project_setting (Dict[str, Any]): Prepared project settings.
|
||||
logger (logging.Logger): Custom logger used for 'filter_profiles'
|
||||
function.
|
||||
|
||||
Returns:
|
||||
str: Template name which should be used for integration.
|
||||
"""
|
||||
|
||||
template = None
|
||||
filter_criteria = {
|
||||
"hosts": host_name,
|
||||
"families": family,
|
||||
"task_names": task_name,
|
||||
"task_types": task_type,
|
||||
}
|
||||
if hero:
|
||||
default_template = DEFAULT_HERO_PUBLISH_TEMPLATE
|
||||
profiles = get_hero_template_name_profiles(
|
||||
project_name, project_settings, logger
|
||||
)
|
||||
|
||||
else:
|
||||
profiles = get_template_name_profiles(
|
||||
project_name, project_settings, logger
|
||||
)
|
||||
default_template = DEFAULT_PUBLISH_TEMPLATE
|
||||
|
||||
profile = filter_profiles(profiles, filter_criteria, logger=logger)
|
||||
if profile:
|
||||
template = profile["template_name"]
|
||||
return template or default_template
|
||||
|
||||
|
||||
class DiscoverResult:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import pyblish.api
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from openpype.client import get_representations
|
||||
|
||||
|
||||
class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin):
|
||||
"""Converts collected input representations to input versions.
|
||||
|
||||
Any data in `instance.data["inputRepresentations"]` gets converted into
|
||||
`instance.data["inputVersions"]` as supported in OpenPype v3.
|
||||
|
||||
"""
|
||||
# This is a ContextPlugin because then we can query the database only once
|
||||
# for the conversion of representation ids to version ids (optimization)
|
||||
label = "Input Representations to Versions"
|
||||
order = pyblish.api.CollectorOrder + 0.499
|
||||
hosts = ["*"]
|
||||
|
||||
def process(self, context):
|
||||
# Query all version ids for representation ids from the database once
|
||||
representations = set()
|
||||
for instance in context:
|
||||
inst_repre = instance.data.get("inputRepresentations", [])
|
||||
representations.update(inst_repre)
|
||||
|
||||
representations_docs = get_representations(
|
||||
project_name=context.data["projectEntity"]["name"],
|
||||
representation_ids=representations,
|
||||
fields=["_id", "parent"])
|
||||
|
||||
representation_id_to_version_id = {
|
||||
repre["_id"]: repre["parent"] for repre in representations_docs
|
||||
}
|
||||
|
||||
for instance in context:
|
||||
inst_repre = instance.data.get("inputRepresentations", [])
|
||||
if not inst_repre:
|
||||
continue
|
||||
|
||||
input_versions = instance.data.get("inputVersions", [])
|
||||
for repre_id in inst_repre:
|
||||
repre_id = ObjectId(repre_id)
|
||||
version_id = representation_id_to_version_id[repre_id]
|
||||
input_versions.append(version_id)
|
||||
instance.data["inputVersions"] = input_versions
|
||||
|
|
@ -29,6 +29,7 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
|||
# get basic variables
|
||||
otio_clip = instance.data["otioClip"]
|
||||
workfile_start = instance.data["workfileFrameStart"]
|
||||
workfile_source_duration = instance.data.get("notRetimedFramerange")
|
||||
|
||||
# get ranges
|
||||
otio_tl_range = otio_clip.range_in_parent()
|
||||
|
|
@ -54,6 +55,11 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
|||
frame_end = frame_start + otio.opentime.to_frames(
|
||||
otio_tl_range.duration, otio_tl_range.duration.rate) - 1
|
||||
|
||||
# in case of retimed clip and frame range should not be retimed
|
||||
if workfile_source_duration:
|
||||
frame_end = frame_start + otio.opentime.to_frames(
|
||||
otio_src_range.duration, otio_src_range.duration.rate) - 1
|
||||
|
||||
data = {
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ class ExtractOTIOFile(publish.Extractor):
|
|||
hosts = ["resolve", "hiero", "traypublisher"]
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.context.data.get("otioTimeline"):
|
||||
return
|
||||
# create representation data
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import copy
|
|||
import clique
|
||||
import six
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client.operations import (
|
||||
OperationsSession,
|
||||
new_subset_document,
|
||||
|
|
@ -14,8 +17,6 @@ from openpype.client.operations import (
|
|||
prepare_version_update_data,
|
||||
prepare_representation_update_data,
|
||||
)
|
||||
from bson.objectid import ObjectId
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import (
|
||||
get_representations,
|
||||
|
|
@ -23,10 +24,12 @@ from openpype.client import (
|
|||
get_version_by_name,
|
||||
)
|
||||
from openpype.lib import source_hash
|
||||
from openpype.lib.profiles_filtering import filter_profiles
|
||||
from openpype.lib.file_transaction import FileTransaction
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.publish import KnownPublishError
|
||||
from openpype.pipeline.publish import (
|
||||
KnownPublishError,
|
||||
get_publish_template_name,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -792,52 +795,26 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
|
|||
|
||||
def get_template_name(self, instance):
|
||||
"""Return anatomy template name to use for integration"""
|
||||
# Define publish template name from profiles
|
||||
filter_criteria = self.get_profile_filter_criteria(instance)
|
||||
template_name_profiles = self._get_template_name_profiles(instance)
|
||||
profile = filter_profiles(
|
||||
template_name_profiles,
|
||||
filter_criteria,
|
||||
logger=self.log
|
||||
)
|
||||
|
||||
if profile:
|
||||
return profile["template_name"]
|
||||
return self.default_template_name
|
||||
|
||||
def _get_template_name_profiles(self, instance):
|
||||
"""Receive profiles for publish template keys.
|
||||
|
||||
Reuse template name profiles from legacy integrator. Goal is to move
|
||||
the profile settings out of plugin settings but until that happens we
|
||||
want to be able set it at one place and don't break backwards
|
||||
compatibility (more then once).
|
||||
"""
|
||||
|
||||
return (
|
||||
instance.context.data["project_settings"]
|
||||
["global"]
|
||||
["publish"]
|
||||
["IntegrateAssetNew"]
|
||||
["template_name_profiles"]
|
||||
)
|
||||
|
||||
def get_profile_filter_criteria(self, instance):
|
||||
"""Return filter criteria for `filter_profiles`"""
|
||||
|
||||
# Anatomy data is pre-filled by Collectors
|
||||
anatomy_data = instance.data["anatomyData"]
|
||||
|
||||
project_name = legacy_io.active_project()
|
||||
|
||||
# Task can be optional in anatomy data
|
||||
task = anatomy_data.get("task", {})
|
||||
host_name = instance.context.data["hostName"]
|
||||
anatomy_data = instance.data["anatomyData"]
|
||||
family = anatomy_data["family"]
|
||||
task_info = anatomy_data.get("task") or {}
|
||||
|
||||
# Return filter criteria
|
||||
return {
|
||||
"families": anatomy_data["family"],
|
||||
"tasks": task.get("name"),
|
||||
"task_types": task.get("type"),
|
||||
"hosts": instance.context.data["hostName"],
|
||||
}
|
||||
return get_publish_template_name(
|
||||
project_name,
|
||||
host_name,
|
||||
family,
|
||||
task_name=task_info.get("name"),
|
||||
task_type=task_info.get("type"),
|
||||
project_settings=instance.context.data["project_settings"],
|
||||
logger=self.log
|
||||
)
|
||||
|
||||
def get_rootless_path(self, anatomy, path):
|
||||
"""Returns, if possible, path without absolute portion from root
|
||||
|
|
|
|||
|
|
@ -14,14 +14,12 @@ from openpype.client import (
|
|||
get_archived_representations,
|
||||
get_representations,
|
||||
)
|
||||
from openpype.lib import (
|
||||
create_hard_link,
|
||||
filter_profiles
|
||||
)
|
||||
from openpype.lib import create_hard_link
|
||||
from openpype.pipeline import (
|
||||
schema,
|
||||
legacy_io,
|
||||
)
|
||||
from openpype.pipeline.publish import get_publish_template_name
|
||||
|
||||
|
||||
class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
||||
|
|
@ -68,10 +66,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
)
|
||||
return
|
||||
|
||||
template_key = self._get_template_key(instance)
|
||||
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
project_name = anatomy.project_name
|
||||
|
||||
template_key = self._get_template_key(project_name, instance)
|
||||
|
||||
if template_key not in anatomy.templates:
|
||||
self.log.warning((
|
||||
"!!! Anatomy of project \"{}\" does not have set"
|
||||
|
|
@ -527,30 +526,24 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
|
|||
|
||||
return publish_folder
|
||||
|
||||
def _get_template_key(self, instance):
|
||||
def _get_template_key(self, project_name, instance):
|
||||
anatomy_data = instance.data["anatomyData"]
|
||||
task_data = anatomy_data.get("task") or {}
|
||||
task_name = task_data.get("name")
|
||||
task_type = task_data.get("type")
|
||||
task_info = anatomy_data.get("task") or {}
|
||||
host_name = instance.context.data["hostName"]
|
||||
|
||||
# TODO raise error if Hero not set?
|
||||
family = self.main_family_from_instance(instance)
|
||||
key_values = {
|
||||
"families": family,
|
||||
"task_names": task_name,
|
||||
"task_types": task_type,
|
||||
"hosts": host_name
|
||||
}
|
||||
profile = filter_profiles(
|
||||
self.template_name_profiles,
|
||||
key_values,
|
||||
|
||||
return get_publish_template_name(
|
||||
project_name,
|
||||
host_name,
|
||||
family,
|
||||
task_info.get("name"),
|
||||
task_info.get("type"),
|
||||
project_settings=instance.context.data["project_settings"],
|
||||
hero=True,
|
||||
logger=self.log
|
||||
)
|
||||
if profile:
|
||||
template_name = profile["template_name"]
|
||||
else:
|
||||
template_name = self._default_template_name
|
||||
return template_name
|
||||
|
||||
def main_family_from_instance(self, instance):
|
||||
"""Returns main family of entered instance."""
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from bson.objectid import ObjectId
|
|||
from pymongo import DeleteOne, InsertOne
|
||||
import pyblish.api
|
||||
|
||||
import openpype.api
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
get_subset_by_id,
|
||||
|
|
@ -25,14 +24,17 @@ from openpype.client import (
|
|||
get_representations,
|
||||
get_archived_representations,
|
||||
)
|
||||
from openpype.lib.profiles_filtering import filter_profiles
|
||||
from openpype.lib import (
|
||||
prepare_template_data,
|
||||
create_hard_link,
|
||||
StringTemplate,
|
||||
TemplateUnsolved
|
||||
TemplateUnsolved,
|
||||
source_hash,
|
||||
filter_profiles,
|
||||
get_local_site_id,
|
||||
)
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.publish import get_publish_template_name
|
||||
|
||||
# this is needed until speedcopy for linux is fixed
|
||||
if sys.platform == "win32":
|
||||
|
|
@ -138,7 +140,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
integrated_file_sizes = {}
|
||||
|
||||
# Attributes set by settings
|
||||
template_name_profiles = None
|
||||
subset_grouping_profiles = None
|
||||
|
||||
def process(self, instance):
|
||||
|
|
@ -388,22 +389,16 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
family = self.main_family_from_instance(instance)
|
||||
|
||||
key_values = {
|
||||
"families": family,
|
||||
"tasks": task_name,
|
||||
"hosts": instance.context.data["hostName"],
|
||||
"task_types": task_type
|
||||
}
|
||||
profile = filter_profiles(
|
||||
self.template_name_profiles,
|
||||
key_values,
|
||||
template_name = get_publish_template_name(
|
||||
project_name,
|
||||
instance.context.data["hostName"],
|
||||
family,
|
||||
task_name=task_info.get("name"),
|
||||
task_type=task_info.get("type"),
|
||||
project_settings=instance.context.data["project_settings"],
|
||||
logger=self.log
|
||||
)
|
||||
|
||||
template_name = "publish"
|
||||
if profile:
|
||||
template_name = profile["template_name"]
|
||||
|
||||
published_representations = {}
|
||||
for idx, repre in enumerate(repres):
|
||||
published_files = []
|
||||
|
|
@ -1058,7 +1053,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
for _src, dest in resources:
|
||||
path = self.get_rootless_path(anatomy, dest)
|
||||
dest = self.get_dest_temp_url(dest)
|
||||
file_hash = openpype.api.source_hash(dest)
|
||||
file_hash = source_hash(dest)
|
||||
if self.TMP_FILE_EXT and \
|
||||
',{}'.format(self.TMP_FILE_EXT) in file_hash:
|
||||
file_hash = file_hash.replace(',{}'.format(self.TMP_FILE_EXT),
|
||||
|
|
@ -1168,7 +1163,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
def _get_sites(self, sync_project_presets):
|
||||
"""Returns tuple (local_site, remote_site)"""
|
||||
local_site_id = openpype.api.get_local_site_id()
|
||||
local_site_id = get_local_site_id()
|
||||
local_site = sync_project_presets["config"]. \
|
||||
get("active_site", "studio").strip()
|
||||
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@
|
|||
"family_mapping": {
|
||||
"camera": "cam",
|
||||
"look": "look",
|
||||
"mayaascii": "scene",
|
||||
"mayaAscii": "scene",
|
||||
"model": "geo",
|
||||
"rig": "rig",
|
||||
"setdress": "setdress",
|
||||
|
|
|
|||
|
|
@ -418,6 +418,10 @@
|
|||
"filter_families": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"publish": {
|
||||
"template_name_profiles": [],
|
||||
"hero_template_name_profiles": []
|
||||
}
|
||||
},
|
||||
"project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets\": {\"characters\": {}, \"locations\": {}}, \"shots\": {}}}",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
{
|
||||
"shelves": [
|
||||
{
|
||||
"shelf_set_name": "OpenPype Shelves",
|
||||
"shelf_set_source_path": {
|
||||
"windows": "",
|
||||
"darwin": "",
|
||||
"linux": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"create": {
|
||||
"CreateArnoldAss": {
|
||||
"enabled": true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"mel_workspace": "workspace -fr \"shaders\" \"renderData/shaders\";\nworkspace -fr \"images\" \"renders\";\nworkspace -fr \"particles\" \"particles\";\nworkspace -fr \"mayaAscii\" \"\";\nworkspace -fr \"mayaBinary\" \"\";\nworkspace -fr \"scene\" \"\";\nworkspace -fr \"alembicCache\" \"cache/alembic\";\nworkspace -fr \"renderData\" \"renderData\";\nworkspace -fr \"sourceImages\" \"sourceimages\";\nworkspace -fr \"fileCache\" \"cache/nCache\";\n",
|
||||
"ext_mapping": {
|
||||
"model": "ma",
|
||||
"mayaAscii": "ma",
|
||||
|
|
@ -677,25 +678,20 @@
|
|||
"isolate_view": true,
|
||||
"off_screen": true
|
||||
},
|
||||
"PanZoom": {
|
||||
"pan_zoom": true
|
||||
},
|
||||
"Renderer": {
|
||||
"rendererName": "vp2Renderer"
|
||||
},
|
||||
"Resolution": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"percent": 1.0,
|
||||
"mode": "Custom"
|
||||
"height": 1080
|
||||
},
|
||||
"Viewport Options": {
|
||||
"override_viewport_options": true,
|
||||
"displayLights": "default",
|
||||
"displayTextures": true,
|
||||
"textureMaxResolution": 1024,
|
||||
"renderDepthOfField": true,
|
||||
"shadows": true,
|
||||
"textures": true,
|
||||
"twoSidedLighting": true,
|
||||
"lineAAEnable": true,
|
||||
"multiSample": 8,
|
||||
|
|
@ -718,7 +714,6 @@
|
|||
"motionBlurShutterOpenFraction": 0.2,
|
||||
"cameras": false,
|
||||
"clipGhosts": false,
|
||||
"controlVertices": false,
|
||||
"deformers": false,
|
||||
"dimensions": false,
|
||||
"dynamicConstraints": false,
|
||||
|
|
@ -730,8 +725,7 @@
|
|||
"grid": false,
|
||||
"hairSystems": true,
|
||||
"handles": false,
|
||||
"hud": false,
|
||||
"hulls": false,
|
||||
"headsUpDisplay": false,
|
||||
"ikHandles": false,
|
||||
"imagePlane": true,
|
||||
"joints": false,
|
||||
|
|
@ -742,7 +736,9 @@
|
|||
"nCloths": false,
|
||||
"nParticles": false,
|
||||
"nRigids": false,
|
||||
"controlVertices": false,
|
||||
"nurbsCurves": false,
|
||||
"hulls": false,
|
||||
"nurbsSurfaces": false,
|
||||
"particleInstancers": false,
|
||||
"pivots": false,
|
||||
|
|
@ -750,7 +746,8 @@
|
|||
"pluginShapes": false,
|
||||
"polymeshes": true,
|
||||
"strokes": false,
|
||||
"subdivSurfaces": false
|
||||
"subdivSurfaces": false,
|
||||
"textures": false
|
||||
},
|
||||
"Camera Options": {
|
||||
"displayGateMask": false,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
"label": "Houdini",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_houdini_scriptshelf"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_houdini_create"
|
||||
|
|
@ -14,4 +18,4 @@
|
|||
"name": "schema_houdini_publish"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,13 @@
|
|||
"label": "Maya",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"multiline" : true,
|
||||
"use_label_wrap": true,
|
||||
"key": "mel_workspace",
|
||||
"label": "Maya MEL Workspace"
|
||||
},
|
||||
{
|
||||
"type": "dict-modifiable",
|
||||
"key": "ext_mapping",
|
||||
|
|
|
|||
|
|
@ -663,10 +663,14 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>NOTE:</b> Publish template profiles settings were moved to <a href=\"settings://project_settings/global/tools/publish/template_name_profiles\"><b>Tools/Publish/Template name profiles</b></a>. Please move values there."
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "template_name_profiles",
|
||||
"label": "Template name profiles",
|
||||
"label": "Template name profiles (DEPRECATED)",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
|
|
@ -771,10 +775,14 @@
|
|||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>NOTE:</b> Hero publish template profiles settings were moved to <a href=\"settings://project_settings/global/tools/publish/hero_template_name_profiles\"><b>Tools/Publish/Hero template name profiles</b></a>. Please move values there."
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "template_name_profiles",
|
||||
"label": "Template name profiles",
|
||||
"label": "Template name profiles (DEPRECATED)",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
|
|
|
|||
|
|
@ -284,6 +284,102 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "publish",
|
||||
"label": "Publish",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>NOTE:</b> For backwards compatibility can be value empty and in that case are used values from <a href=\"settings://project_settings/global/publish/IntegrateAssetNew\"><b>IntegrateAssetNew</b></a>. This will change in future so please move all values here as soon as possible."
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "template_name_profiles",
|
||||
"label": "Template name profiles",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "task_names",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "template_name",
|
||||
"label": "Template name"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "hero_template_name_profiles",
|
||||
"label": "Hero template name profiles",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"key": "families",
|
||||
"label": "Families",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "hosts-enum",
|
||||
"key": "hosts",
|
||||
"label": "Hosts",
|
||||
"multiselection": true
|
||||
},
|
||||
{
|
||||
"key": "task_types",
|
||||
"label": "Task types",
|
||||
"type": "task-types-enum"
|
||||
},
|
||||
{
|
||||
"key": "task_names",
|
||||
"label": "Task names",
|
||||
"type": "list",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "template_name",
|
||||
"label": "Template name",
|
||||
"tooltip": "Name of template from Anatomy templates"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"type": "list",
|
||||
"key": "shelves",
|
||||
"label": "Shelves Manager",
|
||||
"is_group": true,
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "shelf_set_name",
|
||||
"label": "Shelf Set Name"
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"key": "shelf_set_source_path",
|
||||
"label": "Shelf Set Path (optional)",
|
||||
"multipath": false,
|
||||
"multiplatform": true
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "shelf_definition",
|
||||
"label": "Shelves",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "shelf_name",
|
||||
"label": "Shelf Name"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "tools_list",
|
||||
"label": "Tools",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "dict",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "label",
|
||||
"label": "Name"
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"key": "script",
|
||||
"label": "Script"
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"key": "icon",
|
||||
"label": "Icon"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "help",
|
||||
"label": "Help"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -94,18 +94,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "PanZoom",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pan_zoom",
|
||||
"label": " Pan Zoom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
|
|
@ -153,19 +141,6 @@
|
|||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 99999
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "percent",
|
||||
"label": "percent",
|
||||
"decimal": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 200
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "mode",
|
||||
"label": "Mode"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -195,6 +170,11 @@
|
|||
{ "nolights": "No Lights"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayTextures",
|
||||
"label": "Display Textures"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "textureMaxResolution",
|
||||
|
|
@ -217,11 +197,6 @@
|
|||
"key": "shadows",
|
||||
"label": "Display Shadows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "textures",
|
||||
"label": "Display Textures"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "twoSidedLighting",
|
||||
|
|
@ -369,120 +344,114 @@
|
|||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Show</b>"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "cameras",
|
||||
"label": "cameras"
|
||||
"label": "Cameras"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "clipGhosts",
|
||||
"label": "clipGhosts"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "controlVertices",
|
||||
"label": "controlVertices"
|
||||
"label": "Clip Ghosts"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "deformers",
|
||||
"label": "deformers"
|
||||
"label": "Deformers"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dimensions",
|
||||
"label": "dimensions"
|
||||
"label": "Dimensions"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dynamicConstraints",
|
||||
"label": "dynamicConstraints"
|
||||
"label": "Dynamic Constraints"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dynamics",
|
||||
"label": "dynamics"
|
||||
"label": "Dynamics"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "fluids",
|
||||
"label": "fluids"
|
||||
"label": "Fluids"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "follicles",
|
||||
"label": "follicles"
|
||||
"label": "Follicles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "gpuCacheDisplayFilter",
|
||||
"label": "gpuCacheDisplayFilter"
|
||||
"label": "GPU Cache"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "greasePencils",
|
||||
"label": "greasePencils"
|
||||
"label": "Grease Pencil"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "grid",
|
||||
"label": "grid"
|
||||
"label": "Grid"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hairSystems",
|
||||
"label": "hairSystems"
|
||||
"label": "Hair Systems"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "handles",
|
||||
"label": "handles"
|
||||
"label": "Handles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hud",
|
||||
"label": "hud"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hulls",
|
||||
"label": "hulls"
|
||||
"key": "headsUpDisplay",
|
||||
"label": "HUD"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "ikHandles",
|
||||
"label": "ikHandles"
|
||||
"label": "IK Handles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "imagePlane",
|
||||
"label": "imagePlane"
|
||||
"label": "Image Planes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "joints",
|
||||
"label": "joints"
|
||||
"label": "Joints"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "lights",
|
||||
"label": "lights"
|
||||
"label": "Lights"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "locators",
|
||||
"label": "locators"
|
||||
"label": "Locators"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "manipulators",
|
||||
"label": "manipulators"
|
||||
"label": "Manipulators"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "motionTrails",
|
||||
"label": "motionTrails"
|
||||
"label": "Motion Trails"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
|
@ -499,50 +468,65 @@
|
|||
"key": "nRigids",
|
||||
"label": "nRigids"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "controlVertices",
|
||||
"label": "NURBS CVs"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nurbsCurves",
|
||||
"label": "nurbsCurves"
|
||||
"label": "NURBS Curves"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hulls",
|
||||
"label": "NURBS Hulls"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nurbsSurfaces",
|
||||
"label": "nurbsSurfaces"
|
||||
"label": "NURBS Surfaces"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "particleInstancers",
|
||||
"label": "particleInstancers"
|
||||
"label": "Particle Instancers"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pivots",
|
||||
"label": "pivots"
|
||||
"label": "Pivots"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "planes",
|
||||
"label": "planes"
|
||||
"label": "Planes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pluginShapes",
|
||||
"label": "pluginShapes"
|
||||
"label": "Plugin Shapes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "polymeshes",
|
||||
"label": "polymeshes"
|
||||
"label": "Polygons"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "strokes",
|
||||
"label": "strokes"
|
||||
"label": "Strokes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "subdivSurfaces",
|
||||
"label": "subdivSurfaces"
|
||||
"label": "Subdiv Surfaces"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "textures",
|
||||
"label": "Texture Placements"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -555,47 +539,47 @@
|
|||
{
|
||||
"type": "boolean",
|
||||
"key": "displayGateMask",
|
||||
"label": "displayGateMask"
|
||||
"label": "Display Gate Mask"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayResolution",
|
||||
"label": "displayResolution"
|
||||
"label": "Display Resolution"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmGate",
|
||||
"label": "displayFilmGate"
|
||||
"label": "Display Film Gate"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFieldChart",
|
||||
"label": "displayFieldChart"
|
||||
"label": "Display Field Chart"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displaySafeAction",
|
||||
"label": "displaySafeAction"
|
||||
"label": "Display Safe Action"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displaySafeTitle",
|
||||
"label": "displaySafeTitle"
|
||||
"label": "Display Safe Title"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmPivot",
|
||||
"label": "displayFilmPivot"
|
||||
"label": "Display Film Pivot"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmOrigin",
|
||||
"label": "displayFilmOrigin"
|
||||
"label": "Display Film Origin"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "overscan",
|
||||
"label": "overscan",
|
||||
"label": "Overscan",
|
||||
"decimal": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
|
|
|
|||
|
|
@ -281,18 +281,25 @@ class ActionModel(QtGui.QStandardItemModel):
|
|||
if not action_item:
|
||||
return
|
||||
|
||||
action = action_item.data(ACTION_ROLE)
|
||||
actual_data = self._prepare_compare_data(action)
|
||||
actions = action_item.data(ACTION_ROLE)
|
||||
if not isinstance(actions, list):
|
||||
actions = [actions]
|
||||
|
||||
action_actions_data = [
|
||||
self._prepare_compare_data(action)
|
||||
for action in actions
|
||||
]
|
||||
|
||||
stored = self.launcher_registry.get_item("force_not_open_workfile")
|
||||
if is_checked:
|
||||
stored.append(actual_data)
|
||||
else:
|
||||
final_values = []
|
||||
for config in stored:
|
||||
if config != actual_data:
|
||||
final_values.append(config)
|
||||
stored = final_values
|
||||
for actual_data in action_actions_data:
|
||||
if is_checked:
|
||||
stored.append(actual_data)
|
||||
else:
|
||||
final_values = []
|
||||
for config in stored:
|
||||
if config != actual_data:
|
||||
final_values.append(config)
|
||||
stored = final_values
|
||||
|
||||
self.launcher_registry.set_item("force_not_open_workfile", stored)
|
||||
self.launcher_registry._get_item.cache_clear()
|
||||
|
|
@ -329,21 +336,24 @@ class ActionModel(QtGui.QStandardItemModel):
|
|||
item (QStandardItem)
|
||||
stored (list) of dict
|
||||
"""
|
||||
action = item.data(ACTION_ROLE)
|
||||
if not self.is_application_action(action):
|
||||
|
||||
actions = item.data(ACTION_ROLE)
|
||||
if not isinstance(actions, list):
|
||||
actions = [actions]
|
||||
|
||||
if not self.is_application_action(actions[0]):
|
||||
return False
|
||||
|
||||
actual_data = self._prepare_compare_data(action)
|
||||
action_actions_data = [
|
||||
self._prepare_compare_data(action)
|
||||
for action in actions
|
||||
]
|
||||
for config in stored:
|
||||
if config == actual_data:
|
||||
if config in action_actions_data:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _prepare_compare_data(self, action):
|
||||
if isinstance(action, list) and action:
|
||||
action = action[0]
|
||||
|
||||
compare_data = {}
|
||||
if action and action.label:
|
||||
compare_data = {
|
||||
|
|
|
|||
|
|
@ -312,11 +312,12 @@ class ActionBar(QtWidgets.QWidget):
|
|||
|
||||
is_group = index.data(GROUP_ROLE)
|
||||
is_variant_group = index.data(VARIANT_GROUP_ROLE)
|
||||
force_not_open_workfile = index.data(FORCE_NOT_OPEN_WORKFILE_ROLE)
|
||||
if not is_group and not is_variant_group:
|
||||
action = index.data(ACTION_ROLE)
|
||||
# Change data of application action
|
||||
if issubclass(action, ApplicationAction):
|
||||
if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE):
|
||||
if force_not_open_workfile:
|
||||
action.data["start_last_workfile"] = False
|
||||
else:
|
||||
action.data.pop("start_last_workfile", None)
|
||||
|
|
@ -385,10 +386,18 @@ class ActionBar(QtWidgets.QWidget):
|
|||
menu.addMenu(sub_menu)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
action = actions_mapping[result]
|
||||
self._start_animation(index)
|
||||
self.action_clicked.emit(action)
|
||||
if not result:
|
||||
return
|
||||
|
||||
action = actions_mapping[result]
|
||||
if issubclass(action, ApplicationAction):
|
||||
if force_not_open_workfile:
|
||||
action.data["start_last_workfile"] = False
|
||||
else:
|
||||
action.data.pop("start_last_workfile", None)
|
||||
|
||||
self._start_animation(index)
|
||||
self.action_clicked.emit(action)
|
||||
|
||||
|
||||
class ActionHistory(QtWidgets.QPushButton):
|
||||
|
|
|
|||
28
openpype/tools/loader/delegates.py
Normal file
28
openpype/tools/loader/delegates.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from Qt import QtWidgets, QtGui, QtCore
|
||||
|
||||
|
||||
class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Delegate for Loaded in Scene state columns.
|
||||
|
||||
Shows "yes" or "no" for True or False values
|
||||
Colorizes green or dark grey based on True or False values
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedInSceneDelegate, self).__init__(*args, **kwargs)
|
||||
self._colors = {
|
||||
True: QtGui.QColor(80, 170, 80),
|
||||
False: QtGui.QColor(90, 90, 90)
|
||||
}
|
||||
|
||||
def displayText(self, value, locale):
|
||||
return "yes" if value else "no"
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super(LoadedInSceneDelegate, self).initStyleOption(option, index)
|
||||
|
||||
# Colorize based on value
|
||||
value = index.data(QtCore.Qt.DisplayRole)
|
||||
color = self._colors[bool(value)]
|
||||
option.palette.setBrush(QtGui.QPalette.Text, color)
|
||||
|
|
@ -17,6 +17,7 @@ from openpype.client import (
|
|||
get_representations
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
registered_host,
|
||||
HeroVersionType,
|
||||
schema,
|
||||
)
|
||||
|
|
@ -24,6 +25,7 @@ from openpype.pipeline import (
|
|||
from openpype.style import get_default_entity_icon_color
|
||||
from openpype.tools.utils.models import TreeModel, Item
|
||||
from openpype.tools.utils import lib
|
||||
from openpype.host import ILoadHost
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype.tools.utils.constants import (
|
||||
|
|
@ -136,6 +138,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
"duration",
|
||||
"handles",
|
||||
"step",
|
||||
"loaded_in_scene",
|
||||
"repre_info"
|
||||
]
|
||||
|
||||
|
|
@ -150,6 +153,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
"duration": "Duration",
|
||||
"handles": "Handles",
|
||||
"step": "Step",
|
||||
"loaded_in_scene": "In scene",
|
||||
"repre_info": "Availability"
|
||||
}
|
||||
|
||||
|
|
@ -231,8 +235,14 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
self._doc_fetching_stop = False
|
||||
self._doc_payload = {}
|
||||
|
||||
self.doc_fetched.connect(self._on_doc_fetched)
|
||||
self._host = registered_host()
|
||||
self._loaded_representation_ids = set()
|
||||
|
||||
# Refresh loaded scene containers only every 3 seconds at most
|
||||
self._host_loaded_refresh_timeout = 3
|
||||
self._host_loaded_refresh_time = 0
|
||||
|
||||
self.doc_fetched.connect(self._on_doc_fetched)
|
||||
self.refresh()
|
||||
|
||||
def get_item_by_id(self, item_id):
|
||||
|
|
@ -474,6 +484,27 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
|
||||
last_versions_by_subset_id[subset_id] = hero_version
|
||||
|
||||
# Check loaded subsets
|
||||
loaded_subset_ids = set()
|
||||
ids = self._loaded_representation_ids
|
||||
if ids:
|
||||
if self._doc_fetching_stop:
|
||||
return
|
||||
|
||||
# Get subset ids from loaded representations in workfile
|
||||
# todo: optimize with aggregation query to distinct subset id
|
||||
representations = get_representations(project_name,
|
||||
representation_ids=ids,
|
||||
fields=["parent"])
|
||||
version_ids = set(repre["parent"] for repre in representations)
|
||||
versions = get_versions(project_name,
|
||||
version_ids=version_ids,
|
||||
fields=["parent"])
|
||||
loaded_subset_ids = set(version["parent"] for version in versions)
|
||||
|
||||
if self._doc_fetching_stop:
|
||||
return
|
||||
|
||||
repre_info_by_version_id = {}
|
||||
if self.sync_server.enabled:
|
||||
versions_by_id = {}
|
||||
|
|
@ -501,7 +532,8 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
"subset_docs_by_id": subset_docs_by_id,
|
||||
"subset_families": subset_families,
|
||||
"last_versions_by_subset_id": last_versions_by_subset_id,
|
||||
"repre_info_by_version_id": repre_info_by_version_id
|
||||
"repre_info_by_version_id": repre_info_by_version_id,
|
||||
"subsets_loaded_by_id": loaded_subset_ids
|
||||
}
|
||||
|
||||
self.doc_fetched.emit()
|
||||
|
|
@ -533,6 +565,20 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
self.doc_fetched.emit()
|
||||
return
|
||||
|
||||
# Collect scene container representations to compare loaded state
|
||||
# This runs in the main thread because it involves the host DCC
|
||||
if self._host:
|
||||
time_since_refresh = time.time() - self._host_loaded_refresh_time
|
||||
if time_since_refresh > self._host_loaded_refresh_timeout:
|
||||
if isinstance(self._host, ILoadHost):
|
||||
containers = self._host.get_containers()
|
||||
else:
|
||||
containers = self._host.ls()
|
||||
|
||||
repre_ids = {con.get("representation") for con in containers}
|
||||
self._loaded_representation_ids = repre_ids
|
||||
self._host_loaded_refresh_time = time.time()
|
||||
|
||||
self.fetch_subset_and_version()
|
||||
|
||||
def _on_doc_fetched(self):
|
||||
|
|
@ -554,6 +600,10 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
"repre_info_by_version_id"
|
||||
)
|
||||
|
||||
subsets_loaded_by_id = self._doc_payload.get(
|
||||
"subsets_loaded_by_id"
|
||||
)
|
||||
|
||||
if (
|
||||
asset_docs_by_id is None
|
||||
or subset_docs_by_id is None
|
||||
|
|
@ -568,7 +618,8 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
asset_docs_by_id,
|
||||
subset_docs_by_id,
|
||||
last_versions_by_subset_id,
|
||||
repre_info_by_version_id
|
||||
repre_info_by_version_id,
|
||||
subsets_loaded_by_id
|
||||
)
|
||||
self.endResetModel()
|
||||
self.refreshed.emit(True)
|
||||
|
|
@ -596,8 +647,12 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
return merge_group
|
||||
|
||||
def _fill_subset_items(
|
||||
self, asset_docs_by_id, subset_docs_by_id, last_versions_by_subset_id,
|
||||
repre_info_by_version_id
|
||||
self,
|
||||
asset_docs_by_id,
|
||||
subset_docs_by_id,
|
||||
last_versions_by_subset_id,
|
||||
repre_info_by_version_id,
|
||||
subsets_loaded_by_id
|
||||
):
|
||||
_groups_tuple = self.groups_config.split_subsets_for_groups(
|
||||
subset_docs_by_id.values(), self._grouping
|
||||
|
|
@ -621,6 +676,35 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
"index": self.index(group_item.row(), 0)
|
||||
}
|
||||
|
||||
def _add_subset_item(subset_doc, parent_item, parent_index):
|
||||
last_version = last_versions_by_subset_id.get(
|
||||
subset_doc["_id"]
|
||||
)
|
||||
# do not show subset without version
|
||||
if not last_version:
|
||||
return
|
||||
|
||||
data = copy.deepcopy(subset_doc)
|
||||
data["subset"] = subset_doc["name"]
|
||||
|
||||
asset_id = subset_doc["parent"]
|
||||
data["asset"] = asset_docs_by_id[asset_id]["name"]
|
||||
|
||||
data["last_version"] = last_version
|
||||
data["loaded_in_scene"] = subset_doc["_id"] in subsets_loaded_by_id
|
||||
|
||||
# Sync server data
|
||||
data.update(
|
||||
self._get_last_repre_info(repre_info_by_version_id,
|
||||
last_version["_id"]))
|
||||
|
||||
item = Item()
|
||||
item.update(data)
|
||||
self.add_child(item, parent_item)
|
||||
|
||||
index = self.index(item.row(), 0, parent_index)
|
||||
self.set_version(index, last_version)
|
||||
|
||||
subset_counter = 0
|
||||
for group_name, subset_docs_by_name in subset_docs_by_group.items():
|
||||
parent_item = group_item_by_name[group_name]["item"]
|
||||
|
|
@ -643,31 +727,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
_parent_index = parent_index
|
||||
|
||||
for subset_doc in subset_docs:
|
||||
asset_id = subset_doc["parent"]
|
||||
|
||||
data = copy.deepcopy(subset_doc)
|
||||
data["subset"] = subset_name
|
||||
data["asset"] = asset_docs_by_id[asset_id]["name"]
|
||||
|
||||
last_version = last_versions_by_subset_id.get(
|
||||
subset_doc["_id"]
|
||||
)
|
||||
data["last_version"] = last_version
|
||||
|
||||
# do not show subset without version
|
||||
if not last_version:
|
||||
continue
|
||||
|
||||
data.update(
|
||||
self._get_last_repre_info(repre_info_by_version_id,
|
||||
last_version["_id"]))
|
||||
|
||||
item = Item()
|
||||
item.update(data)
|
||||
self.add_child(item, _parent_item)
|
||||
|
||||
index = self.index(item.row(), 0, _parent_index)
|
||||
self.set_version(index, last_version)
|
||||
_add_subset_item(subset_doc,
|
||||
parent_item=_parent_item,
|
||||
parent_index=_parent_index)
|
||||
|
||||
for subset_name in sorted(subset_docs_without_group.keys()):
|
||||
subset_docs = subset_docs_without_group[subset_name]
|
||||
|
|
@ -682,31 +744,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
|
|||
subset_counter += 1
|
||||
|
||||
for subset_doc in subset_docs:
|
||||
asset_id = subset_doc["parent"]
|
||||
|
||||
data = copy.deepcopy(subset_doc)
|
||||
data["subset"] = subset_name
|
||||
data["asset"] = asset_docs_by_id[asset_id]["name"]
|
||||
|
||||
last_version = last_versions_by_subset_id.get(
|
||||
subset_doc["_id"]
|
||||
)
|
||||
data["last_version"] = last_version
|
||||
|
||||
# do not show subset without version
|
||||
if not last_version:
|
||||
continue
|
||||
|
||||
data.update(
|
||||
self._get_last_repre_info(repre_info_by_version_id,
|
||||
last_version["_id"]))
|
||||
|
||||
item = Item()
|
||||
item.update(data)
|
||||
self.add_child(item, parent_item)
|
||||
|
||||
index = self.index(item.row(), 0, parent_index)
|
||||
self.set_version(index, last_version)
|
||||
_add_subset_item(subset_doc,
|
||||
parent_item=parent_item,
|
||||
parent_index=parent_index)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ from .model import (
|
|||
ITEM_ID_ROLE
|
||||
)
|
||||
from . import lib
|
||||
from .delegates import LoadedInSceneDelegate
|
||||
|
||||
from openpype.tools.utils.constants import (
|
||||
LOCAL_PROVIDER_ROLE,
|
||||
|
|
@ -169,6 +170,7 @@ class SubsetWidget(QtWidgets.QWidget):
|
|||
("duration", 60),
|
||||
("handles", 55),
|
||||
("step", 10),
|
||||
("loaded_in_scene", 25),
|
||||
("repre_info", 65)
|
||||
)
|
||||
|
||||
|
|
@ -234,6 +236,10 @@ class SubsetWidget(QtWidgets.QWidget):
|
|||
column = model.Columns.index("repre_info")
|
||||
view.setItemDelegateForColumn(column, avail_delegate)
|
||||
|
||||
loaded_in_scene_delegate = LoadedInSceneDelegate(view)
|
||||
column = model.Columns.index("loaded_in_scene")
|
||||
view.setItemDelegateForColumn(column, loaded_in_scene_delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class HostToolsHelper:
|
|||
self._workfiles_tool = None
|
||||
self._loader_tool = None
|
||||
self._creator_tool = None
|
||||
self._publisher_tool = None
|
||||
self._subset_manager_tool = None
|
||||
self._scene_inventory_tool = None
|
||||
self._library_loader_tool = None
|
||||
|
|
@ -193,7 +194,6 @@ class HostToolsHelper:
|
|||
library_loader_tool.showNormal()
|
||||
library_loader_tool.refresh()
|
||||
|
||||
|
||||
def show_publish(self, parent=None):
|
||||
"""Try showing the most desirable publish GUI
|
||||
|
||||
|
|
@ -269,6 +269,31 @@ class HostToolsHelper:
|
|||
dialog.activateWindow()
|
||||
dialog.showNormal()
|
||||
|
||||
def get_publisher_tool(self, parent):
|
||||
"""Create, cache and return publisher window."""
|
||||
|
||||
if self._publisher_tool is None:
|
||||
from openpype.tools.publisher import PublisherWindow
|
||||
|
||||
host = registered_host()
|
||||
ILoadHost.validate_load_methods(host)
|
||||
|
||||
publisher_window = PublisherWindow(
|
||||
parent=parent or self._parent
|
||||
)
|
||||
self._publisher_tool = publisher_window
|
||||
|
||||
return self._publisher_tool
|
||||
|
||||
def show_publisher_tool(self, parent=None):
|
||||
with qt_app_context():
|
||||
dialog = self.get_publisher_tool(parent)
|
||||
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
dialog.showNormal()
|
||||
|
||||
def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs):
|
||||
"""Show tool by it's name.
|
||||
|
||||
|
|
@ -298,6 +323,10 @@ class HostToolsHelper:
|
|||
elif tool_name == "publish":
|
||||
self.log.info("Can't return publish tool window.")
|
||||
|
||||
# "new" publisher
|
||||
elif tool_name == "publisher":
|
||||
return self.get_publisher_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "experimental_tools":
|
||||
return self.get_experimental_tools_dialog(parent, *args, **kwargs)
|
||||
|
||||
|
|
@ -335,6 +364,9 @@ class HostToolsHelper:
|
|||
elif tool_name == "publish":
|
||||
self.show_publish(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "publisher":
|
||||
self.show_publisher_tool(parent, *args, **kwargs)
|
||||
|
||||
elif tool_name == "experimental_tools":
|
||||
self.show_experimental_tools_dialog(parent, *args, **kwargs)
|
||||
|
||||
|
|
@ -414,6 +446,10 @@ def show_publish(parent=None):
|
|||
_SingletonPoint.show_tool_by_name("publish", parent)
|
||||
|
||||
|
||||
def show_publisher(parent=None):
|
||||
_SingletonPoint.show_tool_by_name("publisher", parent)
|
||||
|
||||
|
||||
def show_experimental_tools_dialog(parent=None):
|
||||
_SingletonPoint.show_tool_by_name("experimental_tools", parent)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.14.2-nightly.3"
|
||||
__version__ = "3.14.3-nightly.1"
|
||||
|
|
|
|||
11
website/docs/admin_hosts_houdini.md
Normal file
11
website/docs/admin_hosts_houdini.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
id: admin_hosts_houdini
|
||||
title: Houdini
|
||||
sidebar_label: Houdini
|
||||
---
|
||||
|
||||
## Shelves Manager
|
||||
You can add your custom shelf set into Houdini by setting your shelf sets, shelves and tools in **Houdini -> Shelves Manager**.
|
||||

|
||||
|
||||
The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools.
|
||||
BIN
website/docs/assets/houdini-admin_shelvesmanager.png
Normal file
BIN
website/docs/assets/houdini-admin_shelvesmanager.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
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