mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 08:54:53 +01:00
Merge remote-tracking branch 'upstream/develop' into fusion_new_publisher
This commit is contained in:
commit
88e0ccfa1c
71 changed files with 3444 additions and 2104 deletions
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -1,13 +1,21 @@
|
|||
# Changelog
|
||||
|
||||
## [3.14.3-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
## [3.14.3-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...HEAD)
|
||||
|
||||
**🚀 Enhancements**
|
||||
|
||||
- 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)
|
||||
- Houdini: Increment current file on workfile publish [\#3840](https://github.com/pypeclub/OpenPype/pull/3840)
|
||||
- Publisher: Add new publisher to host tools [\#3833](https://github.com/pypeclub/OpenPype/pull/3833)
|
||||
- General: lock task workfiles when they are working on [\#3810](https://github.com/pypeclub/OpenPype/pull/3810)
|
||||
|
|
@ -15,9 +23,15 @@
|
|||
|
||||
**🐛 Bug fixes**
|
||||
|
||||
- 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)
|
||||
- Ftrack: Url validation does not require ftrackapp [\#3834](https://github.com/pypeclub/OpenPype/pull/3834)
|
||||
- Maya+Ftrack: Change typo in family name `mayaascii` -\> `mayaAscii` [\#3820](https://github.com/pypeclub/OpenPype/pull/3820)
|
||||
|
|
@ -25,14 +39,16 @@
|
|||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- 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)
|
||||
- Maya: Use new Extractor location [\#3775](https://github.com/pypeclub/OpenPype/pull/3775)
|
||||
- General: Change publish template settings location [\#3755](https://github.com/pypeclub/OpenPype/pull/3755)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- 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)
|
||||
|
|
@ -50,8 +66,6 @@
|
|||
- 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**
|
||||
|
||||
|
|
@ -77,11 +91,9 @@
|
|||
- 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)
|
||||
- General: Move publish utils to pipeline [\#3745](https://github.com/pypeclub/OpenPype/pull/3745)
|
||||
- Houdini: Define houdini as addon [\#3735](https://github.com/pypeclub/OpenPype/pull/3735)
|
||||
- Fusion: Defined fusion as addon [\#3733](https://github.com/pypeclub/OpenPype/pull/3733)
|
||||
- Resolve: Define resolve as addon [\#3727](https://github.com/pypeclub/OpenPype/pull/3727)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
|
|
@ -95,37 +107,18 @@
|
|||
**🚀 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)
|
||||
|
||||
**🐛 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)
|
||||
|
||||
**🔀 Refactored code**
|
||||
|
||||
- General: Move delivery logic to pipeline [\#3751](https://github.com/pypeclub/OpenPype/pull/3751)
|
||||
- General: Host addons cleanup [\#3744](https://github.com/pypeclub/OpenPype/pull/3744)
|
||||
- Webpublisher: Webpublisher is used as addon [\#3740](https://github.com/pypeclub/OpenPype/pull/3740)
|
||||
- Photoshop: Defined photoshop as addon [\#3736](https://github.com/pypeclub/OpenPype/pull/3736)
|
||||
- Harmony: Defined harmony as addon [\#3734](https://github.com/pypeclub/OpenPype/pull/3734)
|
||||
- Flame: Defined flame as addon [\#3732](https://github.com/pypeclub/OpenPype/pull/3732)
|
||||
- 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)
|
||||
- Blender: Define blender as module [\#3729](https://github.com/pypeclub/OpenPype/pull/3729)
|
||||
- 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)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Hiero: Define hiero as module [\#3717](https://github.com/pypeclub/OpenPype/pull/3717)
|
||||
|
||||
## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18)
|
||||
|
||||
|
|
|
|||
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"
|
||||
|
|
@ -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,7 +306,7 @@ class INewPublisher:
|
|||
workflow.
|
||||
"""
|
||||
|
||||
if isinstance(host, INewPublisher):
|
||||
if isinstance(host, IPublishHost):
|
||||
return []
|
||||
|
||||
required = [
|
||||
|
|
@ -330,7 +330,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 +368,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
|
||||
|
|
|
|||
|
|
@ -766,11 +766,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()
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -678,6 +678,7 @@ class ClipLoader(LoaderPlugin):
|
|||
`update` logic.
|
||||
|
||||
"""
|
||||
log = log
|
||||
|
||||
options = [
|
||||
qargparse.Boolean(
|
||||
|
|
@ -694,16 +695,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,17 +756,18 @@ 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')
|
||||
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
|
||||
for tmp_feed in tmp_xml_feeds:
|
||||
tmp_feed.set('vuid', self.feed_version_name)
|
||||
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)
|
||||
|
||||
# add colorspace if any is set
|
||||
if self.feed_colorspace:
|
||||
self._add_colorspace(tmp_feed, self.feed_colorspace)
|
||||
for tmp_feed in tmp_xml_track.iter("feed"):
|
||||
tmp_feed.set('vuid', self.feed_version_name)
|
||||
|
||||
self._clear_handler(tmp_feed)
|
||||
# add colorspace if any is set
|
||||
if self.feed_colorspace:
|
||||
self._add_colorspace(tmp_feed, self.feed_colorspace)
|
||||
|
||||
self._clear_handler(tmp_feed)
|
||||
|
||||
tmp_xml_versions_obj = self.clip_data.find('versions')
|
||||
tmp_xml_versions_obj.set('currentVersion', self.feed_version_name)
|
||||
|
|
@ -774,6 +780,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 +800,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))
|
||||
|
||||
self._clear_handler(tmp_xml_feed)
|
||||
# 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))
|
||||
|
||||
# update fps from MediaInfoFile class
|
||||
if self.fps:
|
||||
tmp_feed_fps_obj = tmp_xml_feed.find(
|
||||
"startTimecode/rate")
|
||||
tmp_feed_fps_obj.text = str(self.fps)
|
||||
# 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
|
||||
|
||||
# update start_frame from MediaInfoFile class
|
||||
if self.start_frame:
|
||||
tmp_feed_nb_ticks_obj = tmp_xml_feed.find(
|
||||
"startTimecode/nbTicks")
|
||||
tmp_feed_nb_ticks_obj.text = str(self.start_frame)
|
||||
# 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
|
||||
|
||||
# update drop_mode from MediaInfoFile class
|
||||
if self.drop_mode:
|
||||
tmp_feed_drop_mode_obj = tmp_xml_feed.find(
|
||||
"startTimecode/dropMode")
|
||||
tmp_feed_drop_mode_obj.text = str(self.drop_mode)
|
||||
# rename versions on feeds
|
||||
tmp_xml_feed.set('vuid', self.feed_version_name)
|
||||
self._clear_handler(tmp_xml_feed)
|
||||
|
||||
new_path_obj = tmp_xml_feed.find(
|
||||
"spans/span/path")
|
||||
new_path = new_path_obj.text
|
||||
# update fps from MediaInfoFile class
|
||||
if self.fps is not None:
|
||||
tmp_feed_fps_obj = tmp_xml_feed.find(
|
||||
"startTimecode/rate")
|
||||
tmp_feed_fps_obj.text = str(self.fps)
|
||||
|
||||
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:
|
||||
self._add_colorspace(tmp_xml_feed, self.feed_colorspace)
|
||||
# update start_frame from MediaInfoFile class
|
||||
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)
|
||||
|
||||
out_feeds = out_track.find('feeds')
|
||||
out_feeds.set('currentVersion', self.feed_version_name)
|
||||
out_feeds.append(tmp_xml_feed)
|
||||
# update drop_mode from MediaInfoFile class
|
||||
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)
|
||||
|
||||
self.log.info(
|
||||
"Appending new feed: {}".format(
|
||||
self.feed_version_name))
|
||||
feed_added = True
|
||||
# add colorspace if any is set
|
||||
if self.feed_colorspace is not None:
|
||||
self._add_colorspace(tmp_xml_feed, self.feed_colorspace)
|
||||
|
||||
if feed_added:
|
||||
# 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))
|
||||
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)
|
||||
|
||||
updated_any = True
|
||||
|
||||
if updated_any:
|
||||
# Append vUID to versions
|
||||
out_xml_versions_obj = out_xml.find('versions')
|
||||
out_xml_versions_obj.set(
|
||||
|
|
|
|||
|
|
@ -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,6 +39,7 @@ 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"]
|
||||
|
|
@ -81,7 +83,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 +101,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,7 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pprint import pformat
|
||||
from copy import deepcopy
|
||||
|
||||
import pyblish.api
|
||||
|
|
@ -80,10 +79,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 +96,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 +121,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 +136,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 +162,30 @@ class ExtractSubsetResources(openpype.api.Extractor):
|
|||
if version_data:
|
||||
instance.data["versionData"].update(version_data)
|
||||
|
||||
if r_speed != 1.0:
|
||||
instance.data["versionData"].update({
|
||||
"frameStart": frame_start_handle,
|
||||
"frameEnd": (
|
||||
(frame_start_handle + source_duration_handles - 1)
|
||||
- (r_handle_start + r_handle_end)
|
||||
)
|
||||
})
|
||||
self.log.debug("_ i_version_data: {}".format(
|
||||
# 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": version_frame_start,
|
||||
"frameEnd": (
|
||||
(version_frame_start + source_duration_handles - 1)
|
||||
- (retimed_handle_start + retimed_handle_end)
|
||||
)
|
||||
})
|
||||
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"]
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -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.client import get_project
|
||||
from openpype.settings import get_anatomy_settings
|
||||
from openpype.pipeline import legacy_io, Anatomy
|
||||
from openpype.pipeline.load import filter_containers
|
||||
from openpype.lib import Logger
|
||||
from . import tags
|
||||
|
||||
|
|
@ -1055,6 +1051,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 +1066,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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from openpype.hosts.maya import MAYA_ROOT_DIR
|
|||
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,
|
||||
|
|
@ -135,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():
|
||||
|
|
|
|||
|
|
@ -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()]
|
||||
)
|
||||
)
|
||||
)
|
||||
330
openpype/hosts/maya/api/workfile_template_builder.py
Normal file
330
openpype/hosts/maya/api/workfile_template_builder.py
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
import json
|
||||
|
||||
from maya import cmds
|
||||
|
||||
from openpype.pipeline import registered_host
|
||||
from openpype.pipeline.workfile.workfile_template_builder import (
|
||||
TemplateAlreadyImported,
|
||||
AbstractTemplateBuilder,
|
||||
PlaceholderPlugin,
|
||||
LoadPlaceholderItem,
|
||||
PlaceholderLoadMixin,
|
||||
)
|
||||
from openpype.tools.workfile_template_build import (
|
||||
WorkfileBuildPlaceholderDialog,
|
||||
)
|
||||
|
||||
from .lib import read, imprint
|
||||
|
||||
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
|
||||
|
||||
|
||||
class MayaTemplateBuilder(AbstractTemplateBuilder):
|
||||
"""Concrete implementation of AbstractTemplateBuilder 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)
|
||||
cmds.file(path, i=True, returnNewNodes=True)
|
||||
|
||||
cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
|
||||
identifier = "maya.load"
|
||||
label = "Maya load"
|
||||
|
||||
def _collect_scene_placeholders(self):
|
||||
# Cache placeholder data to shared data
|
||||
placeholder_nodes = self.builder.get_shared_populate_data(
|
||||
"placeholder_nodes"
|
||||
)
|
||||
if placeholder_nodes is None:
|
||||
attributes = cmds.ls("*.plugin_identifier", long=True)
|
||||
placeholder_nodes = {}
|
||||
for attribute in attributes:
|
||||
node_name = attribute.rpartition(".")[0]
|
||||
placeholder_nodes[node_name] = (
|
||||
self._parse_placeholder_node_data(node_name)
|
||||
)
|
||||
|
||||
self.builder.set_shared_populate_data(
|
||||
"placeholder_nodes", placeholder_nodes
|
||||
)
|
||||
return placeholder_nodes
|
||||
|
||||
def _parse_placeholder_node_data(self, node_name):
|
||||
placeholder_data = read(node_name)
|
||||
parent_name = (
|
||||
cmds.getAttr(node_name + ".parent", asString=True)
|
||||
or node_name.rpartition("|")[0]
|
||||
or ""
|
||||
)
|
||||
if parent_name:
|
||||
siblings = cmds.listRelatives(parent_name, children=True)
|
||||
else:
|
||||
siblings = cmds.ls(assemblies=True)
|
||||
node_shortname = node_name.rpartition("|")[2]
|
||||
current_index = cmds.getAttr(node_name + ".index", asString=True)
|
||||
if current_index < 0:
|
||||
current_index = siblings.index(node_shortname)
|
||||
|
||||
placeholder_data.update({
|
||||
"parent": parent_name,
|
||||
"index": current_index
|
||||
})
|
||||
return placeholder_data
|
||||
|
||||
def _create_placeholder_name(self, placeholder_data):
|
||||
placeholder_name_parts = placeholder_data["builder_type"].split("_")
|
||||
|
||||
pos = 1
|
||||
# add famlily in any
|
||||
placeholder_family = placeholder_data["family"]
|
||||
if placeholder_family:
|
||||
placeholder_name_parts.insert(pos, placeholder_family)
|
||||
pos += 1
|
||||
|
||||
# add loader arguments if any
|
||||
loader_args = placeholder_data["loader_args"]
|
||||
if loader_args:
|
||||
loader_args = json.loads(loader_args.replace('\'', '\"'))
|
||||
values = [v for v in loader_args.values()]
|
||||
for value in values:
|
||||
placeholder_name_parts.insert(pos, value)
|
||||
pos += 1
|
||||
|
||||
placeholder_name = "_".join(placeholder_name_parts)
|
||||
|
||||
return placeholder_name.capitalize()
|
||||
|
||||
def _get_loaded_repre_ids(self):
|
||||
loaded_representation_ids = self.builder.get_shared_populate_data(
|
||||
"loaded_representation_ids"
|
||||
)
|
||||
if loaded_representation_ids is None:
|
||||
try:
|
||||
containers = cmds.sets("AVALON_CONTAINERS", q=True)
|
||||
except ValueError:
|
||||
containers = []
|
||||
|
||||
loaded_representation_ids = {
|
||||
cmds.getAttr(container + ".representation")
|
||||
for container in containers
|
||||
}
|
||||
self.builder.set_shared_populate_data(
|
||||
"loaded_representation_ids", loaded_representation_ids
|
||||
)
|
||||
return loaded_representation_ids
|
||||
|
||||
def create_placeholder(self, placeholder_data):
|
||||
selection = cmds.ls(selection=True)
|
||||
if not selection:
|
||||
raise ValueError("Nothing is selected")
|
||||
if len(selection) > 1:
|
||||
raise ValueError("More then one item are selected")
|
||||
|
||||
placeholder_data["plugin_identifier"] = self.identifier
|
||||
|
||||
placeholder_name = self._create_placeholder_name(placeholder_data)
|
||||
|
||||
placeholder = cmds.spaceLocator(name=placeholder_name)[0]
|
||||
# TODO: this can crash if selection can't be used
|
||||
cmds.parent(placeholder, selection[0])
|
||||
|
||||
# get the long name of the placeholder (with the groups)
|
||||
placeholder_full_name = (
|
||||
cmds.ls(selection[0], long=True)[0]
|
||||
+ "|"
|
||||
+ placeholder.replace("|", "")
|
||||
)
|
||||
|
||||
imprint(placeholder_full_name, placeholder_data)
|
||||
|
||||
# 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 update_placeholder(self, placeholder_item, placeholder_data):
|
||||
node_name = placeholder_item.scene_identifier
|
||||
new_values = {}
|
||||
for key, value in placeholder_data.items():
|
||||
placeholder_value = placeholder_item.data.get(key)
|
||||
if value != placeholder_value:
|
||||
new_values[key] = value
|
||||
placeholder_item.data[key] = value
|
||||
|
||||
for key in new_values.keys():
|
||||
cmds.deleteAttr(node_name + "." + key)
|
||||
|
||||
imprint(node_name, new_values)
|
||||
|
||||
def collect_placeholders(self):
|
||||
output = []
|
||||
scene_placeholders = self._collect_scene_placeholders()
|
||||
for node_name, placeholder_data in scene_placeholders.items():
|
||||
if placeholder_data.get("plugin_identifier") != self.identifier:
|
||||
continue
|
||||
|
||||
# TODO do data validations and maybe updgrades if are invalid
|
||||
output.append(
|
||||
LoadPlaceholderItem(node_name, placeholder_data, self)
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def populate_placeholder(self, placeholder):
|
||||
self.populate_load_placeholder(placeholder)
|
||||
|
||||
def repopulate_placeholder(self, placeholder):
|
||||
repre_ids = self._get_loaded_repre_ids()
|
||||
self.populate_load_placeholder(placeholder, repre_ids)
|
||||
|
||||
def get_placeholder_options(self, options=None):
|
||||
return self.get_load_plugin_options(options)
|
||||
|
||||
def cleanup_placeholder(self, placeholder, failed):
|
||||
"""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 = placeholder._scene_identifier
|
||||
node_parent = placeholder.data["parent"]
|
||||
if node_parent:
|
||||
cmds.setAttr(node + ".parent", node_parent, type="string")
|
||||
|
||||
if cmds.getAttr(node + ".index") < 0:
|
||||
cmds.setAttr(node + ".index", placeholder.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 load_succeed(self, placeholder, container):
|
||||
self._parent_in_hierarhchy(placeholder, container)
|
||||
|
||||
def _parent_in_hierarchy(self, placeholder, container):
|
||||
"""Parent loaded container to placeholder's parent.
|
||||
|
||||
ie : Set loaded content as placeholder's sibling
|
||||
|
||||
Args:
|
||||
container (str): Placeholder loaded containers
|
||||
"""
|
||||
|
||||
if not container:
|
||||
return
|
||||
|
||||
roots = cmds.sets(container, 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 not in cmds.listSets(allSets=True):
|
||||
nodes_to_parent.append(root)
|
||||
|
||||
elif not cmds.sets(root, q=True):
|
||||
return
|
||||
|
||||
if placeholder.data["parent"]:
|
||||
cmds.parent(nodes_to_parent, placeholder.data["parent"])
|
||||
# Move loaded nodes to correct index in outliner hierarchy
|
||||
placeholder_form = cmds.xform(
|
||||
placeholder.scene_identifier,
|
||||
q=True,
|
||||
matrix=True,
|
||||
worldSpace=True
|
||||
)
|
||||
for node in set(nodes_to_parent):
|
||||
cmds.reorder(node, front=True)
|
||||
cmds.reorder(node, relative=placeholder.data["index"])
|
||||
cmds.xform(node, matrix=placeholder_form, ws=True)
|
||||
|
||||
holding_sets = cmds.listSets(object=placeholder.scene_identifier)
|
||||
if not holding_sets:
|
||||
return
|
||||
for holding_set in holding_sets:
|
||||
cmds.sets(roots, forceElement=holding_set)
|
||||
|
||||
|
||||
def build_workfile_template(*args):
|
||||
builder = MayaTemplateBuilder(registered_host())
|
||||
builder.build_template()
|
||||
|
||||
|
||||
def update_workfile_template(*args):
|
||||
builder = MayaTemplateBuilder(registered_host())
|
||||
builder.rebuild_template()
|
||||
|
||||
|
||||
def create_placeholder(*args):
|
||||
host = registered_host()
|
||||
builder = MayaTemplateBuilder(host)
|
||||
window = WorkfileBuildPlaceholderDialog(host, builder)
|
||||
window.exec_()
|
||||
|
||||
|
||||
def update_placeholder(*args):
|
||||
host = registered_host()
|
||||
builder = MayaTemplateBuilder(host)
|
||||
placeholder_items_by_id = {
|
||||
placeholder_item.scene_identifier: placeholder_item
|
||||
for placeholder_item in builder.get_placeholders()
|
||||
}
|
||||
placeholder_items = []
|
||||
for node_name in cmds.ls(selection=True, long=True):
|
||||
if node_name in placeholder_items_by_id:
|
||||
placeholder_items.append(placeholder_items_by_id[node_name])
|
||||
|
||||
# TODO show UI at least
|
||||
if len(placeholder_items) == 0:
|
||||
raise ValueError("No node selected")
|
||||
|
||||
if len(placeholder_items) > 1:
|
||||
raise ValueError("Too many selected nodes")
|
||||
|
||||
placeholder_item = placeholder_items[0]
|
||||
window = WorkfileBuildPlaceholderDialog(host, builder)
|
||||
window.set_update_mode(placeholder_item)
|
||||
window.exec_()
|
||||
|
|
@ -13,6 +13,12 @@ from openpype.pipeline.publish import (
|
|||
from openpype.hosts.maya.api import lib
|
||||
|
||||
|
||||
def get_redshift_image_format_labels():
|
||||
"""Return nice labels for Redshift image formats."""
|
||||
var = "$g_redshiftImageFormatLabels"
|
||||
return mel.eval("{0}={0}".format(var))
|
||||
|
||||
|
||||
class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
||||
"""Validates the global render settings
|
||||
|
||||
|
|
@ -105,8 +111,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
|
||||
# Get the node attributes for current renderer
|
||||
attrs = lib.RENDER_ATTRS.get(renderer, lib.RENDER_ATTRS['default'])
|
||||
# Prefix attribute can return None when a value was never set
|
||||
prefix = lib.get_attr_in_layer(cls.ImagePrefixes[renderer],
|
||||
layer=layer)
|
||||
layer=layer) or ""
|
||||
padding = lib.get_attr_in_layer("{node}.{padding}".format(**attrs),
|
||||
layer=layer)
|
||||
|
||||
|
|
@ -183,18 +190,22 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
redshift_AOV_prefix
|
||||
))
|
||||
invalid = True
|
||||
# get aov format
|
||||
aov_ext = cmds.getAttr(
|
||||
"{}.fileFormat".format(aov), asString=True)
|
||||
|
||||
default_ext = cmds.getAttr(
|
||||
"redshiftOptions.imageFormat", asString=True)
|
||||
# check aov file format
|
||||
aov_ext = cmds.getAttr("{}.fileFormat".format(aov))
|
||||
default_ext = cmds.getAttr("redshiftOptions.imageFormat")
|
||||
aov_type = cmds.getAttr("{}.aovType".format(aov))
|
||||
if aov_type == "Cryptomatte":
|
||||
# redshift Cryptomatte AOV always uses "Cryptomatte (EXR)"
|
||||
# so we ignore validating file format for it.
|
||||
pass
|
||||
|
||||
if default_ext != aov_ext:
|
||||
cls.log.error(("AOV file format is not the same "
|
||||
"as the one set globally "
|
||||
"{} != {}").format(default_ext,
|
||||
aov_ext))
|
||||
elif default_ext != aov_ext:
|
||||
labels = get_redshift_image_format_labels()
|
||||
cls.log.error(
|
||||
"AOV file format {} does not match global file format "
|
||||
"{}".format(labels[aov_ext], labels[default_ext])
|
||||
)
|
||||
invalid = True
|
||||
|
||||
if renderer == "renderman":
|
||||
|
|
@ -302,6 +313,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
default = lib.RENDER_ATTRS['default']
|
||||
render_attrs = lib.RENDER_ATTRS.get(renderer, default)
|
||||
|
||||
# Repair animation must be enabled
|
||||
cmds.setAttr("defaultRenderGlobals.animation", True)
|
||||
|
||||
# Repair prefix
|
||||
if renderer != "renderman":
|
||||
node = render_attrs["node"]
|
||||
|
|
@ -334,8 +348,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
|
|||
cmds.optionMenuGrp("vrayRenderElementSeparator",
|
||||
v=instance.data.get("aovSeparator", "_"))
|
||||
cmds.setAttr(
|
||||
"{}.fileNameRenderElementSeparator".format(
|
||||
node),
|
||||
"{}.fileNameRenderElementSeparator".format(node),
|
||||
instance.data.get("aovSeparator", "_"),
|
||||
type="string"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ from .pipeline import (
|
|||
containerise,
|
||||
parse_container,
|
||||
update_container,
|
||||
|
||||
get_workfile_build_placeholder_plugins,
|
||||
)
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
|
|
@ -55,6 +57,8 @@ __all__ = (
|
|||
"parse_container",
|
||||
"update_container",
|
||||
|
||||
"get_workfile_build_placeholder_plugins",
|
||||
|
||||
"maintained_selection",
|
||||
"reset_selection",
|
||||
"get_view_process_node",
|
||||
|
|
|
|||
|
|
@ -1,220 +0,0 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
import qargparse
|
||||
|
||||
import nuke
|
||||
|
||||
from openpype.tools.utils.widgets import OptionDialog
|
||||
|
||||
from .lib import imprint, get_main_window
|
||||
|
||||
|
||||
# To change as enum
|
||||
build_types = ["context_asset", "linked_asset", "all_assets"]
|
||||
|
||||
|
||||
def get_placeholder_attributes(node, enumerate=False):
|
||||
list_atts = {
|
||||
"builder_type",
|
||||
"family",
|
||||
"representation",
|
||||
"loader",
|
||||
"loader_args",
|
||||
"order",
|
||||
"asset",
|
||||
"subset",
|
||||
"hierarchy",
|
||||
"siblings",
|
||||
"last_loaded"
|
||||
}
|
||||
attributes = {}
|
||||
for attr in node.knobs().keys():
|
||||
if attr in list_atts:
|
||||
if enumerate:
|
||||
try:
|
||||
attributes[attr] = node.knob(attr).values()
|
||||
except AttributeError:
|
||||
attributes[attr] = node.knob(attr).getValue()
|
||||
else:
|
||||
attributes[attr] = node.knob(attr).getValue()
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def delete_placeholder_attributes(node):
|
||||
"""Delete all extra placeholder attributes."""
|
||||
|
||||
extra_attributes = get_placeholder_attributes(node)
|
||||
for attribute in extra_attributes.keys():
|
||||
try:
|
||||
node.removeKnob(node.knob(attribute))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
|
||||
def hide_placeholder_attributes(node):
|
||||
"""Hide all extra placeholder attributes."""
|
||||
|
||||
extra_attributes = get_placeholder_attributes(node)
|
||||
for attribute in extra_attributes.keys():
|
||||
try:
|
||||
node.knob(attribute).setVisible(False)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
|
||||
def create_placeholder():
|
||||
args = placeholder_window()
|
||||
if not args:
|
||||
# operation canceled, no locator created
|
||||
return
|
||||
|
||||
placeholder = nuke.nodes.NoOp()
|
||||
placeholder.setName("PLACEHOLDER")
|
||||
placeholder.knob("tile_color").setValue(4278190335)
|
||||
|
||||
# custom arg parse to force empty data query
|
||||
# and still imprint them on placeholder
|
||||
# and getting items when arg is of type Enumerator
|
||||
options = OrderedDict()
|
||||
for arg in args:
|
||||
if not type(arg) == qargparse.Separator:
|
||||
options[str(arg)] = arg._data.get("items") or arg.read()
|
||||
imprint(placeholder, options)
|
||||
imprint(placeholder, {"is_placeholder": True})
|
||||
placeholder.knob("is_placeholder").setVisible(False)
|
||||
|
||||
|
||||
def update_placeholder():
|
||||
placeholder = nuke.selectedNodes()
|
||||
if not placeholder:
|
||||
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 = OrderedDict()
|
||||
for arg in args:
|
||||
if not type(arg) == qargparse.Separator:
|
||||
options[str(arg)] = arg._data.get("items") or arg.read()
|
||||
imprint(placeholder, 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: idx
|
||||
for idx, build in enumerate(build_types)
|
||||
}
|
||||
attrs = {}
|
||||
for key, value in enum_values.items():
|
||||
attrs[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 OpenPype database under field "inputLinks"
|
||||
"""
|
||||
),
|
||||
qargparse.String(
|
||||
"family",
|
||||
default=options.get("family", ""),
|
||||
label="OpenPype Family",
|
||||
placeholder="ex: image, plate ..."),
|
||||
qargparse.String(
|
||||
"representation",
|
||||
default=options.get("representation", ""),
|
||||
label="OpenPype Representation",
|
||||
placeholder="ex: mov, png ..."),
|
||||
qargparse.String(
|
||||
"loader",
|
||||
default=options.get("loader", ""),
|
||||
label="Loader",
|
||||
placeholder="ex: LoadClip, LoadImage ...",
|
||||
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
|
||||
|
|
@ -22,10 +22,6 @@ from openpype.pipeline import (
|
|||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
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 .command import viewer_update_and_undo_stop
|
||||
|
|
@ -40,8 +36,12 @@ from .lib import (
|
|||
set_avalon_knob_data,
|
||||
read_avalon_data,
|
||||
)
|
||||
from .lib_template_builder import (
|
||||
create_placeholder, update_placeholder
|
||||
from .workfile_template_builder import (
|
||||
NukePlaceholderLoadPlugin,
|
||||
build_workfile_template,
|
||||
update_workfile_template,
|
||||
create_placeholder,
|
||||
update_placeholder,
|
||||
)
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
|
@ -141,6 +141,12 @@ def _show_workfiles():
|
|||
host_tools.show_workfiles(parent=None, on_top=False)
|
||||
|
||||
|
||||
def get_workfile_build_placeholder_plugins():
|
||||
return [
|
||||
NukePlaceholderLoadPlugin
|
||||
]
|
||||
|
||||
|
||||
def _install_menu():
|
||||
# uninstall original avalon menu
|
||||
main_window = get_main_window()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import re
|
||||
import collections
|
||||
|
||||
import nuke
|
||||
|
||||
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 import registered_host
|
||||
from openpype.pipeline.workfile.workfile_template_builder import (
|
||||
AbstractTemplateBuilder,
|
||||
PlaceholderPlugin,
|
||||
LoadPlaceholderItem,
|
||||
PlaceholderLoadMixin,
|
||||
)
|
||||
from openpype.tools.workfile_template_build import (
|
||||
WorkfileBuildPlaceholderDialog,
|
||||
)
|
||||
|
||||
from .lib import (
|
||||
|
|
@ -25,19 +28,11 @@ from .lib import (
|
|||
node_tempfile,
|
||||
)
|
||||
|
||||
from .lib_template_builder import (
|
||||
delete_placeholder_attributes,
|
||||
get_placeholder_attributes,
|
||||
hide_placeholder_attributes
|
||||
)
|
||||
|
||||
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
|
||||
|
||||
|
||||
class NukeTemplateLoader(AbstractTemplateLoader):
|
||||
"""Concrete implementation of AbstractTemplateLoader for Nuke
|
||||
|
||||
"""
|
||||
class NukeTemplateBuilder(AbstractTemplateBuilder):
|
||||
"""Concrete implementation of AbstractTemplateBuilder for maya"""
|
||||
|
||||
def import_template(self, path):
|
||||
"""Import template into current scene.
|
||||
|
|
@ -58,224 +53,255 @@ class NukeTemplateLoader(AbstractTemplateLoader):
|
|||
|
||||
return True
|
||||
|
||||
def preload(self, placeholder, loaders_by_name, last_representation):
|
||||
placeholder.data["nodes_init"] = nuke.allNodes()
|
||||
placeholder.data["last_repre_id"] = str(last_representation["_id"])
|
||||
|
||||
def populate_template(self, ignored_ids=None):
|
||||
processed_key = "_node_processed"
|
||||
class NukePlaceholderPlugin(PlaceholderPlugin):
|
||||
node_color = 4278190335
|
||||
|
||||
processed_nodes = []
|
||||
nodes = self.get_template_nodes()
|
||||
while nodes:
|
||||
# Mark nodes as processed so they're not re-executed
|
||||
# - that can happen if processing of placeholder node fails
|
||||
for node in nodes:
|
||||
imprint(node, {processed_key: True})
|
||||
processed_nodes.append(node)
|
||||
def _collect_scene_placeholders(self):
|
||||
# Cache placeholder data to shared data
|
||||
placeholder_nodes = self.builder.get_shared_populate_data(
|
||||
"placeholder_nodes"
|
||||
)
|
||||
if placeholder_nodes is None:
|
||||
placeholder_nodes = {}
|
||||
all_groups = collections.deque()
|
||||
all_groups.append(nuke.thisGroup())
|
||||
while all_groups:
|
||||
group = all_groups.popleft()
|
||||
for node in group.nodes():
|
||||
if isinstance(node, nuke.Group):
|
||||
all_groups.append(node)
|
||||
|
||||
super(NukeTemplateLoader, self).populate_template(ignored_ids)
|
||||
node_knobs = node.knobs()
|
||||
if (
|
||||
"builder_type" not in node_knobs
|
||||
or "is_placeholder" not in node_knobs
|
||||
or not node.knob("is_placeholder").value()
|
||||
):
|
||||
continue
|
||||
|
||||
# Recollect nodes to repopulate
|
||||
nodes = []
|
||||
for node in self.get_template_nodes():
|
||||
# Skip already processed nodes
|
||||
if (
|
||||
processed_key in node.knobs()
|
||||
and node.knob(processed_key).value()
|
||||
):
|
||||
continue
|
||||
nodes.append(node)
|
||||
if "empty" in node_knobs and node.knob("empty").value():
|
||||
continue
|
||||
|
||||
for node in processed_nodes:
|
||||
knob = node.knob(processed_key)
|
||||
placeholder_nodes[node.fullName()] = node
|
||||
|
||||
self.builder.set_shared_populate_data(
|
||||
"placeholder_nodes", placeholder_nodes
|
||||
)
|
||||
return placeholder_nodes
|
||||
|
||||
def create_placeholder(self, placeholder_data):
|
||||
placeholder_data["plugin_identifier"] = self.identifier
|
||||
|
||||
placeholder = nuke.nodes.NoOp()
|
||||
placeholder.setName("PLACEHOLDER")
|
||||
placeholder.knob("tile_color").setValue(self.node_color)
|
||||
|
||||
imprint(placeholder, placeholder_data)
|
||||
imprint(placeholder, {"is_placeholder": True})
|
||||
placeholder.knob("is_placeholder").setVisible(False)
|
||||
|
||||
def update_placeholder(self, placeholder_item, placeholder_data):
|
||||
node = nuke.toNode(placeholder_item.scene_identifier)
|
||||
imprint(node, placeholder_data)
|
||||
|
||||
def _parse_placeholder_node_data(self, node):
|
||||
placeholder_data = {}
|
||||
for key in self.get_placeholder_keys():
|
||||
knob = node.knob(key)
|
||||
value = None
|
||||
if knob is not None:
|
||||
node.removeKnob(knob)
|
||||
|
||||
@staticmethod
|
||||
def get_template_nodes():
|
||||
placeholders = []
|
||||
all_groups = collections.deque()
|
||||
all_groups.append(nuke.thisGroup())
|
||||
while all_groups:
|
||||
group = all_groups.popleft()
|
||||
for node in group.nodes():
|
||||
if isinstance(node, nuke.Group):
|
||||
all_groups.append(node)
|
||||
|
||||
node_knobs = node.knobs()
|
||||
if (
|
||||
"builder_type" not in node_knobs
|
||||
or "is_placeholder" not in node_knobs
|
||||
or not node.knob("is_placeholder").value()
|
||||
):
|
||||
continue
|
||||
|
||||
if "empty" in node_knobs and node.knob("empty").value():
|
||||
continue
|
||||
|
||||
placeholders.append(node)
|
||||
|
||||
return placeholders
|
||||
|
||||
def update_missing_containers(self):
|
||||
nodes_by_id = collections.defaultdict(list)
|
||||
|
||||
for node in nuke.allNodes():
|
||||
node_knobs = node.knobs().keys()
|
||||
if "repre_id" in node_knobs:
|
||||
repre_id = node.knob("repre_id").getValue()
|
||||
nodes_by_id[repre_id].append(node.name())
|
||||
|
||||
if "empty" in node_knobs:
|
||||
node.removeKnob(node.knob("empty"))
|
||||
imprint(node, {"empty": False})
|
||||
|
||||
for node_names in nodes_by_id.values():
|
||||
node = None
|
||||
for node_name in node_names:
|
||||
node_by_name = nuke.toNode(node_name)
|
||||
if "builder_type" in node_by_name.knobs().keys():
|
||||
node = node_by_name
|
||||
break
|
||||
|
||||
if node is None:
|
||||
continue
|
||||
|
||||
placeholder = nuke.nodes.NoOp()
|
||||
placeholder.setName("PLACEHOLDER")
|
||||
placeholder.knob("tile_color").setValue(4278190335)
|
||||
attributes = get_placeholder_attributes(node, enumerate=True)
|
||||
imprint(placeholder, attributes)
|
||||
pos_x = int(node.knob("x").getValue())
|
||||
pos_y = int(node.knob("y").getValue())
|
||||
placeholder.setXYpos(pos_x, pos_y)
|
||||
imprint(placeholder, {"nb_children": 1})
|
||||
refresh_node(placeholder)
|
||||
|
||||
self.populate_template(self.get_loaded_containers_by_id())
|
||||
|
||||
def get_loaded_containers_by_id(self):
|
||||
repre_ids = set()
|
||||
for node in nuke.allNodes():
|
||||
if "repre_id" in node.knobs():
|
||||
repre_ids.add(node.knob("repre_id").getValue())
|
||||
|
||||
# Removes duplicates in the list
|
||||
return list(repre_ids)
|
||||
|
||||
def delete_placeholder(self, placeholder):
|
||||
placeholder_node = placeholder.data["node"]
|
||||
last_loaded = placeholder.data["last_loaded"]
|
||||
if not placeholder.data["delete"]:
|
||||
if "empty" in placeholder_node.knobs().keys():
|
||||
placeholder_node.removeKnob(placeholder_node.knob("empty"))
|
||||
imprint(placeholder_node, {"empty": True})
|
||||
return
|
||||
|
||||
if not last_loaded:
|
||||
nuke.delete(placeholder_node)
|
||||
return
|
||||
|
||||
if "last_loaded" in placeholder_node.knobs().keys():
|
||||
for node_name in placeholder_node.knob("last_loaded").values():
|
||||
node = nuke.toNode(node_name)
|
||||
try:
|
||||
delete_placeholder_attributes(node)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
last_loaded_names = [
|
||||
loaded_node.name()
|
||||
for loaded_node in last_loaded
|
||||
]
|
||||
imprint(placeholder_node, {"last_loaded": last_loaded_names})
|
||||
|
||||
for node in last_loaded:
|
||||
refresh_node(node)
|
||||
refresh_node(placeholder_node)
|
||||
if "builder_type" not in node.knobs().keys():
|
||||
attributes = get_placeholder_attributes(placeholder_node, True)
|
||||
imprint(node, attributes)
|
||||
imprint(node, {"is_placeholder": False})
|
||||
hide_placeholder_attributes(node)
|
||||
node.knob("is_placeholder").setVisible(False)
|
||||
imprint(
|
||||
node,
|
||||
{
|
||||
"x": placeholder_node.xpos(),
|
||||
"y": placeholder_node.ypos()
|
||||
}
|
||||
)
|
||||
node.knob("x").setVisible(False)
|
||||
node.knob("y").setVisible(False)
|
||||
nuke.delete(placeholder_node)
|
||||
value = knob.getValue()
|
||||
placeholder_data[key] = value
|
||||
return placeholder_data
|
||||
|
||||
|
||||
class NukePlaceholder(AbstractPlaceholder):
|
||||
"""Concrete implementation of AbstractPlaceholder for Nuke"""
|
||||
class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):
|
||||
identifier = "nuke.load"
|
||||
label = "Nuke load"
|
||||
|
||||
optional_keys = {"asset", "subset", "hierarchy"}
|
||||
def _parse_placeholder_node_data(self, node):
|
||||
placeholder_data = super(
|
||||
NukePlaceholderLoadPlugin, self
|
||||
)._parse_placeholder_node_data(node)
|
||||
|
||||
def get_data(self, node):
|
||||
user_data = dict()
|
||||
node_knobs = node.knobs()
|
||||
for attr in self.required_keys.union(self.optional_keys):
|
||||
if attr in node_knobs:
|
||||
user_data[attr] = node_knobs[attr].getValue()
|
||||
user_data["node"] = node
|
||||
|
||||
nb_children = 0
|
||||
if "nb_children" in node_knobs:
|
||||
nb_children = int(node_knobs["nb_children"].getValue())
|
||||
user_data["nb_children"] = nb_children
|
||||
placeholder_data["nb_children"] = nb_children
|
||||
|
||||
siblings = []
|
||||
if "siblings" in node_knobs:
|
||||
siblings = node_knobs["siblings"].values()
|
||||
user_data["siblings"] = siblings
|
||||
placeholder_data["siblings"] = siblings
|
||||
|
||||
node_full_name = node.fullName()
|
||||
user_data["group_name"] = node_full_name.rpartition(".")[0]
|
||||
user_data["last_loaded"] = []
|
||||
user_data["delete"] = False
|
||||
self.data = user_data
|
||||
placeholder_data["group_name"] = node_full_name.rpartition(".")[0]
|
||||
placeholder_data["last_loaded"] = []
|
||||
placeholder_data["delete"] = False
|
||||
return placeholder_data
|
||||
|
||||
def parent_in_hierarchy(self, containers):
|
||||
return
|
||||
def _get_loaded_repre_ids(self):
|
||||
loaded_representation_ids = self.builder.get_shared_populate_data(
|
||||
"loaded_representation_ids"
|
||||
)
|
||||
if loaded_representation_ids is None:
|
||||
loaded_representation_ids = set()
|
||||
for node in nuke.allNodes():
|
||||
if "repre_id" in node.knobs():
|
||||
loaded_representation_ids.add(
|
||||
node.knob("repre_id").getValue()
|
||||
)
|
||||
|
||||
def create_sib_copies(self):
|
||||
""" creating copies of the palce_holder siblings (the ones who were
|
||||
loaded with it) for the new nodes added
|
||||
self.builder.set_shared_populate_data(
|
||||
"loaded_representation_ids", loaded_representation_ids
|
||||
)
|
||||
return loaded_representation_ids
|
||||
|
||||
def _before_repre_load(self, placeholder, representation):
|
||||
placeholder.data["nodes_init"] = nuke.allNodes()
|
||||
placeholder.data["last_repre_id"] = str(representation["_id"])
|
||||
|
||||
def collect_placeholders(self):
|
||||
output = []
|
||||
scene_placeholders = self._collect_scene_placeholders()
|
||||
for node_name, node in scene_placeholders.items():
|
||||
plugin_identifier_knob = node.knob("plugin_identifier")
|
||||
if (
|
||||
plugin_identifier_knob is None
|
||||
or plugin_identifier_knob.getValue() != self.identifier
|
||||
):
|
||||
continue
|
||||
|
||||
placeholder_data = self._parse_placeholder_node_data(node)
|
||||
# TODO do data validations and maybe updgrades if are invalid
|
||||
output.append(
|
||||
LoadPlaceholderItem(node_name, placeholder_data, self)
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def populate_placeholder(self, placeholder):
|
||||
self.populate_load_placeholder(placeholder)
|
||||
|
||||
def repopulate_placeholder(self, placeholder):
|
||||
repre_ids = self._get_loaded_repre_ids()
|
||||
self.populate_load_placeholder(placeholder, repre_ids)
|
||||
|
||||
def get_placeholder_options(self, options=None):
|
||||
return self.get_load_plugin_options(options)
|
||||
|
||||
def cleanup_placeholder(self, placeholder, failed):
|
||||
# deselect all selected nodes
|
||||
placeholder_node = nuke.toNode(placeholder.scene_identifier)
|
||||
|
||||
# getting the latest nodes added
|
||||
# TODO get from shared populate data!
|
||||
nodes_init = placeholder.data["nodes_init"]
|
||||
nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init))
|
||||
self.log.debug("Loaded nodes: {}".format(nodes_loaded))
|
||||
if not nodes_loaded:
|
||||
return
|
||||
|
||||
placeholder.data["delete"] = True
|
||||
|
||||
nodes_loaded = self._move_to_placeholder_group(
|
||||
placeholder, nodes_loaded
|
||||
)
|
||||
placeholder.data["last_loaded"] = nodes_loaded
|
||||
refresh_nodes(nodes_loaded)
|
||||
|
||||
# positioning of the loaded nodes
|
||||
min_x, min_y, _, _ = get_extreme_positions(nodes_loaded)
|
||||
for node in nodes_loaded:
|
||||
xpos = (node.xpos() - min_x) + placeholder_node.xpos()
|
||||
ypos = (node.ypos() - min_y) + placeholder_node.ypos()
|
||||
node.setXYpos(xpos, ypos)
|
||||
refresh_nodes(nodes_loaded)
|
||||
|
||||
# fix the problem of z_order for backdrops
|
||||
self._fix_z_order(placeholder)
|
||||
self._imprint_siblings(placeholder)
|
||||
|
||||
if placeholder.data["nb_children"] == 0:
|
||||
# save initial nodes postions and dimensions, update them
|
||||
# and set inputs and outputs of loaded nodes
|
||||
|
||||
self._imprint_inits()
|
||||
self._update_nodes(placeholder, nuke.allNodes(), nodes_loaded)
|
||||
self._set_loaded_connections(placeholder)
|
||||
|
||||
elif placeholder.data["siblings"]:
|
||||
# create copies of placeholder siblings for the new loaded nodes,
|
||||
# set their inputs and outpus and update all nodes positions and
|
||||
# dimensions and siblings names
|
||||
|
||||
siblings = get_nodes_by_names(placeholder.data["siblings"])
|
||||
refresh_nodes(siblings)
|
||||
copies = self._create_sib_copies(placeholder)
|
||||
new_nodes = list(copies.values()) # copies nodes
|
||||
self._update_nodes(new_nodes, nodes_loaded)
|
||||
placeholder_node.removeKnob(placeholder_node.knob("siblings"))
|
||||
new_nodes_name = get_names_from_nodes(new_nodes)
|
||||
imprint(placeholder_node, {"siblings": new_nodes_name})
|
||||
self._set_copies_connections(placeholder, copies)
|
||||
|
||||
self._update_nodes(
|
||||
nuke.allNodes(),
|
||||
new_nodes + nodes_loaded,
|
||||
20
|
||||
)
|
||||
|
||||
new_siblings = get_names_from_nodes(new_nodes)
|
||||
placeholder.data["siblings"] = new_siblings
|
||||
|
||||
else:
|
||||
# if the placeholder doesn't have siblings, the loaded
|
||||
# nodes will be placed in a free space
|
||||
|
||||
xpointer, ypointer = find_free_space_to_paste_nodes(
|
||||
nodes_loaded, direction="bottom", offset=200
|
||||
)
|
||||
node = nuke.createNode("NoOp")
|
||||
reset_selection()
|
||||
nuke.delete(node)
|
||||
for node in nodes_loaded:
|
||||
xpos = (node.xpos() - min_x) + xpointer
|
||||
ypos = (node.ypos() - min_y) + ypointer
|
||||
node.setXYpos(xpos, ypos)
|
||||
|
||||
placeholder.data["nb_children"] += 1
|
||||
reset_selection()
|
||||
# go back to root group
|
||||
nuke.root().begin()
|
||||
|
||||
def _move_to_placeholder_group(self, placeholder, nodes_loaded):
|
||||
"""
|
||||
opening the placeholder's group and copying loaded nodes in it.
|
||||
|
||||
Returns :
|
||||
copies (dict) : with copied nodes names and their copies
|
||||
nodes_loaded (list): the new list of pasted nodes
|
||||
"""
|
||||
|
||||
copies = {}
|
||||
siblings = get_nodes_by_names(self.data["siblings"])
|
||||
for node in siblings:
|
||||
new_node = duplicate_node(node)
|
||||
groups_name = placeholder.data["group_name"]
|
||||
reset_selection()
|
||||
select_nodes(nodes_loaded)
|
||||
if groups_name:
|
||||
with node_tempfile() as filepath:
|
||||
nuke.nodeCopy(filepath)
|
||||
for node in nuke.selectedNodes():
|
||||
nuke.delete(node)
|
||||
group = nuke.toNode(groups_name)
|
||||
group.begin()
|
||||
nuke.nodePaste(filepath)
|
||||
nodes_loaded = nuke.selectedNodes()
|
||||
return nodes_loaded
|
||||
|
||||
x_init = int(new_node.knob("x_init").getValue())
|
||||
y_init = int(new_node.knob("y_init").getValue())
|
||||
new_node.setXYpos(x_init, y_init)
|
||||
if isinstance(new_node, nuke.BackdropNode):
|
||||
w_init = new_node.knob("w_init").getValue()
|
||||
h_init = new_node.knob("h_init").getValue()
|
||||
new_node.knob("bdwidth").setValue(w_init)
|
||||
new_node.knob("bdheight").setValue(h_init)
|
||||
refresh_node(node)
|
||||
|
||||
if "repre_id" in node.knobs().keys():
|
||||
node.removeKnob(node.knob("repre_id"))
|
||||
copies[node.name()] = new_node
|
||||
return copies
|
||||
|
||||
def fix_z_order(self):
|
||||
def _fix_z_order(self, placeholder):
|
||||
"""Fix the problem of z_order when a backdrop is loaded."""
|
||||
|
||||
nodes_loaded = self.data["last_loaded"]
|
||||
nodes_loaded = placeholder.data["last_loaded"]
|
||||
loaded_backdrops = []
|
||||
bd_orders = set()
|
||||
for node in nodes_loaded:
|
||||
|
|
@ -287,7 +313,7 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
return
|
||||
|
||||
sib_orders = set()
|
||||
for node_name in self.data["siblings"]:
|
||||
for node_name in placeholder.data["siblings"]:
|
||||
node = nuke.toNode(node_name)
|
||||
if isinstance(node, nuke.BackdropNode):
|
||||
sib_orders.add(node.knob("z_order").getValue())
|
||||
|
|
@ -302,7 +328,56 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
backdrop_node.knob("z_order").setValue(
|
||||
z_order + max_order - min_order + 1)
|
||||
|
||||
def update_nodes(self, nodes, considered_nodes, offset_y=None):
|
||||
def _imprint_siblings(self, placeholder):
|
||||
"""
|
||||
- add siblings names to placeholder attributes (nodes loaded with it)
|
||||
- add Id to the attributes of all the other nodes
|
||||
"""
|
||||
|
||||
loaded_nodes = placeholder.data["last_loaded"]
|
||||
loaded_nodes_set = set(loaded_nodes)
|
||||
data = {"repre_id": str(placeholder.data["last_repre_id"])}
|
||||
|
||||
for node in loaded_nodes:
|
||||
node_knobs = node.knobs()
|
||||
if "builder_type" not in node_knobs:
|
||||
# save the id of representation for all imported nodes
|
||||
imprint(node, data)
|
||||
node.knob("repre_id").setVisible(False)
|
||||
refresh_node(node)
|
||||
continue
|
||||
|
||||
if (
|
||||
"is_placeholder" not in node_knobs
|
||||
or (
|
||||
"is_placeholder" in node_knobs
|
||||
and node.knob("is_placeholder").value()
|
||||
)
|
||||
):
|
||||
siblings = list(loaded_nodes_set - {node})
|
||||
siblings_name = get_names_from_nodes(siblings)
|
||||
siblings = {"siblings": siblings_name}
|
||||
imprint(node, siblings)
|
||||
|
||||
def _imprint_inits(self):
|
||||
"""Add initial positions and dimensions to the attributes"""
|
||||
|
||||
for node in nuke.allNodes():
|
||||
refresh_node(node)
|
||||
imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()})
|
||||
node.knob("x_init").setVisible(False)
|
||||
node.knob("y_init").setVisible(False)
|
||||
width = node.screenWidth()
|
||||
height = node.screenHeight()
|
||||
if "bdwidth" in node.knobs():
|
||||
imprint(node, {"w_init": width, "h_init": height})
|
||||
node.knob("w_init").setVisible(False)
|
||||
node.knob("h_init").setVisible(False)
|
||||
refresh_node(node)
|
||||
|
||||
def _update_nodes(
|
||||
self, placeholder, nodes, considered_nodes, offset_y=None
|
||||
):
|
||||
"""Adjust backdrop nodes dimensions and positions.
|
||||
|
||||
Considering some nodes sizes.
|
||||
|
|
@ -314,7 +389,7 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
offset (int): distance between copies
|
||||
"""
|
||||
|
||||
placeholder_node = self.data["node"]
|
||||
placeholder_node = nuke.toNode(placeholder.scene_identifier)
|
||||
|
||||
min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes)
|
||||
|
||||
|
|
@ -330,7 +405,7 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
min_x = placeholder_node.xpos()
|
||||
min_y = placeholder_node.ypos()
|
||||
else:
|
||||
siblings = get_nodes_by_names(self.data["siblings"])
|
||||
siblings = get_nodes_by_names(placeholder.data["siblings"])
|
||||
minX, _, maxX, _ = get_extreme_positions(siblings)
|
||||
diff_y = max_y - min_y + 20
|
||||
diff_x = abs(max_x - min_x - maxX + minX)
|
||||
|
|
@ -369,59 +444,14 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
|
||||
refresh_node(node)
|
||||
|
||||
def imprint_inits(self):
|
||||
"""Add initial positions and dimensions to the attributes"""
|
||||
|
||||
for node in nuke.allNodes():
|
||||
refresh_node(node)
|
||||
imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()})
|
||||
node.knob("x_init").setVisible(False)
|
||||
node.knob("y_init").setVisible(False)
|
||||
width = node.screenWidth()
|
||||
height = node.screenHeight()
|
||||
if "bdwidth" in node.knobs():
|
||||
imprint(node, {"w_init": width, "h_init": height})
|
||||
node.knob("w_init").setVisible(False)
|
||||
node.knob("h_init").setVisible(False)
|
||||
refresh_node(node)
|
||||
|
||||
def imprint_siblings(self):
|
||||
"""
|
||||
- add siblings names to placeholder attributes (nodes loaded with it)
|
||||
- add Id to the attributes of all the other nodes
|
||||
"""
|
||||
|
||||
loaded_nodes = self.data["last_loaded"]
|
||||
loaded_nodes_set = set(loaded_nodes)
|
||||
data = {"repre_id": str(self.data["last_repre_id"])}
|
||||
|
||||
for node in loaded_nodes:
|
||||
node_knobs = node.knobs()
|
||||
if "builder_type" not in node_knobs:
|
||||
# save the id of representation for all imported nodes
|
||||
imprint(node, data)
|
||||
node.knob("repre_id").setVisible(False)
|
||||
refresh_node(node)
|
||||
continue
|
||||
|
||||
if (
|
||||
"is_placeholder" not in node_knobs
|
||||
or (
|
||||
"is_placeholder" in node_knobs
|
||||
and node.knob("is_placeholder").value()
|
||||
)
|
||||
):
|
||||
siblings = list(loaded_nodes_set - {node})
|
||||
siblings_name = get_names_from_nodes(siblings)
|
||||
siblings = {"siblings": siblings_name}
|
||||
imprint(node, siblings)
|
||||
|
||||
def set_loaded_connections(self):
|
||||
def _set_loaded_connections(self, placeholder):
|
||||
"""
|
||||
set inputs and outputs of loaded nodes"""
|
||||
|
||||
placeholder_node = self.data["node"]
|
||||
input_node, output_node = get_group_io_nodes(self.data["last_loaded"])
|
||||
placeholder_node = nuke.toNode(placeholder.scene_identifier)
|
||||
input_node, output_node = get_group_io_nodes(
|
||||
placeholder.data["last_loaded"]
|
||||
)
|
||||
for node in placeholder_node.dependent():
|
||||
for idx in range(node.inputs()):
|
||||
if node.input(idx) == placeholder_node:
|
||||
|
|
@ -432,15 +462,45 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
if placeholder_node.input(idx) == node:
|
||||
input_node.setInput(0, node)
|
||||
|
||||
def set_copies_connections(self, copies):
|
||||
def _create_sib_copies(self, placeholder):
|
||||
""" creating copies of the palce_holder siblings (the ones who were
|
||||
loaded with it) for the new nodes added
|
||||
|
||||
Returns :
|
||||
copies (dict) : with copied nodes names and their copies
|
||||
"""
|
||||
|
||||
copies = {}
|
||||
siblings = get_nodes_by_names(placeholder.data["siblings"])
|
||||
for node in siblings:
|
||||
new_node = duplicate_node(node)
|
||||
|
||||
x_init = int(new_node.knob("x_init").getValue())
|
||||
y_init = int(new_node.knob("y_init").getValue())
|
||||
new_node.setXYpos(x_init, y_init)
|
||||
if isinstance(new_node, nuke.BackdropNode):
|
||||
w_init = new_node.knob("w_init").getValue()
|
||||
h_init = new_node.knob("h_init").getValue()
|
||||
new_node.knob("bdwidth").setValue(w_init)
|
||||
new_node.knob("bdheight").setValue(h_init)
|
||||
refresh_node(node)
|
||||
|
||||
if "repre_id" in node.knobs().keys():
|
||||
node.removeKnob(node.knob("repre_id"))
|
||||
copies[node.name()] = new_node
|
||||
return copies
|
||||
|
||||
def _set_copies_connections(self, placeholder, copies):
|
||||
"""Set inputs and outputs of the copies.
|
||||
|
||||
Args:
|
||||
copies (dict): Copied nodes by their names.
|
||||
"""
|
||||
|
||||
last_input, last_output = get_group_io_nodes(self.data["last_loaded"])
|
||||
siblings = get_nodes_by_names(self.data["siblings"])
|
||||
last_input, last_output = get_group_io_nodes(
|
||||
placeholder.data["last_loaded"]
|
||||
)
|
||||
siblings = get_nodes_by_names(placeholder.data["siblings"])
|
||||
siblings_input, siblings_output = get_group_io_nodes(siblings)
|
||||
copy_input = copies[siblings_input.name()]
|
||||
copy_output = copies[siblings_output.name()]
|
||||
|
|
@ -474,166 +534,45 @@ class NukePlaceholder(AbstractPlaceholder):
|
|||
|
||||
siblings_input.setInput(0, copy_output)
|
||||
|
||||
def move_to_placeholder_group(self, nodes_loaded):
|
||||
"""
|
||||
opening the placeholder's group and copying loaded nodes in it.
|
||||
|
||||
Returns :
|
||||
nodes_loaded (list): the new list of pasted nodes
|
||||
"""
|
||||
def build_workfile_template(*args):
|
||||
builder = NukeTemplateBuilder(registered_host())
|
||||
builder.build_template()
|
||||
|
||||
groups_name = self.data["group_name"]
|
||||
reset_selection()
|
||||
select_nodes(nodes_loaded)
|
||||
if groups_name:
|
||||
with node_tempfile() as filepath:
|
||||
nuke.nodeCopy(filepath)
|
||||
for node in nuke.selectedNodes():
|
||||
nuke.delete(node)
|
||||
group = nuke.toNode(groups_name)
|
||||
group.begin()
|
||||
nuke.nodePaste(filepath)
|
||||
nodes_loaded = nuke.selectedNodes()
|
||||
return nodes_loaded
|
||||
|
||||
def clean(self):
|
||||
# deselect all selected nodes
|
||||
placeholder_node = self.data["node"]
|
||||
def update_workfile_template(*args):
|
||||
builder = NukeTemplateBuilder(registered_host())
|
||||
builder.rebuild_template()
|
||||
|
||||
# getting the latest nodes added
|
||||
nodes_init = self.data["nodes_init"]
|
||||
nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init))
|
||||
self.log.debug("Loaded nodes: {}".format(nodes_loaded))
|
||||
if not nodes_loaded:
|
||||
return
|
||||
|
||||
self.data["delete"] = True
|
||||
def create_placeholder(*args):
|
||||
host = registered_host()
|
||||
builder = NukeTemplateBuilder(host)
|
||||
window = WorkfileBuildPlaceholderDialog(host, builder)
|
||||
window.exec_()
|
||||
|
||||
nodes_loaded = self.move_to_placeholder_group(nodes_loaded)
|
||||
self.data["last_loaded"] = nodes_loaded
|
||||
refresh_nodes(nodes_loaded)
|
||||
|
||||
# positioning of the loaded nodes
|
||||
min_x, min_y, _, _ = get_extreme_positions(nodes_loaded)
|
||||
for node in nodes_loaded:
|
||||
xpos = (node.xpos() - min_x) + placeholder_node.xpos()
|
||||
ypos = (node.ypos() - min_y) + placeholder_node.ypos()
|
||||
node.setXYpos(xpos, ypos)
|
||||
refresh_nodes(nodes_loaded)
|
||||
def update_placeholder(*args):
|
||||
host = registered_host()
|
||||
builder = NukeTemplateBuilder(host)
|
||||
placeholder_items_by_id = {
|
||||
placeholder_item.scene_identifier: placeholder_item
|
||||
for placeholder_item in builder.get_placeholders()
|
||||
}
|
||||
placeholder_items = []
|
||||
for node in nuke.selectedNodes():
|
||||
node_name = node.fullName()
|
||||
if node_name in placeholder_items_by_id:
|
||||
placeholder_items.append(placeholder_items_by_id[node_name])
|
||||
|
||||
self.fix_z_order() # fix the problem of z_order for backdrops
|
||||
self.imprint_siblings()
|
||||
# TODO show UI at least
|
||||
if len(placeholder_items) == 0:
|
||||
raise ValueError("No node selected")
|
||||
|
||||
if self.data["nb_children"] == 0:
|
||||
# save initial nodes postions and dimensions, update them
|
||||
# and set inputs and outputs of loaded nodes
|
||||
if len(placeholder_items) > 1:
|
||||
raise ValueError("Too many selected nodes")
|
||||
|
||||
self.imprint_inits()
|
||||
self.update_nodes(nuke.allNodes(), nodes_loaded)
|
||||
self.set_loaded_connections()
|
||||
|
||||
elif self.data["siblings"]:
|
||||
# create copies of placeholder siblings for the new loaded nodes,
|
||||
# set their inputs and outpus and update all nodes positions and
|
||||
# dimensions and siblings names
|
||||
|
||||
siblings = get_nodes_by_names(self.data["siblings"])
|
||||
refresh_nodes(siblings)
|
||||
copies = self.create_sib_copies()
|
||||
new_nodes = list(copies.values()) # copies nodes
|
||||
self.update_nodes(new_nodes, nodes_loaded)
|
||||
placeholder_node.removeKnob(placeholder_node.knob("siblings"))
|
||||
new_nodes_name = get_names_from_nodes(new_nodes)
|
||||
imprint(placeholder_node, {"siblings": new_nodes_name})
|
||||
self.set_copies_connections(copies)
|
||||
|
||||
self.update_nodes(
|
||||
nuke.allNodes(),
|
||||
new_nodes + nodes_loaded,
|
||||
20
|
||||
)
|
||||
|
||||
new_siblings = get_names_from_nodes(new_nodes)
|
||||
self.data["siblings"] = new_siblings
|
||||
|
||||
else:
|
||||
# if the placeholder doesn't have siblings, the loaded
|
||||
# nodes will be placed in a free space
|
||||
|
||||
xpointer, ypointer = find_free_space_to_paste_nodes(
|
||||
nodes_loaded, direction="bottom", offset=200
|
||||
)
|
||||
node = nuke.createNode("NoOp")
|
||||
reset_selection()
|
||||
nuke.delete(node)
|
||||
for node in nodes_loaded:
|
||||
xpos = (node.xpos() - min_x) + xpointer
|
||||
ypos = (node.ypos() - min_y) + ypointer
|
||||
node.setXYpos(xpos, ypos)
|
||||
|
||||
self.data["nb_children"] += 1
|
||||
reset_selection()
|
||||
# go back to root group
|
||||
nuke.root().begin()
|
||||
|
||||
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": [re.compile(self.data["asset"])],
|
||||
"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": [
|
||||
current_asset_doc["name"],
|
||||
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)
|
||||
|
||||
if not linked_asset_names:
|
||||
return []
|
||||
|
||||
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()]
|
||||
)
|
||||
)
|
||||
)
|
||||
placeholder_item = placeholder_items[0]
|
||||
window = WorkfileBuildPlaceholderDialog(host, builder)
|
||||
window.set_update_mode(placeholder_item)
|
||||
window.exec_()
|
||||
|
|
@ -25,6 +25,8 @@ class CollectReview(pyblish.api.ContextPlugin):
|
|||
hosts = ["photoshop"]
|
||||
order = pyblish.api.CollectorOrder + 0.1
|
||||
|
||||
publish = True
|
||||
|
||||
def process(self, context):
|
||||
family = "review"
|
||||
subset = get_subset_name(
|
||||
|
|
@ -45,5 +47,6 @@ class CollectReview(pyblish.api.ContextPlugin):
|
|||
"family": family,
|
||||
"families": [],
|
||||
"representations": [],
|
||||
"asset": os.environ["AVALON_ASSET"]
|
||||
"asset": os.environ["AVALON_ASSET"],
|
||||
"publish": self.publish
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype.pipeline import (
|
|||
register_creator_plugin_path,
|
||||
legacy_io,
|
||||
)
|
||||
from openpype.host import HostBase, INewPublisher
|
||||
from openpype.host import HostBase, IPublishHost
|
||||
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(
|
||||
|
|
@ -19,7 +19,7 @@ PUBLISH_PATH = os.path.join(ROOT_DIR, "plugins", "publish")
|
|||
CREATE_PATH = os.path.join(ROOT_DIR, "plugins", "create")
|
||||
|
||||
|
||||
class TrayPublisherHost(HostBase, INewPublisher):
|
||||
class TrayPublisherHost(HostBase, IPublishHost):
|
||||
name = "traypublisher"
|
||||
|
||||
def install(self):
|
||||
|
|
|
|||
|
|
@ -646,9 +646,6 @@ def rename_filepaths_by_frame_start(
|
|||
filepaths_by_frame, range_start, range_end, new_frame_start
|
||||
):
|
||||
"""Change frames in filenames of finished images to new frame start."""
|
||||
# Skip if source first frame is same as destination first frame
|
||||
if range_start == new_frame_start:
|
||||
return
|
||||
|
||||
# Calculate frame end
|
||||
new_frame_end = range_end + (new_frame_start - range_start)
|
||||
|
|
@ -669,14 +666,17 @@ def rename_filepaths_by_frame_start(
|
|||
source_range = range(range_start, range_end + 1)
|
||||
output_range = range(new_frame_start, new_frame_end + 1)
|
||||
|
||||
# Skip if source first frame is same as destination first frame
|
||||
new_dst_filepaths = {}
|
||||
for src_frame, dst_frame in zip(source_range, output_range):
|
||||
src_filepath = filepaths_by_frame[src_frame]
|
||||
src_dirpath = os.path.dirname(src_filepath)
|
||||
src_filepath = os.path.normpath(filepaths_by_frame[src_frame])
|
||||
dirpath, src_filename = os.path.split(src_filepath)
|
||||
dst_filename = filename_template.format(frame=dst_frame)
|
||||
dst_filepath = os.path.join(src_dirpath, dst_filename)
|
||||
dst_filepath = os.path.join(dirpath, dst_filename)
|
||||
|
||||
os.rename(src_filepath, dst_filepath)
|
||||
if src_filename != dst_filename:
|
||||
os.rename(src_filepath, dst_filepath)
|
||||
|
||||
new_dst_filepaths[dst_frame] = dst_filepath
|
||||
|
||||
return new_dst_filepaths
|
||||
|
|
|
|||
|
|
@ -9,6 +9,49 @@ import six
|
|||
import clique
|
||||
|
||||
|
||||
def get_attributes_keys(attribute_definitions):
|
||||
"""Collect keys from list of attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AbtractAttrDef]): Objects of attribute
|
||||
definitions.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys that will be created using passed attribute definitions.
|
||||
"""
|
||||
|
||||
keys = set()
|
||||
if not attribute_definitions:
|
||||
return keys
|
||||
|
||||
for attribute_def in attribute_definitions:
|
||||
if not isinstance(attribute_def, UIDef):
|
||||
keys.add(attribute_def.key)
|
||||
return keys
|
||||
|
||||
|
||||
def get_default_values(attribute_definitions):
|
||||
"""Receive default values for attribute definitions.
|
||||
|
||||
Args:
|
||||
attribute_definitions (List[AbtractAttrDef]): Attribute definitions for
|
||||
which default values should be collected.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Default values for passet attribute definitions.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
if not attribute_definitions:
|
||||
return output
|
||||
|
||||
for attr_def in attribute_definitions:
|
||||
# Skip UI definitions
|
||||
if not isinstance(attr_def, UIDef):
|
||||
output[attr_def.key] = attr_def.default
|
||||
return output
|
||||
|
||||
|
||||
class AbstractAttrDefMeta(ABCMeta):
|
||||
"""Meta class to validate existence of 'key' attribute.
|
||||
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ def convert_value_by_type_name(value_type, value, logger=None):
|
|||
return float(value)
|
||||
|
||||
# Vectors will probably have more types
|
||||
if value_type == "vec2f":
|
||||
if value_type in ("vec2f", "float2"):
|
||||
return [float(item) for item in value.split(",")]
|
||||
|
||||
# Matrix should be always have square size of element 3x3, 4x4
|
||||
|
|
@ -204,8 +204,8 @@ def convert_value_by_type_name(value_type, value, logger=None):
|
|||
)
|
||||
return output
|
||||
|
||||
logger.info((
|
||||
"MISSING IMPLEMENTATION:"
|
||||
logger.debug((
|
||||
"Dev note (missing implementation):"
|
||||
" Unknown attrib type \"{}\". Value: {}"
|
||||
).format(value_type, value))
|
||||
return value
|
||||
|
|
@ -263,8 +263,8 @@ def parse_oiio_xml_output(xml_string, logger=None):
|
|||
# - feel free to add more tags
|
||||
else:
|
||||
value = child.text
|
||||
logger.info((
|
||||
"MISSING IMPLEMENTATION:"
|
||||
logger.debug((
|
||||
"Dev note (missing implementation):"
|
||||
" Unknown tag \"{}\". Value \"{}\""
|
||||
).format(tag_name, value))
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ def convert_value_by_type_name(value_type, value):
|
|||
return float(value)
|
||||
|
||||
# Vectors will probably have more types
|
||||
if value_type == "vec2f":
|
||||
if value_type in ("vec2f", "float2"):
|
||||
return [float(item) for item in value.split(",")]
|
||||
|
||||
# Matrix should be always have square size of element 3x3, 4x4
|
||||
|
|
@ -127,7 +127,7 @@ def convert_value_by_type_name(value_type, value):
|
|||
return output
|
||||
|
||||
print((
|
||||
"MISSING IMPLEMENTATION:"
|
||||
"Dev note (missing implementation):"
|
||||
" Unknown attrib type \"{}\". Value: {}"
|
||||
).format(value_type, value))
|
||||
return value
|
||||
|
|
@ -183,7 +183,7 @@ def parse_oiio_xml_output(xml_string):
|
|||
else:
|
||||
value = child.text
|
||||
print((
|
||||
"MISSING IMPLEMENTATION:"
|
||||
"Dev note (missing implementation):"
|
||||
" Unknown tag \"{}\". Value \"{}\""
|
||||
).format(tag_name, value))
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ from uuid import uuid4
|
|||
from contextlib import contextmanager
|
||||
|
||||
from openpype.client import get_assets
|
||||
from openpype.host import INewPublisher
|
||||
from openpype.settings import (
|
||||
get_system_settings,
|
||||
get_project_settings
|
||||
)
|
||||
from openpype.host import IPublishHost
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.pipeline.mongodb import (
|
||||
AvalonMongoDB,
|
||||
|
|
@ -20,11 +24,6 @@ from .creator_plugins import (
|
|||
discover_creator_plugins,
|
||||
)
|
||||
|
||||
from openpype.api import (
|
||||
get_system_settings,
|
||||
get_project_settings
|
||||
)
|
||||
|
||||
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
|
||||
|
||||
|
||||
|
|
@ -402,8 +401,12 @@ class CreatedInstance:
|
|||
self.creator = creator
|
||||
|
||||
# Instance members may have actions on them
|
||||
# TODO implement members logic
|
||||
self._members = []
|
||||
|
||||
# Data that can be used for lifetime of object
|
||||
self._transient_data = {}
|
||||
|
||||
# Create a copy of passed data to avoid changing them on the fly
|
||||
data = copy.deepcopy(data or {})
|
||||
# Store original value of passed data
|
||||
|
|
@ -596,6 +599,26 @@ class CreatedInstance:
|
|||
|
||||
return self
|
||||
|
||||
@property
|
||||
def transient_data(self):
|
||||
"""Data stored for lifetime of instance object.
|
||||
|
||||
These data are not stored to scene and will be lost on object
|
||||
deletion.
|
||||
|
||||
Can be used to store objects. In some host implementations is not
|
||||
possible to reference to object in scene with some unique identifier
|
||||
(e.g. node in Fusion.). In that case it is handy to store the object
|
||||
here. Should be used that way only if instance data are stored on the
|
||||
node itself.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary object where you can store data related
|
||||
to instance for lifetime of instance object.
|
||||
"""
|
||||
|
||||
return self._transient_data
|
||||
|
||||
def changes(self):
|
||||
"""Calculate and return changes."""
|
||||
|
||||
|
|
@ -771,7 +794,7 @@ class CreateContext:
|
|||
"""
|
||||
|
||||
missing = set(
|
||||
INewPublisher.get_missing_publish_methods(host)
|
||||
IPublishHost.get_missing_publish_methods(host)
|
||||
)
|
||||
return missing
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,13 @@ class BaseCreator:
|
|||
# - we may use UI inside processing this attribute should be checked
|
||||
self.headless = headless
|
||||
|
||||
self.apply_settings(project_settings, system_settings)
|
||||
|
||||
def apply_settings(self, project_settings, system_settings):
|
||||
"""Method called on initialization of plugin to apply settings."""
|
||||
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""Identifier of creator (must be unique).
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from .utils import (
|
|||
InvalidRepresentationContext,
|
||||
|
||||
get_repres_contexts,
|
||||
get_contexts_for_repre_docs,
|
||||
get_subset_contexts,
|
||||
get_representation_context,
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ __all__ = (
|
|||
"InvalidRepresentationContext",
|
||||
|
||||
"get_repres_contexts",
|
||||
"get_contexts_for_repre_docs",
|
||||
"get_subset_contexts",
|
||||
"get_representation_context",
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ log = logging.getLogger(__name__)
|
|||
|
||||
ContainersFilterResult = collections.namedtuple(
|
||||
"ContainersFilterResult",
|
||||
["latest", "outdated", "not_foud", "invalid"]
|
||||
["latest", "outdated", "not_found", "invalid"]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -87,13 +87,20 @@ def get_repres_contexts(representation_ids, dbcon=None):
|
|||
if not dbcon:
|
||||
dbcon = legacy_io
|
||||
|
||||
contexts = {}
|
||||
if not representation_ids:
|
||||
return contexts
|
||||
return {}
|
||||
|
||||
project_name = dbcon.active_project()
|
||||
repre_docs = get_representations(project_name, representation_ids)
|
||||
|
||||
return get_contexts_for_repre_docs(project_name, repre_docs)
|
||||
|
||||
|
||||
def get_contexts_for_repre_docs(project_name, repre_docs):
|
||||
contexts = {}
|
||||
if not repre_docs:
|
||||
return contexts
|
||||
|
||||
repre_docs_by_id = {}
|
||||
version_ids = set()
|
||||
for repre_doc in repre_docs:
|
||||
|
|
@ -808,7 +815,7 @@ def filter_containers(containers, project_name):
|
|||
|
||||
Categories are 'latest', 'outdated', 'invalid' and 'not_found'.
|
||||
The 'lastest' containers are from last version, 'outdated' are not,
|
||||
'invalid' are invalid containers (invalid content) and 'not_foud' has
|
||||
'invalid' are invalid containers (invalid content) and 'not_found' has
|
||||
some missing entity in database.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -1,528 +0,0 @@
|
|||
import os
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import six
|
||||
import logging
|
||||
from functools import reduce
|
||||
|
||||
from openpype.client import (
|
||||
get_asset_by_name,
|
||||
get_linked_assets,
|
||||
)
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib import (
|
||||
StringTemplate,
|
||||
Logger,
|
||||
filter_profiles,
|
||||
)
|
||||
from openpype.pipeline import legacy_io, Anatomy
|
||||
from openpype.pipeline.load import (
|
||||
get_loaders_by_name,
|
||||
get_representation_context,
|
||||
load_with_repre_context,
|
||||
)
|
||||
|
||||
from .build_template_exceptions import (
|
||||
TemplateAlreadyImported,
|
||||
TemplateLoadingFailed,
|
||||
TemplateProfileNotFound,
|
||||
TemplateNotFound
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def update_representations(entities, entity):
|
||||
if entity['context']['subset'] not in entities:
|
||||
entities[entity['context']['subset']] = entity
|
||||
else:
|
||||
current = entities[entity['context']['subset']]
|
||||
incomming = entity
|
||||
entities[entity['context']['subset']] = max(
|
||||
current, incomming,
|
||||
key=lambda entity: entity["context"].get("version", -1))
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def parse_loader_args(loader_args):
|
||||
if not loader_args:
|
||||
return dict()
|
||||
try:
|
||||
parsed_args = eval(loader_args)
|
||||
if not isinstance(parsed_args, dict):
|
||||
return dict()
|
||||
else:
|
||||
return parsed_args
|
||||
except Exception as err:
|
||||
print(
|
||||
"Error while parsing loader arguments '{}'.\n{}: {}\n\n"
|
||||
"Continuing with default arguments. . .".format(
|
||||
loader_args,
|
||||
err.__class__.__name__,
|
||||
err))
|
||||
return dict()
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class AbstractTemplateLoader:
|
||||
"""
|
||||
Abstraction of Template Loader.
|
||||
Properties:
|
||||
template_path : property to get current template path
|
||||
Methods:
|
||||
import_template : Abstract Method. Used to load template,
|
||||
depending on current host
|
||||
get_template_nodes : Abstract Method. Used to query nodes acting
|
||||
as placeholders. Depending on current host
|
||||
"""
|
||||
|
||||
_log = None
|
||||
|
||||
def __init__(self, placeholder_class):
|
||||
# TODO template loader should expect host as and argument
|
||||
# - host have all responsibility for most of code (also provide
|
||||
# placeholder class)
|
||||
# - also have responsibility for current context
|
||||
# - this won't work in DCCs where multiple workfiles with
|
||||
# different contexts can be opened at single time
|
||||
# - template loader should have ability to change context
|
||||
project_name = legacy_io.active_project()
|
||||
asset_name = legacy_io.Session["AVALON_ASSET"]
|
||||
|
||||
self.loaders_by_name = get_loaders_by_name()
|
||||
self.current_asset = asset_name
|
||||
self.project_name = project_name
|
||||
self.host_name = legacy_io.Session["AVALON_APP"]
|
||||
self.task_name = legacy_io.Session["AVALON_TASK"]
|
||||
self.placeholder_class = placeholder_class
|
||||
self.current_asset_doc = get_asset_by_name(project_name, asset_name)
|
||||
self.task_type = (
|
||||
self.current_asset_doc
|
||||
.get("data", {})
|
||||
.get("tasks", {})
|
||||
.get(self.task_name, {})
|
||||
.get("type")
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"BUILDING ASSET FROM TEMPLATE :\n"
|
||||
"Starting templated build for {asset} in {project}\n\n"
|
||||
"Asset : {asset}\n"
|
||||
"Task : {task_name} ({task_type})\n"
|
||||
"Host : {host}\n"
|
||||
"Project : {project}\n".format(
|
||||
asset=self.current_asset,
|
||||
host=self.host_name,
|
||||
project=self.project_name,
|
||||
task_name=self.task_name,
|
||||
task_type=self.task_type
|
||||
))
|
||||
# Skip if there is no loader
|
||||
if not self.loaders_by_name:
|
||||
self.log.warning(
|
||||
"There is no registered loaders. No assets will be loaded")
|
||||
return
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def template_already_imported(self, err_msg):
|
||||
"""In case template was already loaded.
|
||||
Raise the error as a default action.
|
||||
Override this method in your template loader implementation
|
||||
to manage this case."""
|
||||
self.log.error("{}: {}".format(
|
||||
err_msg.__class__.__name__,
|
||||
err_msg))
|
||||
raise TemplateAlreadyImported(err_msg)
|
||||
|
||||
def template_loading_failed(self, err_msg):
|
||||
"""In case template loading failed
|
||||
Raise the error as a default action.
|
||||
Override this method in your template loader implementation
|
||||
to manage this case.
|
||||
"""
|
||||
self.log.error("{}: {}".format(
|
||||
err_msg.__class__.__name__,
|
||||
err_msg))
|
||||
raise TemplateLoadingFailed(err_msg)
|
||||
|
||||
@property
|
||||
def template_path(self):
|
||||
"""
|
||||
Property returning template path. Avoiding setter.
|
||||
Getting template path from open pype settings based on current avalon
|
||||
session and solving the path variables if needed.
|
||||
Returns:
|
||||
str: Solved template path
|
||||
Raises:
|
||||
TemplateProfileNotFound: No profile found from settings for
|
||||
current avalon session
|
||||
KeyError: Could not solve path because a key does not exists
|
||||
in avalon context
|
||||
TemplateNotFound: Solved path does not exists on current filesystem
|
||||
"""
|
||||
project_name = self.project_name
|
||||
host_name = self.host_name
|
||||
task_name = self.task_name
|
||||
task_type = self.task_type
|
||||
|
||||
anatomy = Anatomy(project_name)
|
||||
project_settings = get_project_settings(project_name)
|
||||
|
||||
build_info = project_settings[host_name]["templated_workfile_build"]
|
||||
profile = filter_profiles(
|
||||
build_info["profiles"],
|
||||
{
|
||||
"task_types": task_type,
|
||||
"task_names": task_name
|
||||
}
|
||||
)
|
||||
|
||||
if not profile:
|
||||
raise TemplateProfileNotFound(
|
||||
"No matching profile found for task '{}' of type '{}' "
|
||||
"with host '{}'".format(task_name, task_type, host_name)
|
||||
)
|
||||
|
||||
path = profile["path"]
|
||||
if not path:
|
||||
raise TemplateLoadingFailed(
|
||||
"Template path is not set.\n"
|
||||
"Path need to be set in {}\\Template Workfile Build "
|
||||
"Settings\\Profiles".format(host_name.title()))
|
||||
|
||||
# Try fill path with environments and anatomy roots
|
||||
fill_data = {
|
||||
key: value
|
||||
for key, value in os.environ.items()
|
||||
}
|
||||
fill_data["root"] = anatomy.roots
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
|
||||
if path and os.path.exists(path):
|
||||
self.log.info("Found template at: '{}'".format(path))
|
||||
return path
|
||||
|
||||
solved_path = None
|
||||
while True:
|
||||
try:
|
||||
solved_path = anatomy.path_remapper(path)
|
||||
except KeyError as missing_key:
|
||||
raise KeyError(
|
||||
"Could not solve key '{}' in template path '{}'".format(
|
||||
missing_key, path))
|
||||
|
||||
if solved_path is None:
|
||||
solved_path = path
|
||||
if solved_path == path:
|
||||
break
|
||||
path = solved_path
|
||||
|
||||
solved_path = os.path.normpath(solved_path)
|
||||
if not os.path.exists(solved_path):
|
||||
raise TemplateNotFound(
|
||||
"Template found in openPype settings for task '{}' with host "
|
||||
"'{}' does not exists. (Not found : {})".format(
|
||||
task_name, host_name, solved_path))
|
||||
|
||||
self.log.info("Found template at: '{}'".format(solved_path))
|
||||
|
||||
return solved_path
|
||||
|
||||
def populate_template(self, ignored_ids=None):
|
||||
"""
|
||||
Use template placeholders to load assets and parent them in hierarchy
|
||||
Arguments :
|
||||
ignored_ids :
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
loaders_by_name = self.loaders_by_name
|
||||
current_asset_doc = self.current_asset_doc
|
||||
linked_assets = get_linked_assets(current_asset_doc)
|
||||
|
||||
ignored_ids = ignored_ids or []
|
||||
placeholders = self.get_placeholders()
|
||||
self.log.debug("Placeholders found in template: {}".format(
|
||||
[placeholder.name for placeholder in placeholders]
|
||||
))
|
||||
for placeholder in placeholders:
|
||||
self.log.debug("Start to processing placeholder {}".format(
|
||||
placeholder.name
|
||||
))
|
||||
placeholder_representations = self.get_placeholder_representations(
|
||||
placeholder,
|
||||
current_asset_doc,
|
||||
linked_assets
|
||||
)
|
||||
|
||||
if not placeholder_representations:
|
||||
self.log.info(
|
||||
"There's no representation for this placeholder: "
|
||||
"{}".format(placeholder.name)
|
||||
)
|
||||
continue
|
||||
|
||||
for representation in placeholder_representations:
|
||||
self.preload(placeholder, loaders_by_name, representation)
|
||||
|
||||
if self.load_data_is_incorrect(
|
||||
placeholder,
|
||||
representation,
|
||||
ignored_ids):
|
||||
continue
|
||||
|
||||
self.log.info(
|
||||
"Loading {}_{} with loader {}\n"
|
||||
"Loader arguments used : {}".format(
|
||||
representation['context']['asset'],
|
||||
representation['context']['subset'],
|
||||
placeholder.loader_name,
|
||||
placeholder.loader_args))
|
||||
|
||||
try:
|
||||
container = self.load(
|
||||
placeholder, loaders_by_name, representation)
|
||||
except Exception:
|
||||
self.load_failed(placeholder, representation)
|
||||
else:
|
||||
self.load_succeed(placeholder, container)
|
||||
finally:
|
||||
self.postload(placeholder)
|
||||
|
||||
def get_placeholder_representations(
|
||||
self, placeholder, current_asset_doc, linked_asset_docs
|
||||
):
|
||||
placeholder_representations = placeholder.get_representations(
|
||||
current_asset_doc,
|
||||
linked_asset_docs
|
||||
)
|
||||
for repre_doc in reduce(
|
||||
update_representations,
|
||||
placeholder_representations,
|
||||
dict()
|
||||
).values():
|
||||
yield repre_doc
|
||||
|
||||
def load_data_is_incorrect(
|
||||
self, placeholder, last_representation, ignored_ids):
|
||||
if not last_representation:
|
||||
self.log.warning(placeholder.err_message())
|
||||
return True
|
||||
if (str(last_representation['_id']) in ignored_ids):
|
||||
print("Ignoring : ", last_representation['_id'])
|
||||
return True
|
||||
return False
|
||||
|
||||
def preload(self, placeholder, loaders_by_name, last_representation):
|
||||
pass
|
||||
|
||||
def load(self, placeholder, loaders_by_name, last_representation):
|
||||
repre = get_representation_context(last_representation)
|
||||
return load_with_repre_context(
|
||||
loaders_by_name[placeholder.loader_name],
|
||||
repre,
|
||||
options=parse_loader_args(placeholder.loader_args))
|
||||
|
||||
def load_succeed(self, placeholder, container):
|
||||
placeholder.parent_in_hierarchy(container)
|
||||
|
||||
def load_failed(self, placeholder, last_representation):
|
||||
self.log.warning(
|
||||
"Got error trying to load {}:{} with {}".format(
|
||||
last_representation['context']['asset'],
|
||||
last_representation['context']['subset'],
|
||||
placeholder.loader_name
|
||||
),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def postload(self, placeholder):
|
||||
placeholder.clean()
|
||||
|
||||
def update_missing_containers(self):
|
||||
loaded_containers_ids = self.get_loaded_containers_by_id()
|
||||
self.populate_template(ignored_ids=loaded_containers_ids)
|
||||
|
||||
def get_placeholders(self):
|
||||
placeholders = map(self.placeholder_class, self.get_template_nodes())
|
||||
valid_placeholders = filter(
|
||||
lambda i: i.is_valid,
|
||||
placeholders
|
||||
)
|
||||
sorted_placeholders = list(sorted(
|
||||
valid_placeholders,
|
||||
key=lambda i: i.order
|
||||
))
|
||||
return sorted_placeholders
|
||||
|
||||
@abstractmethod
|
||||
def get_loaded_containers_by_id(self):
|
||||
"""
|
||||
Collect already loaded containers for updating scene
|
||||
Return:
|
||||
dict (string, node): A dictionnary id as key
|
||||
and containers as value
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def import_template(self, template_path):
|
||||
"""
|
||||
Import template in current host
|
||||
Args:
|
||||
template_path (str): fullpath to current task and
|
||||
host's template file
|
||||
Return:
|
||||
None
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_template_nodes(self):
|
||||
"""
|
||||
Returning a list of nodes acting as host placeholders for
|
||||
templating. The data representation is by user.
|
||||
AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes
|
||||
Args :
|
||||
None
|
||||
Returns:
|
||||
list(AnyNode): Solved template path
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class AbstractPlaceholder:
|
||||
"""Abstraction of placeholders logic.
|
||||
|
||||
Properties:
|
||||
required_keys: A list of mandatory keys to decribe placeholder
|
||||
and assets to load.
|
||||
optional_keys: A list of optional keys to decribe
|
||||
placeholder and assets to load
|
||||
loader_name: Name of linked loader to use while loading assets
|
||||
|
||||
Args:
|
||||
identifier (str): Placeholder identifier. Should be possible to be
|
||||
used as identifier in "a scene" (e.g. unique node name).
|
||||
"""
|
||||
|
||||
required_keys = {
|
||||
"builder_type",
|
||||
"family",
|
||||
"representation",
|
||||
"order",
|
||||
"loader",
|
||||
"loader_args"
|
||||
}
|
||||
optional_keys = {}
|
||||
|
||||
def __init__(self, identifier):
|
||||
self._log = None
|
||||
self._name = identifier
|
||||
self.get_data(identifier)
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(repr(self))
|
||||
return self._log
|
||||
|
||||
def __repr__(self):
|
||||
return "< {} {} >".format(self.__class__.__name__, self.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def loader_args(self):
|
||||
return self.data["loader_args"]
|
||||
|
||||
@property
|
||||
def builder_type(self):
|
||||
return self.data["builder_type"]
|
||||
|
||||
@property
|
||||
def order(self):
|
||||
return self.data["order"]
|
||||
|
||||
@property
|
||||
def loader_name(self):
|
||||
"""Return placeholder loader name.
|
||||
|
||||
Returns:
|
||||
str: Loader name that will be used to load placeholder
|
||||
representations.
|
||||
"""
|
||||
|
||||
return self.data["loader"]
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
"""Test validity of placeholder.
|
||||
|
||||
i.e.: every required key exists in placeholder data
|
||||
|
||||
Returns:
|
||||
bool: True if every key is in data
|
||||
"""
|
||||
|
||||
if set(self.required_keys).issubset(self.data.keys()):
|
||||
self.log.debug("Valid placeholder : {}".format(self.name))
|
||||
return True
|
||||
self.log.info("Placeholder is not valid : {}".format(self.name))
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def parent_in_hierarchy(self, container):
|
||||
"""Place loaded container in correct hierarchy given by placeholder
|
||||
|
||||
Args:
|
||||
container (Dict[str, Any]): Loaded container created by loader.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clean(self):
|
||||
"""Clean placeholder from hierarchy after loading assets."""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_representations(self, current_asset_doc, linked_asset_docs):
|
||||
"""Query representations based on placeholder data.
|
||||
|
||||
Args:
|
||||
current_asset_doc (Dict[str, Any]): Document of current
|
||||
context asset.
|
||||
linked_asset_docs (List[Dict[str, Any]]): Documents of assets
|
||||
linked to current context asset.
|
||||
|
||||
Returns:
|
||||
Iterable[Dict[str, Any]]: Representations that are matching
|
||||
placeholder filters.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_data(self, identifier):
|
||||
"""Collect information about placeholder by identifier.
|
||||
|
||||
Args:
|
||||
identifier (str): A unique placeholder identifier defined by
|
||||
implementation.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import os
|
||||
from importlib import import_module
|
||||
from openpype.lib import classes_from_module
|
||||
from openpype.host import HostBase
|
||||
from openpype.pipeline import registered_host
|
||||
|
||||
from .abstract_template_loader import (
|
||||
AbstractPlaceholder,
|
||||
AbstractTemplateLoader)
|
||||
|
||||
from .build_template_exceptions import (
|
||||
TemplateLoadingFailed,
|
||||
TemplateAlreadyImported,
|
||||
MissingHostTemplateModule,
|
||||
MissingTemplatePlaceholderClass,
|
||||
MissingTemplateLoaderClass
|
||||
)
|
||||
|
||||
_module_path_format = 'openpype.hosts.{host}.api.template_loader'
|
||||
|
||||
|
||||
def build_workfile_template(*args):
|
||||
template_loader = build_template_loader()
|
||||
try:
|
||||
template_loader.import_template(template_loader.template_path)
|
||||
except TemplateAlreadyImported as err:
|
||||
template_loader.template_already_imported(err)
|
||||
except TemplateLoadingFailed as err:
|
||||
template_loader.template_loading_failed(err)
|
||||
else:
|
||||
template_loader.populate_template()
|
||||
|
||||
|
||||
def update_workfile_template(*args):
|
||||
template_loader = build_template_loader()
|
||||
template_loader.update_missing_containers()
|
||||
|
||||
|
||||
def build_template_loader():
|
||||
# TODO refactor to use advantage of 'HostBase' and don't import dynamically
|
||||
# - hosts should have methods that gives option to return builders
|
||||
host = registered_host()
|
||||
if isinstance(host, HostBase):
|
||||
host_name = host.name
|
||||
else:
|
||||
host_name = os.environ.get("AVALON_APP")
|
||||
if not host_name:
|
||||
host_name = host.__name__.split(".")[-2]
|
||||
|
||||
module_path = _module_path_format.format(host=host_name)
|
||||
module = import_module(module_path)
|
||||
if not module:
|
||||
raise MissingHostTemplateModule(
|
||||
"No template loader found for host {}".format(host_name))
|
||||
|
||||
template_loader_class = classes_from_module(
|
||||
AbstractTemplateLoader,
|
||||
module
|
||||
)
|
||||
template_placeholder_class = classes_from_module(
|
||||
AbstractPlaceholder,
|
||||
module
|
||||
)
|
||||
|
||||
if not template_loader_class:
|
||||
raise MissingTemplateLoaderClass()
|
||||
template_loader_class = template_loader_class[0]
|
||||
|
||||
if not template_placeholder_class:
|
||||
raise MissingTemplatePlaceholderClass()
|
||||
template_placeholder_class = template_placeholder_class[0]
|
||||
return template_loader_class(template_placeholder_class)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
class MissingHostTemplateModule(Exception):
|
||||
"""Error raised when expected module does not exists"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingTemplatePlaceholderClass(Exception):
|
||||
"""Error raised when module doesn't implement a placeholder class"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingTemplateLoaderClass(Exception):
|
||||
"""Error raised when module doesn't implement a template loader class"""
|
||||
pass
|
||||
|
||||
|
||||
class TemplateNotFound(Exception):
|
||||
"""Exception raised when template does not exist."""
|
||||
pass
|
||||
|
||||
|
||||
class TemplateProfileNotFound(Exception):
|
||||
"""Exception raised when current profile
|
||||
doesn't match any template profile"""
|
||||
pass
|
||||
|
||||
|
||||
class TemplateAlreadyImported(Exception):
|
||||
"""Error raised when Template was already imported by host for
|
||||
this session"""
|
||||
pass
|
||||
|
||||
|
||||
class TemplateLoadingFailed(Exception):
|
||||
"""Error raised whend Template loader was unable to load the template"""
|
||||
pass
|
||||
|
|
@ -1,3 +1,14 @@
|
|||
"""Workfile build based on settings.
|
||||
|
||||
Workfile builder will do stuff based on project settings. Advantage is that
|
||||
it need only access to settings. Disadvantage is that it is hard to focus
|
||||
build per context and being explicit about loaded content.
|
||||
|
||||
For more explicit workfile build is recommended 'AbstractTemplateBuilder'
|
||||
from '~/openpype/pipeline/workfile/workfile_template_builder'. Which gives
|
||||
more abilities to define how build happens but require more code to achive it.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import collections
|
||||
|
|
|
|||
1451
openpype/pipeline/workfile/workfile_template_builder.py
Normal file
1451
openpype/pipeline/workfile/workfile_template_builder.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -25,7 +25,9 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
for created_instance in create_context.instances:
|
||||
instance_data = created_instance.data_to_store()
|
||||
if instance_data["active"]:
|
||||
self.create_instance(context, instance_data)
|
||||
self.create_instance(
|
||||
context, instance_data, created_instance.transient_data
|
||||
)
|
||||
|
||||
# Update global data to context
|
||||
context.data.update(create_context.context_data_to_store())
|
||||
|
|
@ -37,7 +39,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
legacy_io.Session[key] = value
|
||||
os.environ[key] = value
|
||||
|
||||
def create_instance(self, context, in_data):
|
||||
def create_instance(self, context, in_data, transient_data):
|
||||
subset = in_data["subset"]
|
||||
# If instance data already contain families then use it
|
||||
instance_families = in_data.get("families") or []
|
||||
|
|
@ -56,5 +58,8 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
for key, value in in_data.items():
|
||||
if key not in instance.data:
|
||||
instance.data[key] = value
|
||||
|
||||
instance.data["transientData"] = transient_data
|
||||
|
||||
self.log.info("collected instance: {}".format(instance.data))
|
||||
self.log.info("parsing data: {}".format(in_data))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import opentimelineio as otio
|
|||
import pyblish.api
|
||||
from pprint import pformat
|
||||
from openpype.pipeline.editorial import (
|
||||
get_media_range_with_retimes,
|
||||
otio_range_to_frame_range,
|
||||
otio_range_with_handles
|
||||
)
|
||||
|
|
@ -29,7 +30,7 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
|||
# get basic variables
|
||||
otio_clip = instance.data["otioClip"]
|
||||
workfile_start = instance.data["workfileFrameStart"]
|
||||
workfile_source_duration = instance.data.get("notRetimedFramerange")
|
||||
workfile_source_duration = instance.data.get("shotDurationFromSource")
|
||||
|
||||
# get ranges
|
||||
otio_tl_range = otio_clip.range_in_parent()
|
||||
|
|
@ -57,8 +58,15 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
|
|||
|
||||
# in case of retimed clip and frame range should not be retimed
|
||||
if workfile_source_duration:
|
||||
frame_end = frame_start + otio.opentime.to_frames(
|
||||
otio_src_range.duration, otio_src_range.duration.rate) - 1
|
||||
# get available range trimmed with processed retimes
|
||||
retimed_attributes = get_media_range_with_retimes(
|
||||
otio_clip, 0, 0)
|
||||
self.log.debug(
|
||||
">> retimed_attributes: {}".format(retimed_attributes))
|
||||
media_in = int(retimed_attributes["mediaIn"])
|
||||
media_out = int(retimed_attributes["mediaOut"])
|
||||
frame_end = frame_start + (media_out - media_in) + 1
|
||||
self.log.debug(frame_end)
|
||||
|
||||
data = {
|
||||
"frameStart": frame_start,
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ class PypeCommands:
|
|||
RuntimeError: When there is no path to process.
|
||||
"""
|
||||
|
||||
from openpype.hosts.webpublisher.cli_functions import (
|
||||
from openpype.hosts.webpublisher.publish_functions import (
|
||||
cli_publish
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
"workfileFrameStart": 1001,
|
||||
"handleStart": 5,
|
||||
"handleEnd": 5,
|
||||
"includeHandles": false
|
||||
"includeHandles": false,
|
||||
"retimedHandles": true,
|
||||
"retimedFramerange": true
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
"CollectInstances": {
|
||||
"flatten_subset_template": ""
|
||||
},
|
||||
"CollectReview": {
|
||||
"publish": true
|
||||
},
|
||||
"CollectVersion": {
|
||||
"enabled": false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -128,6 +128,16 @@
|
|||
"type": "boolean",
|
||||
"key": "includeHandles",
|
||||
"label": "Enable handles including"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "retimedHandles",
|
||||
"label": "Enable retimed handles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "retimedFramerange",
|
||||
"label": "Enable retimed shot frameranges"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,6 +134,18 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "CollectReview",
|
||||
"label": "Collect Review",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "publish",
|
||||
"label": "Active"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "CollectVersion",
|
||||
"label": "Collect Version",
|
||||
"children": [
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
"color": {
|
||||
"font": "#D3D8DE",
|
||||
"font-hover": "#F0F2F5",
|
||||
"font-disabled": "#99A3B2",
|
||||
"font-disabled": "#5b6779",
|
||||
"font-view-selection": "#ffffff",
|
||||
"font-view-hover": "#F0F2F5",
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from openpype.tools.utils import (
|
|||
BaseClickableFrame,
|
||||
set_style_property,
|
||||
)
|
||||
from openpype.style import get_objected_colors
|
||||
from openpype.pipeline.create import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
|
|
@ -125,28 +126,21 @@ class PublishIconBtn(IconButton):
|
|||
def __init__(self, pixmap_path, *args, **kwargs):
|
||||
super(PublishIconBtn, self).__init__(*args, **kwargs)
|
||||
|
||||
loaded_image = QtGui.QImage(pixmap_path)
|
||||
colors = get_objected_colors()
|
||||
icon = self.generate_icon(
|
||||
pixmap_path,
|
||||
enabled_color=colors["font"].get_qcolor(),
|
||||
disabled_color=colors["font-disabled"].get_qcolor())
|
||||
self.setIcon(icon)
|
||||
|
||||
pixmap = self.paint_image_with_color(loaded_image, QtCore.Qt.white)
|
||||
|
||||
self._base_image = loaded_image
|
||||
self._enabled_icon = QtGui.QIcon(pixmap)
|
||||
self._disabled_icon = None
|
||||
|
||||
self.setIcon(self._enabled_icon)
|
||||
|
||||
def get_enabled_icon(self):
|
||||
"""Enabled icon."""
|
||||
return self._enabled_icon
|
||||
|
||||
def get_disabled_icon(self):
|
||||
"""Disabled icon."""
|
||||
if self._disabled_icon is None:
|
||||
pixmap = self.paint_image_with_color(
|
||||
self._base_image, QtCore.Qt.gray
|
||||
)
|
||||
self._disabled_icon = QtGui.QIcon(pixmap)
|
||||
return self._disabled_icon
|
||||
def generate_icon(self, pixmap_path, enabled_color, disabled_color):
|
||||
icon = QtGui.QIcon()
|
||||
image = QtGui.QImage(pixmap_path)
|
||||
enabled_pixmap = self.paint_image_with_color(image, enabled_color)
|
||||
icon.addPixmap(enabled_pixmap, icon.Normal)
|
||||
disabled_pixmap = self.paint_image_with_color(image, disabled_color)
|
||||
icon.addPixmap(disabled_pixmap, icon.Disabled)
|
||||
return icon
|
||||
|
||||
@staticmethod
|
||||
def paint_image_with_color(image, color):
|
||||
|
|
@ -187,13 +181,6 @@ class PublishIconBtn(IconButton):
|
|||
|
||||
return pixmap
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
super(PublishIconBtn, self).setEnabled(enabled)
|
||||
if self.isEnabled():
|
||||
self.setIcon(self.get_enabled_icon())
|
||||
else:
|
||||
self.setIcon(self.get_disabled_icon())
|
||||
|
||||
|
||||
class ResetBtn(PublishIconBtn):
|
||||
"""Publish reset button."""
|
||||
|
|
|
|||
5
openpype/tools/workfile_template_build/__init__.py
Normal file
5
openpype/tools/workfile_template_build/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .window import WorkfileBuildPlaceholderDialog
|
||||
|
||||
__all__ = (
|
||||
"WorkfileBuildPlaceholderDialog",
|
||||
)
|
||||
242
openpype/tools/workfile_template_build/window.py
Normal file
242
openpype/tools/workfile_template_build/window.py
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
from Qt import QtWidgets
|
||||
|
||||
from openpype import style
|
||||
from openpype.lib import Logger
|
||||
from openpype.pipeline import legacy_io
|
||||
from openpype.widgets.attribute_defs import AttributeDefinitionsWidget
|
||||
|
||||
|
||||
class WorkfileBuildPlaceholderDialog(QtWidgets.QDialog):
|
||||
def __init__(self, host, builder, parent=None):
|
||||
super(WorkfileBuildPlaceholderDialog, self).__init__(parent)
|
||||
self.setWindowTitle("Workfile Placeholder Manager")
|
||||
|
||||
self._log = None
|
||||
|
||||
self._first_show = True
|
||||
self._first_refreshed = False
|
||||
|
||||
self._builder = builder
|
||||
self._host = host
|
||||
# Mode can be 0 (create) or 1 (update)
|
||||
# TODO write it a little bit better
|
||||
self._mode = 0
|
||||
self._update_item = None
|
||||
self._last_selected_plugin = None
|
||||
|
||||
host_name = getattr(self._host, "name", None)
|
||||
if not host_name:
|
||||
host_name = legacy_io.Session.get("AVALON_APP") or "NA"
|
||||
self._host_name = host_name
|
||||
|
||||
plugins_combo = QtWidgets.QComboBox(self)
|
||||
|
||||
content_widget = QtWidgets.QWidget(self)
|
||||
content_layout = QtWidgets.QVBoxLayout(content_widget)
|
||||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
create_btn = QtWidgets.QPushButton("Create", btns_widget)
|
||||
save_btn = QtWidgets.QPushButton("Save", btns_widget)
|
||||
close_btn = QtWidgets.QPushButton("Close", btns_widget)
|
||||
|
||||
create_btn.setVisible(False)
|
||||
save_btn.setVisible(False)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(create_btn, 0)
|
||||
btns_layout.addWidget(save_btn, 0)
|
||||
btns_layout.addWidget(close_btn, 0)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.addWidget(plugins_combo, 0)
|
||||
main_layout.addWidget(content_widget, 1)
|
||||
main_layout.addWidget(btns_widget, 0)
|
||||
|
||||
create_btn.clicked.connect(self._on_create_click)
|
||||
save_btn.clicked.connect(self._on_save_click)
|
||||
close_btn.clicked.connect(self._on_close_click)
|
||||
plugins_combo.currentIndexChanged.connect(self._on_plugin_change)
|
||||
|
||||
self._attr_defs_widget = None
|
||||
self._plugins_combo = plugins_combo
|
||||
|
||||
self._content_widget = content_widget
|
||||
self._content_layout = content_layout
|
||||
|
||||
self._create_btn = create_btn
|
||||
self._save_btn = save_btn
|
||||
self._close_btn = close_btn
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def _clear_content_widget(self):
|
||||
while self._content_layout.count() > 0:
|
||||
item = self._content_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
|
||||
def _add_message_to_content(self, message):
|
||||
msg_label = QtWidgets.QLabel(message, self._content_widget)
|
||||
self._content_layout.addWidget(msg_label, 0)
|
||||
self._content_layout.addStretch(1)
|
||||
|
||||
def refresh(self):
|
||||
self._first_refreshed = True
|
||||
|
||||
self._clear_content_widget()
|
||||
|
||||
if not self._builder:
|
||||
self._add_message_to_content((
|
||||
"Host \"{}\" does not have implemented logic"
|
||||
" for template workfile build."
|
||||
).format(self._host_name))
|
||||
self._update_ui_visibility()
|
||||
return
|
||||
|
||||
placeholder_plugins = self._builder.placeholder_plugins
|
||||
|
||||
if self._mode == 1:
|
||||
self._last_selected_plugin
|
||||
plugin = self._builder.placeholder_plugins.get(
|
||||
self._last_selected_plugin
|
||||
)
|
||||
self._create_option_widgets(
|
||||
plugin, self._update_item.to_dict()
|
||||
)
|
||||
self._update_ui_visibility()
|
||||
return
|
||||
|
||||
if not placeholder_plugins:
|
||||
self._add_message_to_content((
|
||||
"Host \"{}\" does not have implemented plugins"
|
||||
" for template workfile build."
|
||||
).format(self._host_name))
|
||||
self._update_ui_visibility()
|
||||
return
|
||||
|
||||
last_selected_plugin = self._last_selected_plugin
|
||||
self._last_selected_plugin = None
|
||||
self._plugins_combo.clear()
|
||||
for identifier, plugin in placeholder_plugins.items():
|
||||
label = plugin.label or identifier
|
||||
self._plugins_combo.addItem(label, identifier)
|
||||
|
||||
index = self._plugins_combo.findData(last_selected_plugin)
|
||||
if index < 0:
|
||||
index = 0
|
||||
self._plugins_combo.setCurrentIndex(index)
|
||||
self._on_plugin_change()
|
||||
|
||||
self._update_ui_visibility()
|
||||
|
||||
def set_create_mode(self):
|
||||
if self._mode == 0:
|
||||
return
|
||||
|
||||
self._mode = 0
|
||||
self._update_item = None
|
||||
self.refresh()
|
||||
|
||||
def set_update_mode(self, update_item):
|
||||
if self._mode == 1:
|
||||
return
|
||||
|
||||
self._mode = 1
|
||||
self._update_item = update_item
|
||||
if update_item:
|
||||
self._last_selected_plugin = update_item.plugin.identifier
|
||||
self.refresh()
|
||||
return
|
||||
|
||||
self._clear_content_widget()
|
||||
self._add_message_to_content((
|
||||
"Nothing to update."
|
||||
" (You maybe don't have selected placeholder.)"
|
||||
))
|
||||
self._update_ui_visibility()
|
||||
|
||||
def _create_option_widgets(self, plugin, options=None):
|
||||
self._clear_content_widget()
|
||||
attr_defs = plugin.get_placeholder_options(options)
|
||||
widget = AttributeDefinitionsWidget(attr_defs, self._content_widget)
|
||||
self._content_layout.addWidget(widget, 0)
|
||||
self._content_layout.addStretch(1)
|
||||
self._attr_defs_widget = widget
|
||||
self._last_selected_plugin = plugin.identifier
|
||||
|
||||
def _update_ui_visibility(self):
|
||||
create_mode = self._mode == 0
|
||||
self._plugins_combo.setVisible(create_mode)
|
||||
|
||||
if not self._builder:
|
||||
self._save_btn.setVisible(False)
|
||||
self._create_btn.setVisible(False)
|
||||
return
|
||||
|
||||
save_enabled = not create_mode
|
||||
if save_enabled:
|
||||
save_enabled = self._update_item is not None
|
||||
self._save_btn.setVisible(save_enabled)
|
||||
self._create_btn.setVisible(create_mode)
|
||||
|
||||
def _on_plugin_change(self):
|
||||
index = self._plugins_combo.currentIndex()
|
||||
plugin_identifier = self._plugins_combo.itemData(index)
|
||||
if plugin_identifier == self._last_selected_plugin:
|
||||
return
|
||||
|
||||
plugin = self._builder.placeholder_plugins.get(plugin_identifier)
|
||||
self._create_option_widgets(plugin)
|
||||
|
||||
def _on_save_click(self):
|
||||
options = self._attr_defs_widget.current_value()
|
||||
plugin = self._builder.placeholder_plugins.get(
|
||||
self._last_selected_plugin
|
||||
)
|
||||
# TODO much better error handling
|
||||
try:
|
||||
plugin.update_placeholder(self._update_item, options)
|
||||
self.accept()
|
||||
except Exception:
|
||||
self.log.warning("Something went wrong", exc_info=True)
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Something went wrong")
|
||||
dialog.setText("Something went wrong")
|
||||
dialog.exec_()
|
||||
|
||||
def _on_create_click(self):
|
||||
options = self._attr_defs_widget.current_value()
|
||||
plugin = self._builder.placeholder_plugins.get(
|
||||
self._last_selected_plugin
|
||||
)
|
||||
# TODO much better error handling
|
||||
try:
|
||||
plugin.create_placeholder(options)
|
||||
self.accept()
|
||||
except Exception:
|
||||
self.log.warning("Something went wrong", exc_info=True)
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Something went wrong")
|
||||
dialog.setText("Something went wrong")
|
||||
dialog.exec_()
|
||||
|
||||
def _on_close_click(self):
|
||||
self.reject()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(WorkfileBuildPlaceholderDialog, self).showEvent(event)
|
||||
if not self._first_refreshed:
|
||||
self.refresh()
|
||||
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.resize(390, 450)
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.14.3-nightly.2"
|
||||
__version__ = "3.14.3-nightly.4"
|
||||
|
|
|
|||
|
|
@ -108,10 +108,12 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
|
||||
row = 0
|
||||
for attr_def in attr_defs:
|
||||
if attr_def.key in self._current_keys:
|
||||
raise KeyError("Duplicated key \"{}\"".format(attr_def.key))
|
||||
if not isinstance(attr_def, UIDef):
|
||||
if attr_def.key in self._current_keys:
|
||||
raise KeyError(
|
||||
"Duplicated key \"{}\"".format(attr_def.key))
|
||||
|
||||
self._current_keys.add(attr_def.key)
|
||||
self._current_keys.add(attr_def.key)
|
||||
widget = create_widget_for_attr_def(attr_def, self)
|
||||
|
||||
expand_cols = 2
|
||||
|
|
|
|||
|
|
@ -111,14 +111,14 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
return QtCore.QSize(width, height)
|
||||
|
||||
def get_width_hint_by_height(self, height):
|
||||
return (
|
||||
height / self._base_size.height()
|
||||
) * self._base_size.width()
|
||||
return int((
|
||||
float(height) / self._base_size.height()
|
||||
) * self._base_size.width())
|
||||
|
||||
def get_height_hint_by_width(self, width):
|
||||
return (
|
||||
width / self._base_size.width()
|
||||
) * self._base_size.height()
|
||||
return int((
|
||||
float(width) / self._base_size.width()
|
||||
) * self._base_size.height())
|
||||
|
||||
def setFixedHeight(self, *args, **kwargs):
|
||||
self._fixed_height_set = True
|
||||
|
|
@ -321,7 +321,7 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
bg_color = self.unchecked_bg_color
|
||||
|
||||
else:
|
||||
offset_ratio = self._current_step / self._steps
|
||||
offset_ratio = float(self._current_step) / self._steps
|
||||
# Animation bg
|
||||
bg_color = self.steped_color(
|
||||
self.checked_bg_color,
|
||||
|
|
@ -332,7 +332,8 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
margins_ratio = self._checker_margins_divider
|
||||
if margins_ratio > 0:
|
||||
size_without_margins = int(
|
||||
(frame_rect.height() / margins_ratio) * (margins_ratio - 2)
|
||||
(float(frame_rect.height()) / margins_ratio)
|
||||
* (margins_ratio - 2)
|
||||
)
|
||||
size_without_margins -= size_without_margins % 2
|
||||
margin_size_c = ceil(
|
||||
|
|
@ -434,21 +435,21 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
def _get_enabled_icon_path(
|
||||
self, painter, checker_rect, step=None, half_steps=None
|
||||
):
|
||||
fifteenth = checker_rect.height() / 15
|
||||
fifteenth = float(checker_rect.height()) / 15
|
||||
# Left point
|
||||
p1 = QtCore.QPoint(
|
||||
checker_rect.x() + (5 * fifteenth),
|
||||
checker_rect.y() + (9 * fifteenth)
|
||||
int(checker_rect.x() + (5 * fifteenth)),
|
||||
int(checker_rect.y() + (9 * fifteenth))
|
||||
)
|
||||
# Middle bottom point
|
||||
p2 = QtCore.QPoint(
|
||||
checker_rect.center().x(),
|
||||
checker_rect.y() + (11 * fifteenth)
|
||||
int(checker_rect.y() + (11 * fifteenth))
|
||||
)
|
||||
# Top right point
|
||||
p3 = QtCore.QPoint(
|
||||
checker_rect.x() + (10 * fifteenth),
|
||||
checker_rect.y() + (5 * fifteenth)
|
||||
int(checker_rect.x() + (10 * fifteenth)),
|
||||
int(checker_rect.y() + (5 * fifteenth))
|
||||
)
|
||||
if step is not None:
|
||||
multiplier = (half_steps - step)
|
||||
|
|
@ -458,16 +459,16 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
p3c = p3 - checker_rect.center()
|
||||
|
||||
p1o = QtCore.QPoint(
|
||||
(p1c.x() / half_steps) * multiplier,
|
||||
(p1c.y() / half_steps) * multiplier
|
||||
int((float(p1c.x()) / half_steps) * multiplier),
|
||||
int((float(p1c.y()) / half_steps) * multiplier)
|
||||
)
|
||||
p2o = QtCore.QPoint(
|
||||
(p2c.x() / half_steps) * multiplier,
|
||||
(p2c.y() / half_steps) * multiplier
|
||||
int((float(p2c.x()) / half_steps) * multiplier),
|
||||
int((float(p2c.y()) / half_steps) * multiplier)
|
||||
)
|
||||
p3o = QtCore.QPoint(
|
||||
(p3c.x() / half_steps) * multiplier,
|
||||
(p3c.y() / half_steps) * multiplier
|
||||
int((float(p3c.x()) / half_steps) * multiplier),
|
||||
int((float(p3c.y()) / half_steps) * multiplier)
|
||||
)
|
||||
|
||||
p1 -= p1o
|
||||
|
|
@ -484,11 +485,12 @@ class NiceCheckbox(QtWidgets.QFrame):
|
|||
self, painter, checker_rect, step=None, half_steps=None
|
||||
):
|
||||
center_point = QtCore.QPointF(
|
||||
checker_rect.width() / 2, checker_rect.height() / 2
|
||||
float(checker_rect.width()) / 2,
|
||||
float(checker_rect.height()) / 2
|
||||
)
|
||||
offset = (
|
||||
offset = float((
|
||||
(center_point + QtCore.QPointF(0, 0)) / 2
|
||||
).x() / 4 * 5
|
||||
).x()) / 4 * 5
|
||||
if step is not None:
|
||||
diff = center_point.x() - offset
|
||||
diff_offset = (diff / half_steps) * (half_steps - step)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import glob
|
|||
import platform
|
||||
|
||||
from tests.lib.db_handler import DBHandler
|
||||
from tests.lib.file_handler import RemoteFileHandler
|
||||
from distribution.file_handler import RemoteFileHandler
|
||||
|
||||
|
||||
class BaseTest:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue