mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge remote-tracking branch 'origin/release/3.15.x' into feature/OP-3553_update-to-python-39
This commit is contained in:
commit
d00d023f53
399 changed files with 10347 additions and 5667 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -6,6 +6,8 @@ labels: bug
|
|||
assignees: ''
|
||||
|
||||
---
|
||||
**Running version**
|
||||
[ex. 3.14.1-nightly.2]
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
|
|
|||
188
CHANGELOG.md
188
CHANGELOG.md
|
|
@ -1,13 +1,96 @@
|
|||
# Changelog
|
||||
|
||||
## [3.14.4-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- General: Set root environments before DCC launch [\#3947](https://github.com/pypeclub/OpenPype/pull/3947)
|
||||
- Refactor: changed legacy way to update database for Hero version integrate [\#3941](https://github.com/pypeclub/OpenPype/pull/3941)
|
||||
- Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939)
|
||||
- Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927)
|
||||
- Publisher: Instances can be marked as stored [\#3846](https://github.com/pypeclub/OpenPype/pull/3846)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Maya: Deadline OutputFilePath hack regression for Renderman [\#3950](https://github.com/pypeclub/OpenPype/pull/3950)
|
||||
- Houdini: Fix validate workfile paths for non-parm file references [\#3948](https://github.com/pypeclub/OpenPype/pull/3948)
|
||||
- Photoshop: missed sync published version of workfile with workfile [\#3946](https://github.com/pypeclub/OpenPype/pull/3946)
|
||||
- Maya: fix regression of Renderman Deadline hack [\#3943](https://github.com/pypeclub/OpenPype/pull/3943)
|
||||
- Tray: Change order of attribute changes [\#3938](https://github.com/pypeclub/OpenPype/pull/3938)
|
||||
- AttributeDefs: Fix crashing multivalue of files widget [\#3937](https://github.com/pypeclub/OpenPype/pull/3937)
|
||||
- General: Fix links query on hero version [\#3900](https://github.com/pypeclub/OpenPype/pull/3900)
|
||||
- Publisher: Files Drag n Drop cleanup [\#3888](https://github.com/pypeclub/OpenPype/pull/3888)
|
||||
- Maya: Render settings validation attribute check tweak logging [\#3821](https://github.com/pypeclub/OpenPype/pull/3821)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- General: Direct settings imports [\#3934](https://github.com/pypeclub/OpenPype/pull/3934)
|
||||
- General: import 'Logger' from 'openpype.lib' [\#3926](https://github.com/pypeclub/OpenPype/pull/3926)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya + Yeti: Load Yeti Cache fix frame number recognition [\#3942](https://github.com/pypeclub/OpenPype/pull/3942)
|
||||
- Fusion: Implement callbacks to Fusion's event system thread [\#3928](https://github.com/pypeclub/OpenPype/pull/3928)
|
||||
- Photoshop: create single frame image in Ftrack as review [\#3908](https://github.com/pypeclub/OpenPype/pull/3908)
|
||||
- Maya: Warn correctly about nodes in render instance with unexpected names [\#3816](https://github.com/pypeclub/OpenPype/pull/3816)
|
||||
|
||||
## [3.14.3](https://github.com/pypeclub/OpenPype/tree/3.14.3) (2022-10-03)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.3-nightly.7...3.14.3)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897)
|
||||
- Maya: better logging in Maketx [\#3886](https://github.com/pypeclub/OpenPype/pull/3886)
|
||||
- Photoshop: review can be turned off [\#3885](https://github.com/pypeclub/OpenPype/pull/3885)
|
||||
- TrayPublisher: added persisting of last selected project [\#3871](https://github.com/pypeclub/OpenPype/pull/3871)
|
||||
- TrayPublisher: added text filter on project name to Tray Publisher [\#3867](https://github.com/pypeclub/OpenPype/pull/3867)
|
||||
- Github issues adding `running version` section [\#3864](https://github.com/pypeclub/OpenPype/pull/3864)
|
||||
- Publisher: Increase size of main window [\#3862](https://github.com/pypeclub/OpenPype/pull/3862)
|
||||
- Flame: make migratable projects after creation [\#3860](https://github.com/pypeclub/OpenPype/pull/3860)
|
||||
- Photoshop: synchronize image version with workfile [\#3854](https://github.com/pypeclub/OpenPype/pull/3854)
|
||||
- General: Transcoding handle float2 attr type [\#3849](https://github.com/pypeclub/OpenPype/pull/3849)
|
||||
- General: Simple script for getting license information about used packages [\#3843](https://github.com/pypeclub/OpenPype/pull/3843)
|
||||
- General: Workfile template build enhancements [\#3838](https://github.com/pypeclub/OpenPype/pull/3838)
|
||||
- General: lock task workfiles when they are working on [\#3810](https://github.com/pypeclub/OpenPype/pull/3810)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Maya: Fix Render single camera validator [\#3929](https://github.com/pypeclub/OpenPype/pull/3929)
|
||||
- Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901)
|
||||
- Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895)
|
||||
- WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891)
|
||||
- TVPaint: Fix renaming of rendered files [\#3882](https://github.com/pypeclub/OpenPype/pull/3882)
|
||||
- Publisher: Nice checkbox visible in Python 2 [\#3877](https://github.com/pypeclub/OpenPype/pull/3877)
|
||||
- Settings: Add missing default settings [\#3870](https://github.com/pypeclub/OpenPype/pull/3870)
|
||||
- General: Copy of workfile does not use 'copy' function but 'copyfile' [\#3869](https://github.com/pypeclub/OpenPype/pull/3869)
|
||||
- Tray Publisher: skip plugin if otioTimeline is missing [\#3856](https://github.com/pypeclub/OpenPype/pull/3856)
|
||||
- Flame: retimed attributes are integrated with settings [\#3855](https://github.com/pypeclub/OpenPype/pull/3855)
|
||||
- Maya: Extract Playblast fix textures + labelize viewport show settings [\#3852](https://github.com/pypeclub/OpenPype/pull/3852)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Maya: Remove unused 'openpype.api' imports in plugins [\#3925](https://github.com/pypeclub/OpenPype/pull/3925)
|
||||
- Resolve: Use new Extractor location [\#3918](https://github.com/pypeclub/OpenPype/pull/3918)
|
||||
- Unreal: Use new Extractor location [\#3917](https://github.com/pypeclub/OpenPype/pull/3917)
|
||||
- Flame: Use new Extractor location [\#3916](https://github.com/pypeclub/OpenPype/pull/3916)
|
||||
- Houdini: Use new Extractor location [\#3894](https://github.com/pypeclub/OpenPype/pull/3894)
|
||||
- Harmony: Use new Extractor location [\#3893](https://github.com/pypeclub/OpenPype/pull/3893)
|
||||
- Hiero: Use new Extractor location [\#3851](https://github.com/pypeclub/OpenPype/pull/3851)
|
||||
- Maya: Remove old legacy \(ftrack\) plug-ins that are of no use anymore [\#3819](https://github.com/pypeclub/OpenPype/pull/3819)
|
||||
- Nuke: Use new Extractor location [\#3799](https://github.com/pypeclub/OpenPype/pull/3799)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923)
|
||||
- Maya: RenderSettings set default image format for V-Ray+Redshift to exr [\#3879](https://github.com/pypeclub/OpenPype/pull/3879)
|
||||
- Remove lockfile during publish [\#3874](https://github.com/pypeclub/OpenPype/pull/3874)
|
||||
|
||||
## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.1...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)
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.2-nightly.5...3.14.2)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
|
|
@ -15,9 +98,6 @@
|
|||
- 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)
|
||||
- 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**
|
||||
|
||||
|
|
@ -27,105 +107,15 @@
|
|||
- 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)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- Photoshop: Use new Extractor location [\#3789](https://github.com/pypeclub/OpenPype/pull/3789)
|
||||
- Blender: Use new Extractor location [\#3787](https://github.com/pypeclub/OpenPype/pull/3787)
|
||||
- AfterEffects: Use new Extractor location [\#3784](https://github.com/pypeclub/OpenPype/pull/3784)
|
||||
- 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)
|
||||
- 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)
|
||||
- Photoshop: Defined photoshop as addon [\#3736](https://github.com/pypeclub/OpenPype/pull/3736)
|
||||
- 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)
|
||||
- Blender: Define blender as module [\#3729](https://github.com/pypeclub/OpenPype/pull/3729)
|
||||
- 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)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.14.1-nightly.4...3.14.1)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Documentation: Few updates [\#3698](https://github.com/pypeclub/OpenPype/pull/3698)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- General: Thumbnail can use project roots [\#3750](https://github.com/pypeclub/OpenPype/pull/3750)
|
||||
- Settings: Remove settings lock on tray exit [\#3720](https://github.com/pypeclub/OpenPype/pull/3720)
|
||||
- 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)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- Maya: Fix typo in getPanel argument `with\_focus` -\> `withFocus` [\#3753](https://github.com/pypeclub/OpenPype/pull/3753)
|
||||
- General: Smaller fixes of imports [\#3748](https://github.com/pypeclub/OpenPype/pull/3748)
|
||||
- General: Logger tweaks [\#3741](https://github.com/pypeclub/OpenPype/pull/3741)
|
||||
- Nuke: missing job dependency if multiple bake streams [\#3737](https://github.com/pypeclub/OpenPype/pull/3737)
|
||||
- Nuke: color-space settings from anatomy is working [\#3721](https://github.com/pypeclub/OpenPype/pull/3721)
|
||||
- Settings: Fix studio default anatomy save [\#3716](https://github.com/pypeclub/OpenPype/pull/3716)
|
||||
- Maya: Use project name instead of project code [\#3709](https://github.com/pypeclub/OpenPype/pull/3709)
|
||||
- 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)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- General: Move delivery logic to pipeline [\#3751](https://github.com/pypeclub/OpenPype/pull/3751)
|
||||
- General: Move publish utils to pipeline [\#3745](https://github.com/pypeclub/OpenPype/pull/3745)
|
||||
- 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)
|
||||
- Harmony: Defined harmony as addon [\#3734](https://github.com/pypeclub/OpenPype/pull/3734)
|
||||
- General: Module interfaces cleanup [\#3731](https://github.com/pypeclub/OpenPype/pull/3731)
|
||||
- AfterEffects: Move AE functions from general lib [\#3730](https://github.com/pypeclub/OpenPype/pull/3730)
|
||||
- AfterEffects: Define AfterEffects as module [\#3728](https://github.com/pypeclub/OpenPype/pull/3728)
|
||||
- General: Replace PypeLogger with Logger [\#3725](https://github.com/pypeclub/OpenPype/pull/3725)
|
||||
- Nuke: Define nuke as module [\#3724](https://github.com/pypeclub/OpenPype/pull/3724)
|
||||
- General: Move subset name functionality [\#3723](https://github.com/pypeclub/OpenPype/pull/3723)
|
||||
- General: Move creators plugin getter [\#3714](https://github.com/pypeclub/OpenPype/pull/3714)
|
||||
- General: Move constants from lib to client [\#3713](https://github.com/pypeclub/OpenPype/pull/3713)
|
||||
- Loader: Subset groups using client operations [\#3710](https://github.com/pypeclub/OpenPype/pull/3710)
|
||||
- TVPaint: Defined as module [\#3707](https://github.com/pypeclub/OpenPype/pull/3707)
|
||||
- StandalonePublisher: Define StandalonePublisher as module [\#3706](https://github.com/pypeclub/OpenPype/pull/3706)
|
||||
- TrayPublisher: Define TrayPublisher as module [\#3705](https://github.com/pypeclub/OpenPype/pull/3705)
|
||||
- General: Move context specific functions to context tools [\#3702](https://github.com/pypeclub/OpenPype/pull/3702)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- 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)
|
||||
|
||||
## [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)
|
||||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- General: Switch from hero version to versioned works [\#3691](https://github.com/pypeclub/OpenPype/pull/3691)
|
||||
|
||||
## [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)
|
||||
|
|
|
|||
18
common/openpype_common/distribution/README.md
Normal file
18
common/openpype_common/distribution/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Addon distribution tool
|
||||
------------------------
|
||||
|
||||
Code in this folder is backend portion of Addon distribution logic for v4 server.
|
||||
|
||||
Each host, module will be separate Addon in the future. Each v4 server could run different set of Addons.
|
||||
|
||||
Client (running on artist machine) will in the first step ask v4 for list of enabled addons.
|
||||
(It expects list of json documents matching to `addon_distribution.py:AddonInfo` object.)
|
||||
Next it will compare presence of enabled addon version in local folder. In the case of missing version of
|
||||
an addon, client will use information in the addon to download (from http/shared local disk/git) zip file
|
||||
and unzip it.
|
||||
|
||||
Required part of addon distribution will be sharing of dependencies (python libraries, utilities) which is not part of this folder.
|
||||
|
||||
Location of this folder might change in the future as it will be required for a clint to add this folder to sys.path reliably.
|
||||
|
||||
This code needs to be independent on Openpype code as much as possible!
|
||||
0
common/openpype_common/distribution/__init__.py
Normal file
0
common/openpype_common/distribution/__init__.py
Normal file
208
common/openpype_common/distribution/addon_distribution.py
Normal file
208
common/openpype_common/distribution/addon_distribution.py
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import os
|
||||
from enum import Enum
|
||||
from abc import abstractmethod
|
||||
import attr
|
||||
import logging
|
||||
import requests
|
||||
import platform
|
||||
import shutil
|
||||
|
||||
from .file_handler import RemoteFileHandler
|
||||
from .addon_info import AddonInfo
|
||||
|
||||
|
||||
class UpdateState(Enum):
|
||||
EXISTS = "exists"
|
||||
UPDATED = "updated"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class AddonDownloader:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self):
|
||||
self._downloaders = {}
|
||||
|
||||
def register_format(self, downloader_type, downloader):
|
||||
self._downloaders[downloader_type.value] = downloader
|
||||
|
||||
def get_downloader(self, downloader_type):
|
||||
downloader = self._downloaders.get(downloader_type)
|
||||
if not downloader:
|
||||
raise ValueError(f"{downloader_type} not implemented")
|
||||
return downloader()
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def download(cls, source, destination):
|
||||
"""Returns url to downloaded addon zip file.
|
||||
|
||||
Args:
|
||||
source (dict): {type:"http", "url":"https://} ...}
|
||||
destination (str): local folder to unzip
|
||||
Returns:
|
||||
(str) local path to addon zip file
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def check_hash(cls, addon_path, addon_hash):
|
||||
"""Compares 'hash' of downloaded 'addon_url' file.
|
||||
|
||||
Args:
|
||||
addon_path (str): local path to addon zip file
|
||||
addon_hash (str): sha256 hash of zip file
|
||||
Raises:
|
||||
ValueError if hashes doesn't match
|
||||
"""
|
||||
if not os.path.exists(addon_path):
|
||||
raise ValueError(f"{addon_path} doesn't exist.")
|
||||
if not RemoteFileHandler.check_integrity(addon_path,
|
||||
addon_hash,
|
||||
hash_type="sha256"):
|
||||
raise ValueError(f"{addon_path} doesn't match expected hash.")
|
||||
|
||||
@classmethod
|
||||
def unzip(cls, addon_zip_path, destination):
|
||||
"""Unzips local 'addon_zip_path' to 'destination'.
|
||||
|
||||
Args:
|
||||
addon_zip_path (str): local path to addon zip file
|
||||
destination (str): local folder to unzip
|
||||
"""
|
||||
RemoteFileHandler.unzip(addon_zip_path, destination)
|
||||
os.remove(addon_zip_path)
|
||||
|
||||
@classmethod
|
||||
def remove(cls, addon_url):
|
||||
pass
|
||||
|
||||
|
||||
class OSAddonDownloader(AddonDownloader):
|
||||
|
||||
@classmethod
|
||||
def download(cls, source, destination):
|
||||
# OS doesnt need to download, unzip directly
|
||||
addon_url = source["path"].get(platform.system().lower())
|
||||
if not os.path.exists(addon_url):
|
||||
raise ValueError("{} is not accessible".format(addon_url))
|
||||
return addon_url
|
||||
|
||||
|
||||
class HTTPAddonDownloader(AddonDownloader):
|
||||
CHUNK_SIZE = 100000
|
||||
|
||||
@classmethod
|
||||
def download(cls, source, destination):
|
||||
source_url = source["url"]
|
||||
cls.log.debug(f"Downloading {source_url} to {destination}")
|
||||
file_name = os.path.basename(destination)
|
||||
_, ext = os.path.splitext(file_name)
|
||||
if (ext.replace(".", '') not
|
||||
in set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS)):
|
||||
file_name += ".zip"
|
||||
RemoteFileHandler.download_url(source_url,
|
||||
destination,
|
||||
filename=file_name)
|
||||
|
||||
return os.path.join(destination, file_name)
|
||||
|
||||
|
||||
def get_addons_info(server_endpoint):
|
||||
"""Returns list of addon information from Server"""
|
||||
# TODO temp
|
||||
# addon_info = AddonInfo(
|
||||
# **{"name": "openpype_slack",
|
||||
# "version": "1.0.0",
|
||||
# "addon_url": "c:/projects/openpype_slack_1.0.0.zip",
|
||||
# "type": UrlType.FILESYSTEM,
|
||||
# "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658"}) # noqa
|
||||
#
|
||||
# http_addon = AddonInfo(
|
||||
# **{"name": "openpype_slack",
|
||||
# "version": "1.0.0",
|
||||
# "addon_url": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa
|
||||
# "type": UrlType.HTTP,
|
||||
# "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658"}) # noqa
|
||||
|
||||
response = requests.get(server_endpoint)
|
||||
if not response.ok:
|
||||
raise Exception(response.text)
|
||||
|
||||
addons_info = []
|
||||
for addon in response.json():
|
||||
addons_info.append(AddonInfo(**addon))
|
||||
return addons_info
|
||||
|
||||
|
||||
def update_addon_state(addon_infos, destination_folder, factory,
|
||||
log=None):
|
||||
"""Loops through all 'addon_infos', compares local version, unzips.
|
||||
|
||||
Loops through server provided list of dictionaries with information about
|
||||
available addons. Looks if each addon is already present and deployed.
|
||||
If isn't, addon zip gets downloaded and unzipped into 'destination_folder'.
|
||||
Args:
|
||||
addon_infos (list of AddonInfo)
|
||||
destination_folder (str): local path
|
||||
factory (AddonDownloader): factory to get appropriate downloader per
|
||||
addon type
|
||||
log (logging.Logger)
|
||||
Returns:
|
||||
(dict): {"addon_full_name": UpdateState.value
|
||||
(eg. "exists"|"updated"|"failed")
|
||||
"""
|
||||
if not log:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
download_states = {}
|
||||
for addon in addon_infos:
|
||||
full_name = "{}_{}".format(addon.name, addon.version)
|
||||
addon_dest = os.path.join(destination_folder, full_name)
|
||||
|
||||
if os.path.isdir(addon_dest):
|
||||
log.debug(f"Addon version folder {addon_dest} already exists.")
|
||||
download_states[full_name] = UpdateState.EXISTS.value
|
||||
continue
|
||||
|
||||
for source in addon.sources:
|
||||
download_states[full_name] = UpdateState.FAILED.value
|
||||
try:
|
||||
downloader = factory.get_downloader(source.type)
|
||||
zip_file_path = downloader.download(attr.asdict(source),
|
||||
addon_dest)
|
||||
downloader.check_hash(zip_file_path, addon.hash)
|
||||
downloader.unzip(zip_file_path, addon_dest)
|
||||
download_states[full_name] = UpdateState.UPDATED.value
|
||||
break
|
||||
except Exception:
|
||||
log.warning(f"Error happened during updating {addon.name}",
|
||||
exc_info=True)
|
||||
if os.path.isdir(addon_dest):
|
||||
log.debug(f"Cleaning {addon_dest}")
|
||||
shutil.rmtree(addon_dest)
|
||||
|
||||
return download_states
|
||||
|
||||
|
||||
def check_addons(server_endpoint, addon_folder, downloaders):
|
||||
"""Main entry point to compare existing addons with those on server.
|
||||
|
||||
Args:
|
||||
server_endpoint (str): url to v4 server endpoint
|
||||
addon_folder (str): local dir path for addons
|
||||
downloaders (AddonDownloader): factory of downloaders
|
||||
|
||||
Raises:
|
||||
(RuntimeError) if any addon failed update
|
||||
"""
|
||||
addons_info = get_addons_info(server_endpoint)
|
||||
result = update_addon_state(addons_info,
|
||||
addon_folder,
|
||||
downloaders)
|
||||
if UpdateState.FAILED.value in result.values():
|
||||
raise RuntimeError(f"Unable to update some addons {result}")
|
||||
|
||||
|
||||
def cli(*args):
|
||||
raise NotImplementedError
|
||||
80
common/openpype_common/distribution/addon_info.py
Normal file
80
common/openpype_common/distribution/addon_info.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import attr
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class UrlType(Enum):
|
||||
HTTP = "http"
|
||||
GIT = "git"
|
||||
FILESYSTEM = "filesystem"
|
||||
|
||||
|
||||
@attr.s
|
||||
class MultiPlatformPath(object):
|
||||
windows = attr.ib(default=None)
|
||||
linux = attr.ib(default=None)
|
||||
darwin = attr.ib(default=None)
|
||||
|
||||
|
||||
@attr.s
|
||||
class AddonSource(object):
|
||||
type = attr.ib()
|
||||
|
||||
|
||||
@attr.s
|
||||
class LocalAddonSource(AddonSource):
|
||||
path = attr.ib(default=attr.Factory(MultiPlatformPath))
|
||||
|
||||
|
||||
@attr.s
|
||||
class WebAddonSource(AddonSource):
|
||||
url = attr.ib(default=None)
|
||||
|
||||
|
||||
@attr.s
|
||||
class VersionData(object):
|
||||
version_data = attr.ib(default=None)
|
||||
|
||||
|
||||
@attr.s
|
||||
class AddonInfo(object):
|
||||
"""Object matching json payload from Server"""
|
||||
name = attr.ib()
|
||||
version = attr.ib()
|
||||
title = attr.ib(default=None)
|
||||
sources = attr.ib(default=attr.Factory(dict))
|
||||
hash = attr.ib(default=None)
|
||||
description = attr.ib(default=None)
|
||||
license = attr.ib(default=None)
|
||||
authors = attr.ib(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
sources = []
|
||||
|
||||
production_version = data.get("productionVersion")
|
||||
if not production_version:
|
||||
return
|
||||
|
||||
# server payload contains info about all versions
|
||||
# active addon must have 'productionVersion' and matching version info
|
||||
version_data = data.get("versions", {})[production_version]
|
||||
|
||||
for source in version_data.get("clientSourceInfo", []):
|
||||
if source.get("type") == UrlType.FILESYSTEM.value:
|
||||
source_addon = LocalAddonSource(type=source["type"],
|
||||
path=source["path"])
|
||||
if source.get("type") == UrlType.HTTP.value:
|
||||
source_addon = WebAddonSource(type=source["type"],
|
||||
url=source["url"])
|
||||
|
||||
sources.append(source_addon)
|
||||
|
||||
return cls(name=data.get("name"),
|
||||
version=production_version,
|
||||
sources=sources,
|
||||
hash=data.get("hash"),
|
||||
description=data.get("description"),
|
||||
title=data.get("title"),
|
||||
license=data.get("license"),
|
||||
authors=data.get("authors"))
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ class RemoteFileHandler:
|
|||
'tar.gz', 'tar.xz', 'tar.bz2']
|
||||
|
||||
@staticmethod
|
||||
def calculate_md5(fpath, chunk_size):
|
||||
def calculate_md5(fpath, chunk_size=10000):
|
||||
md5 = hashlib.md5()
|
||||
with open(fpath, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(chunk_size), b''):
|
||||
|
|
@ -33,17 +33,45 @@ class RemoteFileHandler:
|
|||
return md5 == RemoteFileHandler.calculate_md5(fpath, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def check_integrity(fpath, md5=None):
|
||||
def calculate_sha256(fpath):
|
||||
"""Calculate sha256 for content of the file.
|
||||
|
||||
Args:
|
||||
fpath (str): Path to file.
|
||||
|
||||
Returns:
|
||||
str: hex encoded sha256
|
||||
|
||||
"""
|
||||
h = hashlib.sha256()
|
||||
b = bytearray(128 * 1024)
|
||||
mv = memoryview(b)
|
||||
with open(fpath, 'rb', buffering=0) as f:
|
||||
for n in iter(lambda: f.readinto(mv), 0):
|
||||
h.update(mv[:n])
|
||||
return h.hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def check_sha256(fpath, sha256, **kwargs):
|
||||
return sha256 == RemoteFileHandler.calculate_sha256(fpath, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def check_integrity(fpath, hash_value=None, hash_type=None):
|
||||
if not os.path.isfile(fpath):
|
||||
return False
|
||||
if md5 is None:
|
||||
if hash_value is None:
|
||||
return True
|
||||
return RemoteFileHandler.check_md5(fpath, md5)
|
||||
if not hash_type:
|
||||
raise ValueError("Provide hash type, md5 or sha256")
|
||||
if hash_type == 'md5':
|
||||
return RemoteFileHandler.check_md5(fpath, hash_value)
|
||||
if hash_type == "sha256":
|
||||
return RemoteFileHandler.check_sha256(fpath, hash_value)
|
||||
|
||||
@staticmethod
|
||||
def download_url(
|
||||
url, root, filename=None,
|
||||
md5=None, max_redirect_hops=3
|
||||
sha256=None, max_redirect_hops=3
|
||||
):
|
||||
"""Download a file from a url and place it in root.
|
||||
Args:
|
||||
|
|
@ -51,7 +79,7 @@ class RemoteFileHandler:
|
|||
root (str): Directory to place downloaded file in
|
||||
filename (str, optional): Name to save the file under.
|
||||
If None, use the basename of the URL
|
||||
md5 (str, optional): MD5 checksum of the download.
|
||||
sha256 (str, optional): sha256 checksum of the download.
|
||||
If None, do not check
|
||||
max_redirect_hops (int, optional): Maximum number of redirect
|
||||
hops allowed
|
||||
|
|
@ -64,7 +92,8 @@ class RemoteFileHandler:
|
|||
os.makedirs(root, exist_ok=True)
|
||||
|
||||
# check if file is already present locally
|
||||
if RemoteFileHandler.check_integrity(fpath, md5):
|
||||
if RemoteFileHandler.check_integrity(fpath,
|
||||
sha256, hash_type="sha256"):
|
||||
print('Using downloaded and verified file: ' + fpath)
|
||||
return
|
||||
|
||||
|
|
@ -76,7 +105,7 @@ class RemoteFileHandler:
|
|||
file_id = RemoteFileHandler._get_google_drive_file_id(url)
|
||||
if file_id is not None:
|
||||
return RemoteFileHandler.download_file_from_google_drive(
|
||||
file_id, root, filename, md5)
|
||||
file_id, root, filename, sha256)
|
||||
|
||||
# download the file
|
||||
try:
|
||||
|
|
@ -92,20 +121,21 @@ class RemoteFileHandler:
|
|||
raise e
|
||||
|
||||
# check integrity of downloaded file
|
||||
if not RemoteFileHandler.check_integrity(fpath, md5):
|
||||
if not RemoteFileHandler.check_integrity(fpath,
|
||||
sha256, hash_type="sha256"):
|
||||
raise RuntimeError("File not found or corrupted.")
|
||||
|
||||
@staticmethod
|
||||
def download_file_from_google_drive(file_id, root,
|
||||
filename=None,
|
||||
md5=None):
|
||||
sha256=None):
|
||||
"""Download a Google Drive file from and place it in root.
|
||||
Args:
|
||||
file_id (str): id of file to be downloaded
|
||||
root (str): Directory to place downloaded file in
|
||||
filename (str, optional): Name to save the file under.
|
||||
If None, use the id of the file.
|
||||
md5 (str, optional): MD5 checksum of the download.
|
||||
sha256 (str, optional): sha256 checksum of the download.
|
||||
If None, do not check
|
||||
"""
|
||||
# Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa
|
||||
|
|
@ -119,8 +149,8 @@ class RemoteFileHandler:
|
|||
|
||||
os.makedirs(root, exist_ok=True)
|
||||
|
||||
if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath,
|
||||
md5):
|
||||
if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(
|
||||
fpath, sha256, hash_type="sha256"):
|
||||
print('Using downloaded and verified file: ' + fpath)
|
||||
else:
|
||||
session = requests.Session()
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import pytest
|
||||
import attr
|
||||
import tempfile
|
||||
|
||||
from common.openpype_common.distribution.addon_distribution import (
|
||||
AddonDownloader,
|
||||
OSAddonDownloader,
|
||||
HTTPAddonDownloader,
|
||||
AddonInfo,
|
||||
update_addon_state,
|
||||
UpdateState
|
||||
)
|
||||
from common.openpype_common.distribution.addon_info import UrlType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def addon_downloader():
|
||||
addon_downloader = AddonDownloader()
|
||||
addon_downloader.register_format(UrlType.FILESYSTEM, OSAddonDownloader)
|
||||
addon_downloader.register_format(UrlType.HTTP, HTTPAddonDownloader)
|
||||
|
||||
yield addon_downloader
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_downloader(addon_downloader):
|
||||
yield addon_downloader.get_downloader(UrlType.HTTP.value)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_folder():
|
||||
yield tempfile.mkdtemp()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_addon_info():
|
||||
addon_info = {
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"clientPyproject": {
|
||||
"tool": {
|
||||
"poetry": {
|
||||
"dependencies": {
|
||||
"nxtools": "^1.6",
|
||||
"orjson": "^3.6.7",
|
||||
"typer": "^0.4.1",
|
||||
"email-validator": "^1.1.3",
|
||||
"python": "^3.10",
|
||||
"fastapi": "^0.73.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hasSettings": True,
|
||||
"clientSourceInfo": [
|
||||
{
|
||||
"type": "http",
|
||||
"url": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing" # noqa
|
||||
},
|
||||
{
|
||||
"type": "filesystem",
|
||||
"path": {
|
||||
"windows": ["P:/sources/some_file.zip",
|
||||
"W:/sources/some_file.zip"], # noqa
|
||||
"linux": ["/mnt/srv/sources/some_file.zip"],
|
||||
"darwin": ["/Volumes/srv/sources/some_file.zip"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"frontendScopes": {
|
||||
"project": {
|
||||
"sidebar": "hierarchy"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "",
|
||||
"title": "Slack addon",
|
||||
"name": "openpype_slack",
|
||||
"productionVersion": "1.0.0",
|
||||
"hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658" # noqa
|
||||
}
|
||||
yield addon_info
|
||||
|
||||
|
||||
def test_register(printer):
|
||||
addon_downloader = AddonDownloader()
|
||||
|
||||
assert len(addon_downloader._downloaders) == 0, "Contains registered"
|
||||
|
||||
addon_downloader.register_format(UrlType.FILESYSTEM, OSAddonDownloader)
|
||||
assert len(addon_downloader._downloaders) == 1, "Should contain one"
|
||||
|
||||
|
||||
def test_get_downloader(printer, addon_downloader):
|
||||
assert addon_downloader.get_downloader(UrlType.FILESYSTEM.value), "Should find" # noqa
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
addon_downloader.get_downloader("unknown"), "Shouldn't find"
|
||||
|
||||
|
||||
def test_addon_info(printer, sample_addon_info):
|
||||
"""Tests parsing of expected payload from v4 server into AadonInfo."""
|
||||
valid_minimum = {
|
||||
"name": "openpype_slack",
|
||||
"productionVersion": "1.0.0",
|
||||
"versions": {
|
||||
"1.0.0": {
|
||||
"clientSourceInfo": [
|
||||
{
|
||||
"type": "filesystem",
|
||||
"path": {
|
||||
"windows": [
|
||||
"P:/sources/some_file.zip",
|
||||
"W:/sources/some_file.zip"],
|
||||
"linux": [
|
||||
"/mnt/srv/sources/some_file.zip"],
|
||||
"darwin": [
|
||||
"/Volumes/srv/sources/some_file.zip"] # noqa
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert AddonInfo.from_dict(valid_minimum), "Missing required fields"
|
||||
|
||||
valid_minimum["versions"].pop("1.0.0")
|
||||
with pytest.raises(KeyError):
|
||||
assert not AddonInfo.from_dict(valid_minimum), "Must fail without version data" # noqa
|
||||
|
||||
valid_minimum.pop("productionVersion")
|
||||
assert not AddonInfo.from_dict(
|
||||
valid_minimum), "none if not productionVersion" # noqa
|
||||
|
||||
addon = AddonInfo.from_dict(sample_addon_info)
|
||||
assert addon, "Should be created"
|
||||
assert addon.name == "openpype_slack", "Incorrect name"
|
||||
assert addon.version == "1.0.0", "Incorrect version"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
assert addon["name"], "Dict approach not implemented"
|
||||
|
||||
addon_as_dict = attr.asdict(addon)
|
||||
assert addon_as_dict["name"], "Dict approach should work"
|
||||
|
||||
|
||||
def test_update_addon_state(printer, sample_addon_info,
|
||||
temp_folder, addon_downloader):
|
||||
"""Tests possible cases of addon update."""
|
||||
addon_info = AddonInfo.from_dict(sample_addon_info)
|
||||
orig_hash = addon_info.hash
|
||||
|
||||
addon_info.hash = "brokenhash"
|
||||
result = update_addon_state([addon_info], temp_folder, addon_downloader)
|
||||
assert result["openpype_slack_1.0.0"] == UpdateState.FAILED.value, \
|
||||
"Update should failed because of wrong hash"
|
||||
|
||||
addon_info.hash = orig_hash
|
||||
result = update_addon_state([addon_info], temp_folder, addon_downloader)
|
||||
assert result["openpype_slack_1.0.0"] == UpdateState.UPDATED.value, \
|
||||
"Addon should have been updated"
|
||||
|
||||
result = update_addon_state([addon_info], temp_folder, addon_downloader)
|
||||
assert result["openpype_slack_1.0.0"] == UpdateState.EXISTS.value, \
|
||||
"Addon should already exist"
|
||||
|
|
@ -804,6 +804,8 @@ class BootstrapRepos:
|
|||
"""
|
||||
version = OpenPypeVersion.version_in_str(zip_file.name)
|
||||
destination_dir = self.data_dir / f"{version.major}.{version.minor}"
|
||||
if not destination_dir.exists():
|
||||
destination_dir.mkdir(parents=True)
|
||||
destination = destination_dir / zip_file.name
|
||||
|
||||
if destination.exists():
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ class InstallThread(QThread):
|
|||
|
||||
def __init__(self, parent=None,):
|
||||
self._mongo = None
|
||||
self._path = None
|
||||
self._result = None
|
||||
|
||||
QThread.__init__(self, parent)
|
||||
|
|
@ -62,14 +61,14 @@ class InstallThread(QThread):
|
|||
progress_callback=self.set_progress, message=self.message)
|
||||
local_version = OpenPypeVersion.get_installed_version_str()
|
||||
|
||||
# if user did enter nothing, we install OpenPype from local version.
|
||||
# zip content of `repos`, copy it to user data dir and append
|
||||
# version to it.
|
||||
if not self._path:
|
||||
# user did not entered url
|
||||
if not self._mongo:
|
||||
# it not set in environment
|
||||
if not os.getenv("OPENPYPE_MONGO"):
|
||||
if self._mongo:
|
||||
self.message.emit("Saving mongo connection string ...", False)
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
|
||||
elif os.getenv("OPENPYPE_MONGO"):
|
||||
self._mongo = os.getenv("OPENPYPE_MONGO")
|
||||
else:
|
||||
# try to get it from settings registry
|
||||
try:
|
||||
self._mongo = bs.secure_registry.get_item(
|
||||
|
|
@ -79,64 +78,13 @@ class InstallThread(QThread):
|
|||
"!!! We need MongoDB URL to proceed.", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
else:
|
||||
self._mongo = os.getenv("OPENPYPE_MONGO")
|
||||
else:
|
||||
self.message.emit("Saving mongo connection string ...", False)
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(
|
||||
f"Detecting installed OpenPype versions in {bs.data_dir}",
|
||||
False)
|
||||
detected = bs.find_openpype(include_zips=True)
|
||||
|
||||
if detected:
|
||||
if not OpenPypeVersion.get_installed_version().is_compatible(
|
||||
detected[-1]):
|
||||
self.message.emit((
|
||||
f"Latest detected version {detected[-1]} "
|
||||
"is not compatible with the currently running "
|
||||
f"{local_version}"
|
||||
), True)
|
||||
self.message.emit((
|
||||
"Filtering detected versions to compatible ones..."
|
||||
), False)
|
||||
|
||||
detected = [
|
||||
version for version in detected
|
||||
if version.is_compatible(
|
||||
OpenPypeVersion.get_installed_version())
|
||||
]
|
||||
|
||||
if OpenPypeVersion(
|
||||
version=local_version, path=Path()) < detected[-1]:
|
||||
self.message.emit((
|
||||
f"Latest installed version {detected[-1]} is newer "
|
||||
f"then currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
if detected[-1].path.suffix.lower() == ".zip":
|
||||
bs.extract_openpype(detected[-1])
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa
|
||||
self.message.emit((
|
||||
f"Latest installed version is the same as "
|
||||
f"currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
self.message.emit((
|
||||
"All installed versions are older then "
|
||||
f"currently running one {local_version}"
|
||||
), False)
|
||||
else:
|
||||
if getattr(sys, 'frozen', False):
|
||||
if not detected and getattr(sys, 'frozen', False):
|
||||
self.message.emit("None detected.", True)
|
||||
self.message.emit(("We will use OpenPype coming with "
|
||||
"installer."), False)
|
||||
|
|
@ -152,7 +100,51 @@ class InstallThread(QThread):
|
|||
self.progress.emit(100)
|
||||
self._set_result(1)
|
||||
return
|
||||
else:
|
||||
|
||||
if detected and not OpenPypeVersion.get_installed_version().is_compatible(detected[-1]): # noqa: E501
|
||||
self.message.emit((
|
||||
f"Latest detected version {detected[-1]} "
|
||||
"is not compatible with the currently running "
|
||||
f"{local_version}"
|
||||
), True)
|
||||
self.message.emit((
|
||||
"Filtering detected versions to compatible ones..."
|
||||
), False)
|
||||
|
||||
# filter results to get only compatible versions
|
||||
detected = [
|
||||
version for version in detected
|
||||
if version.is_compatible(
|
||||
OpenPypeVersion.get_installed_version())
|
||||
]
|
||||
|
||||
if detected:
|
||||
if OpenPypeVersion(
|
||||
version=local_version, path=Path()) < detected[-1]:
|
||||
self.message.emit((
|
||||
f"Latest installed version {detected[-1]} is newer "
|
||||
f"then currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
if detected[-1].path.suffix.lower() == ".zip":
|
||||
bs.extract_openpype(detected[-1])
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa: E501
|
||||
self.message.emit((
|
||||
f"Latest installed version is the same as "
|
||||
f"currently running {local_version}"
|
||||
), False)
|
||||
self.message.emit("Skipping OpenPype install ...", False)
|
||||
self._set_result(0)
|
||||
return
|
||||
|
||||
self.message.emit((
|
||||
"All installed versions are older then "
|
||||
f"currently running one {local_version}"
|
||||
), False)
|
||||
|
||||
self.message.emit("None detected.", False)
|
||||
|
||||
self.message.emit(
|
||||
|
|
@ -179,25 +171,6 @@ class InstallThread(QThread):
|
|||
self.progress.emit(100)
|
||||
self._set_result(1)
|
||||
return
|
||||
else:
|
||||
# if we have mongo connection string, validate it, set it to
|
||||
# user settings and get OPENPYPE_PATH from there.
|
||||
if self._mongo:
|
||||
if not validate_mongo_connection(self._mongo):
|
||||
self.message.emit(
|
||||
f"!!! invalid mongo url {self._mongo}", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(f"processing {self._path}", True)
|
||||
repo_file = bs.process_entered_location(self._path)
|
||||
|
||||
if not repo_file:
|
||||
self.message.emit("!!! Cannot install", True)
|
||||
self._set_result(-1)
|
||||
return
|
||||
|
||||
self.progress.emit(100)
|
||||
self._set_result(1)
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ Source: "build\{#build}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
|
|||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\openpype_gui.exe"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\openpype_gui.exe"; Tasks: desktopicon
|
||||
Name: "{autoprograms}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe"
|
||||
Name: "{autodesktop}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\openpype_gui.exe"; Description: "{cm:LaunchProgram,OpenPype}"; Flags: nowait postinstall skipifsilent
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from .mongo import get_project_connection
|
|||
from .entities import (
|
||||
get_assets,
|
||||
get_asset_by_id,
|
||||
get_version_by_id,
|
||||
get_representation_by_id,
|
||||
convert_id,
|
||||
)
|
||||
|
|
@ -127,12 +128,20 @@ def get_linked_representation_id(
|
|||
if not version_id:
|
||||
return []
|
||||
|
||||
version_doc = get_version_by_id(
|
||||
project_name, version_id, fields=["type", "version_id"]
|
||||
)
|
||||
if version_doc["type"] == "hero_version":
|
||||
version_id = version_doc["version_id"]
|
||||
|
||||
if max_depth is None:
|
||||
max_depth = 0
|
||||
|
||||
match = {
|
||||
"_id": version_id,
|
||||
"type": {"$in": ["version", "hero_version"]}
|
||||
# Links are not stored to hero versions at this moment so filter
|
||||
# is limited to just versions
|
||||
"type": "version"
|
||||
}
|
||||
|
||||
graph_lookup = {
|
||||
|
|
@ -187,7 +196,7 @@ def _process_referenced_pipeline_result(result, link_type):
|
|||
referenced_version_ids = set()
|
||||
correctly_linked_ids = set()
|
||||
for item in result:
|
||||
input_links = item["data"].get("inputLinks")
|
||||
input_links = item.get("data", {}).get("inputLinks")
|
||||
if not input_links:
|
||||
continue
|
||||
|
||||
|
|
@ -203,7 +212,7 @@ def _process_referenced_pipeline_result(result, link_type):
|
|||
continue
|
||||
|
||||
for output in sorted(outputs_recursive, key=lambda o: o["depth"]):
|
||||
output_links = output["data"].get("inputLinks")
|
||||
output_links = output.get("data", {}).get("inputLinks")
|
||||
if not output_links:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ CURRENT_PROJECT_CONFIG_SCHEMA = "openpype:config-2.0"
|
|||
CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0"
|
||||
CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0"
|
||||
CURRENT_VERSION_SCHEMA = "openpype:version-3.0"
|
||||
CURRENT_HERO_VERSION_SCHEMA = "openpype:hero_version-1.0"
|
||||
CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0"
|
||||
CURRENT_WORKFILE_INFO_SCHEMA = "openpype:workfile-1.0"
|
||||
CURRENT_THUMBNAIL_SCHEMA = "openpype:thumbnail-1.0"
|
||||
|
|
@ -162,6 +163,34 @@ def new_version_doc(version, subset_id, data=None, entity_id=None):
|
|||
}
|
||||
|
||||
|
||||
def new_hero_version_doc(version_id, subset_id, data=None, entity_id=None):
|
||||
"""Create skeleton data of hero version document.
|
||||
|
||||
Args:
|
||||
version_id (ObjectId): Is considered as unique identifier of version
|
||||
under subset.
|
||||
subset_id (Union[str, ObjectId]): Id of parent subset.
|
||||
data (Dict[str, Any]): Version document data.
|
||||
entity_id (Union[str, ObjectId]): Predefined id of document. New id is
|
||||
created if not passed.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Skeleton of version document.
|
||||
"""
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
return {
|
||||
"_id": _create_or_convert_to_mongo_id(entity_id),
|
||||
"schema": CURRENT_HERO_VERSION_SCHEMA,
|
||||
"type": "hero_version",
|
||||
"version_id": version_id,
|
||||
"parent": subset_id,
|
||||
"data": data
|
||||
}
|
||||
|
||||
|
||||
def new_representation_doc(
|
||||
name, version_id, context, data=None, entity_id=None
|
||||
):
|
||||
|
|
@ -293,6 +322,20 @@ def prepare_version_update_data(old_doc, new_doc, replace=True):
|
|||
return _prepare_update_data(old_doc, new_doc, replace)
|
||||
|
||||
|
||||
def prepare_hero_version_update_data(old_doc, new_doc, replace=True):
|
||||
"""Compare two hero version documents and prepare update data.
|
||||
|
||||
Based on compared values will create update data for 'UpdateOperation'.
|
||||
|
||||
Empty output means that documents are identical.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Changes between old and new document.
|
||||
"""
|
||||
|
||||
return _prepare_update_data(old_doc, new_doc, replace)
|
||||
|
||||
|
||||
def prepare_representation_update_data(old_doc, new_doc, replace=True):
|
||||
"""Compare two representation documents and prepare update data.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
|
|||
"hiero",
|
||||
"houdini",
|
||||
"nukestudio",
|
||||
"fusion",
|
||||
"blender",
|
||||
"photoshop",
|
||||
"tvpaint",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from .host import (
|
|||
from .interfaces import (
|
||||
IWorkfileHost,
|
||||
ILoadHost,
|
||||
IPublishHost,
|
||||
INewPublisher,
|
||||
)
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ __all__ = (
|
|||
|
||||
"IWorkfileHost",
|
||||
"ILoadHost",
|
||||
"IPublishHost",
|
||||
"INewPublisher",
|
||||
|
||||
"HostDirmap",
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ class IWorkfileHost:
|
|||
return self.workfile_has_unsaved_changes()
|
||||
|
||||
|
||||
class INewPublisher:
|
||||
class IPublishHost:
|
||||
"""Functions related to new creation system in new publisher.
|
||||
|
||||
New publisher is not storing information only about each created instance
|
||||
|
|
@ -306,12 +306,14 @@ class INewPublisher:
|
|||
workflow.
|
||||
"""
|
||||
|
||||
if isinstance(host, INewPublisher):
|
||||
if isinstance(host, IPublishHost):
|
||||
return []
|
||||
|
||||
required = [
|
||||
"get_context_data",
|
||||
"update_context_data",
|
||||
"get_context_title",
|
||||
"get_current_context",
|
||||
]
|
||||
missing = []
|
||||
for name in required:
|
||||
|
|
@ -330,7 +332,7 @@ class INewPublisher:
|
|||
MissingMethodsError: If there are missing methods on host
|
||||
implementation.
|
||||
"""
|
||||
missing = INewPublisher.get_missing_publish_methods(host)
|
||||
missing = IPublishHost.get_missing_publish_methods(host)
|
||||
if missing:
|
||||
raise MissingMethodsError(host, missing)
|
||||
|
||||
|
|
@ -368,3 +370,17 @@ class INewPublisher:
|
|||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class INewPublisher(IPublishHost):
|
||||
"""Legacy interface replaced by 'IPublishHost'.
|
||||
|
||||
Deprecated:
|
||||
'INewPublisher' is replaced by 'IPublishHost' please change your
|
||||
imports.
|
||||
There is no "reasonable" way hot mark these classes as deprecated
|
||||
to show warning of wrong import. Deprecated since 3.14.* will be
|
||||
removed in 3.15.*
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from wsrpc_aiohttp import (
|
|||
|
||||
from Qt import QtCore
|
||||
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.tools.adobe_webserver.app import WebServerTool
|
||||
|
|
@ -84,8 +85,6 @@ class ProcessLauncher(QtCore.QObject):
|
|||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
from openpype.api import Logger
|
||||
|
||||
self._log = Logger.get_logger("{}-launcher".format(
|
||||
self.route_name))
|
||||
return self._log
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ from Qt import QtWidgets
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from openpype import lib
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger, register_event_callback
|
||||
from openpype.pipeline import (
|
||||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
|
|
@ -16,9 +15,8 @@ from openpype.pipeline import (
|
|||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
import openpype.hosts.aftereffects
|
||||
from openpype.lib import register_event_callback
|
||||
|
||||
from .launch_logic import get_stub
|
||||
from .launch_logic import get_stub, ConnectionNotEstablishedYet
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
@ -111,7 +109,7 @@ def ls():
|
|||
"""
|
||||
try:
|
||||
stub = get_stub() # only after AfterEffects is up
|
||||
except lib.ConnectionNotEstablishedYet:
|
||||
except ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
|
|
@ -284,7 +282,7 @@ def _get_stub():
|
|||
"""
|
||||
try:
|
||||
stub = get_stub() # only after Photoshop is up
|
||||
except lib.ConnectionNotEstablishedYet:
|
||||
except ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from typing import Dict, List, Union
|
|||
|
||||
import bpy
|
||||
import addon_utils
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
|
||||
from . import pipeline
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ from openpype.pipeline import (
|
|||
deregister_creator_plugin_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import (
|
||||
Logger,
|
||||
register_event_callback,
|
||||
emit_event
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ import argparse
|
|||
import pyblish.api
|
||||
import pyblish.util
|
||||
|
||||
from openpype.api import Logger
|
||||
import openpype
|
||||
import openpype.hosts.celaction
|
||||
from openpype.lib import Logger
|
||||
from openpype.hosts.celaction import api as celaction
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.pipeline import install_openpype_plugins
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import xml.etree.cElementTree as cET
|
|||
from copy import deepcopy, copy
|
||||
from xml.etree import ElementTree as ET
|
||||
from pprint import pformat
|
||||
|
||||
from openpype.lib import Logger, run_subprocess
|
||||
|
||||
from .constants import (
|
||||
MARKER_COLOR,
|
||||
MARKER_DURATION,
|
||||
|
|
@ -20,9 +23,7 @@ from .constants import (
|
|||
MARKER_PUBLISH_DEFAULT
|
||||
)
|
||||
|
||||
import openpype.api as openpype
|
||||
|
||||
log = openpype.Logger.get_logger(__name__)
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
FRAME_PATTERN = re.compile(r"[\._](\d+)[\.]")
|
||||
|
||||
|
|
@ -766,11 +767,11 @@ class MediaInfoFile(object):
|
|||
_drop_mode = None
|
||||
_file_pattern = None
|
||||
|
||||
def __init__(self, path, **kwargs):
|
||||
def __init__(self, path, logger=None):
|
||||
|
||||
# replace log if any
|
||||
if kwargs.get("logger"):
|
||||
self.log = kwargs["logger"]
|
||||
if logger:
|
||||
self.log = logger
|
||||
|
||||
# test if `dl_get_media_info` paht exists
|
||||
self._validate_media_script_path()
|
||||
|
|
@ -1016,7 +1017,7 @@ class MediaInfoFile(object):
|
|||
|
||||
try:
|
||||
# execute creation of clip xml template data
|
||||
openpype.run_subprocess(cmd_args)
|
||||
run_subprocess(cmd_args)
|
||||
except TypeError as error:
|
||||
raise TypeError(
|
||||
"Error creating `{}` due: {}".format(fpath, error))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import os
|
|||
import contextlib
|
||||
from pyblish import api as pyblish
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import (
|
||||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
|
|
@ -90,8 +90,7 @@ def containerise(flame_clip_segment,
|
|||
def ls():
|
||||
"""List available containers.
|
||||
"""
|
||||
# TODO: ls
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def parse_container(tl_segment, validate=True):
|
||||
|
|
@ -107,6 +106,7 @@ def update_container(tl_segment, data=None):
|
|||
# TODO: update_container
|
||||
pass
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle node passthrough states on instance toggles."""
|
||||
|
||||
|
|
|
|||
|
|
@ -6,16 +6,17 @@ from xml.etree import ElementTree as ET
|
|||
|
||||
from Qt import QtCore, QtWidgets
|
||||
|
||||
import openpype.api as openpype
|
||||
import qargparse
|
||||
from openpype import style
|
||||
from openpype.settings import get_current_project_settings
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import LegacyCreator, LoaderPlugin
|
||||
|
||||
from . import constants
|
||||
from . import lib as flib
|
||||
from . import pipeline as fpipeline
|
||||
|
||||
log = openpype.Logger.get_logger(__name__)
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class CreatorWidget(QtWidgets.QDialog):
|
||||
|
|
@ -305,7 +306,7 @@ class Creator(LegacyCreator):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Creator, self).__init__(*args, **kwargs)
|
||||
self.presets = openpype.get_current_project_settings()[
|
||||
self.presets = get_current_project_settings()[
|
||||
"flame"]["create"].get(self.__class__.__name__, {})
|
||||
|
||||
# adding basic current context flame objects
|
||||
|
|
@ -678,6 +679,7 @@ class ClipLoader(LoaderPlugin):
|
|||
`update` logic.
|
||||
|
||||
"""
|
||||
log = log
|
||||
|
||||
options = [
|
||||
qargparse.Boolean(
|
||||
|
|
@ -694,16 +696,20 @@ class OpenClipSolver(flib.MediaInfoFile):
|
|||
|
||||
log = log
|
||||
|
||||
def __init__(self, openclip_file_path, feed_data):
|
||||
def __init__(self, openclip_file_path, feed_data, logger=None):
|
||||
self.out_file = openclip_file_path
|
||||
|
||||
# replace log if any
|
||||
if logger:
|
||||
self.log = logger
|
||||
|
||||
# new feed variables:
|
||||
feed_path = feed_data.pop("path")
|
||||
|
||||
# initialize parent class
|
||||
super(OpenClipSolver, self).__init__(
|
||||
feed_path,
|
||||
**feed_data
|
||||
logger=logger
|
||||
)
|
||||
|
||||
# get other metadata
|
||||
|
|
@ -751,10 +757,11 @@ class OpenClipSolver(flib.MediaInfoFile):
|
|||
self.log.info("Building new openClip")
|
||||
self.log.debug(">> self.clip_data: {}".format(self.clip_data))
|
||||
|
||||
# clip data comming from MediaInfoFile
|
||||
tmp_xml_feeds = self.clip_data.find('tracks/track/feeds')
|
||||
for tmp_xml_track in self.clip_data.iter("track"):
|
||||
tmp_xml_feeds = tmp_xml_track.find('feeds')
|
||||
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
|
||||
for tmp_feed in tmp_xml_feeds:
|
||||
|
||||
for tmp_feed in tmp_xml_track.iter("feed"):
|
||||
tmp_feed.set('vuid', self.feed_version_name)
|
||||
|
||||
# add colorspace if any is set
|
||||
|
|
@ -774,6 +781,17 @@ class OpenClipSolver(flib.MediaInfoFile):
|
|||
|
||||
self.write_clip_data_to_file(self.out_file, self.clip_data)
|
||||
|
||||
def _get_xml_track_obj_by_uid(self, xml_data, uid):
|
||||
# loop all tracks of input xml data
|
||||
for xml_track in xml_data.iter("track"):
|
||||
track_uid = xml_track.get("uid")
|
||||
self.log.debug(
|
||||
">> track_uid:uid: {}:{}".format(track_uid, uid))
|
||||
|
||||
# get matching uids
|
||||
if uid == track_uid:
|
||||
return xml_track
|
||||
|
||||
def _update_open_clip(self):
|
||||
self.log.info("Updating openClip ..")
|
||||
|
||||
|
|
@ -783,52 +801,81 @@ class OpenClipSolver(flib.MediaInfoFile):
|
|||
self.log.debug(">> out_xml: {}".format(out_xml))
|
||||
self.log.debug(">> self.clip_data: {}".format(self.clip_data))
|
||||
|
||||
# Get new feed from tmp file
|
||||
tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed')
|
||||
# loop tmp tracks
|
||||
updated_any = False
|
||||
for tmp_xml_track in self.clip_data.iter("track"):
|
||||
# get tmp track uid
|
||||
tmp_track_uid = tmp_xml_track.get("uid")
|
||||
self.log.debug(">> tmp_track_uid: {}".format(tmp_track_uid))
|
||||
|
||||
# get out data track by uid
|
||||
out_track_element = self._get_xml_track_obj_by_uid(
|
||||
out_xml, tmp_track_uid)
|
||||
self.log.debug(
|
||||
">> out_track_element: {}".format(out_track_element))
|
||||
|
||||
# loop tmp feeds
|
||||
for tmp_xml_feed in tmp_xml_track.iter("feed"):
|
||||
new_path_obj = tmp_xml_feed.find(
|
||||
"spans/span/path")
|
||||
new_path = new_path_obj.text
|
||||
|
||||
# check if feed path already exists in track's feeds
|
||||
if (
|
||||
out_track_element is not None
|
||||
and self._feed_exists(out_track_element, new_path)
|
||||
):
|
||||
continue
|
||||
|
||||
# rename versions on feeds
|
||||
tmp_xml_feed.set('vuid', self.feed_version_name)
|
||||
self._clear_handler(tmp_xml_feed)
|
||||
|
||||
# update fps from MediaInfoFile class
|
||||
if self.fps:
|
||||
if self.fps is not None:
|
||||
tmp_feed_fps_obj = tmp_xml_feed.find(
|
||||
"startTimecode/rate")
|
||||
tmp_feed_fps_obj.text = str(self.fps)
|
||||
|
||||
# update start_frame from MediaInfoFile class
|
||||
if self.start_frame:
|
||||
if self.start_frame is not None:
|
||||
tmp_feed_nb_ticks_obj = tmp_xml_feed.find(
|
||||
"startTimecode/nbTicks")
|
||||
tmp_feed_nb_ticks_obj.text = str(self.start_frame)
|
||||
|
||||
# update drop_mode from MediaInfoFile class
|
||||
if self.drop_mode:
|
||||
if self.drop_mode is not None:
|
||||
tmp_feed_drop_mode_obj = tmp_xml_feed.find(
|
||||
"startTimecode/dropMode")
|
||||
tmp_feed_drop_mode_obj.text = str(self.drop_mode)
|
||||
|
||||
new_path_obj = tmp_xml_feed.find(
|
||||
"spans/span/path")
|
||||
new_path = new_path_obj.text
|
||||
|
||||
feed_added = False
|
||||
if not self._feed_exists(out_xml, new_path):
|
||||
tmp_xml_feed.set('vuid', self.feed_version_name)
|
||||
# Append new temp file feed to .clip source out xml
|
||||
out_track = out_xml.find("tracks/track")
|
||||
# add colorspace if any is set
|
||||
if self.feed_colorspace:
|
||||
if self.feed_colorspace is not None:
|
||||
self._add_colorspace(tmp_xml_feed, self.feed_colorspace)
|
||||
|
||||
out_feeds = out_track.find('feeds')
|
||||
# then append/update feed to correct track in output
|
||||
if out_track_element:
|
||||
self.log.debug("updating track element ..")
|
||||
# update already present track
|
||||
out_feeds = out_track_element.find('feeds')
|
||||
out_feeds.set('currentVersion', self.feed_version_name)
|
||||
out_feeds.append(tmp_xml_feed)
|
||||
|
||||
self.log.info(
|
||||
"Appending new feed: {}".format(
|
||||
self.feed_version_name))
|
||||
feed_added = True
|
||||
else:
|
||||
self.log.debug("adding new track element ..")
|
||||
# create new track as it doesnt exists yet
|
||||
# set current version to feeds on tmp
|
||||
tmp_xml_feeds = tmp_xml_track.find('feeds')
|
||||
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
|
||||
out_tracks = out_xml.find("tracks")
|
||||
out_tracks.append(tmp_xml_track)
|
||||
|
||||
if feed_added:
|
||||
updated_any = True
|
||||
|
||||
if updated_any:
|
||||
# Append vUID to versions
|
||||
out_xml_versions_obj = out_xml.find('versions')
|
||||
out_xml_versions_obj.set(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
from xml.etree import ElementTree as ET
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Flame utils for syncing scripts
|
|||
|
||||
import os
|
||||
import shutil
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
in environment var FLAME_SCRIPT_DIR.
|
||||
"""
|
||||
app_groups = ["flame"]
|
||||
permissions = 0o777
|
||||
|
||||
wtc_script_path = os.path.join(
|
||||
opflame.HOST_DIR, "api", "scripts", "wiretap_com.py")
|
||||
|
|
@ -38,19 +39,12 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
"""Hook entry method."""
|
||||
project_doc = self.data["project_doc"]
|
||||
project_name = project_doc["name"]
|
||||
volume_name = _env.get("FLAME_WIRETAP_VOLUME")
|
||||
|
||||
# get image io
|
||||
project_anatomy = self.data["anatomy"]
|
||||
project_settings = self.data["project_settings"]
|
||||
|
||||
# make sure anatomy settings are having flame key
|
||||
if not project_anatomy["imageio"].get("flame"):
|
||||
raise ApplicationLaunchFailed((
|
||||
"Anatomy project settings are missing `flame` key. "
|
||||
"Please make sure you remove project overides on "
|
||||
"Anatomy Image io")
|
||||
)
|
||||
|
||||
imageio_flame = project_anatomy["imageio"]["flame"]
|
||||
imageio_flame = project_settings["flame"]["imageio"]
|
||||
|
||||
# get user name and host name
|
||||
user_name = get_openpype_username()
|
||||
|
|
@ -81,7 +75,7 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
data_to_script = {
|
||||
# from settings
|
||||
"host_name": _env.get("FLAME_WIRETAP_HOSTNAME") or hostname,
|
||||
"volume_name": _env.get("FLAME_WIRETAP_VOLUME"),
|
||||
"volume_name": volume_name,
|
||||
"group_name": _env.get("FLAME_WIRETAP_GROUP"),
|
||||
"color_policy": str(imageio_flame["project"]["colourPolicy"]),
|
||||
|
||||
|
|
@ -99,8 +93,41 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
|
||||
app_arguments = self._get_launch_arguments(data_to_script)
|
||||
|
||||
# fix project data permission issue
|
||||
self._fix_permissions(project_name, volume_name)
|
||||
|
||||
self.launch_context.launch_args.extend(app_arguments)
|
||||
|
||||
def _fix_permissions(self, project_name, volume_name):
|
||||
"""Work around for project data permissions
|
||||
|
||||
Reported issue: when project is created locally on one machine,
|
||||
it is impossible to migrate it to other machine. Autodesk Flame
|
||||
is crating some unmanagable files which needs to be opened to 0o777.
|
||||
|
||||
Args:
|
||||
project_name (str): project name
|
||||
volume_name (str): studio volume
|
||||
"""
|
||||
dirs_to_modify = [
|
||||
"/usr/discreet/project/{}".format(project_name),
|
||||
"/opt/Autodesk/clip/{}/{}.prj".format(volume_name, project_name),
|
||||
"/usr/discreet/clip/{}/{}.prj".format(volume_name, project_name)
|
||||
]
|
||||
|
||||
for dirtm in dirs_to_modify:
|
||||
for root, dirs, files in os.walk(dirtm):
|
||||
try:
|
||||
for name in set(dirs) | set(files):
|
||||
path = os.path.join(root, name)
|
||||
st = os.stat(path)
|
||||
if oct(st.st_mode) != self.permissions:
|
||||
os.chmod(path, self.permissions)
|
||||
|
||||
except OSError as exc:
|
||||
self.log.warning("Not able to open files: {}".format(exc))
|
||||
|
||||
|
||||
def _get_flame_fps(self, fps_num):
|
||||
fps_table = {
|
||||
float(23.976): "23.976 fps",
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@ class CreateShotClip(opfapi.Creator):
|
|||
# nested dictionary (only one level allowed
|
||||
# for sections and dict)
|
||||
for _k, _v in v["value"].items():
|
||||
if presets.get(_k):
|
||||
if presets.get(_k) is not None:
|
||||
gui_inputs[k][
|
||||
"value"][_k]["value"] = presets[_k]
|
||||
if presets.get(k):
|
||||
|
||||
if presets.get(k) is not None:
|
||||
gui_inputs[k]["value"] = presets[k]
|
||||
|
||||
# open widget for plugins inputs
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from pprint import pformat
|
|||
import openpype.hosts.flame.api as opfapi
|
||||
from openpype.lib import StringTemplate
|
||||
|
||||
|
||||
class LoadClip(opfapi.ClipLoader):
|
||||
"""Load a subset to timeline as clip
|
||||
|
||||
|
|
@ -60,8 +61,6 @@ class LoadClip(opfapi.ClipLoader):
|
|||
"path": self.fname.replace("\\", "/"),
|
||||
"colorspace": colorspace,
|
||||
"version": "v{:0>3}".format(version_name),
|
||||
"logger": self.log
|
||||
|
||||
}
|
||||
self.log.debug(pformat(
|
||||
loading_context
|
||||
|
|
@ -69,7 +68,8 @@ class LoadClip(opfapi.ClipLoader):
|
|||
self.log.debug(openclip_path)
|
||||
|
||||
# make openpype clip file
|
||||
opfapi.OpenClipSolver(openclip_path, loading_context).make()
|
||||
opfapi.OpenClipSolver(
|
||||
openclip_path, loading_context, logger=self.log).make()
|
||||
|
||||
# prepare Reel group in actual desktop
|
||||
opc = self._get_clip(
|
||||
|
|
|
|||
|
|
@ -64,8 +64,6 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
"path": self.fname.replace("\\", "/"),
|
||||
"colorspace": colorspace,
|
||||
"version": "v{:0>3}".format(version_name),
|
||||
"logger": self.log
|
||||
|
||||
}
|
||||
self.log.debug(pformat(
|
||||
loading_context
|
||||
|
|
@ -73,7 +71,8 @@ class LoadClipBatch(opfapi.ClipLoader):
|
|||
self.log.debug(openclip_path)
|
||||
|
||||
# make openpype clip file
|
||||
opfapi.OpenClipSolver(openclip_path, loading_context).make()
|
||||
opfapi.OpenClipSolver(
|
||||
openclip_path, loading_context, logger=self.log).make()
|
||||
|
||||
# prepare Reel group in actual desktop
|
||||
opc = self._get_clip(
|
||||
|
|
|
|||
|
|
@ -131,9 +131,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
"fps": self.fps,
|
||||
"workfileFrameStart": workfile_start,
|
||||
"sourceFirstFrame": int(first_frame),
|
||||
"notRetimedHandles": (
|
||||
not marker_data.get("retimedHandles")),
|
||||
"notRetimedFramerange": (
|
||||
"retimedHandles": marker_data.get("retimedHandles"),
|
||||
"shotDurationFromSource": (
|
||||
not marker_data.get("retimedFramerange")),
|
||||
"path": file_path,
|
||||
"flameAddTasks": self.add_tasks,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
import opentimelineio as otio
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractOTIOFile(openpype.api.Extractor):
|
||||
class ExtractOTIOFile(publish.Extractor):
|
||||
"""
|
||||
Extractor export OTIO file
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pprint import pformat
|
||||
from copy import deepcopy
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.flame import api as opfapi
|
||||
from openpype.hosts.flame.api import MediaInfoFile
|
||||
from openpype.pipeline.editorial import (
|
||||
|
|
@ -15,7 +15,7 @@ from openpype.pipeline.editorial import (
|
|||
import flame
|
||||
|
||||
|
||||
class ExtractSubsetResources(openpype.api.Extractor):
|
||||
class ExtractSubsetResources(publish.Extractor):
|
||||
"""
|
||||
Extractor for transcoding files from Flame clip
|
||||
"""
|
||||
|
|
@ -80,10 +80,10 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
retimed_data = self._get_retimed_attributes(instance)
|
||||
|
||||
# get individual keys
|
||||
r_handle_start = retimed_data["handle_start"]
|
||||
r_handle_end = retimed_data["handle_end"]
|
||||
r_source_dur = retimed_data["source_duration"]
|
||||
r_speed = retimed_data["speed"]
|
||||
retimed_handle_start = retimed_data["handle_start"]
|
||||
retimed_handle_end = retimed_data["handle_end"]
|
||||
retimed_source_duration = retimed_data["source_duration"]
|
||||
retimed_speed = retimed_data["speed"]
|
||||
|
||||
# get handles value - take only the max from both
|
||||
handle_start = instance.data["handleStart"]
|
||||
|
|
@ -97,22 +97,23 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
source_end_handles = instance.data["sourceEndH"]
|
||||
|
||||
# retime if needed
|
||||
if r_speed != 1.0:
|
||||
if retimed_speed != 1.0:
|
||||
if retimed_handles:
|
||||
# handles are retimed
|
||||
source_start_handles = (
|
||||
instance.data["sourceStart"] - r_handle_start)
|
||||
instance.data["sourceStart"] - retimed_handle_start)
|
||||
source_end_handles = (
|
||||
source_start_handles
|
||||
+ (r_source_dur - 1)
|
||||
+ r_handle_start
|
||||
+ r_handle_end
|
||||
+ (retimed_source_duration - 1)
|
||||
+ retimed_handle_start
|
||||
+ retimed_handle_end
|
||||
)
|
||||
|
||||
else:
|
||||
# handles are not retimed
|
||||
source_end_handles = (
|
||||
source_start_handles
|
||||
+ (r_source_dur - 1)
|
||||
+ (retimed_source_duration - 1)
|
||||
+ handle_start
|
||||
+ handle_end
|
||||
)
|
||||
|
|
@ -121,11 +122,11 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
frame_start_handle = frame_start - handle_start
|
||||
repre_frame_start = frame_start_handle
|
||||
if include_handles:
|
||||
if r_speed == 1.0 or not retimed_handles:
|
||||
if retimed_speed == 1.0 or not retimed_handles:
|
||||
frame_start_handle = frame_start
|
||||
else:
|
||||
frame_start_handle = (
|
||||
frame_start - handle_start) + r_handle_start
|
||||
frame_start - handle_start) + retimed_handle_start
|
||||
|
||||
self.log.debug("_ frame_start_handle: {}".format(
|
||||
frame_start_handle))
|
||||
|
|
@ -136,6 +137,9 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
source_duration_handles = (
|
||||
source_end_handles - source_start_handles) + 1
|
||||
|
||||
self.log.debug("_ source_duration_handles: {}".format(
|
||||
source_duration_handles))
|
||||
|
||||
# create staging dir path
|
||||
staging_dir = self.staging_dir(instance)
|
||||
|
||||
|
|
@ -159,15 +163,30 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
if version_data:
|
||||
instance.data["versionData"].update(version_data)
|
||||
|
||||
if r_speed != 1.0:
|
||||
# version data start frame
|
||||
version_frame_start = frame_start
|
||||
if include_handles:
|
||||
version_frame_start = frame_start_handle
|
||||
if retimed_speed != 1.0:
|
||||
if retimed_handles:
|
||||
instance.data["versionData"].update({
|
||||
"frameStart": frame_start_handle,
|
||||
"frameStart": version_frame_start,
|
||||
"frameEnd": (
|
||||
(frame_start_handle + source_duration_handles - 1)
|
||||
- (r_handle_start + r_handle_end)
|
||||
(version_frame_start + source_duration_handles - 1)
|
||||
- (retimed_handle_start + retimed_handle_end)
|
||||
)
|
||||
})
|
||||
self.log.debug("_ i_version_data: {}".format(
|
||||
else:
|
||||
instance.data["versionData"].update({
|
||||
"handleStart": handle_start,
|
||||
"handleEnd": handle_end,
|
||||
"frameStart": version_frame_start,
|
||||
"frameEnd": (
|
||||
(version_frame_start + source_duration_handles - 1)
|
||||
- (handle_start + handle_end)
|
||||
)
|
||||
})
|
||||
self.log.debug("_ version_data: {}".format(
|
||||
instance.data["versionData"]
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -19,5 +19,14 @@ class FusionAddon(OpenPypeModule, IHostAddon):
|
|||
os.path.join(FUSION_HOST_DIR, "hooks")
|
||||
]
|
||||
|
||||
def add_implementation_envs(self, env, _app):
|
||||
# Set default values if are not already set via settings
|
||||
defaults = {
|
||||
"OPENPYPE_LOG_NO_COLORS": "Yes"
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
env[key] = value
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".comp"]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ from .pipeline import (
|
|||
ls,
|
||||
|
||||
imprint_container,
|
||||
parse_container,
|
||||
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
parse_container
|
||||
)
|
||||
|
||||
from .workio import (
|
||||
|
|
@ -22,8 +19,10 @@ from .workio import (
|
|||
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
get_additional_data,
|
||||
update_frame_range
|
||||
update_frame_range,
|
||||
set_asset_framerange,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
from .menu import launch_openpype_menu
|
||||
|
|
@ -38,9 +37,6 @@ __all__ = [
|
|||
"imprint_container",
|
||||
"parse_container",
|
||||
|
||||
"get_current_comp",
|
||||
"comp_lock_and_undo_chunk",
|
||||
|
||||
# workio
|
||||
"open_file",
|
||||
"save_file",
|
||||
|
|
@ -51,8 +47,10 @@ __all__ = [
|
|||
|
||||
# lib
|
||||
"maintained_selection",
|
||||
"get_additional_data",
|
||||
"update_frame_range",
|
||||
"set_asset_framerange",
|
||||
"get_current_comp",
|
||||
"comp_lock_and_undo_chunk",
|
||||
|
||||
# menu
|
||||
"launch_openpype_menu",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import sys
|
|||
import re
|
||||
import contextlib
|
||||
|
||||
from Qt import QtGui
|
||||
|
||||
from openpype.lib import Logger
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
get_subset_by_name,
|
||||
|
|
@ -17,13 +16,14 @@ from openpype.pipeline import (
|
|||
switch_container,
|
||||
legacy_io,
|
||||
)
|
||||
from .pipeline import get_current_comp, comp_lock_and_undo_chunk
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self._project = None
|
||||
|
||||
|
||||
def update_frame_range(start, end, comp=None, set_render_range=True):
|
||||
def update_frame_range(start, end, comp=None, set_render_range=True,
|
||||
handle_start=0, handle_end=0):
|
||||
"""Set Fusion comp's start and end frame range
|
||||
|
||||
Args:
|
||||
|
|
@ -32,6 +32,8 @@ def update_frame_range(start, end, comp=None, set_render_range=True):
|
|||
comp (object, Optional): comp object from fusion
|
||||
set_render_range (bool, Optional): When True this will also set the
|
||||
composition's render start and end frame.
|
||||
handle_start (float, int, Optional): frame handles before start frame
|
||||
handle_end (float, int, Optional): frame handles after end frame
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
|
@ -41,11 +43,16 @@ def update_frame_range(start, end, comp=None, set_render_range=True):
|
|||
if not comp:
|
||||
comp = get_current_comp()
|
||||
|
||||
# Convert any potential none type to zero
|
||||
handle_start = handle_start or 0
|
||||
handle_end = handle_end or 0
|
||||
|
||||
attrs = {
|
||||
"COMPN_GlobalStart": start,
|
||||
"COMPN_GlobalEnd": end
|
||||
"COMPN_GlobalStart": start - handle_start,
|
||||
"COMPN_GlobalEnd": end + handle_end
|
||||
}
|
||||
|
||||
# set frame range
|
||||
if set_render_range:
|
||||
attrs.update({
|
||||
"COMPN_RenderStart": start,
|
||||
|
|
@ -56,24 +63,122 @@ def update_frame_range(start, end, comp=None, set_render_range=True):
|
|||
comp.SetAttrs(attrs)
|
||||
|
||||
|
||||
def get_additional_data(container):
|
||||
"""Get Fusion related data for the container
|
||||
def set_asset_framerange():
|
||||
"""Set Comp's frame range based on current asset"""
|
||||
asset_doc = get_current_project_asset()
|
||||
start = asset_doc["data"]["frameStart"]
|
||||
end = asset_doc["data"]["frameEnd"]
|
||||
handle_start = asset_doc["data"]["handleStart"]
|
||||
handle_end = asset_doc["data"]["handleEnd"]
|
||||
update_frame_range(start, end, set_render_range=True,
|
||||
handle_start=handle_start,
|
||||
handle_end=handle_end)
|
||||
|
||||
Args:
|
||||
container(dict): the container found by the ls() function
|
||||
|
||||
Returns:
|
||||
dict
|
||||
def set_asset_resolution():
|
||||
"""Set Comp's resolution width x height default based on current asset"""
|
||||
asset_doc = get_current_project_asset()
|
||||
width = asset_doc["data"]["resolutionWidth"]
|
||||
height = asset_doc["data"]["resolutionHeight"]
|
||||
comp = get_current_comp()
|
||||
|
||||
print("Setting comp frame format resolution to {}x{}".format(width,
|
||||
height))
|
||||
comp.SetPrefs({
|
||||
"Comp.FrameFormat.Width": width,
|
||||
"Comp.FrameFormat.Height": height,
|
||||
})
|
||||
|
||||
|
||||
def validate_comp_prefs(comp=None, force_repair=False):
|
||||
"""Validate current comp defaults with asset settings.
|
||||
|
||||
Validates fps, resolutionWidth, resolutionHeight, aspectRatio.
|
||||
|
||||
This does *not* validate frameStart, frameEnd, handleStart and handleEnd.
|
||||
"""
|
||||
|
||||
tool = container["_tool"]
|
||||
tile_color = tool.TileColor
|
||||
if tile_color is None:
|
||||
return {}
|
||||
if comp is None:
|
||||
comp = get_current_comp()
|
||||
|
||||
return {"color": QtGui.QColor.fromRgbF(tile_color["R"],
|
||||
tile_color["G"],
|
||||
tile_color["B"])}
|
||||
log = Logger.get_logger("validate_comp_prefs")
|
||||
|
||||
fields = [
|
||||
"name",
|
||||
"data.fps",
|
||||
"data.resolutionWidth",
|
||||
"data.resolutionHeight",
|
||||
"data.pixelAspect"
|
||||
]
|
||||
asset_doc = get_current_project_asset(fields=fields)
|
||||
asset_data = asset_doc["data"]
|
||||
|
||||
comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat")
|
||||
|
||||
# Pixel aspect ratio in Fusion is set as AspectX and AspectY so we convert
|
||||
# the data to something that is more sensible to Fusion
|
||||
asset_data["pixelAspectX"] = asset_data.pop("pixelAspect")
|
||||
asset_data["pixelAspectY"] = 1.0
|
||||
|
||||
validations = [
|
||||
("fps", "Rate", "FPS"),
|
||||
("resolutionWidth", "Width", "Resolution Width"),
|
||||
("resolutionHeight", "Height", "Resolution Height"),
|
||||
("pixelAspectX", "AspectX", "Pixel Aspect Ratio X"),
|
||||
("pixelAspectY", "AspectY", "Pixel Aspect Ratio Y")
|
||||
]
|
||||
|
||||
invalid = []
|
||||
for key, comp_key, label in validations:
|
||||
asset_value = asset_data[key]
|
||||
comp_value = comp_frame_format_prefs.get(comp_key)
|
||||
if asset_value != comp_value:
|
||||
invalid_msg = "{} {} should be {}".format(label,
|
||||
comp_value,
|
||||
asset_value)
|
||||
invalid.append(invalid_msg)
|
||||
|
||||
if not force_repair:
|
||||
# Do not log warning if we force repair anyway
|
||||
log.warning(
|
||||
"Comp {pref} {value} does not match asset "
|
||||
"'{asset_name}' {pref} {asset_value}".format(
|
||||
pref=label,
|
||||
value=comp_value,
|
||||
asset_name=asset_doc["name"],
|
||||
asset_value=asset_value)
|
||||
)
|
||||
|
||||
if invalid:
|
||||
|
||||
def _on_repair():
|
||||
attributes = dict()
|
||||
for key, comp_key, _label in validations:
|
||||
value = asset_data[key]
|
||||
comp_key_full = "Comp.FrameFormat.{}".format(comp_key)
|
||||
attributes[comp_key_full] = value
|
||||
comp.SetPrefs(attributes)
|
||||
|
||||
if force_repair:
|
||||
log.info("Applying default Comp preferences..")
|
||||
_on_repair()
|
||||
return
|
||||
|
||||
from . import menu
|
||||
from openpype.widgets import popup
|
||||
from openpype.style import load_stylesheet
|
||||
dialog = popup.Popup(parent=menu.menu)
|
||||
dialog.setWindowTitle("Fusion comp has invalid configuration")
|
||||
|
||||
msg = "Comp preferences mismatches '{}'".format(asset_doc["name"])
|
||||
msg += "\n" + "\n".join(invalid)
|
||||
dialog.setMessage(msg)
|
||||
dialog.setButtonText("Repair")
|
||||
dialog.on_clicked.connect(_on_repair)
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
dialog.setStyleSheet(load_stylesheet())
|
||||
|
||||
|
||||
def switch_item(container,
|
||||
|
|
@ -195,3 +300,21 @@ def get_frame_path(path):
|
|||
padding = 4 # default Fusion padding
|
||||
|
||||
return filename, padding, ext
|
||||
|
||||
|
||||
def get_current_comp():
|
||||
"""Hack to get current comp in this session"""
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
return fusion.CurrentComp if fusion else None
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"):
|
||||
"""Lock comp and open an undo chunk during the context"""
|
||||
try:
|
||||
comp.Lock()
|
||||
comp.StartUndo(undo_queue_name)
|
||||
yield
|
||||
finally:
|
||||
comp.Unlock()
|
||||
comp.EndUndo()
|
||||
|
|
|
|||
|
|
@ -1,43 +1,26 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import style
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
from openpype.style import load_stylesheet
|
||||
from openpype.lib import register_event_callback
|
||||
from openpype.hosts.fusion.scripts import (
|
||||
set_rendermode,
|
||||
duplicate_with_inputs
|
||||
)
|
||||
from openpype.hosts.fusion.api.lib import (
|
||||
set_asset_framerange,
|
||||
set_asset_resolution
|
||||
)
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
|
||||
from .pipeline import FusionEventHandler
|
||||
from .pulse import FusionPulse
|
||||
|
||||
def load_stylesheet():
|
||||
path = os.path.join(os.path.dirname(__file__), "menu_style.qss")
|
||||
if not os.path.exists(path):
|
||||
print("Unable to load stylesheet, file not found in resources")
|
||||
return ""
|
||||
|
||||
with open(path, "r") as file_stream:
|
||||
stylesheet = file_stream.read()
|
||||
return stylesheet
|
||||
|
||||
|
||||
class Spacer(QtWidgets.QWidget):
|
||||
def __init__(self, height, *args, **kwargs):
|
||||
super(Spacer, self).__init__(*args, **kwargs)
|
||||
|
||||
self.setFixedHeight(height)
|
||||
|
||||
real_spacer = QtWidgets.QWidget(self)
|
||||
real_spacer.setObjectName("Spacer")
|
||||
real_spacer.setFixedHeight(height)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(real_spacer)
|
||||
|
||||
self.setLayout(layout)
|
||||
self = sys.modules[__name__]
|
||||
self.menu = None
|
||||
|
||||
|
||||
class OpenPypeMenu(QtWidgets.QWidget):
|
||||
|
|
@ -46,15 +29,29 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
self.setObjectName("OpenPypeMenu")
|
||||
|
||||
icon_path = get_openpype_icon_filepath()
|
||||
icon = QtGui.QIcon(icon_path)
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
| QtCore.Qt.WindowTitleHint
|
||||
| QtCore.Qt.WindowMinimizeButtonHint
|
||||
| QtCore.Qt.WindowCloseButtonHint
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
self.render_mode_widget = None
|
||||
self.setWindowTitle("OpenPype")
|
||||
|
||||
asset_label = QtWidgets.QLabel("Context", self)
|
||||
asset_label.setStyleSheet("""QLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #5f9fb8;
|
||||
}""")
|
||||
asset_label.setAlignment(QtCore.Qt.AlignHCenter)
|
||||
|
||||
workfiles_btn = QtWidgets.QPushButton("Workfiles...", self)
|
||||
create_btn = QtWidgets.QPushButton("Create...", self)
|
||||
publish_btn = QtWidgets.QPushButton("Publish...", self)
|
||||
|
|
@ -62,77 +59,111 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
manager_btn = QtWidgets.QPushButton("Manage...", self)
|
||||
libload_btn = QtWidgets.QPushButton("Library...", self)
|
||||
rendermode_btn = QtWidgets.QPushButton("Set render mode...", self)
|
||||
set_framerange_btn = QtWidgets.QPushButton("Set Frame Range", self)
|
||||
set_resolution_btn = QtWidgets.QPushButton("Set Resolution", self)
|
||||
duplicate_with_inputs_btn = QtWidgets.QPushButton(
|
||||
"Duplicate with input connections", self
|
||||
)
|
||||
reset_resolution_btn = QtWidgets.QPushButton(
|
||||
"Reset Resolution from project", self
|
||||
)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(10, 20, 10, 20)
|
||||
|
||||
layout.addWidget(asset_label)
|
||||
|
||||
layout.addSpacing(20)
|
||||
|
||||
layout.addWidget(workfiles_btn)
|
||||
|
||||
layout.addSpacing(20)
|
||||
|
||||
layout.addWidget(create_btn)
|
||||
layout.addWidget(publish_btn)
|
||||
layout.addWidget(load_btn)
|
||||
layout.addWidget(publish_btn)
|
||||
layout.addWidget(manager_btn)
|
||||
|
||||
layout.addWidget(Spacer(15, self))
|
||||
layout.addSpacing(20)
|
||||
|
||||
layout.addWidget(libload_btn)
|
||||
|
||||
layout.addWidget(Spacer(15, self))
|
||||
layout.addSpacing(20)
|
||||
|
||||
layout.addWidget(set_framerange_btn)
|
||||
layout.addWidget(set_resolution_btn)
|
||||
layout.addWidget(rendermode_btn)
|
||||
|
||||
layout.addWidget(Spacer(15, self))
|
||||
layout.addSpacing(20)
|
||||
|
||||
layout.addWidget(duplicate_with_inputs_btn)
|
||||
layout.addWidget(reset_resolution_btn)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
# Store reference so we can update the label
|
||||
self.asset_label = asset_label
|
||||
|
||||
workfiles_btn.clicked.connect(self.on_workfile_clicked)
|
||||
create_btn.clicked.connect(self.on_create_clicked)
|
||||
publish_btn.clicked.connect(self.on_publish_clicked)
|
||||
load_btn.clicked.connect(self.on_load_clicked)
|
||||
manager_btn.clicked.connect(self.on_manager_clicked)
|
||||
libload_btn.clicked.connect(self.on_libload_clicked)
|
||||
rendermode_btn.clicked.connect(self.on_rendernode_clicked)
|
||||
rendermode_btn.clicked.connect(self.on_rendermode_clicked)
|
||||
duplicate_with_inputs_btn.clicked.connect(
|
||||
self.on_duplicate_with_inputs_clicked)
|
||||
reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked)
|
||||
set_resolution_btn.clicked.connect(self.on_set_resolution_clicked)
|
||||
set_framerange_btn.clicked.connect(self.on_set_framerange_clicked)
|
||||
|
||||
self._callbacks = []
|
||||
self.register_callback("taskChanged", self.on_task_changed)
|
||||
self.on_task_changed()
|
||||
|
||||
# Force close current process if Fusion is closed
|
||||
self._pulse = FusionPulse(parent=self)
|
||||
self._pulse.start()
|
||||
|
||||
# Detect Fusion events as OpenPype events
|
||||
self._event_handler = FusionEventHandler(parent=self)
|
||||
self._event_handler.start()
|
||||
|
||||
def on_task_changed(self):
|
||||
# Update current context label
|
||||
label = legacy_io.Session["AVALON_ASSET"]
|
||||
self.asset_label.setText(label)
|
||||
|
||||
def register_callback(self, name, fn):
|
||||
|
||||
# Create a wrapper callback that we only store
|
||||
# for as long as we want it to persist as callback
|
||||
def _callback(*args):
|
||||
fn()
|
||||
|
||||
self._callbacks.append(_callback)
|
||||
register_event_callback(name, _callback)
|
||||
|
||||
def deregister_all_callbacks(self):
|
||||
self._callbacks[:] = []
|
||||
|
||||
def on_workfile_clicked(self):
|
||||
print("Clicked Workfile")
|
||||
host_tools.show_workfiles()
|
||||
|
||||
def on_create_clicked(self):
|
||||
print("Clicked Create")
|
||||
host_tools.show_creator()
|
||||
|
||||
def on_publish_clicked(self):
|
||||
print("Clicked Publish")
|
||||
host_tools.show_publish()
|
||||
|
||||
def on_load_clicked(self):
|
||||
print("Clicked Load")
|
||||
host_tools.show_loader(use_context=True)
|
||||
|
||||
def on_manager_clicked(self):
|
||||
print("Clicked Manager")
|
||||
host_tools.show_scene_inventory()
|
||||
|
||||
def on_libload_clicked(self):
|
||||
print("Clicked Library")
|
||||
host_tools.show_library_loader()
|
||||
|
||||
def on_rendernode_clicked(self):
|
||||
print("Clicked Set Render Mode")
|
||||
def on_rendermode_clicked(self):
|
||||
if self.render_mode_widget is None:
|
||||
window = set_rendermode.SetRenderMode()
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.setStyleSheet(load_stylesheet())
|
||||
window.show()
|
||||
self.render_mode_widget = window
|
||||
else:
|
||||
|
|
@ -140,15 +171,16 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
|
||||
def on_duplicate_with_inputs_clicked(self):
|
||||
duplicate_with_inputs.duplicate_with_input_connections()
|
||||
print("Clicked Set Colorspace")
|
||||
|
||||
def on_reset_resolution_clicked(self):
|
||||
print("Clicked Reset Resolution")
|
||||
def on_set_resolution_clicked(self):
|
||||
set_asset_resolution()
|
||||
|
||||
def on_set_framerange_clicked(self):
|
||||
set_asset_framerange()
|
||||
|
||||
|
||||
def launch_openpype_menu():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
pype_menu = OpenPypeMenu()
|
||||
|
||||
|
|
@ -156,5 +188,8 @@ def launch_openpype_menu():
|
|||
pype_menu.setStyleSheet(stylesheet)
|
||||
|
||||
pype_menu.show()
|
||||
self.menu = pype_menu
|
||||
|
||||
sys.exit(app.exec_())
|
||||
result = app.exec_()
|
||||
print("Shutting down..")
|
||||
sys.exit(result)
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
QWidget {
|
||||
background-color: #282828;
|
||||
border-radius: 3;
|
||||
}
|
||||
|
||||
QPushButton {
|
||||
border: 1px solid #090909;
|
||||
background-color: #201f1f;
|
||||
color: #ffffff;
|
||||
padding: 5;
|
||||
}
|
||||
|
||||
QPushButton:focus {
|
||||
background-color: "#171717";
|
||||
color: #d0d0d0;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: "#171717";
|
||||
color: #e64b3d;
|
||||
}
|
||||
|
||||
#OpenPypeMenu {
|
||||
border: 1px solid #fef9ef;
|
||||
}
|
||||
|
||||
#Spacer {
|
||||
background-color: #282828;
|
||||
}
|
||||
|
|
@ -4,11 +4,15 @@ Basic avalon integration
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import contextlib
|
||||
|
||||
import pyblish.api
|
||||
from Qt import QtCore
|
||||
|
||||
from openpype.lib import Logger
|
||||
from openpype.lib import (
|
||||
Logger,
|
||||
register_event_callback,
|
||||
emit_event
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
register_loader_plugin_path,
|
||||
register_creator_plugin_path,
|
||||
|
|
@ -18,7 +22,15 @@ from openpype.pipeline import (
|
|||
deregister_inventory_action_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.hosts.fusion import FUSION_HOST_DIR
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
from .lib import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk,
|
||||
validate_comp_prefs
|
||||
)
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
@ -30,16 +42,17 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
|||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
class CompLogHandler(logging.Handler):
|
||||
class FusionLogHandler(logging.Handler):
|
||||
# Keep a reference to fusion's Print function (Remote Object)
|
||||
_print = getattr(sys.modules["__main__"], "fusion").Print
|
||||
|
||||
def emit(self, record):
|
||||
entry = self.format(record)
|
||||
comp = get_current_comp()
|
||||
if comp:
|
||||
comp.Print(entry)
|
||||
self._print(entry)
|
||||
|
||||
|
||||
def install():
|
||||
"""Install fusion-specific functionality of avalon-core.
|
||||
"""Install fusion-specific functionality of OpenPype.
|
||||
|
||||
This is where you install menus and register families, data
|
||||
and loaders into fusion.
|
||||
|
|
@ -51,20 +64,18 @@ def install():
|
|||
|
||||
"""
|
||||
# Remove all handlers associated with the root logger object, because
|
||||
# that one sometimes logs as "warnings" incorrectly.
|
||||
# that one always logs as "warnings" incorrectly.
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
|
||||
# Attach default logging handler that prints to active comp
|
||||
logger = logging.getLogger()
|
||||
formatter = logging.Formatter(fmt="%(message)s\n")
|
||||
handler = CompLogHandler()
|
||||
handler = FusionLogHandler()
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
log.info("openpype.hosts.fusion installed")
|
||||
|
||||
pyblish.api.register_host("fusion")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
log.info("Registering Fusion plug-ins..")
|
||||
|
|
@ -77,6 +88,11 @@ def install():
|
|||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
# Register events
|
||||
register_event_callback("open", on_after_open)
|
||||
register_event_callback("save", on_save)
|
||||
register_event_callback("new", on_new)
|
||||
|
||||
|
||||
def uninstall():
|
||||
"""Uninstall all that was installed
|
||||
|
|
@ -102,7 +118,7 @@ def uninstall():
|
|||
)
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, new_value, old_value):
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle saver tool passthrough states on instance toggles."""
|
||||
comp = instance.context.data.get("currentComp")
|
||||
if not comp:
|
||||
|
|
@ -125,6 +141,48 @@ def on_pyblish_instance_toggled(instance, new_value, old_value):
|
|||
tool.SetAttrs({"TOOLB_PassThrough": passthrough})
|
||||
|
||||
|
||||
def on_new(event):
|
||||
comp = event["Rets"]["comp"]
|
||||
validate_comp_prefs(comp, force_repair=True)
|
||||
|
||||
|
||||
def on_save(event):
|
||||
comp = event["sender"]
|
||||
validate_comp_prefs(comp)
|
||||
|
||||
|
||||
def on_after_open(event):
|
||||
comp = event["sender"]
|
||||
validate_comp_prefs(comp)
|
||||
|
||||
if any_outdated_containers():
|
||||
log.warning("Scene has outdated content.")
|
||||
|
||||
# Find OpenPype menu to attach to
|
||||
from . import menu
|
||||
|
||||
def _on_show_scene_inventory():
|
||||
# ensure that comp is active
|
||||
frame = comp.CurrentFrame
|
||||
if not frame:
|
||||
print("Comp is closed, skipping show scene inventory")
|
||||
return
|
||||
frame.ActivateFrame() # raise comp window
|
||||
host_tools.show_scene_inventory()
|
||||
|
||||
from openpype.widgets import popup
|
||||
from openpype.style import load_stylesheet
|
||||
dialog = popup.Popup(parent=menu.menu)
|
||||
dialog.setWindowTitle("Fusion comp has outdated content")
|
||||
dialog.setMessage("There are outdated containers in "
|
||||
"your Fusion comp.")
|
||||
dialog.on_clicked.connect(_on_show_scene_inventory)
|
||||
dialog.show()
|
||||
dialog.raise_()
|
||||
dialog.activateWindow()
|
||||
dialog.setStyleSheet(load_stylesheet())
|
||||
|
||||
|
||||
def ls():
|
||||
"""List containers from active Fusion scene
|
||||
|
||||
|
|
@ -138,7 +196,7 @@ def ls():
|
|||
"""
|
||||
|
||||
comp = get_current_comp()
|
||||
tools = comp.GetToolList(False, "Loader").values()
|
||||
tools = comp.GetToolList(False).values()
|
||||
|
||||
for tool in tools:
|
||||
container = parse_container(tool)
|
||||
|
|
@ -210,19 +268,114 @@ def parse_container(tool):
|
|||
return container
|
||||
|
||||
|
||||
def get_current_comp():
|
||||
"""Hack to get current comp in this session"""
|
||||
class FusionEventThread(QtCore.QThread):
|
||||
"""QThread which will periodically ping Fusion app for any events.
|
||||
|
||||
The fusion.UIManager must be set up to be notified of events before they'll
|
||||
be reported by this thread, for example:
|
||||
fusion.UIManager.AddNotify("Comp_Save", None)
|
||||
|
||||
"""
|
||||
|
||||
on_event = QtCore.Signal(dict)
|
||||
|
||||
def run(self):
|
||||
|
||||
app = getattr(sys.modules["__main__"], "app", None)
|
||||
if app is None:
|
||||
# No Fusion app found
|
||||
return
|
||||
|
||||
# As optimization store the GetEvent method directly because every
|
||||
# getattr of UIManager.GetEvent tries to resolve the Remote Function
|
||||
# through the PyRemoteObject
|
||||
get_event = app.UIManager.GetEvent
|
||||
delay = int(os.environ.get("OPENPYPE_FUSION_CALLBACK_INTERVAL", 1000))
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
return
|
||||
|
||||
# Process all events that have been queued up until now
|
||||
while True:
|
||||
event = get_event(False)
|
||||
if not event:
|
||||
break
|
||||
self.on_event.emit(event)
|
||||
|
||||
# Wait some time before processing events again
|
||||
# to not keep blocking the UI
|
||||
self.msleep(delay)
|
||||
|
||||
|
||||
class FusionEventHandler(QtCore.QObject):
|
||||
"""Emits OpenPype events based on Fusion events captured in a QThread.
|
||||
|
||||
This will emit the following OpenPype events based on Fusion actions:
|
||||
save: Comp_Save, Comp_SaveAs
|
||||
open: Comp_Opened
|
||||
new: Comp_New
|
||||
|
||||
To use this you can attach it to you Qt UI so it runs in the background.
|
||||
E.g.
|
||||
>>> handler = FusionEventHandler(parent=window)
|
||||
>>> handler.start()
|
||||
|
||||
|
||||
"""
|
||||
ACTION_IDS = [
|
||||
"Comp_Save",
|
||||
"Comp_SaveAs",
|
||||
"Comp_New",
|
||||
"Comp_Opened"
|
||||
]
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FusionEventHandler, self).__init__(parent=parent)
|
||||
|
||||
# Set up Fusion event callbacks
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
return fusion.CurrentComp if fusion else None
|
||||
ui = fusion.UIManager
|
||||
|
||||
# Add notifications for the ones we want to listen to
|
||||
notifiers = []
|
||||
for action_id in self.ACTION_IDS:
|
||||
notifier = ui.AddNotify(action_id, None)
|
||||
notifiers.append(notifier)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"):
|
||||
"""Lock comp and open an undo chunk during the context"""
|
||||
try:
|
||||
comp.Lock()
|
||||
comp.StartUndo(undo_queue_name)
|
||||
yield
|
||||
finally:
|
||||
comp.Unlock()
|
||||
comp.EndUndo()
|
||||
# TODO: Not entirely sure whether these must be kept to avoid
|
||||
# garbage collection
|
||||
self._notifiers = notifiers
|
||||
|
||||
self._event_thread = FusionEventThread(parent=self)
|
||||
self._event_thread.on_event.connect(self._on_event)
|
||||
|
||||
def start(self):
|
||||
self._event_thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._event_thread.stop()
|
||||
|
||||
def _on_event(self, event):
|
||||
"""Handle Fusion events to emit OpenPype events"""
|
||||
if not event:
|
||||
return
|
||||
|
||||
what = event["what"]
|
||||
|
||||
# Comp Save
|
||||
if what in {"Comp_Save", "Comp_SaveAs"}:
|
||||
if not event["Rets"].get("success"):
|
||||
# If the Save action is cancelled it will still emit an
|
||||
# event but with "success": False so we ignore those cases
|
||||
return
|
||||
# Comp was saved
|
||||
emit_event("save", data=event)
|
||||
return
|
||||
|
||||
# Comp New
|
||||
elif what in {"Comp_New"}:
|
||||
emit_event("new", data=event)
|
||||
|
||||
# Comp Opened
|
||||
elif what in {"Comp_Opened"}:
|
||||
emit_event("open", data=event)
|
||||
|
|
|
|||
63
openpype/hosts/fusion/api/pulse.py
Normal file
63
openpype/hosts/fusion/api/pulse.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from Qt import QtCore
|
||||
|
||||
|
||||
class PulseThread(QtCore.QThread):
|
||||
no_response = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PulseThread, self).__init__(parent=parent)
|
||||
|
||||
def run(self):
|
||||
app = getattr(sys.modules["__main__"], "app", None)
|
||||
|
||||
# Interval in milliseconds
|
||||
interval = os.environ.get("OPENPYPE_FUSION_PULSE_INTERVAL", 1000)
|
||||
|
||||
while True:
|
||||
if self.isInterruptionRequested():
|
||||
return
|
||||
|
||||
# We don't need to call Test because PyRemoteObject of the app
|
||||
# will actually fail to even resolve the Test function if it has
|
||||
# gone down. So we can actually already just check by confirming
|
||||
# the method is still getting resolved. (Optimization)
|
||||
if app.Test is None:
|
||||
self.no_response.emit()
|
||||
|
||||
self.msleep(interval)
|
||||
|
||||
|
||||
class FusionPulse(QtCore.QObject):
|
||||
"""A Timer that checks whether host app is still alive.
|
||||
|
||||
This checks whether the Fusion process is still active at a certain
|
||||
interval. This is useful due to how Fusion runs its scripts. Each script
|
||||
runs in its own environment and process (a `fusionscript` process each).
|
||||
If Fusion would go down and we have a UI process running at the same time
|
||||
then it can happen that the `fusionscript.exe` will remain running in the
|
||||
background in limbo due to e.g. a Qt interface's QApplication that keeps
|
||||
running infinitely.
|
||||
|
||||
Warning:
|
||||
When the host is not detected this will automatically exit
|
||||
the current process.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(FusionPulse, self).__init__(parent=parent)
|
||||
self._thread = PulseThread(parent=self)
|
||||
self._thread.no_response.connect(self.on_no_response)
|
||||
|
||||
def on_no_response(self):
|
||||
print("Pulse detected no response from Fusion..")
|
||||
sys.exit(1)
|
||||
|
||||
def start(self):
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._thread.requestInterruption()
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import sys
|
||||
import os
|
||||
|
||||
from .pipeline import get_current_comp
|
||||
from .lib import get_current_comp
|
||||
|
||||
|
||||
def file_extensions():
|
||||
|
|
|
|||
60
openpype/hosts/fusion/deploy/Config/openpype_menu.fu
Normal file
60
openpype/hosts/fusion/deploy/Config/openpype_menu.fu
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
Action
|
||||
{
|
||||
ID = "OpenPype_Menu",
|
||||
Category = "OpenPype",
|
||||
Name = "OpenPype Menu",
|
||||
|
||||
Targets =
|
||||
{
|
||||
Composition =
|
||||
{
|
||||
Execute = _Lua [=[
|
||||
local scriptPath = app:MapPath("OpenPype:MenuScripts/openpype_menu.py")
|
||||
if bmd.fileexists(scriptPath) == false then
|
||||
print("[OpenPype Error] Can't run file: " .. scriptPath)
|
||||
else
|
||||
target:RunScript(scriptPath)
|
||||
end
|
||||
]=],
|
||||
},
|
||||
},
|
||||
},
|
||||
Action
|
||||
{
|
||||
ID = "OpenPype_Install_PySide2",
|
||||
Category = "OpenPype",
|
||||
Name = "Install PySide2",
|
||||
|
||||
Targets =
|
||||
{
|
||||
Composition =
|
||||
{
|
||||
Execute = _Lua [=[
|
||||
local scriptPath = app:MapPath("OpenPype:MenuScripts/install_pyside2.py")
|
||||
if bmd.fileexists(scriptPath) == false then
|
||||
print("[OpenPype Error] Can't run file: " .. scriptPath)
|
||||
else
|
||||
target:RunScript(scriptPath)
|
||||
end
|
||||
]=],
|
||||
},
|
||||
},
|
||||
},
|
||||
Menus
|
||||
{
|
||||
Target = "ChildFrame",
|
||||
|
||||
Before "Help"
|
||||
{
|
||||
Sub "OpenPype"
|
||||
{
|
||||
"OpenPype_Menu{}",
|
||||
"_",
|
||||
Sub "Admin" {
|
||||
"OpenPype_Install_PySide2{}"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
6
openpype/hosts/fusion/deploy/MenuScripts/README.md
Normal file
6
openpype/hosts/fusion/deploy/MenuScripts/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
### OpenPype deploy MenuScripts
|
||||
|
||||
Note that this `MenuScripts` is not an official Fusion folder.
|
||||
OpenPype only uses this folder in `{fusion}/deploy/` to trigger the OpenPype menu actions.
|
||||
|
||||
They are used in the actions defined in `.fu` files in `{fusion}/deploy/Config`.
|
||||
29
openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py
Normal file
29
openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# This is just a quick hack for users running Py3 locally but having no
|
||||
# Qt library installed
|
||||
import os
|
||||
import subprocess
|
||||
import importlib
|
||||
|
||||
|
||||
try:
|
||||
from Qt import QtWidgets # noqa: F401
|
||||
from Qt import __binding__
|
||||
print(f"Qt binding: {__binding__}")
|
||||
mod = importlib.import_module(__binding__)
|
||||
print(f"Qt path: {mod.__file__}")
|
||||
print("Qt library found, nothing to do..")
|
||||
|
||||
except ImportError:
|
||||
print("Assuming no Qt library is installed..")
|
||||
print('Installing PySide2 for Python 3.6: '
|
||||
f'{os.environ["FUSION16_PYTHON36_HOME"]}')
|
||||
|
||||
# Get full path to python executable
|
||||
exe = "python.exe" if os.name == 'nt' else "python"
|
||||
python = os.path.join(os.environ["FUSION16_PYTHON36_HOME"], exe)
|
||||
assert os.path.exists(python), f"Python doesn't exist: {python}"
|
||||
|
||||
# Do python -m pip install PySide2
|
||||
args = [python, "-m", "pip", "install", "PySide2"]
|
||||
print(f"Args: {args}")
|
||||
subprocess.Popen(args)
|
||||
|
|
@ -9,6 +9,10 @@ from openpype.pipeline import (
|
|||
|
||||
|
||||
def main(env):
|
||||
# This script working directory starts in Fusion application folder.
|
||||
# However the contents of that folder can conflict with Qt library dlls
|
||||
# so we make sure to move out of it to avoid DLL Load Failed errors.
|
||||
os.chdir("..")
|
||||
from openpype.hosts.fusion import api
|
||||
from openpype.hosts.fusion.api import menu
|
||||
|
||||
|
|
@ -20,6 +24,11 @@ def main(env):
|
|||
|
||||
menu.launch_openpype_menu()
|
||||
|
||||
# Initiate a QTimer to check if Fusion is still alive every X interval
|
||||
# If Fusion is not found - kill itself
|
||||
# todo(roy): Implement timer that ensures UI doesn't remain when e.g.
|
||||
# Fusion closes down
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = main(os.environ)
|
||||
19
openpype/hosts/fusion/deploy/fusion_shared.prefs
Normal file
19
openpype/hosts/fusion/deploy/fusion_shared.prefs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
Locked = true,
|
||||
Global = {
|
||||
Paths = {
|
||||
Map = {
|
||||
["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy",
|
||||
["Reactor:"] = "$(REACTOR)",
|
||||
|
||||
["Config:"] = "UserPaths:Config;OpenPype:Config",
|
||||
["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts",
|
||||
["UserPaths:"] = "UserData:;AllData:;Fusion:;Reactor:Deploy"
|
||||
},
|
||||
},
|
||||
Script = {
|
||||
PythonVersion = 3,
|
||||
Python3Forced = true
|
||||
},
|
||||
},
|
||||
}
|
||||
34
openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py
Normal file
34
openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import os
|
||||
import platform
|
||||
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
|
||||
|
||||
class FusionPreLaunchOCIO(PreLaunchHook):
|
||||
"""Set OCIO environment variable for Fusion"""
|
||||
app_groups = ["fusion"]
|
||||
|
||||
def execute(self):
|
||||
"""Hook entry method."""
|
||||
|
||||
# get image io
|
||||
project_settings = self.data["project_settings"]
|
||||
|
||||
# make sure anatomy settings are having flame key
|
||||
imageio_fusion = project_settings["fusion"]["imageio"]
|
||||
|
||||
ocio = imageio_fusion.get("ocio")
|
||||
enabled = ocio.get("enabled", False)
|
||||
if not enabled:
|
||||
return
|
||||
|
||||
platform_key = platform.system().lower()
|
||||
ocio_path = ocio["configFilePath"][platform_key]
|
||||
if not ocio_path:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Fusion OCIO is enabled in project settings but no OCIO config"
|
||||
f"path is set for your current platform: {platform_key}"
|
||||
)
|
||||
|
||||
self.log.info(f"Setting OCIO config path: {ocio_path}")
|
||||
self.launch_context.env["OCIO"] = os.pathsep.join(ocio_path)
|
||||
|
|
@ -1,114 +1,61 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import openpype.hosts.fusion
|
||||
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
|
||||
from openpype.hosts.fusion import FUSION_HOST_DIR
|
||||
|
||||
|
||||
class FusionPrelaunch(PreLaunchHook):
|
||||
"""
|
||||
This hook will check if current workfile path has Fusion
|
||||
project inside.
|
||||
"""Prepares OpenPype Fusion environment
|
||||
|
||||
Requires FUSION_PYTHON3_HOME to be defined in the environment for Fusion
|
||||
to point at a valid Python 3 build for Fusion. That is Python 3.3-3.10
|
||||
for Fusion 18 and Fusion 3.6 for Fusion 16 and 17.
|
||||
|
||||
This also sets FUSION16_MasterPrefs to apply the fusion master prefs
|
||||
as set in openpype/hosts/fusion/deploy/fusion_shared.prefs to enable
|
||||
the OpenPype menu and force Python 3 over Python 2.
|
||||
|
||||
"""
|
||||
app_groups = ["fusion"]
|
||||
|
||||
def execute(self):
|
||||
# making sure python 3.6 is installed at provided path
|
||||
py36_dir = self.launch_context.env.get("PYTHON36")
|
||||
if not py36_dir:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Required environment variable \"PYTHON36\" is not set."
|
||||
"\n\nFusion implementation requires to have"
|
||||
" installed Python 3.6"
|
||||
)
|
||||
# making sure python 3 is installed at provided path
|
||||
# Py 3.3-3.10 for Fusion 18+ or Py 3.6 for Fu 16-17
|
||||
py3_var = "FUSION_PYTHON3_HOME"
|
||||
fusion_python3_home = self.launch_context.env.get(py3_var, "")
|
||||
|
||||
py36_dir = os.path.normpath(py36_dir)
|
||||
if not os.path.isdir(py36_dir):
|
||||
raise ApplicationLaunchFailed(
|
||||
"Python 3.6 is not installed at the provided path.\n"
|
||||
"Either make sure the environments in fusion settings has"
|
||||
" 'PYTHON36' set corectly or make sure Python 3.6 is installed"
|
||||
f" in the given path.\n\nPYTHON36: {py36_dir}"
|
||||
)
|
||||
self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...")
|
||||
self.launch_context.env["PYTHON36"] = py36_dir
|
||||
|
||||
utility_dir = self.launch_context.env.get("FUSION_UTILITY_SCRIPTS_DIR")
|
||||
if not utility_dir:
|
||||
raise ApplicationLaunchFailed(
|
||||
"Required Fusion utility script dir environment variable"
|
||||
" \"FUSION_UTILITY_SCRIPTS_DIR\" is not set."
|
||||
)
|
||||
|
||||
# setting utility scripts dir for scripts syncing
|
||||
utility_dir = os.path.normpath(utility_dir)
|
||||
if not os.path.isdir(utility_dir):
|
||||
raise ApplicationLaunchFailed(
|
||||
"Fusion utility script dir does not exist. Either make sure "
|
||||
"the environments in fusion settings has"
|
||||
" 'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall "
|
||||
f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{utility_dir}'"
|
||||
)
|
||||
|
||||
self._sync_utility_scripts(self.launch_context.env)
|
||||
self.log.info("Fusion Pype wrapper has been installed")
|
||||
|
||||
def _sync_utility_scripts(self, env):
|
||||
""" Synchronizing basic utlility scripts for resolve.
|
||||
|
||||
To be able to run scripts from inside `Fusion/Workspace/Scripts` menu
|
||||
all scripts has to be accessible from defined folder.
|
||||
"""
|
||||
if not env:
|
||||
env = {k: v for k, v in os.environ.items()}
|
||||
|
||||
# initiate inputs
|
||||
scripts = {}
|
||||
us_env = env.get("FUSION_UTILITY_SCRIPTS_SOURCE_DIR")
|
||||
us_dir = env.get("FUSION_UTILITY_SCRIPTS_DIR", "")
|
||||
us_paths = [os.path.join(
|
||||
os.path.dirname(os.path.abspath(openpype.hosts.fusion.__file__)),
|
||||
"utility_scripts"
|
||||
)]
|
||||
|
||||
# collect script dirs
|
||||
if us_env:
|
||||
self.log.info(f"Utility Scripts Env: `{us_env}`")
|
||||
us_paths = us_env.split(
|
||||
os.pathsep) + us_paths
|
||||
|
||||
# collect scripts from dirs
|
||||
for path in us_paths:
|
||||
scripts.update({path: os.listdir(path)})
|
||||
|
||||
self.log.info(f"Utility Scripts Dir: `{us_paths}`")
|
||||
self.log.info(f"Utility Scripts: `{scripts}`")
|
||||
|
||||
# make sure no script file is in folder
|
||||
if next((s for s in os.listdir(us_dir)), None):
|
||||
for s in os.listdir(us_dir):
|
||||
path = os.path.normpath(
|
||||
os.path.join(us_dir, s))
|
||||
self.log.info(f"Removing `{path}`...")
|
||||
|
||||
# remove file or directory if not in our folders
|
||||
if not os.path.isdir(path):
|
||||
os.remove(path)
|
||||
self.log.info(f"Looking for Python 3 in: {fusion_python3_home}")
|
||||
for path in fusion_python3_home.split(os.pathsep):
|
||||
# Allow defining multiple paths to allow "fallback" to other
|
||||
# path. But make to set only a single path as final variable.
|
||||
py3_dir = os.path.normpath(path)
|
||||
if os.path.isdir(py3_dir):
|
||||
break
|
||||
else:
|
||||
shutil.rmtree(path)
|
||||
raise ApplicationLaunchFailed(
|
||||
"Python 3 is not installed at the provided path.\n"
|
||||
"Make sure the environment in fusion settings has "
|
||||
"'FUSION_PYTHON3_HOME' set correctly and make sure "
|
||||
"Python 3 is installed in the given path."
|
||||
f"\n\nPYTHON36: {fusion_python3_home}"
|
||||
)
|
||||
|
||||
# copy scripts into Resolve's utility scripts dir
|
||||
for d, sl in scripts.items():
|
||||
# directory and scripts list
|
||||
for s in sl:
|
||||
# script in script list
|
||||
src = os.path.normpath(os.path.join(d, s))
|
||||
dst = os.path.normpath(os.path.join(us_dir, s))
|
||||
self.log.info(f"Setting {py3_var}: '{py3_dir}'...")
|
||||
self.launch_context.env[py3_var] = py3_dir
|
||||
|
||||
self.log.info(f"Copying `{src}` to `{dst}`...")
|
||||
# Fusion 18+ requires FUSION_PYTHON3_HOME to also be on PATH
|
||||
self.launch_context.env["PATH"] += ";" + py3_dir
|
||||
|
||||
# copy file or directory from our folders to fusion's folder
|
||||
if not os.path.isdir(src):
|
||||
shutil.copy2(src, dst)
|
||||
else:
|
||||
shutil.copytree(src, dst)
|
||||
# Fusion 16 and 17 use FUSION16_PYTHON36_HOME instead of
|
||||
# FUSION_PYTHON3_HOME and will only work with a Python 3.6 version
|
||||
# TODO: Detect Fusion version to only set for specific Fusion build
|
||||
self.launch_context.env["FUSION16_PYTHON36_HOME"] = py3_dir
|
||||
|
||||
# Add our Fusion Master Prefs which is the only way to customize
|
||||
# Fusion to define where it can read custom scripts and tools from
|
||||
self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}")
|
||||
self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR
|
||||
|
||||
pref_var = "FUSION16_MasterPrefs" # used by Fusion 16, 17 and 18
|
||||
prefs = os.path.join(FUSION_HOST_DIR, "deploy", "fusion_shared.prefs")
|
||||
self.log.info(f"Setting {pref_var}: {prefs}")
|
||||
self.launch_context.env[pref_var] = prefs
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import os
|
||||
|
||||
from openpype.pipeline import LegacyCreator
|
||||
from openpype.pipeline import (
|
||||
LegacyCreator,
|
||||
legacy_io
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
|
|
@ -21,12 +24,9 @@ class CreateOpenEXRSaver(LegacyCreator):
|
|||
|
||||
comp = get_current_comp()
|
||||
|
||||
# todo: improve method of getting current environment
|
||||
# todo: pref avalon.Session over os.environ
|
||||
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
|
||||
|
||||
workdir = os.path.normpath(os.environ["AVALON_WORKDIR"])
|
||||
|
||||
filename = "{}..tiff".format(self.name)
|
||||
filename = "{}..exr".format(self.name)
|
||||
filepath = os.path.join(workdir, "render", filename)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp):
|
||||
|
|
@ -39,10 +39,10 @@ class CreateOpenEXRSaver(LegacyCreator):
|
|||
saver["Clip"] = filepath
|
||||
saver["OutputFormat"] = file_format
|
||||
|
||||
# # # Set standard TIFF settings
|
||||
# Check file format settings are available
|
||||
if saver[file_format] is None:
|
||||
raise RuntimeError("File format is not set to TiffFormat, "
|
||||
"this is a bug")
|
||||
raise RuntimeError("File format is not set to {}, "
|
||||
"this is a bug".format(file_format))
|
||||
|
||||
# Set file format attributes
|
||||
saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other
|
||||
|
|
|
|||
70
openpype/hosts/fusion/plugins/load/load_alembic.py
Normal file
70
openpype/hosts/fusion/plugins/load/load_alembic.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from openpype.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
|
||||
class FusionLoadAlembicMesh(load.LoaderPlugin):
|
||||
"""Load Alembic mesh into Fusion"""
|
||||
|
||||
families = ["pointcache", "model"]
|
||||
representations = ["abc"]
|
||||
|
||||
label = "Load alembic mesh"
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
tool_type = "SurfaceAlembicMesh"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
# Fallback to asset name when namespace is None
|
||||
if namespace is None:
|
||||
namespace = context['asset']['name']
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp, "Create tool"):
|
||||
|
||||
path = self.fname
|
||||
|
||||
args = (-32768, -32768)
|
||||
tool = comp.AddTool(self.tool_type, *args)
|
||||
tool["Filename"] = path
|
||||
|
||||
imprint_container(tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def update(self, container, representation):
|
||||
"""Update Alembic path"""
|
||||
|
||||
tool = container["_tool"]
|
||||
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
|
||||
comp = tool.Comp()
|
||||
|
||||
path = get_representation_path(representation)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update tool"):
|
||||
tool["Filename"] = path
|
||||
|
||||
# Update the imprinted representation
|
||||
tool.SetData("avalon.representation", str(representation["_id"]))
|
||||
|
||||
def remove(self, container):
|
||||
tool = container["_tool"]
|
||||
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
|
||||
comp = tool.Comp()
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Remove tool"):
|
||||
tool.Delete()
|
||||
71
openpype/hosts/fusion/plugins/load/load_fbx.py
Normal file
71
openpype/hosts/fusion/plugins/load/load_fbx.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
from openpype.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
)
|
||||
from openpype.hosts.fusion.api import (
|
||||
imprint_container,
|
||||
get_current_comp,
|
||||
comp_lock_and_undo_chunk
|
||||
)
|
||||
|
||||
|
||||
class FusionLoadFBXMesh(load.LoaderPlugin):
|
||||
"""Load FBX mesh into Fusion"""
|
||||
|
||||
families = ["*"]
|
||||
representations = ["fbx"]
|
||||
|
||||
label = "Load FBX mesh"
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
tool_type = "SurfaceFBXMesh"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
# Fallback to asset name when namespace is None
|
||||
if namespace is None:
|
||||
namespace = context['asset']['name']
|
||||
|
||||
# Create the Loader with the filename path set
|
||||
comp = get_current_comp()
|
||||
with comp_lock_and_undo_chunk(comp, "Create tool"):
|
||||
|
||||
path = self.fname
|
||||
|
||||
args = (-32768, -32768)
|
||||
tool = comp.AddTool(self.tool_type, *args)
|
||||
tool["ImportFile"] = path
|
||||
|
||||
imprint_container(tool,
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
context=context,
|
||||
loader=self.__class__.__name__)
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def update(self, container, representation):
|
||||
"""Update path"""
|
||||
|
||||
tool = container["_tool"]
|
||||
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
|
||||
comp = tool.Comp()
|
||||
|
||||
path = get_representation_path(representation)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update tool"):
|
||||
tool["ImportFile"] = path
|
||||
|
||||
# Update the imprinted representation
|
||||
tool.SetData("avalon.representation", str(representation["_id"]))
|
||||
|
||||
def remove(self, container):
|
||||
tool = container["_tool"]
|
||||
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
|
||||
comp = tool.Comp()
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Remove tool"):
|
||||
tool.Delete()
|
||||
|
|
@ -101,6 +101,9 @@ def loader_shift(loader, frame, relative=True):
|
|||
else:
|
||||
shift = frame - old_in
|
||||
|
||||
if not shift:
|
||||
return 0
|
||||
|
||||
# Shifting global in will try to automatically compensate for the change
|
||||
# in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those
|
||||
# input values to "just shift" the clip
|
||||
|
|
@ -149,8 +152,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
tool["Clip"] = path
|
||||
|
||||
# Set global in point to start frame (if in version.data)
|
||||
start = context["version"]["data"].get("frameStart", None)
|
||||
if start is not None:
|
||||
start = self._get_start(context["version"], tool)
|
||||
loader_shift(tool, start, relative=False)
|
||||
|
||||
imprint_container(tool,
|
||||
|
|
@ -214,12 +216,7 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
# Get start frame from version data
|
||||
project_name = legacy_io.active_project()
|
||||
version = get_version_by_id(project_name, representation["parent"])
|
||||
start = version["data"].get("frameStart")
|
||||
if start is None:
|
||||
self.log.warning("Missing start frame for updated version"
|
||||
"assuming starts at frame 0 for: "
|
||||
"{} ({})".format(tool.Name, representation))
|
||||
start = 0
|
||||
start = self._get_start(version, tool)
|
||||
|
||||
with comp_lock_and_undo_chunk(comp, "Update Loader"):
|
||||
|
||||
|
|
@ -256,3 +253,27 @@ class FusionLoadSequence(load.LoaderPlugin):
|
|||
"""Get first file in representation root"""
|
||||
files = sorted(os.listdir(root))
|
||||
return os.path.join(root, files[0])
|
||||
|
||||
def _get_start(self, version_doc, tool):
|
||||
"""Return real start frame of published files (incl. handles)"""
|
||||
data = version_doc["data"]
|
||||
|
||||
# Get start frame directly with handle if it's in data
|
||||
start = data.get("frameStartHandle")
|
||||
if start is not None:
|
||||
return start
|
||||
|
||||
# Get frame start without handles
|
||||
start = data.get("frameStart")
|
||||
if start is None:
|
||||
self.log.warning("Missing start frame for version "
|
||||
"assuming starts at frame 0 for: "
|
||||
"{}".format(tool.Name))
|
||||
return 0
|
||||
|
||||
# Use `handleStart` if the data is available
|
||||
handle_start = data.get("handleStart")
|
||||
if handle_start:
|
||||
start -= handle_start
|
||||
|
||||
return start
|
||||
|
|
|
|||
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)
|
||||
|
|
@ -4,19 +4,21 @@ import pyblish.api
|
|||
|
||||
|
||||
def get_comp_render_range(comp):
|
||||
"""Return comp's start and end render range."""
|
||||
"""Return comp's start-end render range and global start-end range."""
|
||||
comp_attrs = comp.GetAttrs()
|
||||
start = comp_attrs["COMPN_RenderStart"]
|
||||
end = comp_attrs["COMPN_RenderEnd"]
|
||||
global_start = comp_attrs["COMPN_GlobalStart"]
|
||||
global_end = comp_attrs["COMPN_GlobalEnd"]
|
||||
|
||||
# Whenever render ranges are undefined fall back
|
||||
# to the comp's global start and end
|
||||
if start == -1000000000:
|
||||
start = comp_attrs["COMPN_GlobalEnd"]
|
||||
start = global_start
|
||||
if end == -1000000000:
|
||||
end = comp_attrs["COMPN_GlobalStart"]
|
||||
end = global_end
|
||||
|
||||
return start, end
|
||||
return start, end, global_start, global_end
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
|
|
@ -42,9 +44,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
tools = comp.GetToolList(False).values()
|
||||
savers = [tool for tool in tools if tool.ID == "Saver"]
|
||||
|
||||
start, end = get_comp_render_range(comp)
|
||||
start, end, global_start, global_end = get_comp_render_range(comp)
|
||||
context.data["frameStart"] = int(start)
|
||||
context.data["frameEnd"] = int(end)
|
||||
context.data["frameStartHandle"] = int(global_start)
|
||||
context.data["frameEndHandle"] = int(global_end)
|
||||
|
||||
for tool in savers:
|
||||
path = tool["Clip"][comp.TIME_UNDEFINED]
|
||||
|
|
@ -78,8 +82,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
"label": label,
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"frameStartHandle": context.data["frameStartHandle"],
|
||||
"frameEndHandle": context.data["frameStartHandle"],
|
||||
"fps": context.data["fps"],
|
||||
"families": ["render", "review", "ftrack"],
|
||||
"families": ["render", "review"],
|
||||
"family": "render",
|
||||
"active": active,
|
||||
"publish": active # backwards compatibility
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class Fusionlocal(pyblish.api.InstancePlugin):
|
|||
|
||||
def process(self, instance):
|
||||
|
||||
# This plug-in runs only once and thus assumes all instances
|
||||
# currently will render the same frame range
|
||||
context = instance.context
|
||||
key = "__hasRun{}".format(self.__class__.__name__)
|
||||
if context.data.get(key, False):
|
||||
|
|
@ -28,8 +30,8 @@ class Fusionlocal(pyblish.api.InstancePlugin):
|
|||
context.data[key] = True
|
||||
|
||||
current_comp = context.data["currentComp"]
|
||||
frame_start = current_comp.GetAttrs("COMPN_RenderStart")
|
||||
frame_end = current_comp.GetAttrs("COMPN_RenderEnd")
|
||||
frame_start = context.data["frameStartHandle"]
|
||||
frame_end = context.data["frameEndHandle"]
|
||||
path = instance.data["path"]
|
||||
output_dir = instance.data["outputDir"]
|
||||
|
||||
|
|
@ -40,7 +42,11 @@ class Fusionlocal(pyblish.api.InstancePlugin):
|
|||
self.log.info("End frame: {}".format(frame_end))
|
||||
|
||||
with comp_lock_and_undo_chunk(current_comp):
|
||||
result = current_comp.Render()
|
||||
result = current_comp.Render({
|
||||
"Start": frame_start,
|
||||
"End": frame_end,
|
||||
"Wait": True
|
||||
})
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
|
|
|||
|
|
@ -1,284 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
get_versions,
|
||||
)
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
install_host,
|
||||
registered_host,
|
||||
)
|
||||
from openpype.lib import version_up
|
||||
from openpype.hosts.fusion import api
|
||||
from openpype.hosts.fusion.api import lib
|
||||
from openpype.pipeline.context_tools import get_workdir_from_session
|
||||
|
||||
log = logging.getLogger("Update Slap Comp")
|
||||
|
||||
|
||||
def _format_version_folder(folder):
|
||||
"""Format a version folder based on the filepath
|
||||
|
||||
Assumption here is made that, if the path does not exists the folder
|
||||
will be "v001"
|
||||
|
||||
Args:
|
||||
folder: file path to a folder
|
||||
|
||||
Returns:
|
||||
str: new version folder name
|
||||
"""
|
||||
|
||||
new_version = 1
|
||||
if os.path.isdir(folder):
|
||||
re_version = re.compile(r"v\d+$")
|
||||
versions = [i for i in os.listdir(folder) if os.path.isdir(i)
|
||||
and re_version.match(i)]
|
||||
if versions:
|
||||
# ensure the "v" is not included
|
||||
new_version = int(max(versions)[1:]) + 1
|
||||
|
||||
version_folder = "v{:03d}".format(new_version)
|
||||
|
||||
return version_folder
|
||||
|
||||
|
||||
def _get_fusion_instance():
|
||||
fusion = getattr(sys.modules["__main__"], "fusion", None)
|
||||
if fusion is None:
|
||||
try:
|
||||
# Support for FuScript.exe, BlackmagicFusion module for py2 only
|
||||
import BlackmagicFusion as bmf
|
||||
fusion = bmf.scriptapp("Fusion")
|
||||
except ImportError:
|
||||
raise RuntimeError("Could not find a Fusion instance")
|
||||
return fusion
|
||||
|
||||
|
||||
def _format_filepath(session):
|
||||
|
||||
project = session["AVALON_PROJECT"]
|
||||
asset = session["AVALON_ASSET"]
|
||||
|
||||
# Save updated slap comp
|
||||
work_path = get_workdir_from_session(session)
|
||||
walk_to_dir = os.path.join(work_path, "scenes", "slapcomp")
|
||||
slapcomp_dir = os.path.abspath(walk_to_dir)
|
||||
|
||||
# Ensure destination exists
|
||||
if not os.path.isdir(slapcomp_dir):
|
||||
log.warning("Folder did not exist, creating folder structure")
|
||||
os.makedirs(slapcomp_dir)
|
||||
|
||||
# Compute output path
|
||||
new_filename = "{}_{}_slapcomp_v001.comp".format(project, asset)
|
||||
new_filepath = os.path.join(slapcomp_dir, new_filename)
|
||||
|
||||
# Create new unique filepath
|
||||
if os.path.exists(new_filepath):
|
||||
new_filepath = version_up(new_filepath)
|
||||
|
||||
return new_filepath
|
||||
|
||||
|
||||
def _update_savers(comp, session):
|
||||
"""Update all savers of the current comp to ensure the output is correct
|
||||
|
||||
This will refactor the Saver file outputs to the renders of the new session
|
||||
that is provided.
|
||||
|
||||
In the case the original saver path had a path set relative to a /fusion/
|
||||
folder then that relative path will be matched with the exception of all
|
||||
"version" (e.g. v010) references will be reset to v001. Otherwise only a
|
||||
version folder will be computed in the new session's work "render" folder
|
||||
to dump the files in and keeping the original filenames.
|
||||
|
||||
Args:
|
||||
comp (object): current comp instance
|
||||
session (dict): the current Avalon session
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
new_work = get_workdir_from_session(session)
|
||||
renders = os.path.join(new_work, "renders")
|
||||
version_folder = _format_version_folder(renders)
|
||||
renders_version = os.path.join(renders, version_folder)
|
||||
|
||||
comp.Print("New renders to: %s\n" % renders)
|
||||
|
||||
with api.comp_lock_and_undo_chunk(comp):
|
||||
savers = comp.GetToolList(False, "Saver").values()
|
||||
for saver in savers:
|
||||
filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0]
|
||||
|
||||
# Get old relative path to the "fusion" app folder so we can apply
|
||||
# the same relative path afterwards. If not found fall back to
|
||||
# using just a version folder with the filename in it.
|
||||
# todo: can we make this less magical?
|
||||
relpath = filepath.replace("\\", "/").rsplit("/fusion/", 1)[-1]
|
||||
|
||||
if os.path.isabs(relpath):
|
||||
# If not relative to a "/fusion/" folder then just use filename
|
||||
filename = os.path.basename(filepath)
|
||||
log.warning("Can't parse relative path, refactoring to only"
|
||||
"filename in a version folder: %s" % filename)
|
||||
new_path = os.path.join(renders_version, filename)
|
||||
|
||||
else:
|
||||
# Else reuse the relative path
|
||||
# Reset version in folder and filename in the relative path
|
||||
# to v001. The version should be is only detected when prefixed
|
||||
# with either `_v` (underscore) or `/v` (folder)
|
||||
version_pattern = r"(/|_)v[0-9]+"
|
||||
if re.search(version_pattern, relpath):
|
||||
new_relpath = re.sub(version_pattern,
|
||||
r"\1v001",
|
||||
relpath)
|
||||
log.info("Resetting version folders to v001: "
|
||||
"%s -> %s" % (relpath, new_relpath))
|
||||
relpath = new_relpath
|
||||
|
||||
new_path = os.path.join(new_work, relpath)
|
||||
|
||||
saver["Clip"] = new_path
|
||||
|
||||
|
||||
def update_frame_range(comp, representations):
|
||||
"""Update the frame range of the comp and render length
|
||||
|
||||
The start and end frame are based on the lowest start frame and the highest
|
||||
end frame
|
||||
|
||||
Args:
|
||||
comp (object): current focused comp
|
||||
representations (list) collection of dicts
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
project_name = legacy_io.active_project()
|
||||
version_ids = {r["parent"] for r in representations}
|
||||
versions = list(get_versions(project_name, version_ids))
|
||||
|
||||
versions = [v for v in versions
|
||||
if v["data"].get("frameStart", None) is not None]
|
||||
|
||||
if not versions:
|
||||
log.warning("No versions loaded to match frame range to.\n")
|
||||
return
|
||||
|
||||
start = min(v["data"]["frameStart"] for v in versions)
|
||||
end = max(v["data"]["frameEnd"] for v in versions)
|
||||
|
||||
lib.update_frame_range(start, end, comp=comp)
|
||||
|
||||
|
||||
def switch(asset_name, filepath=None, new=True):
|
||||
"""Switch the current containers of the file to the other asset (shot)
|
||||
|
||||
Args:
|
||||
filepath (str): file path of the comp file
|
||||
asset_name (str): name of the asset (shot)
|
||||
new (bool): Save updated comp under a different name
|
||||
|
||||
Returns:
|
||||
comp path (str): new filepath of the updated comp
|
||||
|
||||
"""
|
||||
|
||||
# If filepath provided, ensure it is valid absolute path
|
||||
if filepath is not None:
|
||||
if not os.path.isabs(filepath):
|
||||
filepath = os.path.abspath(filepath)
|
||||
|
||||
assert os.path.exists(filepath), "%s must exist " % filepath
|
||||
|
||||
# Assert asset name exists
|
||||
# It is better to do this here then to wait till switch_shot does it
|
||||
project_name = legacy_io.active_project()
|
||||
asset = get_asset_by_name(project_name, asset_name)
|
||||
assert asset, "Could not find '%s' in the database" % asset_name
|
||||
|
||||
# Go to comp
|
||||
if not filepath:
|
||||
current_comp = api.get_current_comp()
|
||||
assert current_comp is not None, "Could not find current comp"
|
||||
else:
|
||||
fusion = _get_fusion_instance()
|
||||
current_comp = fusion.LoadComp(filepath, quiet=True)
|
||||
assert current_comp is not None, (
|
||||
"Fusion could not load '{}'").format(filepath)
|
||||
|
||||
host = registered_host()
|
||||
containers = list(host.ls())
|
||||
assert containers, "Nothing to update"
|
||||
|
||||
representations = []
|
||||
for container in containers:
|
||||
try:
|
||||
representation = lib.switch_item(
|
||||
container,
|
||||
asset_name=asset_name)
|
||||
representations.append(representation)
|
||||
except Exception as e:
|
||||
current_comp.Print("Error in switching! %s\n" % e.message)
|
||||
|
||||
message = "Switched %i Loaders of the %i\n" % (len(representations),
|
||||
len(containers))
|
||||
current_comp.Print(message)
|
||||
|
||||
# Build the session to switch to
|
||||
switch_to_session = legacy_io.Session.copy()
|
||||
switch_to_session["AVALON_ASSET"] = asset['name']
|
||||
|
||||
if new:
|
||||
comp_path = _format_filepath(switch_to_session)
|
||||
|
||||
# Update savers output based on new session
|
||||
_update_savers(current_comp, switch_to_session)
|
||||
else:
|
||||
comp_path = version_up(filepath)
|
||||
|
||||
current_comp.Print(comp_path)
|
||||
|
||||
current_comp.Print("\nUpdating frame range")
|
||||
update_frame_range(current_comp, representations)
|
||||
|
||||
current_comp.Save(comp_path)
|
||||
|
||||
return comp_path
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
# QUESTION: can we convert this to gui rather then standalone script?
|
||||
# TODO: convert to gui tool
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Switch to a shot within an"
|
||||
"existing comp file")
|
||||
|
||||
parser.add_argument("--file_path",
|
||||
type=str,
|
||||
default=True,
|
||||
help="File path of the comp to use")
|
||||
|
||||
parser.add_argument("--asset_name",
|
||||
type=str,
|
||||
default=True,
|
||||
help="Name of the asset (shot) to switch")
|
||||
|
||||
args, unknown = parser.parse_args()
|
||||
|
||||
install_host(api)
|
||||
switch(args.asset_name, args.file_path)
|
||||
|
||||
sys.exit(0)
|
||||
|
|
@ -6,10 +6,10 @@ import csv
|
|||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
import openpype.hosts.harmony.api as harmony
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractPalette(openpype.api.Extractor):
|
||||
class ExtractPalette(publish.Extractor):
|
||||
"""Extract palette."""
|
||||
|
||||
label = "Extract Palette"
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
import openpype.hosts.harmony.api as harmony
|
||||
import openpype.hosts.harmony
|
||||
|
||||
|
||||
class ExtractTemplate(openpype.api.Extractor):
|
||||
class ExtractTemplate(publish.Extractor):
|
||||
"""Extract the connected nodes to the composite instance."""
|
||||
|
||||
label = "Extract Template"
|
||||
|
|
@ -50,7 +49,7 @@ class ExtractTemplate(openpype.api.Extractor):
|
|||
dependencies.remove(instance.data["setMembers"][0])
|
||||
|
||||
# Export template.
|
||||
openpype.hosts.harmony.api.export_template(
|
||||
harmony.export_template(
|
||||
unique_backdrops, dependencies, filepath
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import os
|
|||
import shutil
|
||||
from zipfile import ZipFile
|
||||
|
||||
import openpype.api
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractWorkfile(openpype.api.Extractor):
|
||||
class ExtractWorkfile(publish.Extractor):
|
||||
"""Extract and zip complete workfile folder into zip."""
|
||||
|
||||
label = "Extract Workfile"
|
||||
|
|
|
|||
|
|
@ -13,14 +13,10 @@ import hiero
|
|||
|
||||
from Qt import QtWidgets
|
||||
|
||||
from openpype.client import (
|
||||
get_project,
|
||||
get_versions,
|
||||
get_last_versions,
|
||||
get_representations,
|
||||
)
|
||||
from openpype.settings import get_anatomy_settings
|
||||
from openpype.client import get_project
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import legacy_io, Anatomy
|
||||
from openpype.pipeline.load import filter_containers
|
||||
from openpype.lib import Logger
|
||||
from . import tags
|
||||
|
||||
|
|
@ -882,8 +878,7 @@ def apply_colorspace_project():
|
|||
project.close()
|
||||
|
||||
# get presets for hiero
|
||||
imageio = get_anatomy_settings(
|
||||
project_name)["imageio"].get("hiero", None)
|
||||
imageio = get_project_settings(project_name)["hiero"]["imageio"]
|
||||
presets = imageio.get("workfile")
|
||||
|
||||
# save the workfile as subversion "comment:_colorspaceChange"
|
||||
|
|
@ -936,8 +931,7 @@ def apply_colorspace_clips():
|
|||
clips = project.clips()
|
||||
|
||||
# get presets for hiero
|
||||
imageio = get_anatomy_settings(
|
||||
project_name)["imageio"].get("hiero", None)
|
||||
imageio = get_project_settings(project_name)["hiero"]["imageio"]
|
||||
from pprint import pprint
|
||||
|
||||
presets = imageio.get("regexInputs", {}).get("inputs", {})
|
||||
|
|
@ -1055,6 +1049,10 @@ def sync_clip_name_to_data_asset(track_items_list):
|
|||
print("asset was changed in clip: {}".format(ti_name))
|
||||
|
||||
|
||||
def set_track_color(track_item, color):
|
||||
track_item.source().binItem().setColor(color)
|
||||
|
||||
|
||||
def check_inventory_versions(track_items=None):
|
||||
"""
|
||||
Actual version color idetifier of Loaded containers
|
||||
|
|
@ -1066,68 +1064,29 @@ def check_inventory_versions(track_items=None):
|
|||
"""
|
||||
from . import parse_container
|
||||
|
||||
track_item = track_items or get_track_items()
|
||||
track_items = track_items or get_track_items()
|
||||
# presets
|
||||
clip_color_last = "green"
|
||||
clip_color = "red"
|
||||
|
||||
item_with_repre_id = []
|
||||
repre_ids = set()
|
||||
containers = []
|
||||
# Find all containers and collect it's node and representation ids
|
||||
for track_item in track_item:
|
||||
for track_item in track_items:
|
||||
container = parse_container(track_item)
|
||||
if container:
|
||||
repre_id = container["representation"]
|
||||
repre_ids.add(repre_id)
|
||||
item_with_repre_id.append((track_item, repre_id))
|
||||
containers.append(container)
|
||||
|
||||
# Skip if nothing was found
|
||||
if not repre_ids:
|
||||
if not containers:
|
||||
return
|
||||
|
||||
project_name = legacy_io.active_project()
|
||||
# Find representations based on found containers
|
||||
repre_docs = get_representations(
|
||||
project_name,
|
||||
repre_ids=repre_ids,
|
||||
fields=["_id", "parent"]
|
||||
)
|
||||
# Store representations by id and collect version ids
|
||||
repre_docs_by_id = {}
|
||||
version_ids = set()
|
||||
for repre_doc in repre_docs:
|
||||
# Use stringed representation id to match value in containers
|
||||
repre_id = str(repre_doc["_id"])
|
||||
repre_docs_by_id[repre_id] = repre_doc
|
||||
version_ids.add(repre_doc["parent"])
|
||||
filter_result = filter_containers(containers, project_name)
|
||||
for container in filter_result.latest:
|
||||
set_track_color(container["_track_item"], clip_color)
|
||||
|
||||
version_docs = get_versions(
|
||||
project_name, version_ids, fields=["_id", "name", "parent"]
|
||||
)
|
||||
# Store versions by id and collect subset ids
|
||||
version_docs_by_id = {}
|
||||
subset_ids = set()
|
||||
for version_doc in version_docs:
|
||||
version_docs_by_id[version_doc["_id"]] = version_doc
|
||||
subset_ids.add(version_doc["parent"])
|
||||
|
||||
# Query last versions based on subset ids
|
||||
last_versions_by_subset_id = get_last_versions(
|
||||
project_name, subset_ids=subset_ids, fields=["_id", "parent"]
|
||||
)
|
||||
|
||||
for item in item_with_repre_id:
|
||||
# Some python versions of nuke can't unfold tuple in for loop
|
||||
track_item, repre_id = item
|
||||
|
||||
repre_doc = repre_docs_by_id[repre_id]
|
||||
version_doc = version_docs_by_id[repre_doc["parent"]]
|
||||
last_version_doc = last_versions_by_subset_id[version_doc["parent"]]
|
||||
# Check if last version is same as current version
|
||||
if version_doc["_id"] == last_version_doc["_id"]:
|
||||
track_item.source().binItem().setColor(clip_color_last)
|
||||
else:
|
||||
track_item.source().binItem().setColor(clip_color)
|
||||
for container in filter_result.outdated:
|
||||
set_track_color(container["_track_item"], clip_color_last)
|
||||
|
||||
|
||||
def selection_changed_timeline(event):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import sys
|
|||
import hiero.core
|
||||
from hiero.ui import findMenuAction
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import hiero
|
|||
from Qt import QtWidgets, QtCore
|
||||
import qargparse
|
||||
|
||||
import openpype.api as openpype
|
||||
from openpype.settings import get_current_project_settings
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import LoaderPlugin, LegacyCreator
|
||||
from openpype.pipeline.context_tools import get_current_project_asset
|
||||
|
|
@ -606,7 +606,7 @@ class Creator(LegacyCreator):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(Creator, self).__init__(*args, **kwargs)
|
||||
import openpype.hosts.hiero.api as phiero
|
||||
self.presets = openpype.get_current_project_settings()[
|
||||
self.presets = get_current_project_settings()[
|
||||
"hiero"]["create"].get(self.__class__.__name__, {})
|
||||
|
||||
# adding basic current context resolve objects
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import os
|
|||
import hiero
|
||||
|
||||
from openpype.client import get_project, get_assets
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import legacy_io
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import hiero
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import Logger
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
import os
|
||||
import json
|
||||
import pyblish.api
|
||||
import openpype
|
||||
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractClipEffects(openpype.api.Extractor):
|
||||
class ExtractClipEffects(publish.Extractor):
|
||||
"""Extract clip effects instances."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
import openpype
|
||||
|
||||
from openpype.lib import (
|
||||
get_oiio_tools_path,
|
||||
run_subprocess,
|
||||
)
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractFrames(openpype.api.Extractor):
|
||||
class ExtractFrames(publish.Extractor):
|
||||
"""Extracts frames"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
|
|
@ -13,7 +18,7 @@ class ExtractFrames(openpype.api.Extractor):
|
|||
movie_extensions = ["mov", "mp4"]
|
||||
|
||||
def process(self, instance):
|
||||
oiio_tool_path = openpype.lib.get_oiio_tools_path()
|
||||
oiio_tool_path = get_oiio_tools_path()
|
||||
staging_dir = self.staging_dir(instance)
|
||||
output_template = os.path.join(staging_dir, instance.data["name"])
|
||||
sequence = instance.context.data["activeTimeline"]
|
||||
|
|
@ -43,7 +48,7 @@ class ExtractFrames(openpype.api.Extractor):
|
|||
args.extend(["--powc", "0.45,0.45,0.45,1.0"])
|
||||
|
||||
args.extend([input_path, "-o", output_path])
|
||||
output = openpype.api.run_subprocess(args)
|
||||
output = run_subprocess(args)
|
||||
|
||||
failed_output = "oiiotool produced no output."
|
||||
if failed_output in output:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractThumnail(openpype.api.Extractor):
|
||||
class ExtractThumnail(publish.Extractor):
|
||||
"""
|
||||
Extractor for track item's tumnails
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
182
openpype/hosts/houdini/api/shelves.py
Normal file
182
openpype/hosts/houdini/api/shelves.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import os
|
||||
import logging
|
||||
import platform
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
import hou
|
||||
|
||||
log = logging.getLogger("openpype.hosts.houdini.shelves")
|
||||
|
||||
|
||||
def generate_shelves():
|
||||
"""This function generates complete shelves from shelf set to tools
|
||||
in Houdini from openpype project settings houdini shelf definition.
|
||||
"""
|
||||
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')
|
||||
shelf_set_os_filepath = shelf_set_filepath[current_os]
|
||||
if shelf_set_os_filepath:
|
||||
if not os.path.isfile(shelf_set_os_filepath):
|
||||
log.error("Shelf path doesn't exist - "
|
||||
"{}".format(shelf_set_os_filepath))
|
||||
continue
|
||||
|
||||
hou.shelves.newShelfSet(file_path=shelf_set_os_filepath)
|
||||
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.")
|
||||
continue
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
shelf_set = get_or_create_shelf_set(shelf_set_name)
|
||||
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.")
|
||||
continue
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
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:
|
||||
continue
|
||||
|
||||
# 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_set = next((shelf for shelf in all_shelves_sets if
|
||||
shelf.label() == shelf_set_label), None)
|
||||
if shelf_set:
|
||||
return shelf_set
|
||||
|
||||
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 = next((s for s in all_shelves if s.label() == shelf_label), None)
|
||||
if shelf:
|
||||
return shelf
|
||||
|
||||
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 = next(
|
||||
(tool for tool in existing_tools if tool.label() == tool_label),
|
||||
None
|
||||
)
|
||||
if existing_tool:
|
||||
tool_definition.pop('name', None)
|
||||
tool_definition.pop('label', None)
|
||||
existing_tool.setData(**tool_definition)
|
||||
return existing_tool
|
||||
|
||||
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
|
||||
|
|
@ -73,7 +73,7 @@ class ImageLoader(load.LoaderPlugin):
|
|||
|
||||
# Imprint it manually
|
||||
data = {
|
||||
"schema": "avalon-core:container-2.0",
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": node_name,
|
||||
"namespace": namespace,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class USDSublayerLoader(load.LoaderPlugin):
|
|||
|
||||
# Imprint it manually
|
||||
data = {
|
||||
"schema": "avalon-core:container-2.0",
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": node_name,
|
||||
"namespace": namespace,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class USDReferenceLoader(load.LoaderPlugin):
|
|||
|
||||
# Imprint it manually
|
||||
data = {
|
||||
"schema": "avalon-core:container-2.0",
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": node_name,
|
||||
"namespace": namespace,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
import hou
|
||||
from openpype.pipeline.publish import RepairAction
|
||||
from openpype.hosts.houdini.api import lib
|
||||
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ class CollectRemotePublishSettings(pyblish.api.ContextPlugin):
|
|||
hosts = ["houdini"]
|
||||
targets = ["deadline"]
|
||||
label = "Remote Publish Submission Settings"
|
||||
actions = [openpype.api.RepairAction]
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
|
||||
class ExtractAlembic(openpype.api.Extractor):
|
||||
class ExtractAlembic(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Alembic"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
|
||||
class ExtractAss(openpype.api.Extractor):
|
||||
class ExtractAss(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder + 0.1
|
||||
label = "Extract Ass"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
|
||||
class ExtractComposite(openpype.api.Extractor):
|
||||
class ExtractComposite(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Composite (Image Sequence)"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import os
|
|||
from pprint import pformat
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
|
||||
|
||||
class ExtractHDA(openpype.api.Extractor):
|
||||
class ExtractHDA(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract HDA"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
|
||||
class ExtractRedshiftProxy(openpype.api.Extractor):
|
||||
class ExtractRedshiftProxy(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder + 0.1
|
||||
label = "Extract Redshift Proxy"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
|
||||
class ExtractUSD(openpype.api.Extractor):
|
||||
class ExtractUSD(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract USD"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import sys
|
|||
from collections import deque
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
|
|
@ -16,6 +15,7 @@ from openpype.client import (
|
|||
from openpype.pipeline import (
|
||||
get_representation_path,
|
||||
legacy_io,
|
||||
publish,
|
||||
)
|
||||
import openpype.hosts.houdini.api.usd as hou_usdlib
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
|
@ -160,7 +160,7 @@ def parm_values(overrides):
|
|||
parm.set(value)
|
||||
|
||||
|
||||
class ExtractUSDLayered(openpype.api.Extractor):
|
||||
class ExtractUSDLayered(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Layered USD"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.houdini.api.lib import render_rop
|
||||
|
||||
|
||||
class ExtractVDBCache(openpype.api.Extractor):
|
||||
class ExtractVDBCache(publish.Extractor):
|
||||
|
||||
order = pyblish.api.ExtractorOrder + 0.1
|
||||
label = "Extract VDB Cache"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -35,6 +35,9 @@ class ValidateWorkfilePaths(pyblish.api.InstancePlugin):
|
|||
def get_invalid(cls):
|
||||
invalid = []
|
||||
for param, _ in hou.fileReferences():
|
||||
if param is None:
|
||||
continue
|
||||
|
||||
# skip nodes we are not interested in
|
||||
if param.node().type().name() not in cls.node_types:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ class MayaAddon(OpenPypeModule, IHostAddon):
|
|||
|
||||
env["PYTHONPATH"] = os.pathsep.join(new_python_paths)
|
||||
|
||||
# Set default values if are not already set via settings
|
||||
defaults = {
|
||||
"OPENPYPE_LOG_NO_COLORS": "Yes"
|
||||
# Set default environments
|
||||
envs = {
|
||||
"OPENPYPE_LOG_NO_COLORS": "Yes",
|
||||
# For python module 'qtpy'
|
||||
"QT_API": "PySide2",
|
||||
# For python module 'Qt'
|
||||
"QT_PREFERRED_BINDING": "PySide2"
|
||||
}
|
||||
for key, value in defaults.items():
|
||||
if not env.get(key):
|
||||
for key, value in envs.items():
|
||||
env[key] = value
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from functools import partial
|
|||
import maya.cmds as cmds
|
||||
import maya.mel as mel
|
||||
|
||||
from openpype.api import resources
|
||||
from openpype import resources
|
||||
from openpype.tools.utils import host_tools
|
||||
from .lib import get_main_window
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from openpype.client import (
|
|||
get_last_versions,
|
||||
get_representation_by_name
|
||||
)
|
||||
from openpype.api import get_anatomy_settings
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import (
|
||||
legacy_io,
|
||||
discover_loader_plugins,
|
||||
|
|
@ -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:
|
||||
|
|
@ -3159,7 +3159,7 @@ def set_colorspace():
|
|||
"""Set Colorspace from project configuration
|
||||
"""
|
||||
project_name = os.getenv("AVALON_PROJECT")
|
||||
imageio = get_anatomy_settings(project_name)["imageio"]["maya"]
|
||||
imageio = get_project_settings(project_name)["maya"]["imageio"]
|
||||
|
||||
# Maya 2022+ introduces new OCIO v2 color management settings that
|
||||
# can override the old color managenement preferences. OpenPype has
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import maya.mel as mel
|
|||
import six
|
||||
import sys
|
||||
|
||||
from openpype.api import (
|
||||
from openpype.lib import Logger
|
||||
from openpype.settings import (
|
||||
get_project_settings,
|
||||
get_current_project_settings
|
||||
)
|
||||
|
|
@ -38,6 +39,8 @@ class RenderSettings(object):
|
|||
"underscore": "_"
|
||||
}
|
||||
|
||||
log = Logger.get_logger("RenderSettings")
|
||||
|
||||
@classmethod
|
||||
def get_image_prefix_attr(cls, renderer):
|
||||
return cls._image_prefix_nodes[renderer]
|
||||
|
|
@ -133,20 +136,7 @@ class RenderSettings(object):
|
|||
|
||||
cmds.setAttr(
|
||||
"defaultArnoldDriver.mergeAOVs", multi_exr)
|
||||
# Passes additional options in from the schema as a list
|
||||
# but converts it to a dictionary because ftrack doesn't
|
||||
# allow fullstops in custom attributes. Then checks for
|
||||
# type of MtoA attribute passed to adjust the `setAttr`
|
||||
# command accordingly.
|
||||
self._additional_attribs_setter(additional_options)
|
||||
for item in additional_options:
|
||||
attribute, value = item
|
||||
if (cmds.getAttr(str(attribute), type=True)) == "long":
|
||||
cmds.setAttr(str(attribute), int(value))
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "bool":
|
||||
cmds.setAttr(str(attribute), int(value), type = "Boolean") # noqa
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "string":
|
||||
cmds.setAttr(str(attribute), str(value), type = "string") # noqa
|
||||
reset_frame_range()
|
||||
|
||||
def _set_redshift_settings(self, width, height):
|
||||
|
|
@ -230,12 +220,20 @@ class RenderSettings(object):
|
|||
cmds.setAttr("defaultRenderGlobals.extensionPadding", 4)
|
||||
|
||||
def _additional_attribs_setter(self, additional_attribs):
|
||||
print(additional_attribs)
|
||||
for item in additional_attribs:
|
||||
attribute, value = item
|
||||
if (cmds.getAttr(str(attribute), type=True)) == "long":
|
||||
cmds.setAttr(str(attribute), int(value))
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "bool":
|
||||
cmds.setAttr(str(attribute), int(value)) # noqa
|
||||
elif (cmds.getAttr(str(attribute), type=True)) == "string":
|
||||
cmds.setAttr(str(attribute), str(value), type = "string") # noqa
|
||||
attribute = str(attribute) # ensure str conversion from settings
|
||||
attribute_type = cmds.getAttr(attribute, type=True)
|
||||
if attribute_type in {"long", "bool"}:
|
||||
cmds.setAttr(attribute, int(value))
|
||||
elif attribute_type == "string":
|
||||
cmds.setAttr(attribute, str(value), type="string")
|
||||
elif attribute_type in {"double", "doubleAngle", "doubleLinear"}:
|
||||
cmds.setAttr(attribute, float(value))
|
||||
else:
|
||||
self.log.error(
|
||||
"Attribute {attribute} can not be set due to unsupported "
|
||||
"type: {attribute_type}".format(
|
||||
attribute=attribute,
|
||||
attribute_type=attribute_type)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -1,253 +0,0 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
import maya.cmds as cmds
|
||||
|
||||
import qargparse
|
||||
from openpype.tools.utils.widgets import OptionDialog
|
||||
from .lib import get_main_window, imprint
|
||||
|
||||
# To change as enum
|
||||
build_types = ["context_asset", "linked_asset", "all_assets"]
|
||||
|
||||
|
||||
def get_placeholder_attributes(node):
|
||||
return {
|
||||
attr: cmds.getAttr("{}.{}".format(node, attr))
|
||||
for attr in cmds.listAttr(node, userDefined=True)}
|
||||
|
||||
|
||||
def delete_placeholder_attributes(node):
|
||||
'''
|
||||
function to delete all extra placeholder attributes
|
||||
'''
|
||||
extra_attributes = get_placeholder_attributes(node)
|
||||
for attribute in extra_attributes:
|
||||
cmds.deleteAttr(node + '.' + attribute)
|
||||
|
||||
|
||||
def create_placeholder():
|
||||
args = placeholder_window()
|
||||
|
||||
if not args:
|
||||
return # operation canceled, no locator created
|
||||
|
||||
# custom arg parse to force empty data query
|
||||
# and still imprint them on placeholder
|
||||
# and getting items when arg is of type Enumerator
|
||||
options = create_options(args)
|
||||
|
||||
# create placeholder name dynamically from args and options
|
||||
placeholder_name = create_placeholder_name(args, options)
|
||||
|
||||
selection = cmds.ls(selection=True)
|
||||
if not selection:
|
||||
raise ValueError("Nothing is selected")
|
||||
|
||||
placeholder = cmds.spaceLocator(name=placeholder_name)[0]
|
||||
|
||||
# get the long name of the placeholder (with the groups)
|
||||
placeholder_full_name = cmds.ls(selection[0], long=True)[
|
||||
0] + '|' + placeholder.replace('|', '')
|
||||
|
||||
if selection:
|
||||
cmds.parent(placeholder, selection[0])
|
||||
|
||||
imprint(placeholder_full_name, options)
|
||||
|
||||
# Some tweaks because imprint force enums to to default value so we get
|
||||
# back arg read and force them to attributes
|
||||
imprint_enum(placeholder_full_name, args)
|
||||
|
||||
# Add helper attributes to keep placeholder info
|
||||
cmds.addAttr(
|
||||
placeholder_full_name,
|
||||
longName="parent",
|
||||
hidden=True,
|
||||
dataType="string"
|
||||
)
|
||||
cmds.addAttr(
|
||||
placeholder_full_name,
|
||||
longName="index",
|
||||
hidden=True,
|
||||
attributeType="short",
|
||||
defaultValue=-1
|
||||
)
|
||||
|
||||
cmds.setAttr(placeholder_full_name + '.parent', "", type="string")
|
||||
|
||||
|
||||
def create_placeholder_name(args, options):
|
||||
placeholder_builder_type = [
|
||||
arg.read() for arg in args if 'builder_type' in str(arg)
|
||||
][0]
|
||||
placeholder_family = options['family']
|
||||
placeholder_name = placeholder_builder_type.split('_')
|
||||
|
||||
# add famlily in any
|
||||
if placeholder_family:
|
||||
placeholder_name.insert(1, placeholder_family)
|
||||
|
||||
# add loader arguments if any
|
||||
if options['loader_args']:
|
||||
pos = 2
|
||||
loader_args = options['loader_args'].replace('\'', '\"')
|
||||
loader_args = json.loads(loader_args)
|
||||
values = [v for v in loader_args.values()]
|
||||
for i in range(len(values)):
|
||||
placeholder_name.insert(i + pos, values[i])
|
||||
|
||||
placeholder_name = '_'.join(placeholder_name)
|
||||
|
||||
return placeholder_name.capitalize()
|
||||
|
||||
|
||||
def update_placeholder():
|
||||
placeholder = cmds.ls(selection=True)
|
||||
if len(placeholder) == 0:
|
||||
raise ValueError("No node selected")
|
||||
if len(placeholder) > 1:
|
||||
raise ValueError("Too many selected nodes")
|
||||
placeholder = placeholder[0]
|
||||
|
||||
args = placeholder_window(get_placeholder_attributes(placeholder))
|
||||
|
||||
if not args:
|
||||
return # operation canceled
|
||||
|
||||
# delete placeholder attributes
|
||||
delete_placeholder_attributes(placeholder)
|
||||
|
||||
options = create_options(args)
|
||||
|
||||
imprint(placeholder, options)
|
||||
imprint_enum(placeholder, args)
|
||||
|
||||
cmds.addAttr(
|
||||
placeholder,
|
||||
longName="parent",
|
||||
hidden=True,
|
||||
dataType="string"
|
||||
)
|
||||
cmds.addAttr(
|
||||
placeholder,
|
||||
longName="index",
|
||||
hidden=True,
|
||||
attributeType="short",
|
||||
defaultValue=-1
|
||||
)
|
||||
|
||||
cmds.setAttr(placeholder + '.parent', '', type="string")
|
||||
|
||||
|
||||
def create_options(args):
|
||||
options = OrderedDict()
|
||||
for arg in args:
|
||||
if not type(arg) == qargparse.Separator:
|
||||
options[str(arg)] = arg._data.get("items") or arg.read()
|
||||
return options
|
||||
|
||||
|
||||
def imprint_enum(placeholder, args):
|
||||
"""
|
||||
Imprint method doesn't act properly with enums.
|
||||
Replacing the functionnality with this for now
|
||||
"""
|
||||
enum_values = {str(arg): arg.read()
|
||||
for arg in args if arg._data.get("items")}
|
||||
string_to_value_enum_table = {
|
||||
build: i for i, build
|
||||
in enumerate(build_types)}
|
||||
for key, value in enum_values.items():
|
||||
cmds.setAttr(
|
||||
placeholder + "." + key,
|
||||
string_to_value_enum_table[value])
|
||||
|
||||
|
||||
def placeholder_window(options=None):
|
||||
options = options or dict()
|
||||
dialog = OptionDialog(parent=get_main_window())
|
||||
dialog.setWindowTitle("Create Placeholder")
|
||||
|
||||
args = [
|
||||
qargparse.Separator("Main attributes"),
|
||||
qargparse.Enum(
|
||||
"builder_type",
|
||||
label="Asset Builder Type",
|
||||
default=options.get("builder_type", 0),
|
||||
items=build_types,
|
||||
help="""Asset Builder Type
|
||||
Builder type describe what template loader will look for.
|
||||
context_asset : Template loader will look for subsets of
|
||||
current context asset (Asset bob will find asset)
|
||||
linked_asset : Template loader will look for assets linked
|
||||
to current context asset.
|
||||
Linked asset are looked in avalon database under field "inputLinks"
|
||||
"""
|
||||
),
|
||||
qargparse.String(
|
||||
"family",
|
||||
default=options.get("family", ""),
|
||||
label="OpenPype Family",
|
||||
placeholder="ex: model, look ..."),
|
||||
qargparse.String(
|
||||
"representation",
|
||||
default=options.get("representation", ""),
|
||||
label="OpenPype Representation",
|
||||
placeholder="ex: ma, abc ..."),
|
||||
qargparse.String(
|
||||
"loader",
|
||||
default=options.get("loader", ""),
|
||||
label="Loader",
|
||||
placeholder="ex: ReferenceLoader, LightLoader ...",
|
||||
help="""Loader
|
||||
Defines what openpype loader will be used to load assets.
|
||||
Useable loader depends on current host's loader list.
|
||||
Field is case sensitive.
|
||||
"""),
|
||||
qargparse.String(
|
||||
"loader_args",
|
||||
default=options.get("loader_args", ""),
|
||||
label="Loader Arguments",
|
||||
placeholder='ex: {"camera":"persp", "lights":True}',
|
||||
help="""Loader
|
||||
Defines a dictionnary of arguments used to load assets.
|
||||
Useable arguments depend on current placeholder Loader.
|
||||
Field should be a valid python dict. Anything else will be ignored.
|
||||
"""),
|
||||
qargparse.Integer(
|
||||
"order",
|
||||
default=options.get("order", 0),
|
||||
min=0,
|
||||
max=999,
|
||||
label="Order",
|
||||
placeholder="ex: 0, 100 ... (smallest order loaded first)",
|
||||
help="""Order
|
||||
Order defines asset loading priority (0 to 999)
|
||||
Priority rule is : "lowest is first to load"."""),
|
||||
qargparse.Separator(
|
||||
"Optional attributes"),
|
||||
qargparse.String(
|
||||
"asset",
|
||||
default=options.get("asset", ""),
|
||||
label="Asset filter",
|
||||
placeholder="regex filtering by asset name",
|
||||
help="Filtering assets by matching field regex to asset's name"),
|
||||
qargparse.String(
|
||||
"subset",
|
||||
default=options.get("subset", ""),
|
||||
label="Subset filter",
|
||||
placeholder="regex filtering by subset name",
|
||||
help="Filtering assets by matching field regex to subset's name"),
|
||||
qargparse.String(
|
||||
"hierarchy",
|
||||
default=options.get("hierarchy", ""),
|
||||
label="Hierarchy filter",
|
||||
placeholder="regex filtering by asset's hierarchy",
|
||||
help="Filtering assets by matching field asset's hierarchy")
|
||||
]
|
||||
dialog.create(args)
|
||||
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
||||
return args
|
||||
|
|
@ -9,16 +9,17 @@ import maya.cmds as cmds
|
|||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.workfile import BuildWorkfile
|
||||
from openpype.pipeline.workfile.build_template import (
|
||||
build_workfile_template,
|
||||
update_workfile_template
|
||||
)
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.hosts.maya.api import lib, lib_rendersettings
|
||||
from .lib import get_main_window, IS_HEADLESS
|
||||
from .commands import reset_frame_range
|
||||
from .lib_template_builder import create_placeholder, update_placeholder
|
||||
|
||||
from .workfile_template_builder import (
|
||||
create_placeholder,
|
||||
update_placeholder,
|
||||
build_workfile_template,
|
||||
update_workfile_template,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -161,12 +162,12 @@ def install():
|
|||
cmds.menuItem(
|
||||
"Create Placeholder",
|
||||
parent=builder_menu,
|
||||
command=lambda *args: create_placeholder()
|
||||
command=create_placeholder
|
||||
)
|
||||
cmds.menuItem(
|
||||
"Update Placeholder",
|
||||
parent=builder_menu,
|
||||
command=lambda *args: update_placeholder()
|
||||
command=update_placeholder
|
||||
)
|
||||
cmds.menuItem(
|
||||
"Build Workfile from template",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from openpype.host import (
|
|||
HostDirmap,
|
||||
)
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.tools.workfiles.lock_dialog import WorkfileLockDialog
|
||||
from openpype.lib import (
|
||||
register_event_callback,
|
||||
emit_event
|
||||
|
|
@ -31,10 +32,17 @@ from openpype.pipeline import (
|
|||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from openpype.pipeline.load import any_outdated_containers
|
||||
from openpype.pipeline.workfile.lock_workfile import (
|
||||
create_workfile_lock,
|
||||
remove_workfile_lock,
|
||||
is_workfile_locked,
|
||||
is_workfile_lock_enabled
|
||||
)
|
||||
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 .workfile_template_builder import MayaPlaceholderLoadPlugin
|
||||
from .workio import (
|
||||
open_file,
|
||||
save_file,
|
||||
|
|
@ -63,7 +71,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)
|
||||
|
|
@ -99,8 +107,13 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
|
|||
register_event_callback("open", on_open)
|
||||
register_event_callback("new", on_new)
|
||||
register_event_callback("before.save", on_before_save)
|
||||
register_event_callback("after.save", on_after_save)
|
||||
register_event_callback("before.close", on_before_close)
|
||||
register_event_callback("before.file.open", before_file_open)
|
||||
register_event_callback("taskChanged", on_task_changed)
|
||||
register_event_callback("workfile.open.before", before_workfile_open)
|
||||
register_event_callback("workfile.save.before", before_workfile_save)
|
||||
register_event_callback("workfile.save.before", after_workfile_save)
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
return open_file(filepath)
|
||||
|
|
@ -123,6 +136,11 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
|
|||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def get_workfile_build_placeholder_plugins(self):
|
||||
return [
|
||||
MayaPlaceholderLoadPlugin
|
||||
]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection(self):
|
||||
with lib.maintained_selection():
|
||||
|
|
@ -143,6 +161,13 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
|
|||
OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save
|
||||
)
|
||||
|
||||
self._op_events[_after_scene_save] = (
|
||||
OpenMaya.MSceneMessage.addCallback(
|
||||
OpenMaya.MSceneMessage.kAfterSave,
|
||||
_after_scene_save
|
||||
)
|
||||
)
|
||||
|
||||
self._op_events[_before_scene_save] = (
|
||||
OpenMaya.MSceneMessage.addCheckCallback(
|
||||
OpenMaya.MSceneMessage.kBeforeSaveCheck,
|
||||
|
|
@ -161,15 +186,35 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
|
|||
)
|
||||
)
|
||||
|
||||
self._op_events[_on_scene_open] = OpenMaya.MSceneMessage.addCallback(
|
||||
OpenMaya.MSceneMessage.kAfterOpen, _on_scene_open
|
||||
self._op_events[_on_scene_open] = (
|
||||
OpenMaya.MSceneMessage.addCallback(
|
||||
OpenMaya.MSceneMessage.kAfterOpen,
|
||||
_on_scene_open
|
||||
)
|
||||
)
|
||||
|
||||
self._op_events[_before_scene_open] = (
|
||||
OpenMaya.MSceneMessage.addCallback(
|
||||
OpenMaya.MSceneMessage.kBeforeOpen,
|
||||
_before_scene_open
|
||||
)
|
||||
)
|
||||
|
||||
self._op_events[_before_close_maya] = (
|
||||
OpenMaya.MSceneMessage.addCallback(
|
||||
OpenMaya.MSceneMessage.kMayaExiting,
|
||||
_before_close_maya
|
||||
)
|
||||
)
|
||||
|
||||
self.log.info("Installed event handler _on_scene_save..")
|
||||
self.log.info("Installed event handler _before_scene_save..")
|
||||
self.log.info("Installed event handler _on_after_save..")
|
||||
self.log.info("Installed event handler _on_scene_new..")
|
||||
self.log.info("Installed event handler _on_maya_initialized..")
|
||||
self.log.info("Installed event handler _on_scene_open..")
|
||||
self.log.info("Installed event handler _check_lock_file..")
|
||||
self.log.info("Installed event handler _before_close_maya..")
|
||||
|
||||
|
||||
def _set_project():
|
||||
|
|
@ -208,6 +253,10 @@ def _on_scene_new(*args):
|
|||
emit_event("new")
|
||||
|
||||
|
||||
def _after_scene_save(*arg):
|
||||
emit_event("after.save")
|
||||
|
||||
|
||||
def _on_scene_save(*args):
|
||||
emit_event("save")
|
||||
|
||||
|
|
@ -216,6 +265,14 @@ def _on_scene_open(*args):
|
|||
emit_event("open")
|
||||
|
||||
|
||||
def _before_close_maya(*args):
|
||||
emit_event("before.close")
|
||||
|
||||
|
||||
def _before_scene_open(*args):
|
||||
emit_event("before.file.open")
|
||||
|
||||
|
||||
def _before_scene_save(return_code, client_data):
|
||||
|
||||
# Default to allowing the action. Registered
|
||||
|
|
@ -229,6 +286,23 @@ def _before_scene_save(return_code, client_data):
|
|||
)
|
||||
|
||||
|
||||
def _remove_workfile_lock():
|
||||
"""Remove workfile lock on current file"""
|
||||
if not handle_workfile_locks():
|
||||
return
|
||||
filepath = current_file()
|
||||
log.info("Removing lock on current file {}...".format(filepath))
|
||||
if filepath:
|
||||
remove_workfile_lock(filepath)
|
||||
|
||||
|
||||
def handle_workfile_locks():
|
||||
if lib.IS_HEADLESS:
|
||||
return False
|
||||
project_name = legacy_io.active_project()
|
||||
return is_workfile_lock_enabled(MayaHost.name, project_name)
|
||||
|
||||
|
||||
def uninstall():
|
||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
pyblish.api.deregister_host("mayabatch")
|
||||
|
|
@ -426,6 +500,46 @@ def on_before_save():
|
|||
return lib.validate_fps()
|
||||
|
||||
|
||||
def on_after_save():
|
||||
"""Check if there is a lockfile after save"""
|
||||
check_lock_on_current_file()
|
||||
|
||||
|
||||
def check_lock_on_current_file():
|
||||
|
||||
"""Check if there is a user opening the file"""
|
||||
if not handle_workfile_locks():
|
||||
return
|
||||
log.info("Running callback on checking the lock file...")
|
||||
|
||||
# add the lock file when opening the file
|
||||
filepath = current_file()
|
||||
|
||||
if is_workfile_locked(filepath):
|
||||
# add lockfile dialog
|
||||
workfile_dialog = WorkfileLockDialog(filepath)
|
||||
if not workfile_dialog.exec_():
|
||||
cmds.file(new=True)
|
||||
return
|
||||
|
||||
create_workfile_lock(filepath)
|
||||
|
||||
|
||||
def on_before_close():
|
||||
"""Delete the lock file after user quitting the Maya Scene"""
|
||||
log.info("Closing Maya...")
|
||||
# delete the lock file
|
||||
filepath = current_file()
|
||||
if handle_workfile_locks():
|
||||
remove_workfile_lock(filepath)
|
||||
|
||||
|
||||
def before_file_open():
|
||||
"""check lock file when the file changed"""
|
||||
# delete the lock file
|
||||
_remove_workfile_lock()
|
||||
|
||||
|
||||
def on_save():
|
||||
"""Automatically add IDs to new nodes
|
||||
|
||||
|
|
@ -434,6 +548,8 @@ def on_save():
|
|||
"""
|
||||
|
||||
log.info("Running callback on save..")
|
||||
# remove lockfile if users jumps over from one scene to another
|
||||
_remove_workfile_lock()
|
||||
|
||||
# # Update current task for the current scene
|
||||
# update_task_from_path(cmds.file(query=True, sceneName=True))
|
||||
|
|
@ -491,6 +607,9 @@ def on_open():
|
|||
dialog.on_clicked.connect(_on_show_inventory)
|
||||
dialog.show()
|
||||
|
||||
# create lock file for the maya scene
|
||||
check_lock_on_current_file()
|
||||
|
||||
|
||||
def on_new():
|
||||
"""Set project resolution and fps when create a new file"""
|
||||
|
|
@ -506,6 +625,7 @@ def on_new():
|
|||
"from openpype.hosts.maya.api import lib;"
|
||||
"lib.add_render_layer_change_observer()")
|
||||
lib.set_context_settings()
|
||||
_remove_workfile_lock()
|
||||
|
||||
|
||||
def on_task_changed():
|
||||
|
|
@ -533,7 +653,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"]
|
||||
)
|
||||
|
|
@ -544,10 +664,26 @@ def on_task_changed():
|
|||
)
|
||||
|
||||
|
||||
def before_workfile_open():
|
||||
if handle_workfile_locks():
|
||||
_remove_workfile_lock()
|
||||
|
||||
|
||||
def before_workfile_save(event):
|
||||
project_name = legacy_io.active_project()
|
||||
if handle_workfile_locks():
|
||||
_remove_workfile_lock()
|
||||
workdir_path = event["workdir_path"]
|
||||
if workdir_path:
|
||||
copy_workspace_mel(workdir_path)
|
||||
create_workspace_mel(workdir_path, project_name)
|
||||
|
||||
|
||||
def after_workfile_save(event):
|
||||
workfile_name = event["filename"]
|
||||
if handle_workfile_locks():
|
||||
if workfile_name:
|
||||
if not is_workfile_locked(workfile_name):
|
||||
create_workfile_lock(workfile_name)
|
||||
|
||||
|
||||
class MayaDirmap(HostDirmap):
|
||||
|
|
|
|||
|
|
@ -1,252 +0,0 @@
|
|||
import re
|
||||
from maya import cmds
|
||||
|
||||
from openpype.client import get_representations
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.workfile.abstract_template_loader import (
|
||||
AbstractPlaceholder,
|
||||
AbstractTemplateLoader
|
||||
)
|
||||
from openpype.pipeline.workfile.build_template_exceptions import (
|
||||
TemplateAlreadyImported
|
||||
)
|
||||
|
||||
PLACEHOLDER_SET = 'PLACEHOLDERS_SET'
|
||||
|
||||
|
||||
class MayaTemplateLoader(AbstractTemplateLoader):
|
||||
"""Concrete implementation of AbstractTemplateLoader for maya
|
||||
"""
|
||||
|
||||
def import_template(self, path):
|
||||
"""Import template into current scene.
|
||||
Block if a template is already loaded.
|
||||
Args:
|
||||
path (str): A path to current template (usually given by
|
||||
get_template_path implementation)
|
||||
Returns:
|
||||
bool: Wether the template was succesfully imported or not
|
||||
"""
|
||||
if cmds.objExists(PLACEHOLDER_SET):
|
||||
raise TemplateAlreadyImported(
|
||||
"Build template already loaded\n"
|
||||
"Clean scene if needed (File > New Scene)")
|
||||
|
||||
cmds.sets(name=PLACEHOLDER_SET, empty=True)
|
||||
self.new_nodes = cmds.file(path, i=True, returnNewNodes=True)
|
||||
cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True)
|
||||
|
||||
for set in cmds.listSets(allSets=True):
|
||||
if (cmds.objExists(set) and
|
||||
cmds.attributeQuery('id', node=set, exists=True) and
|
||||
cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'):
|
||||
if cmds.attributeQuery('asset', node=set, exists=True):
|
||||
cmds.setAttr(
|
||||
set + '.asset',
|
||||
legacy_io.Session['AVALON_ASSET'], type='string'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def template_already_imported(self, err_msg):
|
||||
clearButton = "Clear scene and build"
|
||||
updateButton = "Update template"
|
||||
abortButton = "Abort"
|
||||
|
||||
title = "Scene already builded"
|
||||
message = (
|
||||
"It's seems a template was already build for this scene.\n"
|
||||
"Error message reveived :\n\n\"{}\"".format(err_msg))
|
||||
buttons = [clearButton, updateButton, abortButton]
|
||||
defaultButton = clearButton
|
||||
cancelButton = abortButton
|
||||
dismissString = abortButton
|
||||
answer = cmds.confirmDialog(
|
||||
t=title,
|
||||
m=message,
|
||||
b=buttons,
|
||||
db=defaultButton,
|
||||
cb=cancelButton,
|
||||
ds=dismissString)
|
||||
|
||||
if answer == clearButton:
|
||||
cmds.file(newFile=True, force=True)
|
||||
self.import_template(self.template_path)
|
||||
self.populate_template()
|
||||
elif answer == updateButton:
|
||||
self.update_missing_containers()
|
||||
elif answer == abortButton:
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def get_template_nodes():
|
||||
attributes = cmds.ls('*.builder_type', long=True)
|
||||
return [attribute.rpartition('.')[0] for attribute in attributes]
|
||||
|
||||
def get_loaded_containers_by_id(self):
|
||||
try:
|
||||
containers = cmds.sets("AVALON_CONTAINERS", q=True)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return [
|
||||
cmds.getAttr(container + '.representation')
|
||||
for container in containers]
|
||||
|
||||
|
||||
class MayaPlaceholder(AbstractPlaceholder):
|
||||
"""Concrete implementation of AbstractPlaceholder for maya
|
||||
"""
|
||||
|
||||
optional_keys = {'asset', 'subset', 'hierarchy'}
|
||||
|
||||
def get_data(self, node):
|
||||
user_data = dict()
|
||||
for attr in self.required_keys.union(self.optional_keys):
|
||||
attribute_name = '{}.{}'.format(node, attr)
|
||||
if not cmds.attributeQuery(attr, node=node, exists=True):
|
||||
print("{} not found".format(attribute_name))
|
||||
continue
|
||||
user_data[attr] = cmds.getAttr(
|
||||
attribute_name,
|
||||
asString=True)
|
||||
user_data['parent'] = (
|
||||
cmds.getAttr(node + '.parent', asString=True)
|
||||
or node.rpartition('|')[0]
|
||||
or ""
|
||||
)
|
||||
user_data['node'] = node
|
||||
if user_data['parent']:
|
||||
siblings = cmds.listRelatives(user_data['parent'], children=True)
|
||||
else:
|
||||
siblings = cmds.ls(assemblies=True)
|
||||
node_shortname = user_data['node'].rpartition('|')[2]
|
||||
current_index = cmds.getAttr(node + '.index', asString=True)
|
||||
user_data['index'] = (
|
||||
current_index if current_index >= 0
|
||||
else siblings.index(node_shortname))
|
||||
|
||||
self.data = user_data
|
||||
|
||||
def parent_in_hierarchy(self, containers):
|
||||
"""Parent loaded container to placeholder's parent
|
||||
ie : Set loaded content as placeholder's sibling
|
||||
Args:
|
||||
containers (String): Placeholder loaded containers
|
||||
"""
|
||||
if not containers:
|
||||
return
|
||||
|
||||
roots = cmds.sets(containers, q=True)
|
||||
nodes_to_parent = []
|
||||
for root in roots:
|
||||
if root.endswith("_RN"):
|
||||
refRoot = cmds.referenceQuery(root, n=True)[0]
|
||||
refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot]
|
||||
nodes_to_parent.extend(refRoot)
|
||||
elif root in cmds.listSets(allSets=True):
|
||||
if not cmds.sets(root, q=True):
|
||||
return
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
nodes_to_parent.append(root)
|
||||
|
||||
if self.data['parent']:
|
||||
cmds.parent(nodes_to_parent, self.data['parent'])
|
||||
# Move loaded nodes to correct index in outliner hierarchy
|
||||
placeholder_node = self.data['node']
|
||||
placeholder_form = cmds.xform(
|
||||
placeholder_node,
|
||||
q=True,
|
||||
matrix=True,
|
||||
worldSpace=True
|
||||
)
|
||||
for node in set(nodes_to_parent):
|
||||
cmds.reorder(node, front=True)
|
||||
cmds.reorder(node, relative=self.data['index'])
|
||||
cmds.xform(node, matrix=placeholder_form, ws=True)
|
||||
|
||||
holding_sets = cmds.listSets(object=placeholder_node)
|
||||
if not holding_sets:
|
||||
return
|
||||
for holding_set in holding_sets:
|
||||
cmds.sets(roots, forceElement=holding_set)
|
||||
|
||||
def clean(self):
|
||||
"""Hide placeholder, parent them to root
|
||||
add them to placeholder set and register placeholder's parent
|
||||
to keep placeholder info available for future use
|
||||
"""
|
||||
node = self.data['node']
|
||||
if self.data['parent']:
|
||||
cmds.setAttr(node + '.parent', self.data['parent'], type='string')
|
||||
if cmds.getAttr(node + '.index') < 0:
|
||||
cmds.setAttr(node + '.index', self.data['index'])
|
||||
|
||||
holding_sets = cmds.listSets(object=node)
|
||||
if holding_sets:
|
||||
for set in holding_sets:
|
||||
cmds.sets(node, remove=set)
|
||||
|
||||
if cmds.listRelatives(node, p=True):
|
||||
node = cmds.parent(node, world=True)[0]
|
||||
cmds.sets(node, addElement=PLACEHOLDER_SET)
|
||||
cmds.hide(node)
|
||||
cmds.setAttr(node + '.hiddenInOutliner', True)
|
||||
|
||||
def get_representations(self, current_asset_doc, linked_asset_docs):
|
||||
project_name = legacy_io.active_project()
|
||||
|
||||
builder_type = self.data["builder_type"]
|
||||
if builder_type == "context_asset":
|
||||
context_filters = {
|
||||
"asset": [current_asset_doc["name"]],
|
||||
"subset": [re.compile(self.data["subset"])],
|
||||
"hierarchy": [re.compile(self.data["hierarchy"])],
|
||||
"representations": [self.data["representation"]],
|
||||
"family": [self.data["family"]]
|
||||
}
|
||||
|
||||
elif builder_type != "linked_asset":
|
||||
context_filters = {
|
||||
"asset": [re.compile(self.data["asset"])],
|
||||
"subset": [re.compile(self.data["subset"])],
|
||||
"hierarchy": [re.compile(self.data["hierarchy"])],
|
||||
"representation": [self.data["representation"]],
|
||||
"family": [self.data["family"]]
|
||||
}
|
||||
|
||||
else:
|
||||
asset_regex = re.compile(self.data["asset"])
|
||||
linked_asset_names = []
|
||||
for asset_doc in linked_asset_docs:
|
||||
asset_name = asset_doc["name"]
|
||||
if asset_regex.match(asset_name):
|
||||
linked_asset_names.append(asset_name)
|
||||
|
||||
context_filters = {
|
||||
"asset": linked_asset_names,
|
||||
"subset": [re.compile(self.data["subset"])],
|
||||
"hierarchy": [re.compile(self.data["hierarchy"])],
|
||||
"representation": [self.data["representation"]],
|
||||
"family": [self.data["family"]],
|
||||
}
|
||||
|
||||
return list(get_representations(
|
||||
project_name,
|
||||
context_filters=context_filters
|
||||
))
|
||||
|
||||
def err_message(self):
|
||||
return (
|
||||
"Error while trying to load a representation.\n"
|
||||
"Either the subset wasn't published or the template is malformed."
|
||||
"\n\n"
|
||||
"Builder was looking for :\n{attributes}".format(
|
||||
attributes="\n".join([
|
||||
"{}: {}".format(key.title(), value)
|
||||
for key, value in self.data.items()]
|
||||
)
|
||||
)
|
||||
)
|
||||
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