Merge branch 'develop' into feature/OP-1537_Flame-Submitting-jobs-to-Burner-farm

This commit is contained in:
Jakub Jezek 2022-01-20 11:01:13 +01:00
commit c1ac832f5b
No known key found for this signature in database
GPG key ID: D8548FBF690B100A
132 changed files with 8095 additions and 991 deletions

View file

@ -37,6 +37,7 @@ jobs:
- name: 🔨 Build
shell: pwsh
run: |
$env:SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.ps1
Ubuntu-latest:
@ -61,6 +62,7 @@ jobs:
- name: 🔨 Build
run: |
export SKIP_THIRD_PARTY_VALIDATION="1"
./tools/build.sh
# MacOS-latest:

View file

@ -1,18 +1,29 @@
# Changelog
## [3.8.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.8.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.7.0...HEAD)
### 📖 Documentation
- Renamed to proper name [\#2546](https://github.com/pypeclub/OpenPype/pull/2546)
- Slack: Add review to notification message [\#2498](https://github.com/pypeclub/OpenPype/pull/2498)
**🆕 New features**
- Flame: OpenTimelineIO Export Modul [\#2398](https://github.com/pypeclub/OpenPype/pull/2398)
**🚀 Enhancements**
- Settings: PathInput strip passed string [\#2550](https://github.com/pypeclub/OpenPype/pull/2550)
- General: Validate if current process OpenPype version is requested version [\#2529](https://github.com/pypeclub/OpenPype/pull/2529)
- General: Be able to use anatomy data in ffmpeg output arguments [\#2525](https://github.com/pypeclub/OpenPype/pull/2525)
- Expose toggle publish plug-in settings for Maya Look Shading Engine Naming [\#2521](https://github.com/pypeclub/OpenPype/pull/2521)
- Photoshop: Move implementation to OpenPype [\#2510](https://github.com/pypeclub/OpenPype/pull/2510)
- TimersManager: Move module one hierarchy higher [\#2501](https://github.com/pypeclub/OpenPype/pull/2501)
- Slack: notifications are sent with Openpype logo and bot name [\#2499](https://github.com/pypeclub/OpenPype/pull/2499)
- Ftrack: Event handlers settings [\#2496](https://github.com/pypeclub/OpenPype/pull/2496)
- Flame - create publishable clips [\#2495](https://github.com/pypeclub/OpenPype/pull/2495)
- Tools: Fix style and modality of errors in loader and creator [\#2489](https://github.com/pypeclub/OpenPype/pull/2489)
- Project Manager: Remove project button cleanup [\#2482](https://github.com/pypeclub/OpenPype/pull/2482)
- Tools: Be able to change models of tasks and assets widgets [\#2475](https://github.com/pypeclub/OpenPype/pull/2475)
@ -23,32 +34,34 @@
- Fix \#2453 Refactor missing \_get\_reference\_node method [\#2455](https://github.com/pypeclub/OpenPype/pull/2455)
- Houdini: Remove broken unique name counter [\#2450](https://github.com/pypeclub/OpenPype/pull/2450)
- Maya: Improve lib.polyConstraint performance when Select tool is not the active tool context [\#2447](https://github.com/pypeclub/OpenPype/pull/2447)
- General: Validate third party before build [\#2425](https://github.com/pypeclub/OpenPype/pull/2425)
- Maya : add option to not group reference in ReferenceLoader [\#2383](https://github.com/pypeclub/OpenPype/pull/2383)
**🐛 Bug fixes**
- Fix published frame content for sequence starting with 0 [\#2513](https://github.com/pypeclub/OpenPype/pull/2513)
- Fix \#2497: reset empty string attributes correctly to "" instead of "None" [\#2506](https://github.com/pypeclub/OpenPype/pull/2506)
- General: Settings work if OpenPypeVersion is available [\#2494](https://github.com/pypeclub/OpenPype/pull/2494)
- General: PYTHONPATH may break OpenPype dependencies [\#2493](https://github.com/pypeclub/OpenPype/pull/2493)
- Workfiles tool: Files widget show files on first show [\#2488](https://github.com/pypeclub/OpenPype/pull/2488)
- General: Custom template paths filter fix [\#2483](https://github.com/pypeclub/OpenPype/pull/2483)
- Loader: Remove always on top flag in tray [\#2480](https://github.com/pypeclub/OpenPype/pull/2480)
- General: Anatomy does not return root envs as unicode [\#2465](https://github.com/pypeclub/OpenPype/pull/2465)
- Maya: Validate Shape Zero do not keep fixed geometry vertices selected/active after repair [\#2456](https://github.com/pypeclub/OpenPype/pull/2456)
**Merged pull requests:**
- General: Fix install thread in igniter [\#2549](https://github.com/pypeclub/OpenPype/pull/2549)
- AfterEffects: Move implementation to OpenPype [\#2543](https://github.com/pypeclub/OpenPype/pull/2543)
- Fix create zip tool - path argument [\#2522](https://github.com/pypeclub/OpenPype/pull/2522)
- General: Modules import function output fix [\#2492](https://github.com/pypeclub/OpenPype/pull/2492)
- AE: fix hiding of alert window below Publish [\#2491](https://github.com/pypeclub/OpenPype/pull/2491)
- Maya: Validate NGONs re-use polyConstraint code from openpype.host.maya.api.lib [\#2458](https://github.com/pypeclub/OpenPype/pull/2458)
- Version handling [\#2363](https://github.com/pypeclub/OpenPype/pull/2363)
## [3.7.0](https://github.com/pypeclub/OpenPype/tree/3.7.0) (2022-01-04)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.7.0-nightly.14...3.7.0)
**Deprecated:**
- General: Default modules hierarchy n2 [\#2368](https://github.com/pypeclub/OpenPype/pull/2368)
**🚀 Enhancements**
- General: Workdir extra folders [\#2462](https://github.com/pypeclub/OpenPype/pull/2462)
@ -67,8 +80,6 @@
- Enhancement: Settings: Use project settings values from another project [\#2382](https://github.com/pypeclub/OpenPype/pull/2382)
- Blender 3: Support auto install for new blender version [\#2377](https://github.com/pypeclub/OpenPype/pull/2377)
- Maya add render image path to settings [\#2375](https://github.com/pypeclub/OpenPype/pull/2375)
- Hiero: python3 compatibility [\#2365](https://github.com/pypeclub/OpenPype/pull/2365)
- Maya: Add is\_static\_image\_plane and is\_in\_all\_views option in imagePlaneLoader [\#2356](https://github.com/pypeclub/OpenPype/pull/2356)
**🐛 Bug fixes**
@ -86,9 +97,6 @@
- hiero: fix workio and flatten [\#2378](https://github.com/pypeclub/OpenPype/pull/2378)
- Nuke: fixing menu re-drawing during context change [\#2374](https://github.com/pypeclub/OpenPype/pull/2374)
- Webpublisher: Fix assignment of families of TVpaint instances [\#2373](https://github.com/pypeclub/OpenPype/pull/2373)
- Nuke: fixing node name based on switched asset name [\#2369](https://github.com/pypeclub/OpenPype/pull/2369)
- Tools: Placeholder color [\#2359](https://github.com/pypeclub/OpenPype/pull/2359)
- Houdini: Fix HDA creation [\#2350](https://github.com/pypeclub/OpenPype/pull/2350)
**Merged pull requests:**
@ -96,7 +104,6 @@
- Maya: Replaced PATH usage with vendored oiio path for maketx utility [\#2405](https://github.com/pypeclub/OpenPype/pull/2405)
- \[Fix\]\[MAYA\] Handle message type attribute within CollectLook [\#2394](https://github.com/pypeclub/OpenPype/pull/2394)
- Add validator to check correct version of extension for PS and AE [\#2387](https://github.com/pypeclub/OpenPype/pull/2387)
- Linux : flip updating submodules logic [\#2357](https://github.com/pypeclub/OpenPype/pull/2357)
## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23)

View file

@ -762,7 +762,7 @@ class BootstrapRepos:
destination = self._move_zip_to_data_dir(temp_zip)
return OpenPypeVersion(version=version, path=destination)
return OpenPypeVersion(version=version, path=Path(destination))
def _move_zip_to_data_dir(self, zip_file) -> Union[None, Path]:
"""Move zip with OpenPype version to user data directory.
@ -912,7 +912,6 @@ class BootstrapRepos:
processed_path = file
self._print(f"- processing {processed_path}")
checksums.append(
(
sha256sum(file.as_posix()),
@ -1544,7 +1543,8 @@ class BootstrapRepos:
Args:
zip_item (Path): Zip file to test.
detected_version (OpenPypeVersion): Pype version detected from name.
detected_version (OpenPypeVersion): Pype version detected from
name.
Returns:
True if it is valid OpenPype version, False otherwise.

View file

@ -60,7 +60,7 @@ class InstallThread(QThread):
# find local version of OpenPype
bs = BootstrapRepos(
progress_callback=self.set_progress, message=self.message)
local_version = bs.get_local_live_version()
local_version = OpenPypeVersion.get_installed_version_str()
# if user did entered nothing, we install OpenPype from local version.
# zip content of `repos`, copy it to user data dir and append

View file

@ -0,0 +1,66 @@
# AfterEffects Integration
Requirements: This extension requires use of Javascript engine, which is
available since CC 16.0.
Please check your File>Project Settings>Expressions>Expressions Engine
## Setup
The After Effects integration requires two components to work; `extension` and `server`.
### Extension
To install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd).
```
ExManCmd /install {path to avalon-core}\avalon\photoshop\extension.zxp
```
OR
download [Anastasiys Extension Manager](https://install.anastasiy.com/)
### Server
The easiest way to get the server and After Effects launch is with:
```
python -c ^"import avalon.photoshop;avalon.aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^"
```
`avalon.aftereffects.launch` launches the application and server, and also closes the server when After Effects exists.
## Usage
The After Effects extension can be found under `Window > Extensions > OpenPype`. Once launched you should be presented with a panel like this:
![Avalon Panel](panel.PNG "Avalon Panel")
## Developing
### Extension
When developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions).
When signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide).
```
ZXPSignCmd -selfSignedCert NA NA Avalon Avalon-After-Effects avalon extension.p12
ZXPSignCmd -sign {path to avalon-core}\avalon\aftereffects\extension {path to avalon-core}\avalon\aftereffects\extension.zxp extension.p12 avalon
```
### Plugin Examples
These plugins were made with the [polly config](https://github.com/mindbender-studio/config). To fully integrate and load, you will have to use this config and add `image` to the [integration plugin](https://github.com/mindbender-studio/config/blob/master/polly/plugins/publish/integrate_asset.py).
Expected deployed extension location on default Windows:
`c:\Program Files (x86)\Common Files\Adobe\CEP\extensions\com.openpype.AE.panel`
For easier debugging of Javascript:
https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1
Add (optional) --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome
then localhost:8092
Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01
## Resources
- https://javascript-tools-guide.readthedocs.io/introduction/index.html
- https://github.com/Adobe-CEP/Getting-Started-guides
- https://github.com/Adobe-CEP/CEP-Resources

View file

@ -1,115 +1,68 @@
import os
import sys
import logging
"""Public API
from avalon import io
from avalon import api as avalon
from Qt import QtWidgets
from openpype import lib, api
import pyblish.api as pyblish
import openpype.hosts.aftereffects
Anything that isn't defined here is INTERNAL and unreliable for external use.
"""
from .launch_logic import (
get_stub,
stub,
)
from .pipeline import (
ls,
get_asset_settings,
install,
uninstall,
list_instances,
remove_instance,
containerise
)
from .workio import (
file_extensions,
has_unsaved_changes,
save_file,
open_file,
current_file,
work_root,
)
from .lib import (
maintained_selection,
get_extension_manifest_path
)
from .plugin import (
AfterEffectsLoader
)
log = logging.getLogger("openpype.hosts.aftereffects")
__all__ = [
# launch_logic
"get_stub",
"stub",
# pipeline
"ls",
"get_asset_settings",
"install",
"uninstall",
"list_instances",
"remove_instance",
"containerise",
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.aftereffects.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
"file_extensions",
"has_unsaved_changes",
"save_file",
"open_file",
"current_file",
"work_root",
# lib
"maintained_selection",
"get_extension_manifest_path",
def check_inventory():
if not lib.any_outdated():
return
host = pyblish.registered_host()
outdated_containers = []
for container in host.ls():
representation = container['representation']
representation_doc = io.find_one(
{
"_id": io.ObjectId(representation),
"type": "representation"
},
projection={"parent": True}
)
if representation_doc and not lib.is_latest(representation_doc):
outdated_containers.append(container)
# Warn about outdated containers.
print("Starting new QApplication..")
app = QtWidgets.QApplication(sys.argv)
message_box = QtWidgets.QMessageBox()
message_box.setIcon(QtWidgets.QMessageBox.Warning)
msg = "There are outdated containers in the scene."
message_box.setText(msg)
message_box.exec_()
# Garbage collect QApplication.
del app
def application_launch():
check_inventory()
def install():
print("Installing Pype config...")
pyblish.register_plugin_path(PUBLISH_PATH)
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
avalon.on("application.launched", application_launch)
def uninstall():
pyblish.deregister_plugin_path(PUBLISH_PATH)
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle layer visibility on instance toggles."""
instance[0].Visible = new_value
def get_asset_settings():
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = lib.get_asset()["data"]
fps = asset_data.get("fps")
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
handle_start = asset_data.get("handleStart")
handle_end = asset_data.get("handleEnd")
resolution_width = asset_data.get("resolutionWidth")
resolution_height = asset_data.get("resolutionHeight")
duration = (frame_end - frame_start + 1) + handle_start + handle_end
entity_type = asset_data.get("entityType")
scene_data = {
"fps": fps,
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end,
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"duration": duration
}
return scene_data
# plugin
"AfterEffectsLoader"
]

Binary file not shown.

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionList>
<Extension Id="com.openpype.AE.panel">
<HostList>
<!-- Comment Host tags according to the apps you want your panel to support -->
<!-- Photoshop -->
<Host Name="PHXS" Port="8088"/>
<!-- Illustrator -->
<Host Name="ILST" Port="8089"/>
<!-- InDesign -->
<Host Name="IDSN" Port="8090" />
<!-- Premiere -->
<Host Name="PPRO" Port="8091" />
<!-- AfterEffects -->
<Host Name="AEFT" Port="8092" />
<!-- PRELUDE -->
<Host Name="PRLD" Port="8093" />
<!-- FLASH Pro -->
<Host Name="FLPR" Port="8094" />
</HostList>
</Extension>
</ExtensionList>

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.21"
ExtensionBundleName="openpype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExtensionList>
<Extension Id="com.openpype.AE.panel" Version="1.0" />
</ExtensionList>
<ExecutionEnvironment>
<HostList>
<!-- Uncomment Host tags according to the apps you want your panel to support -->
<!-- Photoshop -->
<!--<Host Name="PHXS" Version="[14.0,19.0]" /> -->
<!-- <Host Name="PHSP" Version="[14.0,19.0]" /> -->
<!-- Illustrator -->
<!-- <Host Name="ILST" Version="[18.0,22.0]" /> -->
<!-- InDesign -->
<!-- <Host Name="IDSN" Version="[10.0,13.0]" /> -->
<!-- Premiere -->
<!-- <Host Name="PPRO" Version="[8.0,12.0]" /> -->
<!-- AfterEffects -->
<Host Name="AEFT" Version="[13.0,99.0]" />
<!-- PRELUDE -->
<!-- <Host Name="PRLD" Version="[3.0,7.0]" /> -->
<!-- FLASH Pro -->
<!-- <Host Name="FLPR" Version="[14.0,18.0]" /> -->
</HostList>
<LocaleList>
<Locale Code="All" />
</LocaleList>
<RequiredRuntimeList>
<RequiredRuntime Name="CSXS" Version="9.0" />
</RequiredRuntimeList>
</ExecutionEnvironment>
<DispatchInfoList>
<Extension Id="com.openpype.AE.panel">
<DispatchInfo >
<Resources>
<MainPath>./index.html</MainPath>
<ScriptPath>./jsx/hostscript.jsx</ScriptPath>
</Resources>
<Lifecycle>
<AutoVisible>true</AutoVisible>
</Lifecycle>
<UI>
<Type>Panel</Type>
<Menu>OpenPype</Menu>
<Geometry>
<Size>
<Height>200</Height>
<Width>100</Width>
</Size>
<!--<MinSize>
<Height>550</Height>
<Width>400</Width>
</MinSize>
<MaxSize>
<Height>550</Height>
<Width>400</Width>
</MaxSize>-->
</Geometry>
<Icons>
<Icon Type="Normal">./icons/iconNormal.png</Icon>
<Icon Type="RollOver">./icons/iconRollover.png</Icon>
<Icon Type="Disabled">./icons/iconDisabled.png</Icon>
<Icon Type="DarkNormal">./icons/iconDarkNormal.png</Icon>
<Icon Type="DarkRollOver">./icons/iconDarkRollover.png</Icon>
</Icons>
</UI>
</DispatchInfo>
</Extension>
</DispatchInfoList>
</ExtensionManifest>

View file

@ -0,0 +1,327 @@
/*
* HTML5 Boilerplate
*
* What follows is the result of much research on cross-browser styling.
* Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal,
* Kroc Camen, and the H5BP dev community and team.
*
* Detailed information about this CSS: h5bp.com/css
*
* ==|== normalize ==========================================================
*/
/* =============================================================================
HTML5 display definitions
========================================================================== */
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; }
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; }
audio:not([controls]) { display: none; }
[hidden] { display: none; }
/* =============================================================================
Base
========================================================================== */
/*
* 1. Correct text resizing oddly in IE6/7 when body font-size is set using em units
* 2. Force vertical scrollbar in non-IE
* 3. Prevent iOS text size adjust on device orientation change, without disabling user zoom: h5bp.com/g
*/
html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
body { margin: 0; font-size: 100%; line-height: 1.231; }
body, button, input, select, textarea { font-family: helvetica, arial,"lucida grande", verdana, "メイリオ", " Pゴシック", sans-serif; color: #222; }
/*
* Remove text-shadow in selection highlight: h5bp.com/i
* These selection declarations have to be separate
* Also: hot pink! (or customize the background color to match your design)
*/
::selection { text-shadow: none; background-color: highlight; color: highlighttext; }
/* =============================================================================
Links
========================================================================== */
a { color: #00e; }
a:visited { color: #551a8b; }
a:hover { color: #06e; }
a:focus { outline: thin dotted; }
/* Improve readability when focused and hovered in all browsers: h5bp.com/h */
a:hover, a:active { outline: 0; }
/* =============================================================================
Typography
========================================================================== */
abbr[title] { border-bottom: 1px dotted; }
b, strong { font-weight: bold; }
blockquote { margin: 1em 40px; }
dfn { font-style: italic; }
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
ins { background: #ff9; color: #000; text-decoration: none; }
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
/* Redeclare monospace font family: h5bp.com/j */
pre, code, kbd, samp { font-family: monospace, serif; _font-family: 'courier new', monospace; font-size: 1em; }
/* Improve readability of pre-formatted text in all browsers */
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
q { quotes: none; }
q:before, q:after { content: ""; content: none; }
small { font-size: 85%; }
/* Position subscript and superscript content without affecting line-height: h5bp.com/k */
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
/* =============================================================================
Lists
========================================================================== */
ul, ol { margin: 1em 0; padding: 0 0 0 40px; }
dd { margin: 0 0 0 40px; }
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; }
/* =============================================================================
Embedded content
========================================================================== */
/*
* 1. Improve image quality when scaled in IE7: h5bp.com/d
* 2. Remove the gap between images and borders on image containers: h5bp.com/e
*/
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
/*
* Correct overflow not hidden in IE9
*/
svg:not(:root) { overflow: hidden; }
/* =============================================================================
Figures
========================================================================== */
figure { margin: 0; }
/* =============================================================================
Forms
========================================================================== */
form { margin: 0; }
fieldset { border: 0; margin: 0; padding: 0; }
/* Indicate that 'label' will shift focus to the associated form element */
label { cursor: pointer; }
/*
* 1. Correct color not inheriting in IE6/7/8/9
* 2. Correct alignment displayed oddly in IE6/7
*/
legend { border: 0; *margin-left: -7px; padding: 0; }
/*
* 1. Correct font-size not inheriting in all browsers
* 2. Remove margins in FF3/4 S5 Chrome
* 3. Define consistent vertical alignment display in all browsers
*/
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; }
/*
* 1. Define line-height as normal to match FF3/4 (set using !important in the UA stylesheet)
*/
button, input { line-height: normal; }
/*
* 1. Display hand cursor for clickable form elements
* 2. Allow styling of clickable form elements in iOS
* 3. Correct inner spacing displayed oddly in IE7 (doesn't effect IE6)
*/
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; *overflow: visible; }
/*
* Consistent box sizing and appearance
*/
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; padding: 0; }
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; }
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
/*
* Remove inner padding and border in FF3/4: h5bp.com/l
*/
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
/*
* 1. Remove default vertical scrollbar in IE6/7/8/9
* 2. Allow only vertical resizing
*/
textarea { overflow: auto; vertical-align: top; resize: vertical; }
/* Colors for form validity */
input:valid, textarea:valid { }
input:invalid, textarea:invalid { background-color: #f0dddd; }
/* =============================================================================
Tables
========================================================================== */
table { border-collapse: collapse; border-spacing: 0; }
td { vertical-align: top; }
/* ==|== primary styles =====================================================
Author:
========================================================================== */
/* ==|== media queries ======================================================
PLACEHOLDER Media Queries for Responsive Design.
These override the primary ('mobile first') styles
Modify as content requires.
========================================================================== */
@media only screen and (min-width: 480px) {
/* Style adjustments for viewports 480px and over go here */
}
@media only screen and (min-width: 768px) {
/* Style adjustments for viewports 768px and over go here */
}
/* ==|== non-semantic helper classes ========================================
Please define your styles before this section.
========================================================================== */
/* For image replacement */
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; }
.ir br { display: none; }
/* Hide from both screenreaders and browsers: h5bp.com/u */
.hidden { display: none !important; visibility: hidden; }
/* Hide only visually, but have it available for screenreaders: h5bp.com/v */
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
/* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: h5bp.com/p */
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; }
/* Hide visually and from screenreaders, but maintain layout */
.invisible { visibility: hidden; }
/* Contain floats: h5bp.com/q */
.clearfix:before, .clearfix:after { content: ""; display: table; }
.clearfix:after { clear: both; }
.clearfix { *zoom: 1; }
/* ==|== print styles =======================================================
Print styles.
Inlined to avoid required HTTP connection: h5bp.com/r
========================================================================== */
@media print {
* { background: transparent !important; color: black !important; box-shadow:none !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } /* Black prints faster: h5bp.com/s */
a, a:visited { text-decoration: underline; }
a[href]:after { content: " (" attr(href) ")"; }
abbr[title]:after { content: " (" attr(title) ")"; }
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; }
table { display: table-header-group; } /* h5bp.com/t */
tr, img { page-break-inside: avoid; }
img { max-width: 100% !important; }
@page { margin: 0.5cm; }
p, h2, h3 { orphans: 3; widows: 3; }
h2, h3 { page-break-after: avoid; }
}
/* reflow reset for -webkit-margin-before: 1em */
p { margin: 0; }
html {
overflow-y: auto;
background-color: transparent;
height: 100%;
}
body {
background: #fff;
font: normal 100%;
position: relative;
height: 100%;
}
body, div, img, p, button, input, select, textarea {
box-sizing: border-box;
}
.image {
display: block;
}
input {
cursor: default;
display: block;
}
input[type=button] {
background-color: #e5e9e8;
border: 1px solid #9daca9;
border-radius: 4px;
box-shadow: inset 0 1px #fff;
font: inherit;
letter-spacing: inherit;
text-indent: inherit;
color: inherit;
}
input[type=button]:hover {
background-color: #eff1f1;
}
input[type=button]:active {
background-color: #d2d6d6;
border: 1px solid #9daca9;
box-shadow: inset 0 1px rgba(0,0,0,0.1);
}
/* Reset anchor styles to an unstyled default to be in parity with design surface. It
is presumed that most link styles in real-world designs are custom (non-default). */
a, a:visited, a:hover, a:active {
color: inherit;
text-decoration: inherit;
}

View file

@ -0,0 +1,51 @@
/*Your styles*/
body {
margin: 10px;
}
#content {
margin-right:auto;
margin-left:auto;
vertical-align:middle;
width:100%;
}
#btn_test{
width: 100%;
}
/*
Those classes will be edited at runtime with values specified
by the settings of the CC application
*/
.hostFontColor{}
.hostFontFamily{}
.hostFontSize{}
/*font family, color and size*/
.hostFont{}
/*background color*/
.hostBgd{}
/*lighter background color*/
.hostBgdLight{}
/*darker background color*/
.hostBgdDark{}
/*background color and font*/
.hostElt{}
.hostButton{
border:1px solid;
border-radius:2px;
height:20px;
vertical-align:bottom;
font-family:inherit;
color:inherit;
font-size:inherit;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,136 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="css/topcoat-desktop-dark.min.css"/>
<link id="hostStyle" rel="stylesheet" href="css/styles.css"/>
<style type="text/css">
html, body, iframe {
width: 100%;
height: 100%;
border: 0px;
margin: 0px;
overflow: hidden;
}
button {width: 100%;}
</style>
<style>
button {width: 100%;}
body {margin:0; padding:0; height: 100%;}
html {height: 100%;}
</style>
<title></title>
<script src="js/libs/jquery-2.0.2.min.js"></script>
<script type=text/javascript>
$(function() {
$("a#workfiles-button").bind("click", function() {
RPC.call('AfterEffects.workfiles_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#creator-button").bind("click", function() {
RPC.call('AfterEffects.creator_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#loader-button").bind("click", function() {
RPC.call('AfterEffects.loader_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#publish-button").bind("click", function() {
RPC.call('AfterEffects.publish_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#sceneinventory-button").bind("click", function() {
RPC.call('AfterEffects.sceneinventory_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#subsetmanager-button").bind("click", function() {
RPC.call('AfterEffects.subsetmanager_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#experimental-button").bind("click", function() {
RPC.call('AfterEffects.experimental_tools_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
</head>
<body class="hostElt">
<div id="content">
<div>
<div></div><a href=# id=workfiles-button><button class="hostFontSize">Workfiles...</button></a></div>
<div> <a href=# id=creator-button><button class="hostFontSize">Create...</button></a></div>
<div><a href=# id=loader-button><button class="hostFontSize">Load...</button></a></div>
<div><a href=# id=publish-button><button class="hostFontSize">Publish...</button></a></div>
<div><a href=# id=sceneinventory-button><button class="hostFontSize">Manage...</button></a></div>
<div><a href=# id=subsetmanager-button><button class="hostFontSize">Subset Manager...</button></a></div>
<div><a href=# id=experimental-button><button class="hostFontSize">Experimental Tools...</button></a></div>
</div>
</div>
<!-- <script src="js/libs/PlayerDebugMode"></script> -->
<script src="js/libs/wsrpc.js"></script>
<script src="js/libs/loglevel.min.js"></script>
<script src="js/libs/CSInterface.js"></script>
<script src="js/themeManager.js"></script>
<script src="js/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,530 @@
// json2.js
// 2017-06-12
// Public Domain.
// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
// NOT CONTROL.
// This file creates a global JSON object containing two methods: stringify
// and parse. This file provides the ES5 JSON capability to ES3 systems.
// If a project might run on IE8 or earlier, then this file should be included.
// This file does nothing on ES5 systems.
// JSON.stringify(value, replacer, space)
// value any JavaScript value, usually an object or array.
// replacer an optional parameter that determines how object
// values are stringified for objects. It can be a
// function or an array of strings.
// space an optional parameter that specifies the indentation
// of nested structures. If it is omitted, the text will
// be packed without extra whitespace. If it is a number,
// it will specify the number of spaces to indent at each
// level. If it is a string (such as "\t" or "&nbsp;"),
// it contains the characters used to indent at each level.
// This method produces a JSON text from a JavaScript value.
// When an object value is found, if the object contains a toJSON
// method, its toJSON method will be called and the result will be
// stringified. A toJSON method does not serialize: it returns the
// value represented by the name/value pair that should be serialized,
// or undefined if nothing should be serialized. The toJSON method
// will be passed the key associated with the value, and this will be
// bound to the value.
// For example, this would serialize Dates as ISO strings.
// Date.prototype.toJSON = function (key) {
// function f(n) {
// // Format integers to have at least two digits.
// return (n < 10)
// ? "0" + n
// : n;
// }
// return this.getUTCFullYear() + "-" +
// f(this.getUTCMonth() + 1) + "-" +
// f(this.getUTCDate()) + "T" +
// f(this.getUTCHours()) + ":" +
// f(this.getUTCMinutes()) + ":" +
// f(this.getUTCSeconds()) + "Z";
// };
// You can provide an optional replacer method. It will be passed the
// key and value of each member, with this bound to the containing
// object. The value that is returned from your method will be
// serialized. If your method returns undefined, then the member will
// be excluded from the serialization.
// If the replacer parameter is an array of strings, then it will be
// used to select the members to be serialized. It filters the results
// such that only members with keys listed in the replacer array are
// stringified.
// Values that do not have JSON representations, such as undefined or
// functions, will not be serialized. Such values in objects will be
// dropped; in arrays they will be replaced with null. You can use
// a replacer function to replace those with JSON values.
// JSON.stringify(undefined) returns undefined.
// The optional space parameter produces a stringification of the
// value that is filled with line breaks and indentation to make it
// easier to read.
// If the space parameter is a non-empty string, then that string will
// be used for indentation. If the space parameter is a number, then
// the indentation will be that many spaces.
// Example:
// text = JSON.stringify(["e", {pluribus: "unum"}]);
// // text is '["e",{"pluribus":"unum"}]'
// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
// text = JSON.stringify([new Date()], function (key, value) {
// return this[key] instanceof Date
// ? "Date(" + this[key] + ")"
// : value;
// });
// // text is '["Date(---current time---)"]'
// JSON.parse(text, reviver)
// This method parses a JSON text to produce an object or array.
// It can throw a SyntaxError exception.
// The optional reviver parameter is a function that can filter and
// transform the results. It receives each of the keys and values,
// and its return value is used instead of the original value.
// If it returns what it received, then the structure is not modified.
// If it returns undefined then the member is deleted.
// Example:
// // Parse the text. Values that look like ISO date strings will
// // be converted to Date objects.
// myData = JSON.parse(text, function (key, value) {
// var a;
// if (typeof value === "string") {
// a =
// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
// if (a) {
// return new Date(Date.UTC(
// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
// ));
// }
// return value;
// }
// });
// myData = JSON.parse(
// "[\"Date(09/09/2001)\"]",
// function (key, value) {
// var d;
// if (
// typeof value === "string"
// && value.slice(0, 5) === "Date("
// && value.slice(-1) === ")"
// ) {
// d = new Date(value.slice(5, -1));
// if (d) {
// return d;
// }
// }
// return value;
// }
// );
// This is a reference implementation. You are free to copy, modify, or
// redistribute.
/*jslint
eval, for, this
*/
/*property
JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (typeof JSON !== "object") {
JSON = {};
}
(function () {
"use strict";
var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
function f(n) {
// Format integers to have at least two digits.
return (n < 10)
? "0" + n
: n;
}
function this_value() {
return this.valueOf();
}
if (typeof Date.prototype.toJSON !== "function") {
Date.prototype.toJSON = function () {
return isFinite(this.valueOf())
? (
this.getUTCFullYear()
+ "-"
+ f(this.getUTCMonth() + 1)
+ "-"
+ f(this.getUTCDate())
+ "T"
+ f(this.getUTCHours())
+ ":"
+ f(this.getUTCMinutes())
+ ":"
+ f(this.getUTCSeconds())
+ "Z"
)
: null;
};
Boolean.prototype.toJSON = this_value;
Number.prototype.toJSON = this_value;
String.prototype.toJSON = this_value;
}
var gap;
var indent;
var meta;
var rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
rx_escapable.lastIndex = 0;
return rx_escapable.test(string)
? "\"" + string.replace(rx_escapable, function (a) {
var c = meta[a];
return typeof c === "string"
? c
: "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
}) + "\""
: "\"" + string + "\"";
}
function str(key, holder) {
// Produce a string from holder[key].
var i; // The loop counter.
var k; // The member key.
var v; // The member value.
var length;
var mind = gap;
var partial;
var value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (
value
&& typeof value === "object"
&& typeof value.toJSON === "function"
) {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === "function") {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case "string":
return quote(value);
case "number":
// JSON numbers must be finite. Encode non-finite numbers as null.
return (isFinite(value))
? String(value)
: "null";
case "boolean":
case "null":
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce "null". The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is "object", we might be dealing with an object or an array or
// null.
case "object":
// Due to a specification blunder in ECMAScript, typeof null is "object",
// so watch out for that case.
if (!value) {
return "null";
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === "[object Array]") {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || "null";
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0
? "[]"
: gap
? (
"[\n"
+ gap
+ partial.join(",\n" + gap)
+ "\n"
+ mind
+ "]"
)
: "[" + partial.join(",") + "]";
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === "object") {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === "string") {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (
(gap)
? ": "
: ":"
) + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (
(gap)
? ": "
: ":"
) + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0
? "{}"
: gap
? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
: "{" + partial.join(",") + "}";
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== "function") {
meta = { // table of character substitutions
"\b": "\\b",
"\t": "\\t",
"\n": "\\n",
"\f": "\\f",
"\r": "\\r",
"\"": "\\\"",
"\\": "\\\\"
};
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = "";
indent = "";
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === "number") {
for (i = 0; i < space; i += 1) {
indent += " ";
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === "string") {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== "function" && (
typeof replacer !== "object"
|| typeof replacer.length !== "number"
)) {
throw new Error("JSON.stringify");
}
// Make a fake root object containing our value under the key of "".
// Return the result of stringifying the value.
return str("", {"": value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== "function") {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k;
var v;
var value = holder[key];
if (value && typeof value === "object") {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
rx_dangerous.lastIndex = 0;
if (rx_dangerous.test(text)) {
text = text.replace(rx_dangerous, function (a) {
return (
"\\u"
+ ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with "()" and "new"
// because they can cause invocation, and "=" because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
// replace all simple value tokens with "]" characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or "]" or
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
if (
rx_one.test(
text
.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)
) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval("(" + text + ")");
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return (typeof reviver === "function")
? walk({"": j}, "")
: j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError("JSON.parse");
};
}
}());

View file

@ -0,0 +1,2 @@
/*! loglevel - v1.6.8 - https://github.com/pimterry/loglevel - (c) 2020 Tim Perry - licensed MIT */
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(b):"object"==typeof module&&module.exports?module.exports=b():a.log=b()}(this,function(){"use strict";function a(a,b){var c=a[b];if("function"==typeof c.bind)return c.bind(a);try{return Function.prototype.bind.call(c,a)}catch(b){return function(){return Function.prototype.apply.apply(c,[a,arguments])}}}function b(){console.log&&(console.log.apply?console.log.apply(console,arguments):Function.prototype.apply.apply(console.log,[console,arguments])),console.trace&&console.trace()}function c(c){return"debug"===c&&(c="log"),typeof console!==i&&("trace"===c&&j?b:void 0!==console[c]?a(console,c):void 0!==console.log?a(console,"log"):h)}function d(a,b){for(var c=0;c<k.length;c++){var d=k[c];this[d]=c<a?h:this.methodFactory(d,a,b)}this.log=this.debug}function e(a,b,c){return function(){typeof console!==i&&(d.call(this,b,c),this[a].apply(this,arguments))}}function f(a,b,d){return c(a)||e.apply(this,arguments)}function g(a,b,c){function e(a){var b=(k[a]||"silent").toUpperCase();if(typeof window!==i){try{return void(window.localStorage[l]=b)}catch(a){}try{window.document.cookie=encodeURIComponent(l)+"="+b+";"}catch(a){}}}function g(){var a;if(typeof window!==i){try{a=window.localStorage[l]}catch(a){}if(typeof a===i)try{var b=window.document.cookie,c=b.indexOf(encodeURIComponent(l)+"=");-1!==c&&(a=/^([^;]+)/.exec(b.slice(c))[1])}catch(a){}return void 0===j.levels[a]&&(a=void 0),a}}var h,j=this,l="loglevel";a&&(l+=":"+a),j.name=a,j.levels={TRACE:0,DEBUG:1,INFO:2,WARN:3,ERROR:4,SILENT:5},j.methodFactory=c||f,j.getLevel=function(){return h},j.setLevel=function(b,c){if("string"==typeof b&&void 0!==j.levels[b.toUpperCase()]&&(b=j.levels[b.toUpperCase()]),!("number"==typeof b&&b>=0&&b<=j.levels.SILENT))throw"log.setLevel() called with invalid level: "+b;if(h=b,!1!==c&&e(b),d.call(j,b,a),typeof console===i&&b<j.levels.SILENT)return"No console available for logging"},j.setDefaultLevel=function(a){g()||j.setLevel(a,!1)},j.enableAll=function(a){j.setLevel(j.levels.TRACE,a)},j.disableAll=function(a){j.setLevel(j.levels.SILENT,a)};var m=g();null==m&&(m=null==b?"WARN":b),j.setLevel(m,!1)}var h=function(){},i="undefined",j=typeof window!==i&&typeof window.navigator!==i&&/Trident\/|MSIE /.test(window.navigator.userAgent),k=["trace","debug","info","warn","error"],l=new g,m={};l.getLogger=function(a){if("string"!=typeof a||""===a)throw new TypeError("You must supply a name when creating a logger.");var b=m[a];return b||(b=m[a]=new g(a,l.getLevel(),l.methodFactory)),b};var n=typeof window!==i?window.log:void 0;return l.noConflict=function(){return typeof window!==i&&window.log===l&&(window.log=n),l},l.getLoggers=function(){return m},l});

View file

@ -0,0 +1,393 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.WSRPC = factory());
}(this, function () { 'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Deferred = function Deferred() {
_classCallCheck(this, Deferred);
var self = this;
self.resolve = null;
self.reject = null;
self.done = false;
function wrapper(func) {
return function () {
if (self.done) throw new Error('Promise already done');
self.done = true;
return func.apply(this, arguments);
};
}
self.promise = new Promise(function (resolve, reject) {
self.resolve = wrapper(resolve);
self.reject = wrapper(reject);
});
self.promise.isPending = function () {
return !self.done;
};
return self;
};
function logGroup(group, level, args) {
console.group(group);
console[level].apply(this, args);
console.groupEnd();
}
function log() {
if (!WSRPC.DEBUG) return;
logGroup('WSRPC.DEBUG', 'trace', arguments);
}
function trace(msg) {
if (!WSRPC.TRACE) return;
var payload = msg;
if ('data' in msg) payload = JSON.parse(msg.data);
logGroup("WSRPC.TRACE", 'trace', [payload]);
}
function getAbsoluteWsUrl(url) {
if (/^\w+:\/\//.test(url)) return url;
if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error("Can not construct absolute URL from ".concat(window.location));
var scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
var port = window.location.port === '' ? ":".concat(window.location.port) : '';
var host = window.location.host;
var path = url.replace(/^\/+/gm, '');
return "".concat(scheme, "//").concat(host).concat(port, "/").concat(path);
}
var readyState = Object.freeze({
0: 'CONNECTING',
1: 'OPEN',
2: 'CLOSING',
3: 'CLOSED'
});
var WSRPC = function WSRPC(URL) {
var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;
_classCallCheck(this, WSRPC);
var self = this;
URL = getAbsoluteWsUrl(URL);
self.id = 1;
self.eventId = 0;
self.socketStarted = false;
self.eventStore = {
onconnect: {},
onerror: {},
onclose: {},
onchange: {}
};
self.connectionNumber = 0;
self.oneTimeEventStore = {
onconnect: [],
onerror: [],
onclose: [],
onchange: []
};
self.callQueue = [];
function createSocket() {
var ws = new WebSocket(URL);
var rejectQueue = function rejectQueue() {
self.connectionNumber++; // rejects incoming calls
var deferred; //reject all pending calls
while (0 < self.callQueue.length) {
var callObj = self.callQueue.shift();
deferred = self.store[callObj.id];
delete self.store[callObj.id];
if (deferred && deferred.promise.isPending()) {
deferred.reject('WebSocket error occurred');
}
} // reject all from the store
for (var key in self.store) {
if (!self.store.hasOwnProperty(key)) continue;
deferred = self.store[key];
if (deferred && deferred.promise.isPending()) {
deferred.reject('WebSocket error occurred');
}
}
};
function reconnect(callEvents) {
setTimeout(function () {
try {
self.socket = createSocket();
self.id = 1;
} catch (exc) {
callEvents('onerror', exc);
delete self.socket;
console.error(exc);
}
}, reconnectTimeout);
}
ws.onclose = function (err) {
log('ONCLOSE CALLED', 'STATE', self.public.state());
trace(err);
for (var serial in self.store) {
if (!self.store.hasOwnProperty(serial)) continue;
if (self.store[serial].hasOwnProperty('reject')) {
self.store[serial].reject('Connection closed');
}
}
rejectQueue();
callEvents('onclose', err);
callEvents('onchange', err);
reconnect(callEvents);
};
ws.onerror = function (err) {
log('ONERROR CALLED', 'STATE', self.public.state());
trace(err);
rejectQueue();
callEvents('onerror', err);
callEvents('onchange', err);
log('WebSocket has been closed by error: ', err);
};
function tryCallEvent(func, event) {
try {
return func(event);
} catch (e) {
if (e.hasOwnProperty('stack')) {
log(e.stack);
} else {
log('Event function', func, 'raised unknown error:', e);
}
console.error(e);
}
}
function callEvents(evName, event) {
while (0 < self.oneTimeEventStore[evName].length) {
var deferred = self.oneTimeEventStore[evName].shift();
if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();
}
for (var i in self.eventStore[evName]) {
if (!self.eventStore[evName].hasOwnProperty(i)) continue;
var cur = self.eventStore[evName][i];
tryCallEvent(cur, event);
}
}
ws.onopen = function (ev) {
log('ONOPEN CALLED', 'STATE', self.public.state());
trace(ev);
while (0 < self.callQueue.length) {
// noinspection JSUnresolvedFunction
self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));
}
callEvents('onconnect', ev);
callEvents('onchange', ev);
};
function handleCall(self, data) {
if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');
var connectionNumber = self.connectionNumber;
var deferred = new Deferred();
deferred.promise.then(function (result) {
if (connectionNumber !== self.connectionNumber) return;
self.socket.send(JSON.stringify({
id: data.id,
result: result
}));
}, function (error) {
if (connectionNumber !== self.connectionNumber) return;
self.socket.send(JSON.stringify({
id: data.id,
error: error
}));
});
var func = self.routes[data.method];
if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);
function badPromise() {
throw new Error("You should register route with async flag.");
}
var promiseMock = {
resolve: badPromise,
reject: badPromise
};
try {
deferred.resolve(func.apply(promiseMock, [data.params]));
} catch (e) {
deferred.reject(e);
console.error(e);
}
}
function handleError(self, data) {
if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');
var deferred = self.store[data.id];
if (typeof deferred === 'undefined') return log('Confirmation without handler');
delete self.store[data.id];
log('REJECTING', data.error);
deferred.reject(data.error);
}
function handleResult(self, data) {
var deferred = self.store[data.id];
if (typeof deferred === 'undefined') return log('Confirmation without handler');
delete self.store[data.id];
if (data.hasOwnProperty('result')) {
return deferred.resolve(data.result);
}
return deferred.reject(data.error);
}
ws.onmessage = function (message) {
log('ONMESSAGE CALLED', 'STATE', self.public.state());
trace(message);
if (message.type !== 'message') return;
var data;
try {
data = JSON.parse(message.data);
log(data);
if (data.hasOwnProperty('method')) {
return handleCall(self, data);
} else if (data.hasOwnProperty('error') && data.error === null) {
return handleError(self, data);
} else {
return handleResult(self, data);
}
} catch (exception) {
var err = {
error: exception.message,
result: null,
id: data ? data.id : null
};
self.socket.send(JSON.stringify(err));
console.error(exception);
}
};
return ws;
}
function makeCall(func, args, params) {
self.id += 2;
var deferred = new Deferred();
var callObj = Object.freeze({
id: self.id,
method: func,
params: args
});
var state = self.public.state();
if (state === 'OPEN') {
self.store[self.id] = deferred;
self.socket.send(JSON.stringify(callObj));
} else if (state === 'CONNECTING') {
log('SOCKET IS', state);
self.store[self.id] = deferred;
self.callQueue.push(callObj);
} else {
log('SOCKET IS', state);
if (params && params['noWait']) {
deferred.reject("Socket is: ".concat(state));
} else {
self.store[self.id] = deferred;
self.callQueue.push(callObj);
}
}
return deferred.promise;
}
self.asyncRoutes = {};
self.routes = {};
self.store = {};
self.public = Object.freeze({
call: function call(func, args, params) {
return makeCall(func, args, params);
},
addRoute: function addRoute(route, callback, isAsync) {
self.asyncRoutes[route] = isAsync || false;
self.routes[route] = callback;
},
deleteRoute: function deleteRoute(route) {
delete self.asyncRoutes[route];
return delete self.routes[route];
},
addEventListener: function addEventListener(event, func) {
var eventId = self.eventId++;
self.eventStore[event][eventId] = func;
return eventId;
},
removeEventListener: function removeEventListener(event, index) {
if (self.eventStore[event].hasOwnProperty(index)) {
delete self.eventStore[event][index];
return true;
} else {
return false;
}
},
onEvent: function onEvent(event) {
var deferred = new Deferred();
self.oneTimeEventStore[event].push(deferred);
return deferred.promise;
},
destroy: function destroy() {
return self.socket.close();
},
state: function state() {
return readyState[this.stateCode()];
},
stateCode: function stateCode() {
if (self.socketStarted && self.socket) return self.socket.readyState;
return 3;
},
connect: function connect() {
self.socketStarted = true;
self.socket = createSocket();
}
});
self.public.addRoute('log', function (argsObj) {
//console.info("Websocket sent: ".concat(argsObj));
});
self.public.addRoute('ping', function (data) {
return data;
});
return self.public;
};
WSRPC.DEBUG = false;
WSRPC.TRACE = false;
return WSRPC;
}));
//# sourceMappingURL=wsrpc.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,347 @@
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
indent: 4, maxerr: 50 */
/*global $, window, location, CSInterface, SystemPath, themeManager*/
var csInterface = new CSInterface();
log.warn("script start");
WSRPC.DEBUG = false;
WSRPC.TRACE = false;
// get websocket server url from environment value
async function startUp(url){
promis = runEvalScript("getEnv('" + url + "')");
var res = await promis;
log.warn("res: " + res);
promis = runEvalScript("getEnv('OPENPYPE_DEBUG')");
var debug = await promis;
log.warn("debug: " + debug);
if (debug && debug.toString() == '3'){
WSRPC.DEBUG = true;
WSRPC.TRACE = true;
}
// run rest only after resolved promise
main(res);
}
function get_extension_version(){
/** Returns version number from extension manifest.xml **/
log.debug("get_extension_version")
var path = csInterface.getSystemPath(SystemPath.EXTENSION);
log.debug("extension path " + path);
var result = window.cep.fs.readFile(path + "/CSXS/manifest.xml");
var version = undefined;
if(result.err === 0){
if (window.DOMParser) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(result.data.toString(),
'text/xml');
const children = xmlDoc.children;
for (let i = 0; i <= children.length; i++) {
if (children[i] &&
children[i].getAttribute('ExtensionBundleVersion')) {
version =
children[i].getAttribute('ExtensionBundleVersion');
}
}
}
}
return '{"result":"' + version + '"}'
}
function main(websocket_url){
// creates connection to 'websocket_url', registers routes
var default_url = 'ws://localhost:8099/ws/';
if (websocket_url == ''){
websocket_url = default_url;
}
RPC = new WSRPC(websocket_url, 5000); // spin connection
RPC.connect();
log.warn("connected");
RPC.addRoute('AfterEffects.open', function (data) {
log.warn('Server called client route "open":', data);
var escapedPath = EscapeStringForJSX(data.path);
return runEvalScript("fileOpen('" + escapedPath +"')")
.then(function(result){
log.warn("open: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_metadata', function (data) {
log.warn('Server called client route "get_metadata":', data);
return runEvalScript("getMetadata()")
.then(function(result){
log.warn("getMetadata: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_active_document_name', function (data) {
log.warn('Server called client route ' +
'"get_active_document_name":', data);
return runEvalScript("getActiveDocumentName()")
.then(function(result){
log.warn("get_active_document_name: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){
log.warn('Server called client route ' +
'"get_active_document_full_name":', data);
return runEvalScript("getActiveDocumentFullName()")
.then(function(result){
log.warn("get_active_document_full_name: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_items', function (data) {
log.warn('Server called client route "get_items":', data);
return runEvalScript("getItems(" + data.comps + "," +
data.folders + "," +
data.footages + ")")
.then(function(result){
log.warn("get_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_selected_items', function (data) {
log.warn('Server called client route "get_selected_items":', data);
return runEvalScript("getSelectedItems(" + data.comps + "," +
data.folders + "," +
data.footages + ")")
.then(function(result){
log.warn("get_items: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.import_file', function (data) {
log.warn('Server called client route "import_file":', data);
var escapedPath = EscapeStringForJSX(data.path);
return runEvalScript("importFile('" + escapedPath +"', " +
"'" + data.item_name + "'," +
"'" + JSON.stringify(
data.import_options) + "')")
.then(function(result){
log.warn("importFile: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.replace_item', function (data) {
log.warn('Server called client route "replace_item":', data);
var escapedPath = EscapeStringForJSX(data.path);
return runEvalScript("replaceItem(" + data.item_id + ", " +
"'" + escapedPath + "', " +
"'" + data.item_name + "')")
.then(function(result){
log.warn("replaceItem: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.rename_item', function (data) {
log.warn('Server called client route "rename_item":', data);
return runEvalScript("renameItem(" + data.item_id + ", " +
"'" + data.item_name + "')")
.then(function(result){
log.warn("renameItem: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.delete_item', function (data) {
log.warn('Server called client route "delete_item":', data);
return runEvalScript("deleteItem(" + data.item_id + ")")
.then(function(result){
log.warn("deleteItem: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.imprint', function (data) {
log.warn('Server called client route "imprint":', data);
var escaped = data.payload.replace(/\n/g, "\\n");
return runEvalScript("imprint('" + escaped +"')")
.then(function(result){
log.warn("imprint: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.set_label_color', function (data) {
log.warn('Server called client route "set_label_color":', data);
return runEvalScript("setLabelColor(" + data.item_id + "," +
data.color_idx + ")")
.then(function(result){
log.warn("imprint: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_work_area', function (data) {
log.warn('Server called client route "get_work_area":', data);
return runEvalScript("getWorkArea(" + data.item_id + ")")
.then(function(result){
log.warn("getWorkArea: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.set_work_area', function (data) {
log.warn('Server called client route "set_work_area":', data);
return runEvalScript("setWorkArea(" + data.item_id + ',' +
data.start + ',' +
data.duration + ',' +
data.frame_rate + ")")
.then(function(result){
log.warn("getWorkArea: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.saveAs', function (data) {
log.warn('Server called client route "saveAs":', data);
var escapedPath = EscapeStringForJSX(data.image_path);
return runEvalScript("saveAs('" + escapedPath + "', " +
data.as_copy + ")")
.then(function(result){
log.warn("saveAs: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.save', function (data) {
log.warn('Server called client route "save":', data);
return runEvalScript("save()")
.then(function(result){
log.warn("save: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_render_info', function (data) {
log.warn('Server called client route "get_render_info":', data);
return runEvalScript("getRenderInfo()")
.then(function(result){
log.warn("get_render_info: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_audio_url', function (data) {
log.warn('Server called client route "get_audio_url":', data);
return runEvalScript("getAudioUrlForComp(" + data.item_id + ")")
.then(function(result){
log.warn("getAudioUrlForComp: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.import_background', function (data) {
log.warn('Server called client route "import_background":', data);
return runEvalScript("importBackground(" + data.comp_id + ", " +
"'" + data.comp_name + "', " +
JSON.stringify(data.files) + ")")
.then(function(result){
log.warn("importBackground: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.reload_background', function (data) {
log.warn('Server called client route "reload_background":', data);
return runEvalScript("reloadBackground(" + data.comp_id + ", " +
"'" + data.comp_name + "', " +
JSON.stringify(data.files) + ")")
.then(function(result){
log.warn("reloadBackground: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.add_item_as_layer', function (data) {
log.warn('Server called client route "add_item_as_layer":', data);
return runEvalScript("addItemAsLayerToComp(" + data.comp_id + ", " +
data.item_id + "," +
" null )")
.then(function(result){
log.warn("addItemAsLayerToComp: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.render', function (data) {
log.warn('Server called client route "render":', data);
var escapedPath = EscapeStringForJSX(data.folder_url);
return runEvalScript("render('" + escapedPath +"')")
.then(function(result){
log.warn("render: " + result);
return result;
});
});
RPC.addRoute('AfterEffects.get_extension_version', function (data) {
log.warn('Server called client route "get_extension_version":', data);
return get_extension_version();
});
RPC.addRoute('AfterEffects.close', function (data) {
log.warn('Server called client route "close":', data);
return runEvalScript("close()");
});
}
/** main entry point **/
startUp("WEBSOCKET_URL");
(function () {
'use strict';
var csInterface = new CSInterface();
function init() {
themeManager.init();
$("#btn_test").click(function () {
csInterface.evalScript('sayHello()');
});
}
init();
}());
function EscapeStringForJSX(str){
// Replaces:
// \ with \\
// ' with \'
// " with \"
// See: https://stackoverflow.com/a/3967927/5285364
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g,'\\"');
}
function runEvalScript(script) {
// because of asynchronous nature of functions in jsx
// this waits for response
return new Promise(function(resolve, reject){
csInterface.evalScript(script, resolve);
});
}

View file

@ -0,0 +1,128 @@
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global window, document, CSInterface*/
/*
Responsible for overwriting CSS at runtime according to CC app
settings as defined by the end user.
*/
var themeManager = (function () {
'use strict';
/**
* Convert the Color object to string in hexadecimal format;
*/
function toHex(color, delta) {
function computeValue(value, delta) {
var computedValue = !isNaN(delta) ? value + delta : value;
if (computedValue < 0) {
computedValue = 0;
} else if (computedValue > 255) {
computedValue = 255;
}
computedValue = Math.floor(computedValue);
computedValue = computedValue.toString(16);
return computedValue.length === 1 ? "0" + computedValue : computedValue;
}
var hex = "";
if (color) {
hex = computeValue(color.red, delta) + computeValue(color.green, delta) + computeValue(color.blue, delta);
}
return hex;
}
function reverseColor(color, delta) {
return toHex({
red: Math.abs(255 - color.red),
green: Math.abs(255 - color.green),
blue: Math.abs(255 - color.blue)
},
delta);
}
function addRule(stylesheetId, selector, rule) {
var stylesheet = document.getElementById(stylesheetId);
if (stylesheet) {
stylesheet = stylesheet.sheet;
if (stylesheet.addRule) {
stylesheet.addRule(selector, rule);
} else if (stylesheet.insertRule) {
stylesheet.insertRule(selector + ' { ' + rule + ' }', stylesheet.cssRules.length);
}
}
}
/**
* Update the theme with the AppSkinInfo retrieved from the host product.
*/
function updateThemeWithAppSkinInfo(appSkinInfo) {
var panelBgColor = appSkinInfo.panelBackgroundColor.color;
var bgdColor = toHex(panelBgColor);
var darkBgdColor = toHex(panelBgColor, 20);
var fontColor = "F0F0F0";
if (panelBgColor.red > 122) {
fontColor = "000000";
}
var lightBgdColor = toHex(panelBgColor, -100);
var styleId = "hostStyle";
addRule(styleId, ".hostElt", "background-color:" + "#" + bgdColor);
addRule(styleId, ".hostElt", "font-size:" + appSkinInfo.baseFontSize + "px;");
addRule(styleId, ".hostElt", "font-family:" + appSkinInfo.baseFontFamily);
addRule(styleId, ".hostElt", "color:" + "#" + fontColor);
addRule(styleId, ".hostBgd", "background-color:" + "#" + bgdColor);
addRule(styleId, ".hostBgdDark", "background-color: " + "#" + darkBgdColor);
addRule(styleId, ".hostBgdLight", "background-color: " + "#" + lightBgdColor);
addRule(styleId, ".hostFontSize", "font-size:" + appSkinInfo.baseFontSize + "px;");
addRule(styleId, ".hostFontFamily", "font-family:" + appSkinInfo.baseFontFamily);
addRule(styleId, ".hostFontColor", "color:" + "#" + fontColor);
addRule(styleId, ".hostFont", "font-size:" + appSkinInfo.baseFontSize + "px;");
addRule(styleId, ".hostFont", "font-family:" + appSkinInfo.baseFontFamily);
addRule(styleId, ".hostFont", "color:" + "#" + fontColor);
addRule(styleId, ".hostButton", "background-color:" + "#" + darkBgdColor);
addRule(styleId, ".hostButton:hover", "background-color:" + "#" + bgdColor);
addRule(styleId, ".hostButton:active", "background-color:" + "#" + darkBgdColor);
addRule(styleId, ".hostButton", "border-color: " + "#" + lightBgdColor);
}
function onAppThemeColorChanged(event) {
var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo;
updateThemeWithAppSkinInfo(skinInfo);
}
function init() {
var csInterface = new CSInterface();
updateThemeWithAppSkinInfo(csInterface.hostEnvironment.appSkinInfo);
csInterface.addEventListener(CSInterface.THEME_COLOR_CHANGED_EVENT, onAppThemeColorChanged);
}
return {
init: init
};
}());

View file

@ -0,0 +1,723 @@
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true,
indent: 4, maxerr: 50 */
/*global $, Folder*/
#include "../js/libs/json.js";
/* All public API function should return JSON! */
app.preferences.savePrefAsBool("General Section", "Show Welcome Screen", false) ;
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function ( item ) {
var index = 0, length = this.length;
for ( ; index < length; index++ ) {
if ( this[index] === item )
return index;
}
return -1;
};
}
function sayHello(){
alert("hello from ExtendScript");
}
function getEnv(variable){
return $.getenv(variable);
}
function getMetadata(){
/**
* Returns payload in 'Label' field of project's metadata
*
**/
if (ExternalObject.AdobeXMPScript === undefined){
ExternalObject.AdobeXMPScript =
new ExternalObject('lib:AdobeXMPScript');
}
var proj = app.project;
var meta = new XMPMeta(app.project.xmpPacket);
var schemaNS = XMPMeta.getNamespaceURI("xmp");
var label = "xmp:Label";
if (meta.doesPropertyExist(schemaNS, label)){
var prop = meta.getProperty(schemaNS, label);
return prop.value;
}
return _prepareSingleValue([]);
}
function imprint(payload){
/**
* Stores payload in 'Label' field of project's metadata
*
* Args:
* payload (string): json content
*/
if (ExternalObject.AdobeXMPScript === undefined){
ExternalObject.AdobeXMPScript =
new ExternalObject('lib:AdobeXMPScript');
}
var proj = app.project;
var meta = new XMPMeta(app.project.xmpPacket);
var schemaNS = XMPMeta.getNamespaceURI("xmp");
var label = "xmp:Label";
meta.setProperty(schemaNS, label, payload);
app.project.xmpPacket = meta.serialize();
}
function fileOpen(path){
/**
* Opens (project) file on 'path'
*/
fp = new File(path);
return _prepareSingleValue(app.open(fp))
}
function getActiveDocumentName(){
/**
* Returns file name of active document
* */
var file = app.project.file;
if (file){
return _prepareSingleValue(file.name)
}
return _prepareError("No file open currently");
}
function getActiveDocumentFullName(){
/**
* Returns absolute path to current project
* */
var file = app.project.file;
if (file){
var f = new File(file.fullName);
var path = f.fsName;
f.close();
return _prepareSingleValue(path)
}
return _prepareError("No file open currently");
}
function getItems(comps, folders, footages){
/**
* Returns JSON representation of compositions and
* if 'collectLayers' then layers in comps too.
*
* Args:
* comps (bool): return selected compositions
* folders (bool): return folders
* footages (bool): return FootageItem
* Returns:
* (list) of JSON items
*/
var items = []
for (i = 1; i <= app.project.items.length; ++i){
var item = app.project.items[i];
if (!item){
continue;
}
var ret = _getItem(item, comps, folders, footages);
if (ret){
items.push(ret);
}
}
return '[' + items.join() + ']';
}
function getSelectedItems(comps, folders, footages){
/**
* Returns list of selected items from Project menu
*
* Args:
* comps (bool): return selected compositions
* folders (bool): return folders
* footages (bool): return FootageItem
* Returns:
* (list) of JSON items
*/
var items = []
for (i = 0; i < app.project.selection.length; ++i){
var item = app.project.selection[i];
if (!item){
continue;
}
var ret = _getItem(item, comps, folders, footages);
if (ret){
items.push(ret);
}
}
return '[' + items.join() + ']';
}
function _getItem(item, comps, folders, footages){
/**
* Auxiliary function as project items and selections
* are indexed in different way :/
* Refactor
*/
var item_type = '';
if (item instanceof FolderItem){
item_type = 'folder';
if (!folders){
return "{}";
}
}
if (item instanceof FootageItem){
item_type = 'footage';
if (!footages){
return "{}";
}
}
if (item instanceof CompItem){
item_type = 'comp';
if (!comps){
return "{}";
}
}
var item = {"name": item.name,
"id": item.id,
"type": item_type};
return JSON.stringify(item);
}
function importFile(path, item_name, import_options){
/**
* Imports file (image tested for now) as a FootageItem.
* Creates new composition
*
* Args:
* path (string): absolute path to image file
* item_name (string): label for composition
* Returns:
* JSON {name, id}
*/
var comp;
var ret = {};
try{
import_options = JSON.parse(import_options);
} catch (e){
return _prepareError("Couldn't parse import options " + import_options);
}
app.beginUndoGroup("Import File");
fp = new File(path);
if (fp.exists){
try {
im_opt = new ImportOptions(fp);
importAsType = import_options["ImportAsType"];
if ('ImportAsType' in import_options){ // refactor
if (importAsType.indexOf('COMP') > 0){
im_opt.importAs = ImportAsType.COMP;
}
if (importAsType.indexOf('FOOTAGE') > 0){
im_opt.importAs = ImportAsType.FOOTAGE;
}
if (importAsType.indexOf('COMP_CROPPED_LAYERS') > 0){
im_opt.importAs = ImportAsType.COMP_CROPPED_LAYERS;
}
if (importAsType.indexOf('PROJECT') > 0){
im_opt.importAs = ImportAsType.PROJECT;
}
}
if ('sequence' in import_options){
im_opt.sequence = true;
}
comp = app.project.importFile(im_opt);
if (app.project.selection.length == 2 &&
app.project.selection[0] instanceof FolderItem){
comp.parentFolder = app.project.selection[0]
}
} catch (error) {
return _prepareError(error.toString() + importOptions.file.fsName);
} finally {
fp.close();
}
}else{
return _prepareError("File " + path + " not found.");
}
if (comp){
comp.name = item_name;
comp.label = 9; // Green
ret = {"name": comp.name, "id": comp.id}
}
app.endUndoGroup();
return JSON.stringify(ret);
}
function setLabelColor(comp_id, color_idx){
/**
* Set item_id label to 'color_idx' color
* Args:
* item_id (int): item id
* color_idx (int): 0-16 index from Label
*/
var item = app.project.itemByID(comp_id);
if (item){
item.label = color_idx;
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function replaceItem(comp_id, path, item_name){
/**
* Replaces loaded file with new file and updates name
*
* Args:
* comp_id (int): id of composition, not a index!
* path (string): absolute path to new file
* item_name (string): new composition name
*/
app.beginUndoGroup("Replace File");
fp = new File(path);
if (!fp.exists){
return _prepareError("File " + path + " not found.");
}
var item = app.project.itemByID(comp_id);
if (item){
try{
if (isFileSequence(item)) {
item.replaceWithSequence(fp, false);
}else{
item.replace(fp);
}
item.name = item_name;
} catch (error) {
return _prepareError(error.toString() + path);
} finally {
fp.close();
}
}else{
return _prepareError("There is no composition with "+ comp_id);
}
app.endUndoGroup();
}
function renameItem(item_id, new_name){
/**
* Renames item with 'item_id' to 'new_name'
*
* Args:
* item_id (int): id to search item
* new_name (str)
*/
var item = app.project.itemByID(item_id);
if (item){
item.name = new_name;
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function deleteItem(item_id){
/**
* Delete any 'item_id'
*
* Not restricted only to comp, it could delete
* any item with 'id'
*/
var item = app.project.itemByID(item_id);
if (item){
item.remove();
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function getWorkArea(comp_id){
/**
* Returns information about workarea - are that will be
* rendered. All calculation will be done in OpenPype,
* easier to modify without redeploy of extension.
*
* Returns
* (dict)
*/
var item = app.project.itemByID(comp_id);
if (item){
return JSON.stringify({
"workAreaStart": item.displayStartFrame,
"workAreaDuration": item.duration,
"frameRate": item.frameRate});
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function setWorkArea(comp_id, workAreaStart, workAreaDuration, frameRate){
/**
* Sets work area info from outside (from Ftrack via OpenPype)
*/
var item = app.project.itemByID(comp_id);
if (item){
item.displayStartTime = workAreaStart;
item.duration = workAreaDuration;
item.frameRate = frameRate;
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function save(){
/**
* Saves current project
*/
app.project.save(); //TODO path is wrong, File instead
}
function saveAs(path){
/**
* Saves current project as 'path'
* */
app.project.save(fp = new File(path));
}
function getRenderInfo(){
/***
Get info from render queue.
Currently pulls only file name to parse extension and
if it is sequence in Python
**/
try{
var render_item = app.project.renderQueue.item(1);
if (render_item.status == RQItemStatus.DONE){
render_item.duplicate(); // create new, cannot change status if DONE
render_item.remove(); // remove existing to limit duplications
render_item = app.project.renderQueue.item(1);
}
render_item.render = true; // always set render queue to render
var item = render_item.outputModule(1);
} catch (error) {
return _prepareError("There is no render queue, create one");
}
var file_url = item.file.toString();
return JSON.stringify({
"file_name": file_url
})
}
function getAudioUrlForComp(comp_id){
/**
* Searches composition for audio layer
*
* Only single AVLayer is expected!
* Used for collecting Audio
*
* Args:
* comp_id (int): id of composition
* Return:
* (str) with url to audio content
*/
var item = app.project.itemByID(comp_id);
if (item){
for (i = 1; i <= item.numLayers; ++i){
var layer = item.layers[i];
if (layer instanceof AVLayer){
return layer.source.file.fsName.toString();
}
}
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function addItemAsLayerToComp(comp_id, item_id, found_comp){
/**
* Adds already imported FootageItem ('item_id') as a new
* layer to composition ('comp_id').
*
* Args:
* comp_id (int): id of target composition
* item_id (int): FootageItem.id
* found_comp (CompItem, optional): to limit quering if
* comp already found previously
*/
var comp = found_comp || app.project.itemByID(comp_id);
if (comp){
item = app.project.itemByID(item_id);
if (item){
comp.layers.add(item);
}else{
return _prepareError("There is no item with " + item_id);
}
}else{
return _prepareError("There is no composition with "+ comp_id);
}
}
function importBackground(comp_id, composition_name, files_to_import){
/**
* Imports backgrounds images to existing or new composition.
*
* If comp_id is not provided, new composition is created, basic
* values (width, heights, frameRatio) takes from first imported
* image.
*
* Args:
* comp_id (int): id of existing composition (null if new)
* composition_name (str): used when new composition
* files_to_import (list): list of absolute paths to import and
* add as layers
*
* Returns:
* (str): json representation (id, name, members)
*/
var comp;
var folder;
var imported_ids = [];
if (comp_id){
comp = app.project.itemByID(comp_id);
folder = comp.parentFolder;
}else{
if (app.project.selection.length > 1){
return _prepareError(
"Too many items selected, select only target composition!");
}else{
selected_item = app.project.activeItem;
if (selected_item instanceof Folder){
comp = selected_item;
folder = selected_item;
}
}
}
if (files_to_import){
for (i = 0; i < files_to_import.length; ++i){
item = _importItem(files_to_import[i]);
if (!item){
return _prepareError(
"No item for " + item_json["id"] +
". Import background failed.")
}
if (!comp){
folder = app.project.items.addFolder(composition_name);
imported_ids.push(folder.id);
comp = app.project.items.addComp(composition_name, item.width,
item.height, item.pixelAspect,
1, 26.7); // hardcode defaults
imported_ids.push(comp.id);
comp.parentFolder = folder;
}
imported_ids.push(item.id)
item.parentFolder = folder;
addItemAsLayerToComp(comp.id, item.id, comp);
}
}
var item = {"name": comp.name,
"id": folder.id,
"members": imported_ids};
return JSON.stringify(item);
}
function reloadBackground(comp_id, composition_name, files_to_import){
/**
* Reloads existing composition.
*
* It deletes complete composition with encompassing folder, recreates
* from scratch via 'importBackground' functionality.
*
* Args:
* comp_id (int): id of existing composition (null if new)
* composition_name (str): used when new composition
* files_to_import (list): list of absolute paths to import and
* add as layers
*
* Returns:
* (str): json representation (id, name, members)
*
*/
var imported_ids = []; // keep track of members of composition
comp = app.project.itemByID(comp_id);
folder = comp.parentFolder;
if (folder){
renameItem(folder.id, composition_name);
imported_ids.push(folder.id);
}
if (comp){
renameItem(comp.id, composition_name);
imported_ids.push(comp.id);
}
var existing_layer_names = [];
var existing_layer_ids = []; // because ExtendedScript doesnt have keys()
for (i = 1; i <= folder.items.length; ++i){
layer = folder.items[i];
//because comp.layers[i] doesnt have 'id' accessible
if (layer instanceof CompItem){
continue;
}
existing_layer_names.push(layer.name);
existing_layer_ids.push(layer.id);
}
var new_filenames = [];
if (files_to_import){
for (i = 0; i < files_to_import.length; ++i){
file_name = _get_file_name(files_to_import[i]);
new_filenames.push(file_name);
idx = existing_layer_names.indexOf(file_name);
if (idx >= 0){ // update
var layer_id = existing_layer_ids[idx];
replaceItem(layer_id, files_to_import[i], file_name);
imported_ids.push(layer_id);
}else{ // new layer
item = _importItem(files_to_import[i]);
if (!item){
return _prepareError(
"No item for " + files_to_import[i] +
". Reload background failed.");
}
imported_ids.push(item.id);
item.parentFolder = folder;
addItemAsLayerToComp(comp.id, item.id, comp);
}
}
}
_delete_obsolete_items(folder, new_filenames);
var item = {"name": comp.name,
"id": folder.id,
"members": imported_ids};
return JSON.stringify(item);
}
function _get_file_name(file_url){
/**
* Returns file name without extension from 'file_url'
*
* Args:
* file_url (str): full absolute url
* Returns:
* (str)
*/
fp = new File(file_url);
file_name = fp.name.substring(0, fp.name.lastIndexOf("."));
return file_name;
}
function _delete_obsolete_items(folder, new_filenames){
/***
* Goes through 'folder' and removes layers not in new
* background
*
* Args:
* folder (FolderItem)
* new_filenames (array): list of layer names in new bg
*/
// remove items in old, but not in new
delete_ids = []
for (i = 1; i <= folder.items.length; ++i){
layer = folder.items[i];
//because comp.layers[i] doesnt have 'id' accessible
if (layer instanceof CompItem){
continue;
}
if (new_filenames.indexOf(layer.name) < 0){
delete_ids.push(layer.id);
}
}
for (i = 0; i < delete_ids.length; ++i){
deleteItem(delete_ids[i]);
}
}
function _importItem(file_url){
/**
* Imports 'file_url' as new FootageItem
*
* Args:
* file_url (str): file url with content
* Returns:
* (FootageItem)
*/
file_name = _get_file_name(file_url);
//importFile prepared previously to return json
item_json = importFile(file_url, file_name, JSON.stringify({"ImportAsType":"FOOTAGE"}));
item_json = JSON.parse(item_json);
item = app.project.itemByID(item_json["id"]);
return item;
}
function isFileSequence (item){
/**
* Check that item is a recognizable sequence
*/
if (item instanceof FootageItem && item.mainSource instanceof FileSource && !(item.mainSource.isStill) && item.hasVideo){
var extname = item.mainSource.file.fsName.split('.').pop();
return extname.match(new RegExp("(ai|bmp|bw|cin|cr2|crw|dcr|dng|dib|dpx|eps|erf|exr|gif|hdr|ico|icb|iff|jpe|jpeg|jpg|mos|mrw|nef|orf|pbm|pef|pct|pcx|pdf|pic|pict|png|ps|psd|pxr|raf|raw|rgb|rgbe|rla|rle|rpf|sgi|srf|tdi|tga|tif|tiff|vda|vst|x3f|xyze)", "i")) !== null;
}
return false;
}
function render(target_folder){
var out_dir = new Folder(target_folder);
var out_dir = out_dir.fsName;
for (i = 1; i <= app.project.renderQueue.numItems; ++i){
var render_item = app.project.renderQueue.item(i);
var om1 = app.project.renderQueue.item(i).outputModule(1);
var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space?
var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE );
if (render_item.status == RQItemStatus.DONE){
render_item.duplicate();
render_item.remove();
continue;
}
var targetFolder = new Folder(target_folder);
if (!targetFolder.exists) {
targetFolder.create();
}
om1.file = new File(targetFolder.fsName + '/' + file_name);
}
app.project.renderQueue.render();
}
function close(){
app.project.close(CloseOptions.DO_NOT_SAVE_CHANGES);
app.quit();
}
function _prepareSingleValue(value){
return JSON.stringify({"result": value})
}
function _prepareError(error_msg){
return JSON.stringify({"error": error_msg})
}

View file

@ -0,0 +1,319 @@
import os
import subprocess
import collections
import logging
import asyncio
import functools
from wsrpc_aiohttp import (
WebSocketRoute,
WebSocketAsync
)
from Qt import QtCore
from openpype.tools.utils import host_tools
from avalon import api
from avalon.tools.webserver.app import WebServerTool
from .ws_stub import AfterEffectsServerStub
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
class ConnectionNotEstablishedYet(Exception):
pass
def get_stub():
"""
Convenience function to get server RPC stub to call methods directed
for host (Photoshop).
It expects already created connection, started from client.
Currently created when panel is opened (PS: Window>Extensions>Avalon)
:return: <PhotoshopClientStub> where functions could be called from
"""
ae_stub = AfterEffectsServerStub()
if not ae_stub.client:
raise ConnectionNotEstablishedYet("Connection is not created yet")
return ae_stub
def stub():
return get_stub()
def show_tool_by_name(tool_name):
kwargs = {}
if tool_name == "loader":
kwargs["use_context"] = True
host_tools.show_tool_by_name(tool_name, **kwargs)
class ProcessLauncher(QtCore.QObject):
route_name = "AfterEffects"
_main_thread_callbacks = collections.deque()
def __init__(self, subprocess_args):
self._subprocess_args = subprocess_args
self._log = None
super(ProcessLauncher, self).__init__()
# Keep track if launcher was alreadu started
self._started = False
self._process = None
self._websocket_server = None
start_process_timer = QtCore.QTimer()
start_process_timer.setInterval(100)
loop_timer = QtCore.QTimer()
loop_timer.setInterval(200)
start_process_timer.timeout.connect(self._on_start_process_timer)
loop_timer.timeout.connect(self._on_loop_timer)
self._start_process_timer = start_process_timer
self._loop_timer = loop_timer
@property
def log(self):
if self._log is None:
from openpype.api import Logger
self._log = Logger.get_logger("{}-launcher".format(
self.route_name))
return self._log
@property
def websocket_server_is_running(self):
if self._websocket_server is not None:
return self._websocket_server.is_running
return False
@property
def is_process_running(self):
if self._process is not None:
return self._process.poll() is None
return False
@property
def is_host_connected(self):
"""Returns True if connected, False if app is not running at all."""
if not self.is_process_running:
return False
try:
_stub = get_stub()
if _stub:
return True
except Exception:
pass
return None
@classmethod
def execute_in_main_thread(cls, callback):
cls._main_thread_callbacks.append(callback)
def start(self):
if self._started:
return
self.log.info("Started launch logic of AfterEffects")
self._started = True
self._start_process_timer.start()
def exit(self):
""" Exit whole application. """
if self._start_process_timer.isActive():
self._start_process_timer.stop()
if self._loop_timer.isActive():
self._loop_timer.stop()
if self._websocket_server is not None:
self._websocket_server.stop()
if self._process:
self._process.kill()
self._process.wait()
QtCore.QCoreApplication.exit()
def _on_loop_timer(self):
# TODO find better way and catch errors
# Run only callbacks that are in queue at the moment
cls = self.__class__
for _ in range(len(cls._main_thread_callbacks)):
if cls._main_thread_callbacks:
callback = cls._main_thread_callbacks.popleft()
callback()
if not self.is_process_running:
self.log.info("Host process is not running. Closing")
self.exit()
elif not self.websocket_server_is_running:
self.log.info("Websocket server is not running. Closing")
self.exit()
def _on_start_process_timer(self):
# TODO add try except validations for each part in this method
# Start server as first thing
if self._websocket_server is None:
self._init_server()
return
# TODO add waiting time
# Wait for webserver
if not self.websocket_server_is_running:
return
# Start application process
if self._process is None:
self._start_process()
self.log.info("Waiting for host to connect")
return
# TODO add waiting time
# Wait until host is connected
if self.is_host_connected:
self._start_process_timer.stop()
self._loop_timer.start()
elif (
not self.is_process_running
or not self.websocket_server_is_running
):
self.exit()
def _init_server(self):
if self._websocket_server is not None:
return
self.log.debug(
"Initialization of websocket server for host communication"
)
self._websocket_server = websocket_server = WebServerTool()
if websocket_server.port_occupied(
websocket_server.host_name,
websocket_server.port
):
self.log.info(
"Server already running, sending actual context and exit."
)
asyncio.run(websocket_server.send_context_change(self.route_name))
self.exit()
return
# Add Websocket route
websocket_server.add_route("*", "/ws/", WebSocketAsync)
# Add after effects route to websocket handler
print("Adding {} route".format(self.route_name))
WebSocketAsync.add_route(
self.route_name, AfterEffectsRoute
)
self.log.info("Starting websocket server for host communication")
websocket_server.start_server()
def _start_process(self):
if self._process is not None:
return
self.log.info("Starting host process")
try:
self._process = subprocess.Popen(
self._subprocess_args,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception:
self.log.info("exce", exc_info=True)
self.exit()
class AfterEffectsRoute(WebSocketRoute):
"""
One route, mimicking external application (like Harmony, etc).
All functions could be called from client.
'do_notify' function calls function on the client - mimicking
notification after long running job on the server or similar
"""
instance = None
def init(self, **kwargs):
# Python __init__ must be return "self".
# This method might return anything.
log.debug("someone called AfterEffects route")
self.instance = self
return kwargs
# server functions
async def ping(self):
log.debug("someone called AfterEffects route ping")
# This method calls function on the client side
# client functions
async def set_context(self, project, asset, task):
"""
Sets 'project' and 'asset' to envs, eg. setting context
Args:
project (str)
asset (str)
"""
log.info("Setting context change")
log.info("project {} asset {} ".format(project, asset))
if project:
api.Session["AVALON_PROJECT"] = project
os.environ["AVALON_PROJECT"] = project
if asset:
api.Session["AVALON_ASSET"] = asset
os.environ["AVALON_ASSET"] = asset
if task:
api.Session["AVALON_TASK"] = task
os.environ["AVALON_TASK"] = task
async def read(self):
log.debug("aftereffects.read client calls server server calls "
"aftereffects client")
return await self.socket.call('aftereffects.read')
# panel routes for tools
async def creator_route(self):
self._tool_route("creator")
async def workfiles_route(self):
self._tool_route("workfiles")
async def loader_route(self):
self._tool_route("loader")
async def publish_route(self):
self._tool_route("publish")
async def sceneinventory_route(self):
self._tool_route("sceneinventory")
async def subsetmanager_route(self):
self._tool_route("subsetmanager")
async def experimental_tools_route(self):
self._tool_route("experimental_tools")
def _tool_route(self, _tool_name):
"""The address accessed when clicking on the buttons."""
partial_method = functools.partial(show_tool_by_name,
_tool_name)
ProcessLauncher.execute_in_main_thread(partial_method)
# Required return statement.
return "nothing"

View file

@ -0,0 +1,71 @@
import os
import sys
import contextlib
import traceback
import logging
from Qt import QtWidgets
from openpype.lib.remote_publish import headless_publish
from openpype.tools.utils import host_tools
from .launch_logic import ProcessLauncher, get_stub
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
def safe_excepthook(*args):
traceback.print_exception(*args)
def main(*subprocess_args):
sys.excepthook = safe_excepthook
import avalon.api
from openpype.hosts.aftereffects import api
avalon.api.install(api)
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
app = QtWidgets.QApplication([])
app.setQuitOnLastWindowClosed(False)
launcher = ProcessLauncher(subprocess_args)
launcher.start()
if os.environ.get("HEADLESS_PUBLISH"):
# reusing ConsoleTrayApp approach as it was already implemented
launcher.execute_in_main_thread(lambda: headless_publish(
log,
"CloseAE",
os.environ.get("IS_TEST")))
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
save = False
if os.getenv("WORKFILES_SAVE_AS"):
save = True
launcher.execute_in_main_thread(
lambda: host_tools.show_tool_by_name("workfiles", save=save)
)
sys.exit(app.exec_())
@contextlib.contextmanager
def maintained_selection():
"""Maintain selection during context."""
selection = get_stub().get_selected_items(True, False, False)
try:
yield selection
finally:
pass
def get_extension_manifest_path():
return os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"extension",
"CSXS",
"manifest.xml"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,272 @@
import os
import sys
from Qt import QtWidgets
import pyblish.api
import avalon.api
from avalon import io, pipeline
from openpype import lib
from openpype.api import Logger
import openpype.hosts.aftereffects
from .launch_logic import get_stub
log = Logger.get_logger(__name__)
HOST_DIR = os.path.dirname(
os.path.abspath(openpype.hosts.aftereffects.__file__)
)
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
def check_inventory():
if not lib.any_outdated():
return
host = pyblish.api.registered_host()
outdated_containers = []
for container in host.ls():
representation = container['representation']
representation_doc = io.find_one(
{
"_id": io.ObjectId(representation),
"type": "representation"
},
projection={"parent": True}
)
if representation_doc and not lib.is_latest(representation_doc):
outdated_containers.append(container)
# Warn about outdated containers.
print("Starting new QApplication..")
app = QtWidgets.QApplication(sys.argv)
message_box = QtWidgets.QMessageBox()
message_box.setIcon(QtWidgets.QMessageBox.Warning)
msg = "There are outdated containers in the scene."
message_box.setText(msg)
message_box.exec_()
def application_launch():
check_inventory()
def install():
print("Installing Pype config...")
pyblish.api.register_host("aftereffects")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
avalon.api.on("application.launched", application_launch)
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle layer visibility on instance toggles."""
instance[0].Visible = new_value
def get_asset_settings():
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = lib.get_asset()["data"]
fps = asset_data.get("fps")
frame_start = asset_data.get("frameStart")
frame_end = asset_data.get("frameEnd")
handle_start = asset_data.get("handleStart")
handle_end = asset_data.get("handleEnd")
resolution_width = asset_data.get("resolutionWidth")
resolution_height = asset_data.get("resolutionHeight")
duration = (frame_end - frame_start + 1) + handle_start + handle_end
return {
"fps": fps,
"frameStart": frame_start,
"frameEnd": frame_end,
"handleStart": handle_start,
"handleEnd": handle_end,
"resolutionWidth": resolution_width,
"resolutionHeight": resolution_height,
"duration": duration
}
def containerise(name,
namespace,
comp,
context,
loader=None,
suffix="_CON"):
"""
Containerisation enables a tracking of version, author and origin
for loaded assets.
Creates dictionary payloads that gets saved into file metadata. Each
container contains of who loaded (loader) and members (single or multiple
in case of background).
Arguments:
name (str): Name of resulting assembly
namespace (str): Namespace under which to host container
comp (Comp): Composition to containerise
context (dict): Asset information
loader (str, optional): Name of loader used to produce this container.
suffix (str, optional): Suffix of container, defaults to `_CON`.
Returns:
container (str): Name of container assembly
"""
data = {
"schema": "openpype:container-2.0",
"id": pipeline.AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace,
"loader": str(loader),
"representation": str(context["representation"]["_id"]),
"members": comp.members or [comp.id]
}
stub = get_stub()
stub.imprint(comp, data)
return comp
def _get_stub():
"""
Handle pulling stub from PS to run operations on host
Returns:
(AEServerStub) or None
"""
try:
stub = get_stub() # only after Photoshop is up
except lib.ConnectionNotEstablishedYet:
print("Not connected yet, ignoring")
return
if not stub.get_active_document_name():
return
return stub
def ls():
"""Yields containers from active AfterEffects document.
This is the host-equivalent of api.ls(), but instead of listing
assets on disk, it lists assets already loaded in AE; once loaded
they are called 'containers'. Used in Manage tool.
Containers could be on multiple levels, single images/videos/was as a
FootageItem, or multiple items - backgrounds (folder with automatically
created composition and all imported layers).
Yields:
dict: container
"""
try:
stub = get_stub() # only after AfterEffects is up
except lib.ConnectionNotEstablishedYet:
print("Not connected yet, ignoring")
return
layers_meta = stub.get_metadata()
for item in stub.get_items(comps=True,
folders=True,
footages=True):
data = stub.read(item, layers_meta)
# Skip non-tagged layers.
if not data:
continue
# Filter to only containers.
if "container" not in data["id"]:
continue
# Append transient data
data["objectName"] = item.name.replace(stub.LOADED_ICON, '')
data["layer"] = item
yield data
def list_instances():
"""
List all created instances from current workfile which
will be published.
Pulls from File > File Info
For SubsetManager
Returns:
(list) of dictionaries matching instances format
"""
stub = _get_stub()
if not stub:
return []
instances = []
layers_meta = stub.get_metadata()
for instance in layers_meta:
if instance.get("schema") and \
"container" in instance.get("schema"):
continue
uuid_val = instance.get("uuid")
if uuid_val:
instance['uuid'] = uuid_val
else:
instance['uuid'] = instance.get("members")[0] # legacy
instances.append(instance)
return instances
def remove_instance(instance):
"""
Remove instance from current workfile metadata.
Updates metadata of current file in File > File Info and removes
icon highlight on group layer.
For SubsetManager
Args:
instance (dict): instance representation from subsetmanager model
"""
stub = _get_stub()
if not stub:
return
stub.remove_instance(instance.get("uuid"))
item = stub.get_item(instance.get("uuid"))
if item:
stub.rename_item(item.id,
item.name.replace(stub.PUBLISH_ICON, ''))

View file

@ -0,0 +1,9 @@
import avalon.api
from .launch_logic import get_stub
class AfterEffectsLoader(avalon.api.Loader):
@staticmethod
def get_stub():
return get_stub()

View file

@ -0,0 +1,49 @@
"""Host API required Work Files tool"""
import os
from .launch_logic import get_stub
from avalon import api
def _active_document():
document_name = get_stub().get_active_document_name()
if not document_name:
return None
return document_name
def file_extensions():
return api.HOST_WORKFILE_EXTENSIONS["aftereffects"]
def has_unsaved_changes():
if _active_document():
return not get_stub().is_saved()
return False
def save_file(filepath):
get_stub().saveAs(filepath, True)
def open_file(filepath):
get_stub().open(filepath)
return True
def current_file():
try:
full_name = get_stub().get_active_document_full_name()
if full_name and full_name != "null":
return os.path.normpath(full_name).replace("\\", "/")
except Exception:
pass
return None
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")

View file

@ -0,0 +1,605 @@
"""
Stub handling connection from server to client.
Used anywhere solution is calling client methods.
"""
import json
import logging
import attr
from wsrpc_aiohttp import WebSocketAsync
from avalon.tools.webserver.app import WebServerTool
@attr.s
class AEItem(object):
"""
Object denoting Item in AE. Each item is created in AE by any Loader,
but contains same fields, which are being used in later processing.
"""
# metadata
id = attr.ib() # id created by AE, could be used for querying
name = attr.ib() # name of item
item_type = attr.ib(default=None) # item type (footage, folder, comp)
# all imported elements, single for
# regular image, array for Backgrounds
members = attr.ib(factory=list)
workAreaStart = attr.ib(default=None)
workAreaDuration = attr.ib(default=None)
frameRate = attr.ib(default=None)
file_name = attr.ib(default=None)
class AfterEffectsServerStub():
"""
Stub for calling function on client (Photoshop js) side.
Expects that client is already connected (started when avalon menu
is opened).
'self.websocketserver.call' is used as async wrapper
"""
PUBLISH_ICON = '\u2117 '
LOADED_ICON = '\u25bc'
def __init__(self):
self.websocketserver = WebServerTool.get_instance()
self.client = self.get_client()
self.log = logging.getLogger(self.__class__.__name__)
@staticmethod
def get_client():
"""
Return first connected client to WebSocket
TODO implement selection by Route
:return: <WebSocketAsync> client
"""
clients = WebSocketAsync.get_clients()
client = None
if len(clients) > 0:
key = list(clients.keys())[0]
client = clients.get(key)
return client
def open(self, path):
"""
Open file located at 'path' (local).
Args:
path(string): file path locally
Returns: None
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.open', path=path))
return self._handle_return(res)
def get_metadata(self):
"""
Get complete stored JSON with metadata from AE.Metadata.Label
field.
It contains containers loaded by any Loader OR instances creted
by Creator.
Returns:
(list)
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.get_metadata'))
metadata = self._handle_return(res)
return metadata or []
def read(self, item, layers_meta=None):
"""
Parses item metadata from Label field of active document.
Used as filter to pick metadata for specific 'item' only.
Args:
item (AEItem): pulled info from AE
layers_meta (dict): full list from Headline
(load and inject for better performance in loops)
Returns:
(dict):
"""
if layers_meta is None:
layers_meta = self.get_metadata()
for item_meta in layers_meta:
if 'container' in item_meta.get('id') and \
str(item.id) == str(item_meta.get('members')[0]):
return item_meta
self.log.debug("Couldn't find layer metadata")
def imprint(self, item, data, all_items=None, items_meta=None):
"""
Save item metadata to Label field of metadata of active document
Args:
item (AEItem):
data(string): json representation for single layer
all_items (list of item): for performance, could be
injected for usage in loop, if not, single call will be
triggered
items_meta(string): json representation from Headline
(for performance - provide only if imprint is in
loop - value should be same)
Returns: None
"""
if not items_meta:
items_meta = self.get_metadata()
result_meta = []
# fix existing
is_new = True
for item_meta in items_meta:
if item_meta.get('members') \
and str(item.id) == str(item_meta.get('members')[0]):
is_new = False
if data:
item_meta.update(data)
result_meta.append(item_meta)
else:
result_meta.append(item_meta)
if is_new:
result_meta.append(data)
# Ensure only valid ids are stored.
if not all_items:
# loaders create FootageItem now
all_items = self.get_items(comps=True,
folders=True,
footages=True)
item_ids = [int(item.id) for item in all_items]
cleaned_data = []
for meta in result_meta:
# for creation of instance OR loaded container
if 'instance' in meta.get('id') or \
int(meta.get('members')[0]) in item_ids:
cleaned_data.append(meta)
payload = json.dumps(cleaned_data, indent=4)
res = self.websocketserver.call(self.client.call
('AfterEffects.imprint',
payload=payload))
return self._handle_return(res)
def get_active_document_full_name(self):
"""
Returns just a name of active document via ws call
Returns(string): file name
"""
res = self.websocketserver.call(self.client.call(
'AfterEffects.get_active_document_full_name'))
return self._handle_return(res)
def get_active_document_name(self):
"""
Returns just a name of active document via ws call
Returns(string): file name
"""
res = self.websocketserver.call(self.client.call(
'AfterEffects.get_active_document_name'))
return self._handle_return(res)
def get_items(self, comps, folders=False, footages=False):
"""
Get all items from Project panel according to arguments.
There are multiple different types:
CompItem (could have multiple layers - source for Creator,
will be rendered)
FolderItem (collection type, currently used for Background
loading)
FootageItem (imported file - created by Loader)
Args:
comps (bool): return CompItems
folders (bool): return FolderItem
footages (bool: return FootageItem
Returns:
(list) of namedtuples
"""
res = self.websocketserver.call(
self.client.call('AfterEffects.get_items',
comps=comps,
folders=folders,
footages=footages)
)
return self._to_records(self._handle_return(res))
def get_selected_items(self, comps, folders=False, footages=False):
"""
Same as get_items but using selected items only
Args:
comps (bool): return CompItems
folders (bool): return FolderItem
footages (bool: return FootageItem
Returns:
(list) of namedtuples
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.get_selected_items',
comps=comps,
folders=folders,
footages=footages)
)
return self._to_records(self._handle_return(res))
def get_item(self, item_id):
"""
Returns metadata for particular 'item_id' or None
Args:
item_id (int, or string)
"""
for item in self.get_items(True, True, True):
if str(item.id) == str(item_id):
return item
return None
def import_file(self, path, item_name, import_options=None):
"""
Imports file as a FootageItem. Used in Loader
Args:
path (string): absolute path for asset file
item_name (string): label for created FootageItem
import_options (dict): different files (img vs psd) need different
config
"""
res = self.websocketserver.call(
self.client.call('AfterEffects.import_file',
path=path,
item_name=item_name,
import_options=import_options)
)
records = self._to_records(self._handle_return(res))
if records:
return records.pop()
def replace_item(self, item_id, path, item_name):
""" Replace FootageItem with new file
Args:
item_id (int):
path (string):absolute path
item_name (string): label on item in Project list
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.replace_item',
item_id=item_id,
path=path, item_name=item_name))
return self._handle_return(res)
def rename_item(self, item_id, item_name):
""" Replace item with item_name
Args:
item_id (int):
item_name (string): label on item in Project list
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.rename_item',
item_id=item_id,
item_name=item_name))
return self._handle_return(res)
def delete_item(self, item_id):
""" Deletes *Item in a file
Args:
item_id (int):
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.delete_item',
item_id=item_id))
return self._handle_return(res)
def remove_instance(self, instance_id):
"""
Removes instance with 'instance_id' from file's metadata and
saves them.
Keep matching item in file though.
Args:
instance_id(string): instance uuid
"""
cleaned_data = []
for instance in self.get_metadata():
uuid_val = instance.get("uuid")
if not uuid_val:
uuid_val = instance.get("members")[0] # legacy
if uuid_val != instance_id:
cleaned_data.append(instance)
payload = json.dumps(cleaned_data, indent=4)
res = self.websocketserver.call(self.client.call
('AfterEffects.imprint',
payload=payload))
return self._handle_return(res)
def is_saved(self):
# TODO
return True
def set_label_color(self, item_id, color_idx):
"""
Used for highlight additional information in Project panel.
Green color is loaded asset, blue is created asset
Args:
item_id (int):
color_idx (int): 0-16 Label colors from AE Project view
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.set_label_color',
item_id=item_id,
color_idx=color_idx))
return self._handle_return(res)
def get_work_area(self, item_id):
""" Get work are information for render purposes
Args:
item_id (int):
Returns:
(namedtuple)
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.get_work_area',
item_id=item_id
))
records = self._to_records(self._handle_return(res))
if records:
return records.pop()
def set_work_area(self, item, start, duration, frame_rate):
"""
Set work area to predefined values (from Ftrack).
Work area directs what gets rendered.
Beware of rounding, AE expects seconds, not frames directly.
Args:
item (dict):
start (float): workAreaStart in seconds
duration (float): in seconds
frame_rate (float): frames in seconds
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.set_work_area',
item_id=item.id,
start=start,
duration=duration,
frame_rate=frame_rate))
return self._handle_return(res)
def save(self):
"""
Saves active document
Returns: None
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.save'))
return self._handle_return(res)
def saveAs(self, project_path, as_copy):
"""
Saves active project to aep (copy) or png or jpg
Args:
project_path(string): full local path
as_copy: <boolean>
Returns: None
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.saveAs',
image_path=project_path,
as_copy=as_copy))
return self._handle_return(res)
def get_render_info(self):
""" Get render queue info for render purposes
Returns:
(namedtuple): with 'file_name' field
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.get_render_info'))
records = self._to_records(self._handle_return(res))
if records:
return records.pop()
def get_audio_url(self, item_id):
""" Get audio layer absolute url for comp
Args:
item_id (int): composition id
Returns:
(str): absolute path url
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.get_audio_url',
item_id=item_id))
return self._handle_return(res)
def import_background(self, comp_id, comp_name, files):
"""
Imports backgrounds images to existing or new composition.
If comp_id is not provided, new composition is created, basic
values (width, heights, frameRatio) takes from first imported
image.
All images from background json are imported as a FootageItem and
separate layer is created for each of them under composition.
Order of imported 'files' is important.
Args:
comp_id (int): id of existing composition (null if new)
comp_name (str): used when new composition
files (list): list of absolute paths to import and
add as layers
Returns:
(AEItem): object with id of created folder, all imported images
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.import_background',
comp_id=comp_id,
comp_name=comp_name,
files=files))
records = self._to_records(self._handle_return(res))
if records:
return records.pop()
def reload_background(self, comp_id, comp_name, files):
"""
Reloads backgrounds images to existing composition.
It actually deletes complete folder with imported images and
created composition for safety.
Args:
comp_id (int): id of existing composition to be overwritten
comp_name (str): new name of composition (could be same as old
if version up only)
files (list): list of absolute paths to import and
add as layers
Returns:
(AEItem): object with id of created folder, all imported images
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.reload_background',
comp_id=comp_id,
comp_name=comp_name,
files=files))
records = self._to_records(self._handle_return(res))
if records:
return records.pop()
def add_item_as_layer(self, comp_id, item_id):
"""
Adds already imported FootageItem ('item_id') as a new
layer to composition ('comp_id').
Args:
comp_id (int): id of target composition
item_id (int): FootageItem.id
comp already found previously
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.add_item_as_layer',
comp_id=comp_id,
item_id=item_id))
records = self._to_records(self._handle_return(res))
if records:
return records.pop()
def render(self, folder_url):
"""
Render all renderqueueitem to 'folder_url'
Args:
folder_url(string): local folder path for collecting
Returns: None
"""
res = self.websocketserver.call(self.client.call
('AfterEffects.render',
folder_url=folder_url))
return self._handle_return(res)
def get_extension_version(self):
"""Returns version number of installed extension."""
res = self.websocketserver.call(self.client.call(
'AfterEffects.get_extension_version'))
return self._handle_return(res)
def close(self):
res = self.websocketserver.call(self.client.call('AfterEffects.close'))
return self._handle_return(res)
def _handle_return(self, res):
"""Wraps return, throws ValueError if 'error' key is present."""
if res and isinstance(res, str) and res != "undefined":
try:
parsed = json.loads(res)
except json.decoder.JSONDecodeError:
raise ValueError("Received broken JSON {}".format(res))
if not parsed: # empty list
return parsed
first_item = parsed
if isinstance(parsed, list):
first_item = parsed[0]
if first_item:
if first_item.get("error"):
raise ValueError(first_item["error"])
# singular values (file name etc)
if first_item.get("result") is not None:
return first_item["result"]
return parsed # parsed
return res
def _to_records(self, payload):
"""
Converts string json representation into list of AEItem
dot notation access to work.
Returns: <list of AEItem>
payload(dict): - dictionary from json representation, expected to
come from _handle_return
"""
if not payload:
return []
if isinstance(payload, str): # safety fallback
try:
payload = json.loads(payload)
except json.decoder.JSONDecodeError:
raise ValueError("Received broken JSON {}".format(payload))
if isinstance(payload, dict):
payload = [payload]
ret = []
# convert to AEItem to use dot donation
for d in payload:
if not d:
continue
# currently implemented and expected fields
item = AEItem(d.get('id'),
d.get('name'),
d.get('type'),
d.get('members'),
d.get('workAreaStart'),
d.get('workAreaDuration'),
d.get('frameRate'),
d.get('file_name'))
ret.append(item)
return ret

View file

@ -1,9 +1,5 @@
from openpype.hosts.aftereffects.plugins.create import create_render
import logging
log = logging.getLogger(__name__)
class CreateLocalRender(create_render.CreateRender):
""" Creator to render locally.

View file

@ -1,11 +1,10 @@
from avalon import aftereffects
from avalon.api import CreatorError
import openpype.api
import logging
log = logging.getLogger(__name__)
from openpype.hosts.aftereffects.api import (
get_stub,
list_instances
)
class CreateRender(openpype.api.Creator):
@ -22,22 +21,27 @@ class CreateRender(openpype.api.Creator):
family = "render"
def process(self):
stub = aftereffects.stub() # only after After Effects is up
stub = get_stub() # only after After Effects is up
if (self.options or {}).get("useSelection"):
items = stub.get_selected_items(comps=True,
folders=False,
footages=False)
items = stub.get_selected_items(
comps=True, folders=False, footages=False
)
if len(items) > 1:
raise CreatorError("Please select only single "
"composition at time.")
raise CreatorError(
"Please select only single composition at time."
)
if not items:
raise CreatorError("Nothing to create. Select composition " +
"if 'useSelection' or create at least " +
"one composition.")
raise CreatorError((
"Nothing to create. Select composition "
"if 'useSelection' or create at least "
"one composition."
))
existing_subsets = [instance['subset'].lower()
for instance in aftereffects.list_instances()]
existing_subsets = [
instance['subset'].lower()
for instance in list_instances()
]
item = items.pop()
if self.name.lower() in existing_subsets:
@ -46,9 +50,11 @@ class CreateRender(openpype.api.Creator):
self.data["members"] = [item.id]
self.data["uuid"] = item.id # for SubsetManager
self.data["subset"] = self.data["subset"]\
.replace(stub.PUBLISH_ICON, '')\
self.data["subset"] = (
self.data["subset"]
.replace(stub.PUBLISH_ICON, '')
.replace(stub.LOADED_ICON, '')
)
stub.imprint(item, self.data)
stub.set_label_color(item.id, 14) # Cyan options 0 - 16

View file

@ -1,13 +1,18 @@
import re
from avalon import api, aftereffects
import avalon.api
from openpype.lib import get_background_layers, get_unique_layer_name
stub = aftereffects.stub()
from openpype.lib import (
get_background_layers,
get_unique_layer_name
)
from openpype.hosts.aftereffects.api import (
AfterEffectsLoader,
containerise
)
class BackgroundLoader(api.Loader):
class BackgroundLoader(AfterEffectsLoader):
"""
Load images from Background family
Creates for each background separate folder with all imported images
@ -21,6 +26,7 @@ class BackgroundLoader(api.Loader):
representations = ["json"]
def load(self, context, name=None, namespace=None, data=None):
stub = self.get_stub()
items = stub.get_items(comps=True)
existing_items = [layer.name.replace(stub.LOADED_ICON, '')
for layer in items]
@ -43,7 +49,7 @@ class BackgroundLoader(api.Loader):
self[:] = [comp]
namespace = namespace or comp_name
return aftereffects.containerise(
return containerise(
name,
namespace,
comp,
@ -53,6 +59,7 @@ class BackgroundLoader(api.Loader):
def update(self, container, representation):
""" Switch asset or change version """
stub = self.get_stub()
context = representation.get("context", {})
_ = container.pop("layer")
@ -71,7 +78,7 @@ class BackgroundLoader(api.Loader):
else: # switching version - keep same name
comp_name = container["namespace"]
path = api.get_representation_path(representation)
path = avalon.api.get_representation_path(representation)
layers = get_background_layers(path)
comp = stub.reload_background(container["members"][1],
@ -94,6 +101,7 @@ class BackgroundLoader(api.Loader):
container (dict): container to be removed - used to get layer_id
"""
print("!!!! container:: {}".format(container))
stub = self.get_stub()
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_item(layer.id)

View file

@ -1,11 +1,15 @@
from avalon import api, aftereffects
from openpype import lib
import re
stub = aftereffects.stub()
import avalon.api
from openpype import lib
from openpype.hosts.aftereffects.api import (
AfterEffectsLoader,
containerise
)
class FileLoader(api.Loader):
class FileLoader(AfterEffectsLoader):
"""Load images
Stores the imported asset in a container named after the asset.
@ -21,6 +25,7 @@ class FileLoader(api.Loader):
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
stub = self.get_stub()
layers = stub.get_items(comps=True, folders=True, footages=True)
existing_layers = [layer.name for layer in layers]
comp_name = lib.get_unique_layer_name(
@ -60,7 +65,7 @@ class FileLoader(api.Loader):
self[:] = [comp]
namespace = namespace or comp_name
return aftereffects.containerise(
return containerise(
name,
namespace,
comp,
@ -70,6 +75,7 @@ class FileLoader(api.Loader):
def update(self, container, representation):
""" Switch asset or change version """
stub = self.get_stub()
layer = container.pop("layer")
context = representation.get("context", {})
@ -86,7 +92,7 @@ class FileLoader(api.Loader):
"{}_{}".format(context["asset"], context["subset"]))
else: # switching version - keep same name
layer_name = container["namespace"]
path = api.get_representation_path(representation)
path = avalon.api.get_representation_path(representation)
# with aftereffects.maintained_selection(): # TODO
stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name)
stub.imprint(
@ -101,6 +107,7 @@ class FileLoader(api.Loader):
Args:
container (dict): container to be removed - used to get layer_id
"""
stub = self.get_stub()
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_item(layer.id)

View file

@ -1,6 +1,6 @@
import pyblish.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class AddPublishHighlight(pyblish.api.InstancePlugin):
@ -15,7 +15,7 @@ class AddPublishHighlight(pyblish.api.InstancePlugin):
optional = True
def process(self, instance):
stub = aftereffects.stub()
stub = get_stub()
item = instance.data
# comp name contains highlight icon
stub.rename_item(item["comp_id"], item["comp_name"])

View file

@ -2,7 +2,7 @@
"""Close AE after publish. For Webpublishing only."""
import pyblish.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class CloseAE(pyblish.api.ContextPlugin):
@ -20,7 +20,7 @@ class CloseAE(pyblish.api.ContextPlugin):
def process(self, context):
self.log.info("CloseAE")
stub = aftereffects.stub()
stub = get_stub()
self.log.info("Shutting down AE")
stub.save()
stub.close()

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class CollectAudio(pyblish.api.ContextPlugin):
@ -21,7 +21,8 @@ class CollectAudio(pyblish.api.ContextPlugin):
comp_id = instance.data["comp_id"]
if not comp_id:
self.log.debug("No comp_id filled in instance")
# @iLLiCiTiT QUESTION Should return or continue?
return
context.data["audioFile"] = os.path.normpath(
aftereffects.stub().get_audio_url(comp_id)
get_stub().get_audio_url(comp_id)
).replace("\\", "/")

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class CollectCurrentFile(pyblish.api.ContextPlugin):
@ -14,5 +14,5 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
def process(self, context):
context.data["currentFile"] = os.path.normpath(
aftereffects.stub().get_active_document_full_name()
get_stub().get_active_document_full_name()
).replace("\\", "/")

View file

@ -2,7 +2,10 @@ import os
import re
import pyblish.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import (
get_stub,
get_extension_manifest_path
)
class CollectExtensionVersion(pyblish.api.ContextPlugin):
@ -27,13 +30,12 @@ class CollectExtensionVersion(pyblish.api.ContextPlugin):
active = True
def process(self, context):
installed_version = aftereffects.stub().get_extension_version()
installed_version = get_stub().get_extension_version()
if not installed_version:
raise ValueError("Unknown version, probably old extension")
manifest_url = os.path.join(os.path.dirname(aftereffects.__file__),
"extension", "CSXS", "manifest.xml")
manifest_url = get_extension_manifest_path()
if not os.path.exists(manifest_url):
self.log.debug("Unable to locate extension manifest, not checking")

View file

@ -1,7 +1,7 @@
import os
import re
import attr
import tempfile
import attr
from avalon import aftereffects
import pyblish.api
@ -10,6 +10,8 @@ from openpype.settings import get_project_settings
from openpype.lib import abstract_collect_render
from openpype.lib.abstract_collect_render import RenderInstance
from openpype.hosts.aftereffects.api import get_stub
@attr.s
class AERenderInstance(RenderInstance):
@ -35,7 +37,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
padding_width = 6
rendered_extension = 'png'
stub = aftereffects.stub()
stub = get_stub()
def get_instances(self, context):
instances = []

View file

@ -1,9 +1,9 @@
import os
import six
import sys
import six
import openpype.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class ExtractLocalRender(openpype.api.Extractor):
@ -15,7 +15,7 @@ class ExtractLocalRender(openpype.api.Extractor):
families = ["render"]
def process(self, instance):
stub = aftereffects.stub()
stub = get_stub()
staging_dir = instance.data["stagingDir"]
self.log.info("staging_dir::{}".format(staging_dir))
@ -55,8 +55,7 @@ class ExtractLocalRender(openpype.api.Extractor):
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
# Generate thumbnail.
thumbnail_path = os.path.join(staging_dir,
"thumbnail.jpg")
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
args = [
ffmpeg_path, "-y",

View file

@ -1,5 +1,5 @@
import openpype.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class ExtractSaveScene(openpype.api.Extractor):
@ -11,5 +11,5 @@ class ExtractSaveScene(openpype.api.Extractor):
families = ["workfile"]
def process(self, instance):
stub = aftereffects.stub()
stub = get_stub()
stub.save()

View file

@ -2,7 +2,7 @@ import pyblish.api
from openpype.action import get_errored_plugins_from_data
from openpype.lib import version_up
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class IncrementWorkfile(pyblish.api.InstancePlugin):
@ -25,6 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
)
scene_path = version_up(instance.context.data["currentFile"])
aftereffects.stub().saveAs(scene_path, True)
get_stub().saveAs(scene_path, True)
self.log.info("Incremented workfile to: {}".format(scene_path))

View file

@ -1,5 +1,5 @@
import openpype.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class RemovePublishHighlight(openpype.api.Extractor):
@ -16,7 +16,7 @@ class RemovePublishHighlight(openpype.api.Extractor):
families = ["render.farm"]
def process(self, instance):
stub = aftereffects.stub()
stub = get_stub()
self.log.debug("instance::{}".format(instance.data))
item = instance.data
comp_name = item["comp_name"].replace(stub.PUBLISH_ICON, '')

View file

@ -1,7 +1,7 @@
from avalon import api
import pyblish.api
import openpype.api
from avalon import aftereffects
from openpype.hosts.aftereffects.api import get_stub
class ValidateInstanceAssetRepair(pyblish.api.Action):
@ -22,7 +22,7 @@ class ValidateInstanceAssetRepair(pyblish.api.Action):
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
stub = aftereffects.stub()
stub = get_stub()
for instance in instances:
data = stub.read(instance[0])

View file

@ -5,11 +5,7 @@ import re
import pyblish.api
from avalon import aftereffects
import openpype.hosts.aftereffects.api as api
stub = aftereffects.stub()
from openpype.hosts.aftereffects.api import get_asset_settings
class ValidateSceneSettings(pyblish.api.InstancePlugin):
@ -47,7 +43,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
resolutionWidth
resolutionHeight
TODO support in extension is missing for now
By defaults validates duration (how many frames should be published)
"""
@ -62,7 +58,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin):
def process(self, instance):
"""Plugin entry point."""
expected_settings = api.get_asset_settings()
expected_settings = get_asset_settings()
self.log.info("config from DB::{}".format(expected_settings))
if any(re.search(pattern, os.getenv('AVALON_TASK'))

View file

@ -1,6 +1,6 @@
import os
import importlib
from openpype.lib import PreLaunchHook
from openpype.lib import PreLaunchHook, ApplicationLaunchFailed
from openpype.hosts.fusion.api import utils
@ -14,25 +14,27 @@ class FusionPrelaunch(PreLaunchHook):
def execute(self):
# making sure pyton 3.6 is installed at provided path
py36_dir = os.path.normpath(self.launch_context.env.get("PYTHON36", ""))
assert os.path.isdir(py36_dir), (
"Python 3.6 is not installed at the provided folder path. Either "
"make sure the `environments\resolve.json` is having correctly "
"set `PYTHON36` or make sure Python 3.6 is installed "
f"in given path. \nPYTHON36E: `{py36_dir}`"
)
self.log.info(f"Path to Fusion Python folder: `{py36_dir}`...")
if not os.path.isdir(py36_dir):
raise ApplicationLaunchFailed(
"Python 3.6 is not installed at the provided path.\n"
"Either make sure the 'environments/fusion.json' has "
"'PYTHON36' set corectly or make sure Python 3.6 is installed "
f"in the given path.\n\nPYTHON36: {py36_dir}"
)
self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...")
self.launch_context.env["PYTHON36"] = py36_dir
# setting utility scripts dir for scripts syncing
us_dir = os.path.normpath(
self.launch_context.env.get("FUSION_UTILITY_SCRIPTS_DIR", "")
)
assert os.path.isdir(us_dir), (
"Fusion utility script dir does not exists. Either make sure "
"the `environments\fusion.json` is having correctly set "
"`FUSION_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n"
f"FUSION_UTILITY_SCRIPTS_DIR: `{us_dir}`"
)
if not os.path.isdir(us_dir):
raise ApplicationLaunchFailed(
"Fusion utility script dir does not exist. Either make sure "
"the 'environments/fusion.json' has "
"'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall "
f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'"
)
try:
__import__("avalon.fusion")

View file

@ -37,7 +37,7 @@ class CollectFrames(pyblish.api.InstancePlugin):
# Check if frames are bigger than 1 (file collection)
# override the result
if end_frame - start_frame > 1:
if end_frame - start_frame > 0:
result = self.create_file_list(
match, int(start_frame), int(end_frame)
)

View file

@ -313,13 +313,7 @@ def attribute_values(attr_values):
"""
# NOTE(antirotor): this didn't work for some reason for Yeti attributes
# original = [(attr, cmds.getAttr(attr)) for attr in attr_values]
original = []
for attr in attr_values:
type = cmds.getAttr(attr, type=True)
value = cmds.getAttr(attr)
original.append((attr, str(value) if type == "string" else value))
original = [(attr, cmds.getAttr(attr)) for attr in attr_values]
try:
for attr, value in attr_values.items():
if isinstance(value, string_types):
@ -331,6 +325,12 @@ def attribute_values(attr_values):
for attr, value in original:
if isinstance(value, string_types):
cmds.setAttr(attr, value, type="string")
elif value is None and cmds.getAttr(attr, type=True) == "string":
# In some cases the maya.cmds.getAttr command returns None
# for string attributes but this value cannot assigned.
# Note: After setting it once to "" it will then return ""
# instead of None. So this would only happen once.
cmds.setAttr(attr, "", type="string")
else:
cmds.setAttr(attr, value)

View file

@ -3,14 +3,15 @@ from maya import cmds
import pyblish.api
import openpype.api
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api import lib
from avalon.maya import maintained_selection
class ValidateShapeZero(pyblish.api.Validator):
"""shape can't have any values
"""Shape components may not have any "tweak" values
To solve this issue, try freezing the shapes. So long
as the translation, rotation and scaling values are zero,
you're all good.
To solve this issue, try freezing the shapes.
"""
@ -47,13 +48,22 @@ class ValidateShapeZero(pyblish.api.Validator):
@classmethod
def repair(cls, instance):
invalid_shapes = cls.get_invalid(instance)
for shape in invalid_shapes:
cmds.polyCollapseTweaks(shape)
if not invalid_shapes:
return
with maintained_selection():
with lib.tool("selectSuperContext"):
for shape in invalid_shapes:
cmds.polyCollapseTweaks(shape)
# cmds.polyCollapseTweaks keeps selecting the geometry
# after each command. When running on many meshes
# after one another this tends to get really heavy
cmds.select(clear=True)
def process(self, instance):
"""Process all the nodes in the instance "objectSet"""
invalid = self.get_invalid(instance)
if invalid:
raise ValueError("Nodes found with shape or vertices not freezed"
"values: {0}".format(invalid))
raise ValueError("Shapes found with non-zero component tweaks: "
"{0}".format(invalid))

View file

@ -6,10 +6,7 @@ def add_implementation_envs(env, _app):
# Add requirements to NUKE_PATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
new_nuke_paths = [
os.path.join(pype_root, "openpype", "hosts", "nuke", "startup"),
os.path.join(
pype_root, "repos", "avalon-core", "setup", "nuke", "nuke_path"
)
os.path.join(pype_root, "openpype", "hosts", "nuke", "startup")
]
old_nuke_path = env.get("NUKE_PATH") or ""
for path in old_nuke_path.split(os.pathsep):

View file

@ -1,130 +1,57 @@
import os
import nuke
from .workio import (
file_extensions,
has_unsaved_changes,
save_file,
open_file,
current_file,
work_root,
)
import avalon.api
import pyblish.api
import openpype
from . import lib, menu
from .command import (
reset_frame_range,
get_handles,
reset_resolution,
viewer_update_and_undo_stop
)
log = openpype.api.Logger().get_logger(__name__)
from .plugin import OpenPypeCreator
from .pipeline import (
install,
uninstall,
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
ls,
containerise,
parse_container,
update_container,
)
from .lib import (
maintained_selection
)
# registering pyblish gui regarding settings in presets
if os.getenv("PYBLISH_GUI", None):
pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None))
__all__ = (
"file_extensions",
"has_unsaved_changes",
"save_file",
"open_file",
"current_file",
"work_root",
"reset_frame_range",
"get_handles",
"reset_resolution",
"viewer_update_and_undo_stop",
def reload_config():
"""Attempt to reload pipeline at run-time.
"OpenPypeCreator",
"install",
"uninstall",
CAUTION: This is primarily for development and debugging purposes.
"ls",
"""
"containerise",
"parse_container",
"update_container",
import importlib
for module in (
"{}.api".format(AVALON_CONFIG),
"{}.hosts.nuke.api.actions".format(AVALON_CONFIG),
"{}.hosts.nuke.api.menu".format(AVALON_CONFIG),
"{}.hosts.nuke.api.plugin".format(AVALON_CONFIG),
"{}.hosts.nuke.api.lib".format(AVALON_CONFIG),
):
log.info("Reloading module: {}...".format(module))
module = importlib.import_module(module)
try:
importlib.reload(module)
except AttributeError as e:
from importlib import reload
log.warning("Cannot reload module: {}".format(e))
reload(module)
def install():
''' Installing all requarements for Nuke host
'''
# remove all registred callbacks form avalon.nuke
from avalon import pipeline
pipeline._registered_event_handlers.clear()
log.info("Registering Nuke plug-ins..")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH)
# Register Avalon event for workfiles loading.
avalon.api.on("workio.open_file", lib.check_inventory_versions)
avalon.api.on("taskChanged", menu.change_context_label)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled)
workfile_settings = lib.WorkfileSettings()
# Disable all families except for the ones we explicitly want to see
family_states = [
"write",
"review",
"nukenodes",
"model",
"gizmo"
]
avalon.api.data["familiesStateDefault"] = False
avalon.api.data["familiesStateToggled"] = family_states
# Set context settings.
nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root")
nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root")
nuke.addOnCreate(lib.process_workfile_builder, nodeClass="Root")
nuke.addOnCreate(lib.launch_workfiles_app, nodeClass="Root")
menu.install()
def uninstall():
'''Uninstalling host's integration
'''
log.info("Deregistering Nuke plug-ins..")
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
pyblish.api.deregister_callback(
"instanceToggled", on_pyblish_instance_toggled)
reload_config()
menu.uninstall()
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node passthrough states on instance toggles."""
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
instance, old_value, new_value))
from avalon.nuke import (
viewer_update_and_undo_stop,
add_publish_knob
)
# Whether instances should be passthrough based on new value
with viewer_update_and_undo_stop():
n = instance[0]
try:
n["publish"].value()
except ValueError:
n = add_publish_knob(n)
log.info(" `Publish` knob was added to write node..")
n["publish"].setValue(new_value)
"maintained_selection",
)

View file

@ -1,12 +1,11 @@
import pyblish.api
from avalon.nuke.lib import (
from openpype.api import get_errored_instances_from_context
from .lib import (
reset_selection,
select_nodes
)
from openpype.api import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):
"""Select invalid nodes in Nuke when plug-in failed.

View file

@ -0,0 +1,135 @@
import logging
import contextlib
import nuke
from avalon import api, io
log = logging.getLogger(__name__)
def reset_frame_range():
""" Set frame range to current asset
Also it will set a Viewer range with
displayed handles
"""
fps = float(api.Session.get("AVALON_FPS", 25))
nuke.root()["fps"].setValue(fps)
name = api.Session["AVALON_ASSET"]
asset = io.find_one({"name": name, "type": "asset"})
asset_data = asset["data"]
handles = get_handles(asset)
frame_start = int(asset_data.get(
"frameStart",
asset_data.get("edit_in")))
frame_end = int(asset_data.get(
"frameEnd",
asset_data.get("edit_out")))
if not all([frame_start, frame_end]):
missing = ", ".join(["frame_start", "frame_end"])
msg = "'{}' are not set for asset '{}'!".format(missing, name)
log.warning(msg)
nuke.message(msg)
return
frame_start -= handles
frame_end += handles
nuke.root()["first_frame"].setValue(frame_start)
nuke.root()["last_frame"].setValue(frame_end)
# setting active viewers
vv = nuke.activeViewer().node()
vv["frame_range_lock"].setValue(True)
vv["frame_range"].setValue("{0}-{1}".format(
int(asset_data["frameStart"]),
int(asset_data["frameEnd"]))
)
def get_handles(asset):
""" Gets handles data
Arguments:
asset (dict): avalon asset entity
Returns:
handles (int)
"""
data = asset["data"]
if "handles" in data and data["handles"] is not None:
return int(data["handles"])
parent_asset = None
if "visualParent" in data:
vp = data["visualParent"]
if vp is not None:
parent_asset = io.find_one({"_id": io.ObjectId(vp)})
if parent_asset is None:
parent_asset = io.find_one({"_id": io.ObjectId(asset["parent"])})
if parent_asset is not None:
return get_handles(parent_asset)
else:
return 0
def reset_resolution():
"""Set resolution to project resolution."""
project = io.find_one({"type": "project"})
p_data = project["data"]
width = p_data.get("resolution_width",
p_data.get("resolutionWidth"))
height = p_data.get("resolution_height",
p_data.get("resolutionHeight"))
if not all([width, height]):
missing = ", ".join(["width", "height"])
msg = "No resolution information `{0}` found for '{1}'.".format(
missing,
project["name"])
log.warning(msg)
nuke.message(msg)
return
current_width = nuke.root()["format"].value().width()
current_height = nuke.root()["format"].value().height()
if width != current_width or height != current_height:
fmt = None
for f in nuke.formats():
if f.width() == width and f.height() == height:
fmt = f.name()
if not fmt:
nuke.addFormat(
"{0} {1} {2}".format(int(width), int(height), project["name"])
)
fmt = project["name"]
nuke.root()["format"].setValue(fmt)
@contextlib.contextmanager
def viewer_update_and_undo_stop():
"""Lock viewer from updating and stop recording undo steps"""
try:
# stop active viewer to update any change
viewer = nuke.activeViewer()
if viewer:
viewer.stop()
else:
log.warning("No available active Viewer")
nuke.Undo.disable()
yield
finally:
nuke.Undo.enable()

View file

@ -3,15 +3,15 @@ import re
import sys
import six
import platform
import contextlib
from collections import OrderedDict
import clique
import nuke
from avalon import api, io, lib
import avalon.nuke
from avalon.nuke import lib as anlib
from avalon.nuke import (
save_file, open_file
)
from openpype.api import (
Logger,
Anatomy,
@ -28,21 +28,476 @@ from openpype.lib.path_tools import HostDirmap
from openpype.settings import get_project_settings
from openpype.modules import ModulesManager
import nuke
from .workio import (
save_file,
open_file
)
from .utils import set_context_favorites
log = Logger.get_logger(__name__)
log = Logger().get_logger(__name__)
_NODE_TAB_NAME = "{}".format(os.getenv("AVALON_LABEL") or "Avalon")
AVALON_LABEL = os.getenv("AVALON_LABEL") or "Avalon"
AVALON_TAB = "{}".format(AVALON_LABEL)
AVALON_DATA_GROUP = "{}DataGroup".format(AVALON_LABEL.capitalize())
EXCLUDED_KNOB_TYPE_ON_READ = (
20, # Tab Knob
26, # Text Knob (But for backward compatibility, still be read
# if value is not an empty string.)
)
opnl = sys.modules[__name__]
opnl._project = None
opnl.project_name = os.getenv("AVALON_PROJECT")
opnl.workfiles_launched = False
opnl._node_tab_name = "{}".format(os.getenv("AVALON_LABEL") or "Avalon")
class Context:
main_window = None
context_label = None
project_name = os.getenv("AVALON_PROJECT")
workfiles_launched = False
# Seems unused
_project_doc = None
class Knobby(object):
"""For creating knob which it's type isn't mapped in `create_knobs`
Args:
type (string): Nuke knob type name
value: Value to be set with `Knob.setValue`, put `None` if not required
flags (list, optional): Knob flags to be set with `Knob.setFlag`
*args: Args other than knob name for initializing knob class
"""
def __init__(self, type, value, flags=None, *args):
self.type = type
self.value = value
self.flags = flags or []
self.args = args
def create(self, name, nice=None):
knob_cls = getattr(nuke, self.type)
knob = knob_cls(name, nice, *self.args)
if self.value is not None:
knob.setValue(self.value)
for flag in self.flags:
knob.setFlag(flag)
return knob
def create_knobs(data, tab=None):
"""Create knobs by data
Depending on the type of each dict value and creates the correct Knob.
Mapped types:
bool: nuke.Boolean_Knob
int: nuke.Int_Knob
float: nuke.Double_Knob
list: nuke.Enumeration_Knob
six.string_types: nuke.String_Knob
dict: If it's a nested dict (all values are dict), will turn into
A tabs group. Or just a knobs group.
Args:
data (dict): collection of attributes and their value
tab (string, optional): Knobs' tab name
Returns:
list: A list of `nuke.Knob` objects
"""
def nice_naming(key):
"""Convert camelCase name into UI Display Name"""
words = re.findall('[A-Z][^A-Z]*', key[0].upper() + key[1:])
return " ".join(words)
# Turn key-value pairs into knobs
knobs = list()
if tab:
knobs.append(nuke.Tab_Knob(tab))
for key, value in data.items():
# Knob name
if isinstance(key, tuple):
name, nice = key
else:
name, nice = key, nice_naming(key)
# Create knob by value type
if isinstance(value, Knobby):
knobby = value
knob = knobby.create(name, nice)
elif isinstance(value, float):
knob = nuke.Double_Knob(name, nice)
knob.setValue(value)
elif isinstance(value, bool):
knob = nuke.Boolean_Knob(name, nice)
knob.setValue(value)
knob.setFlag(nuke.STARTLINE)
elif isinstance(value, int):
knob = nuke.Int_Knob(name, nice)
knob.setValue(value)
elif isinstance(value, six.string_types):
knob = nuke.String_Knob(name, nice)
knob.setValue(value)
elif isinstance(value, list):
knob = nuke.Enumeration_Knob(name, nice, value)
elif isinstance(value, dict):
if all(isinstance(v, dict) for v in value.values()):
# Create a group of tabs
begain = nuke.BeginTabGroup_Knob()
end = nuke.EndTabGroup_Knob()
begain.setName(name)
end.setName(name + "_End")
knobs.append(begain)
for k, v in value.items():
knobs += create_knobs(v, tab=k)
knobs.append(end)
else:
# Create a group of knobs
knobs.append(nuke.Tab_Knob(
name, nice, nuke.TABBEGINCLOSEDGROUP))
knobs += create_knobs(value)
knobs.append(
nuke.Tab_Knob(name + "_End", nice, nuke.TABENDGROUP))
continue
else:
raise TypeError("Unsupported type: %r" % type(value))
knobs.append(knob)
return knobs
def imprint(node, data, tab=None):
"""Store attributes with value on node
Parse user data into Node knobs.
Use `collections.OrderedDict` to ensure knob order.
Args:
node(nuke.Node): node object from Nuke
data(dict): collection of attributes and their value
Returns:
None
Examples:
```
import nuke
from avalon.nuke import lib
node = nuke.createNode("NoOp")
data = {
# Regular type of attributes
"myList": ["x", "y", "z"],
"myBool": True,
"myFloat": 0.1,
"myInt": 5,
# Creating non-default imprint type of knob
"MyFilePath": lib.Knobby("File_Knob", "/file/path"),
"divider": lib.Knobby("Text_Knob", ""),
# Manual nice knob naming
("my_knob", "Nice Knob Name"): "some text",
# dict type will be created as knob group
"KnobGroup": {
"knob1": 5,
"knob2": "hello",
"knob3": ["a", "b"],
},
# Nested dict will be created as tab group
"TabGroup": {
"tab1": {"count": 5},
"tab2": {"isGood": True},
"tab3": {"direction": ["Left", "Right"]},
},
}
lib.imprint(node, data, tab="Demo")
```
"""
for knob in create_knobs(data, tab):
node.addKnob(knob)
def add_publish_knob(node):
"""Add Publish knob to node
Arguments:
node (nuke.Node): nuke node to be processed
Returns:
node (nuke.Node): processed nuke node
"""
if "publish" not in node.knobs():
body = OrderedDict()
body[("divd", "Publishing")] = Knobby("Text_Knob", '')
body["publish"] = True
imprint(node, body)
return node
def set_avalon_knob_data(node, data=None, prefix="avalon:"):
""" Sets data into nodes's avalon knob
Arguments:
node (nuke.Node): Nuke node to imprint with data,
data (dict, optional): Data to be imprinted into AvalonTab
prefix (str, optional): filtering prefix
Returns:
node (nuke.Node)
Examples:
data = {
'asset': 'sq020sh0280',
'family': 'render',
'subset': 'subsetMain'
}
"""
data = data or dict()
create = OrderedDict()
tab_name = AVALON_TAB
editable = ["asset", "subset", "name", "namespace"]
existed_knobs = node.knobs()
for key, value in data.items():
knob_name = prefix + key
gui_name = key
if knob_name in existed_knobs:
# Set value
try:
node[knob_name].setValue(value)
except TypeError:
node[knob_name].setValue(str(value))
else:
# New knob
name = (knob_name, gui_name) # Hide prefix on GUI
if key in editable:
create[name] = value
else:
create[name] = Knobby("String_Knob",
str(value),
flags=[nuke.READ_ONLY])
if tab_name in existed_knobs:
tab_name = None
else:
tab = OrderedDict()
warn = Knobby("Text_Knob", "Warning! Do not change following data!")
divd = Knobby("Text_Knob", "")
head = [
(("warn", ""), warn),
(("divd", ""), divd),
]
tab[AVALON_DATA_GROUP] = OrderedDict(head + list(create.items()))
create = tab
imprint(node, create, tab=tab_name)
return node
def get_avalon_knob_data(node, prefix="avalon:"):
""" Gets a data from nodes's avalon knob
Arguments:
node (obj): Nuke node to search for data,
prefix (str, optional): filtering prefix
Returns:
data (dict)
"""
# check if lists
if not isinstance(prefix, list):
prefix = list([prefix])
data = dict()
# loop prefix
for p in prefix:
# check if the node is avalon tracked
if AVALON_TAB not in node.knobs():
continue
try:
# check if data available on the node
test = node[AVALON_DATA_GROUP].value()
log.debug("Only testing if data avalable: `{}`".format(test))
except NameError as e:
# if it doesn't then create it
log.debug("Creating avalon knob: `{}`".format(e))
node = set_avalon_knob_data(node)
return get_avalon_knob_data(node)
# get data from filtered knobs
data.update({k.replace(p, ''): node[k].value()
for k in node.knobs().keys()
if p in k})
return data
def fix_data_for_node_create(data):
"""Fixing data to be used for nuke knobs
"""
for k, v in data.items():
if isinstance(v, six.text_type):
data[k] = str(v)
if str(v).startswith("0x"):
data[k] = int(v, 16)
return data
def add_write_node(name, **kwarg):
"""Adding nuke write node
Arguments:
name (str): nuke node name
kwarg (attrs): data for nuke knobs
Returns:
node (obj): nuke write node
"""
frame_range = kwarg.get("frame_range", None)
w = nuke.createNode(
"Write",
"name {}".format(name))
w["file"].setValue(kwarg["file"])
for k, v in kwarg.items():
if "frame_range" in k:
continue
log.info([k, v])
try:
w[k].setValue(v)
except KeyError as e:
log.debug(e)
continue
if frame_range:
w["use_limit"].setValue(True)
w["first"].setValue(frame_range[0])
w["last"].setValue(frame_range[1])
return w
def read(node):
"""Return user-defined knobs from given `node`
Args:
node (nuke.Node): Nuke node object
Returns:
list: A list of nuke.Knob object
"""
def compat_prefixed(knob_name):
if knob_name.startswith("avalon:"):
return knob_name[len("avalon:"):]
elif knob_name.startswith("ak:"):
return knob_name[len("ak:"):]
else:
return knob_name
data = dict()
pattern = ("(?<=addUserKnob {)"
"([0-9]*) (\\S*)" # Matching knob type and knob name
"(?=[ |}])")
tcl_script = node.writeKnobs(nuke.WRITE_USER_KNOB_DEFS)
result = re.search(pattern, tcl_script)
if result:
first_user_knob = result.group(2)
# Collect user knobs from the end of the knob list
for knob in reversed(node.allKnobs()):
knob_name = knob.name()
if not knob_name:
# Ignore unnamed knob
continue
knob_type = nuke.knob(knob.fullyQualifiedName(), type=True)
value = knob.value()
if (
knob_type not in EXCLUDED_KNOB_TYPE_ON_READ or
# For compating read-only string data that imprinted
# by `nuke.Text_Knob`.
(knob_type == 26 and value)
):
key = compat_prefixed(knob_name)
data[key] = value
if knob_name == first_user_knob:
break
return data
def get_node_path(path, padding=4):
"""Get filename for the Nuke write with padded number as '#'
Arguments:
path (str): The path to render to.
Returns:
tuple: head, padding, tail (extension)
Examples:
>>> get_frame_path("test.exr")
('test', 4, '.exr')
>>> get_frame_path("filename.#####.tif")
('filename.', 5, '.tif')
>>> get_frame_path("foobar##.tif")
('foobar', 2, '.tif')
>>> get_frame_path("foobar_%08d.tif")
('foobar_', 8, '.tif')
"""
filename, ext = os.path.splitext(path)
# Find a final number group
if '%' in filename:
match = re.match('.*?(%[0-9]+d)$', filename)
if match:
padding = int(match.group(1).replace('%', '').replace('d', ''))
# remove number from end since fusion
# will swap it with the frame number
filename = filename.replace(match.group(1), '')
elif '#' in filename:
match = re.match('.*?(#+)$', filename)
if match:
padding = len(match.group(1))
# remove number from end since fusion
# will swap it with the frame number
filename = filename.replace(match.group(1), '')
return filename, padding, ext
def get_nuke_imageio_settings():
return get_anatomy_settings(opnl.project_name)["imageio"]["nuke"]
return get_anatomy_settings(Context.project_name)["imageio"]["nuke"]
def get_created_node_imageio_setting(**kwarg):
@ -103,14 +558,15 @@ def check_inventory_versions():
and check if the node is having actual version. If not then it will color
it to red.
"""
from .pipeline import parse_container
# get all Loader nodes by avalon attribute metadata
for each in nuke.allNodes():
container = avalon.nuke.parse_container(each)
container = parse_container(each)
if container:
node = nuke.toNode(container["objectName"])
avalon_knob_data = avalon.nuke.read(
node)
avalon_knob_data = read(node)
# get representation from io
representation = io.find_one({
@ -163,11 +619,10 @@ def writes_version_sync():
for each in nuke.allNodes(filter="Write"):
# check if the node is avalon tracked
if opnl._node_tab_name not in each.knobs():
if _NODE_TAB_NAME not in each.knobs():
continue
avalon_knob_data = avalon.nuke.read(
each)
avalon_knob_data = read(each)
try:
if avalon_knob_data['families'] not in ["render"]:
@ -209,14 +664,14 @@ def check_subsetname_exists(nodes, subset_name):
bool: True of False
"""
return next((True for n in nodes
if subset_name in avalon.nuke.read(n).get("subset", "")),
if subset_name in read(n).get("subset", "")),
False)
def get_render_path(node):
''' Generate Render path from presets regarding avalon knob data
'''
data = {'avalon': avalon.nuke.read(node)}
data = {'avalon': read(node)}
data_preset = {
"nodeclass": data['avalon']['family'],
"families": [data['avalon']['families']],
@ -385,7 +840,7 @@ def create_write_node(name, data, input=None, prenodes=None,
for knob in imageio_writes["knobs"]:
_data.update({knob["name"]: knob["value"]})
_data = anlib.fix_data_for_node_create(_data)
_data = fix_data_for_node_create(_data)
log.debug("_data: `{}`".format(_data))
@ -466,7 +921,7 @@ def create_write_node(name, data, input=None, prenodes=None,
prev_node = now_node
# creating write node
write_node = now_node = anlib.add_write_node(
write_node = now_node = add_write_node(
"inside_{}".format(name),
**_data
)
@ -484,8 +939,8 @@ def create_write_node(name, data, input=None, prenodes=None,
now_node.setInput(0, prev_node)
# imprinting group node
anlib.set_avalon_knob_data(GN, data["avalon"])
anlib.add_publish_knob(GN)
set_avalon_knob_data(GN, data["avalon"])
add_publish_knob(GN)
add_rendering_knobs(GN, farm)
if review:
@ -537,7 +992,7 @@ def create_write_node(name, data, input=None, prenodes=None,
add_deadline_tab(GN)
# open the our Tab as default
GN[opnl._node_tab_name].setFlag(0)
GN[_NODE_TAB_NAME].setFlag(0)
# set tile color
tile_color = _data.get("tile_color", "0xff0000ff")
@ -663,7 +1118,7 @@ class WorkfileSettings(object):
root_node=None,
nodes=None,
**kwargs):
opnl._project = kwargs.get(
Context._project_doc = kwargs.get(
"project") or io.find_one({"type": "project"})
self._asset = kwargs.get("asset_name") or api.Session["AVALON_ASSET"]
self._asset_entity = get_asset(self._asset)
@ -804,8 +1259,6 @@ class WorkfileSettings(object):
''' Adds correct colorspace to write node dict
'''
from avalon.nuke import read
for node in nuke.allNodes(filter="Group"):
# get data from avalon knob
@ -1005,7 +1458,7 @@ class WorkfileSettings(object):
node['frame_range_lock'].setValue(True)
# adding handle_start/end to root avalon knob
if not anlib.set_avalon_knob_data(self._root_node, {
if not set_avalon_knob_data(self._root_node, {
"handleStart": int(handle_start),
"handleEnd": int(handle_end)
}):
@ -1089,6 +1542,8 @@ class WorkfileSettings(object):
self.set_colorspace()
def set_favorites(self):
from .utils import set_context_favorites
work_dir = os.getenv("AVALON_WORKDIR")
asset = os.getenv("AVALON_ASSET")
favorite_items = OrderedDict()
@ -1096,9 +1551,9 @@ class WorkfileSettings(object):
# project
# get project's root and split to parts
projects_root = os.path.normpath(work_dir.split(
opnl.project_name)[0])
Context.project_name)[0])
# add project name
project_dir = os.path.join(projects_root, opnl.project_name) + "/"
project_dir = os.path.join(projects_root, Context.project_name) + "/"
# add to favorites
favorite_items.update({"Project dir": project_dir.replace("\\", "/")})
@ -1145,8 +1600,7 @@ def get_write_node_template_attr(node):
'''
# get avalon data from node
data = dict()
data['avalon'] = avalon.nuke.read(
node)
data['avalon'] = read(node)
data_preset = {
"nodeclass": data['avalon']['family'],
"families": [data['avalon']['families']],
@ -1167,7 +1621,7 @@ def get_write_node_template_attr(node):
if k not in ["_id", "_previous"]}
# fix badly encoded data
return anlib.fix_data_for_node_create(correct_data)
return fix_data_for_node_create(correct_data)
def get_dependent_nodes(nodes):
@ -1274,13 +1728,53 @@ def find_free_space_to_paste_nodes(
return xpos, ypos
@contextlib.contextmanager
def maintained_selection():
"""Maintain selection during context
Example:
>>> with maintained_selection():
... node['selected'].setValue(True)
>>> print(node['selected'].value())
False
"""
previous_selection = nuke.selectedNodes()
try:
yield
finally:
# unselect all selection in case there is some
current_seletion = nuke.selectedNodes()
[n['selected'].setValue(False) for n in current_seletion]
# and select all previously selected nodes
if previous_selection:
[n['selected'].setValue(True) for n in previous_selection]
def reset_selection():
"""Deselect all selected nodes"""
for node in nuke.selectedNodes():
node["selected"].setValue(False)
def select_nodes(nodes):
"""Selects all inputed nodes
Arguments:
nodes (list): nuke nodes to be selected
"""
assert isinstance(nodes, (list, tuple)), "nodes has to be list or tuple"
for node in nodes:
node["selected"].setValue(True)
def launch_workfiles_app():
'''Function letting start workfiles after start of host
'''
from openpype.lib import (
env_value_to_bool
)
from avalon.nuke.pipeline import get_main_window
from .pipeline import get_main_window
# get all imortant settings
open_at_start = env_value_to_bool(
@ -1291,8 +1785,8 @@ def launch_workfiles_app():
if not open_at_start:
return
if not opnl.workfiles_launched:
opnl.workfiles_launched = True
if not Context.workfiles_launched:
Context.workfiles_launched = True
main_window = get_main_window()
host_tools.show_workfiles(parent=main_window)
@ -1378,7 +1872,7 @@ def recreate_instance(origin_node, avalon_data=None):
knobs_wl = ["render", "publish", "review", "ypos",
"use_limit", "first", "last"]
# get data from avalon knobs
data = anlib.get_avalon_knob_data(
data = get_avalon_knob_data(
origin_node)
# add input data to avalon data
@ -1494,3 +1988,45 @@ def dirmap_file_name_filter(file_name):
if os.path.exists(dirmap_processor.file_name):
return dirmap_processor.file_name
return file_name
# ------------------------------------
# This function seems to be deprecated
# ------------------------------------
def ls_img_sequence(path):
"""Listing all available coherent image sequence from path
Arguments:
path (str): A nuke's node object
Returns:
data (dict): with nuke formated path and frameranges
"""
file = os.path.basename(path)
dirpath = os.path.dirname(path)
base, ext = os.path.splitext(file)
name, padding = os.path.splitext(base)
# populate list of files
files = [
f for f in os.listdir(dirpath)
if name in f
if ext in f
]
# create collection from list of files
collections, reminder = clique.assemble(files)
if len(collections) > 0:
head = collections[0].format("{head}")
padding = collections[0].format("{padding}") % 1
padding = "#" * len(padding)
tail = collections[0].format("{tail}")
file = head + padding + tail
return {
"path": os.path.join(dirpath, file).replace("\\", "/"),
"frames": collections[0].format("[{ranges}]")
}
return False

View file

@ -1,166 +0,0 @@
import os
import nuke
from avalon.nuke.pipeline import get_main_window
from .lib import WorkfileSettings
from openpype.api import Logger, BuildWorkfile, get_current_project_settings
from openpype.tools.utils import host_tools
log = Logger().get_logger(__name__)
menu_label = os.environ["AVALON_LABEL"]
context_label = None
def change_context_label(*args):
global context_label
menubar = nuke.menu("Nuke")
menu = menubar.findItem(menu_label)
label = "{0}, {1}".format(
os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"]
)
rm_item = [
(i, item) for i, item in enumerate(menu.items())
if context_label in item.name()
][0]
menu.removeItem(rm_item[1].name())
context_action = menu.addCommand(
label,
index=(rm_item[0])
)
context_action.setEnabled(False)
log.info("Task label changed from `{}` to `{}`".format(
context_label, label))
context_label = label
def install():
from openpype.hosts.nuke.api import reload_config
global context_label
# uninstall original avalon menu
uninstall()
main_window = get_main_window()
menubar = nuke.menu("Nuke")
menu = menubar.addMenu(menu_label)
label = "{0}, {1}".format(
os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"]
)
context_label = label
context_action = menu.addCommand(label)
context_action.setEnabled(False)
menu.addSeparator()
menu.addCommand(
"Work Files...",
lambda: host_tools.show_workfiles(parent=main_window)
)
menu.addSeparator()
menu.addCommand(
"Create...",
lambda: host_tools.show_creator(parent=main_window)
)
menu.addCommand(
"Load...",
lambda: host_tools.show_loader(
parent=main_window,
use_context=True
)
)
menu.addCommand(
"Publish...",
lambda: host_tools.show_publish(parent=main_window)
)
menu.addCommand(
"Manage...",
lambda: host_tools.show_scene_inventory(parent=main_window)
)
menu.addSeparator()
menu.addCommand(
"Set Resolution",
lambda: WorkfileSettings().reset_resolution()
)
menu.addCommand(
"Set Frame Range",
lambda: WorkfileSettings().reset_frame_range_handles()
)
menu.addCommand(
"Set Colorspace",
lambda: WorkfileSettings().set_colorspace()
)
menu.addCommand(
"Apply All Settings",
lambda: WorkfileSettings().set_context_settings()
)
menu.addSeparator()
menu.addCommand(
"Build Workfile",
lambda: BuildWorkfile().process()
)
menu.addSeparator()
menu.addCommand(
"Experimental tools...",
lambda: host_tools.show_experimental_tools_dialog(parent=main_window)
)
# add reload pipeline only in debug mode
if bool(os.getenv("NUKE_DEBUG")):
menu.addSeparator()
menu.addCommand("Reload Pipeline", reload_config)
# adding shortcuts
add_shortcuts_from_presets()
def uninstall():
menubar = nuke.menu("Nuke")
menu = menubar.findItem(menu_label)
for item in menu.items():
log.info("Removing menu item: {}".format(item.name()))
menu.removeItem(item.name())
def add_shortcuts_from_presets():
menubar = nuke.menu("Nuke")
nuke_presets = get_current_project_settings()["nuke"]["general"]
if nuke_presets.get("menu"):
menu_label_mapping = {
"manage": "Manage...",
"create": "Create...",
"load": "Load...",
"build_workfile": "Build Workfile",
"publish": "Publish..."
}
for command_name, shortcut_str in nuke_presets.get("menu").items():
log.info("menu_name `{}` | menu_label `{}`".format(
command_name, menu_label
))
log.info("Adding Shortcut `{}` to `{}`".format(
shortcut_str, command_name
))
try:
menu = menubar.findItem(menu_label)
item_label = menu_label_mapping[command_name]
menuitem = menu.findItem(item_label)
menuitem.setShortcut(shortcut_str)
except AttributeError as e:
log.error(e)

View file

@ -0,0 +1,421 @@
import os
import importlib
from collections import OrderedDict
import nuke
import pyblish.api
import avalon.api
from avalon import pipeline
import openpype
from openpype.api import (
Logger,
BuildWorkfile,
get_current_project_settings
)
from openpype.tools.utils import host_tools
from .command import viewer_update_and_undo_stop
from .lib import (
add_publish_knob,
WorkfileSettings,
process_workfile_builder,
launch_workfiles_app,
check_inventory_versions,
set_avalon_knob_data,
read,
Context
)
log = Logger.get_logger(__name__)
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
MENU_LABEL = os.environ["AVALON_LABEL"]
# registering pyblish gui regarding settings in presets
if os.getenv("PYBLISH_GUI", None):
pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None))
def get_main_window():
"""Acquire Nuke's main window"""
if Context.main_window is None:
from Qt import QtWidgets
top_widgets = QtWidgets.QApplication.topLevelWidgets()
name = "Foundry::UI::DockMainWindow"
for widget in top_widgets:
if (
widget.inherits("QMainWindow")
and widget.metaObject().className() == name
):
Context.main_window = widget
break
return Context.main_window
def reload_config():
"""Attempt to reload pipeline at run-time.
CAUTION: This is primarily for development and debugging purposes.
"""
for module in (
"{}.api".format(AVALON_CONFIG),
"{}.hosts.nuke.api.actions".format(AVALON_CONFIG),
"{}.hosts.nuke.api.menu".format(AVALON_CONFIG),
"{}.hosts.nuke.api.plugin".format(AVALON_CONFIG),
"{}.hosts.nuke.api.lib".format(AVALON_CONFIG),
):
log.info("Reloading module: {}...".format(module))
module = importlib.import_module(module)
try:
importlib.reload(module)
except AttributeError as e:
from importlib import reload
log.warning("Cannot reload module: {}".format(e))
reload(module)
def install():
''' Installing all requarements for Nuke host
'''
pyblish.api.register_host("nuke")
log.info("Registering Nuke plug-ins..")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH)
# Register Avalon event for workfiles loading.
avalon.api.on("workio.open_file", check_inventory_versions)
avalon.api.on("taskChanged", change_context_label)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled)
workfile_settings = WorkfileSettings()
# Disable all families except for the ones we explicitly want to see
family_states = [
"write",
"review",
"nukenodes",
"model",
"gizmo"
]
avalon.api.data["familiesStateDefault"] = False
avalon.api.data["familiesStateToggled"] = family_states
# Set context settings.
nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root")
nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root")
nuke.addOnCreate(process_workfile_builder, nodeClass="Root")
nuke.addOnCreate(launch_workfiles_app, nodeClass="Root")
_install_menu()
def uninstall():
'''Uninstalling host's integration
'''
log.info("Deregistering Nuke plug-ins..")
pyblish.deregister_host("nuke")
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
pyblish.api.deregister_callback(
"instanceToggled", on_pyblish_instance_toggled)
reload_config()
_uninstall_menu()
def _install_menu():
# uninstall original avalon menu
main_window = get_main_window()
menubar = nuke.menu("Nuke")
menu = menubar.addMenu(MENU_LABEL)
label = "{0}, {1}".format(
os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"]
)
Context.context_label = label
context_action = menu.addCommand(label)
context_action.setEnabled(False)
menu.addSeparator()
menu.addCommand(
"Work Files...",
lambda: host_tools.show_workfiles(parent=main_window)
)
menu.addSeparator()
menu.addCommand(
"Create...",
lambda: host_tools.show_creator(parent=main_window)
)
menu.addCommand(
"Load...",
lambda: host_tools.show_loader(
parent=main_window,
use_context=True
)
)
menu.addCommand(
"Publish...",
lambda: host_tools.show_publish(parent=main_window)
)
menu.addCommand(
"Manage...",
lambda: host_tools.show_scene_inventory(parent=main_window)
)
menu.addSeparator()
menu.addCommand(
"Set Resolution",
lambda: WorkfileSettings().reset_resolution()
)
menu.addCommand(
"Set Frame Range",
lambda: WorkfileSettings().reset_frame_range_handles()
)
menu.addCommand(
"Set Colorspace",
lambda: WorkfileSettings().set_colorspace()
)
menu.addCommand(
"Apply All Settings",
lambda: WorkfileSettings().set_context_settings()
)
menu.addSeparator()
menu.addCommand(
"Build Workfile",
lambda: BuildWorkfile().process()
)
menu.addSeparator()
menu.addCommand(
"Experimental tools...",
lambda: host_tools.show_experimental_tools_dialog(parent=main_window)
)
# add reload pipeline only in debug mode
if bool(os.getenv("NUKE_DEBUG")):
menu.addSeparator()
menu.addCommand("Reload Pipeline", reload_config)
# adding shortcuts
add_shortcuts_from_presets()
def _uninstall_menu():
menubar = nuke.menu("Nuke")
menu = menubar.findItem(MENU_LABEL)
for item in menu.items():
log.info("Removing menu item: {}".format(item.name()))
menu.removeItem(item.name())
def change_context_label(*args):
menubar = nuke.menu("Nuke")
menu = menubar.findItem(MENU_LABEL)
label = "{0}, {1}".format(
os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"]
)
rm_item = [
(i, item) for i, item in enumerate(menu.items())
if Context.context_label in item.name()
][0]
menu.removeItem(rm_item[1].name())
context_action = menu.addCommand(
label,
index=(rm_item[0])
)
context_action.setEnabled(False)
log.info("Task label changed from `{}` to `{}`".format(
Context.context_label, label))
def add_shortcuts_from_presets():
menubar = nuke.menu("Nuke")
nuke_presets = get_current_project_settings()["nuke"]["general"]
if nuke_presets.get("menu"):
menu_label_mapping = {
"manage": "Manage...",
"create": "Create...",
"load": "Load...",
"build_workfile": "Build Workfile",
"publish": "Publish..."
}
for command_name, shortcut_str in nuke_presets.get("menu").items():
log.info("menu_name `{}` | menu_label `{}`".format(
command_name, MENU_LABEL
))
log.info("Adding Shortcut `{}` to `{}`".format(
shortcut_str, command_name
))
try:
menu = menubar.findItem(MENU_LABEL)
item_label = menu_label_mapping[command_name]
menuitem = menu.findItem(item_label)
menuitem.setShortcut(shortcut_str)
except AttributeError as e:
log.error(e)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node passthrough states on instance toggles."""
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
instance, old_value, new_value))
# Whether instances should be passthrough based on new value
with viewer_update_and_undo_stop():
n = instance[0]
try:
n["publish"].value()
except ValueError:
n = add_publish_knob(n)
log.info(" `Publish` knob was added to write node..")
n["publish"].setValue(new_value)
def containerise(node,
name,
namespace,
context,
loader=None,
data=None):
"""Bundle `node` into an assembly and imprint it with metadata
Containerisation enables a tracking of version, author and origin
for loaded assets.
Arguments:
node (nuke.Node): Nuke's node object to imprint as container
name (str): Name of resulting assembly
namespace (str): Namespace under which to host container
context (dict): Asset information
loader (str, optional): Name of node used to produce this container.
Returns:
node (nuke.Node): containerised nuke's node object
"""
data = OrderedDict(
[
("schema", "openpype:container-2.0"),
("id", pipeline.AVALON_CONTAINER_ID),
("name", name),
("namespace", namespace),
("loader", str(loader)),
("representation", context["representation"]["_id"]),
],
**data or dict()
)
set_avalon_knob_data(node, data)
return node
def parse_container(node):
"""Returns containerised data of a node
Reads the imprinted data from `containerise`.
Arguments:
node (nuke.Node): Nuke's node object to read imprinted data
Returns:
dict: The container schema data for this container node.
"""
data = read(node)
# (TODO) Remove key validation when `ls` has re-implemented.
#
# If not all required data return the empty container
required = ["schema", "id", "name",
"namespace", "loader", "representation"]
if not all(key in data for key in required):
return
# Store the node's name
data["objectName"] = node["name"].value()
return data
def update_container(node, keys=None):
"""Returns node with updateted containder data
Arguments:
node (nuke.Node): The node in Nuke to imprint as container,
keys (dict, optional): data which should be updated
Returns:
node (nuke.Node): nuke node with updated container data
Raises:
TypeError on given an invalid container node
"""
keys = keys or dict()
container = parse_container(node)
if not container:
raise TypeError("Not a valid container node.")
container.update(keys)
node = set_avalon_knob_data(node, container)
return node
def ls():
"""List available containers.
This function is used by the Container Manager in Nuke. You'll
need to implement a for-loop that then *yields* one Container at
a time.
See the `container.json` schema for details on how it should look,
and the Maya equivalent, which is in `avalon.maya.pipeline`
"""
all_nodes = nuke.allNodes(recurseGroups=False)
# TODO: add readgeo, readcamera, readimage
nodes = [n for n in all_nodes]
for n in nodes:
log.debug("name: `{}`".format(n.name()))
container = parse_container(n)
if container:
yield container

View file

@ -2,23 +2,30 @@ import os
import random
import string
import avalon.nuke
from avalon.nuke import lib as anlib
from avalon import api
import nuke
import avalon.api
from openpype.api import (
get_current_project_settings,
PypeCreatorMixin
)
from .lib import check_subsetname_exists
import nuke
from .lib import (
Knobby,
check_subsetname_exists,
reset_selection,
maintained_selection,
set_avalon_knob_data,
add_publish_knob
)
class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator):
"""Pype Nuke Creator class wrapper
"""
class OpenPypeCreator(PypeCreatorMixin, avalon.api.Creator):
"""Pype Nuke Creator class wrapper"""
node_color = "0xdfea5dff"
def __init__(self, *args, **kwargs):
super(PypeCreator, self).__init__(*args, **kwargs)
super(OpenPypeCreator, self).__init__(*args, **kwargs)
self.presets = get_current_project_settings()["nuke"]["create"].get(
self.__class__.__name__, {}
)
@ -31,6 +38,38 @@ class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator):
raise NameError("`{0}: {1}".format(__name__, msg))
return
def process(self):
from nukescripts import autoBackdrop
instance = None
if (self.options or {}).get("useSelection"):
nodes = nuke.selectedNodes()
if not nodes:
nuke.message("Please select nodes that you "
"wish to add to a container")
return
elif len(nodes) == 1:
# only one node is selected
instance = nodes[0]
if not instance:
# Not using selection or multiple nodes selected
bckd_node = autoBackdrop()
bckd_node["tile_color"].setValue(int(self.node_color, 16))
bckd_node["note_font_size"].setValue(24)
bckd_node["label"].setValue("[{}]".format(self.name))
instance = bckd_node
# add avalon knobs
set_avalon_knob_data(instance, self.data)
add_publish_knob(instance)
return instance
def get_review_presets_config():
settings = get_current_project_settings()
@ -48,7 +87,7 @@ def get_review_presets_config():
return [str(name) for name, _prop in outputs.items()]
class NukeLoader(api.Loader):
class NukeLoader(avalon.api.Loader):
container_id_knob = "containerId"
container_id = None
@ -74,7 +113,7 @@ class NukeLoader(api.Loader):
node[self.container_id_knob].setValue(source_id)
else:
HIDEN_FLAG = 0x00040000
_knob = anlib.Knobby(
_knob = Knobby(
"String_Knob",
self.container_id,
flags=[
@ -183,7 +222,7 @@ class ExporterReview(object):
Returns:
nuke.Node: copy node of Input Process node
"""
anlib.reset_selection()
reset_selection()
ipn_orig = None
for v in nuke.allNodes(filter="Viewer"):
ip = v["input_process"].getValue()
@ -196,7 +235,7 @@ class ExporterReview(object):
# copy selected to clipboard
nuke.nodeCopy("%clipboard%")
# reset selection
anlib.reset_selection()
reset_selection()
# paste node and selection is on it only
nuke.nodePaste("%clipboard%")
# assign to variable
@ -396,7 +435,7 @@ class ExporterReviewMov(ExporterReview):
def save_file(self):
import shutil
with anlib.maintained_selection():
with maintained_selection():
self.log.info("Saving nodes as file... ")
# create nk path
path = os.path.splitext(self.path)[0] + ".nk"

View file

@ -1,7 +1,8 @@
import os
import nuke
from avalon.nuke import lib as anlib
from openpype.api import resources
from .lib import maintained_selection
def set_context_favorites(favorites=None):
@ -48,14 +49,16 @@ def gizmo_is_nuke_default(gizmo):
return gizmo.filename().startswith(plug_dir)
def bake_gizmos_recursively(in_group=nuke.Root()):
def bake_gizmos_recursively(in_group=None):
"""Converting a gizmo to group
Argumets:
is_group (nuke.Node)[optonal]: group node or all nodes
"""
if in_group is None:
in_group = nuke.Root()
# preserve selection after all is done
with anlib.maintained_selection():
with maintained_selection():
# jump to the group
with in_group:
for node in nuke.allNodes():

View file

@ -0,0 +1,55 @@
"""Host API required Work Files tool"""
import os
import nuke
import avalon.api
def file_extensions():
return avalon.api.HOST_WORKFILE_EXTENSIONS["nuke"]
def has_unsaved_changes():
return nuke.root().modified()
def save_file(filepath):
path = filepath.replace("\\", "/")
nuke.scriptSaveAs(path)
nuke.Root()["name"].setValue(path)
nuke.Root()["project_directory"].setValue(os.path.dirname(path))
nuke.Root().setModified(False)
def open_file(filepath):
filepath = filepath.replace("\\", "/")
# To remain in the same window, we have to clear the script and read
# in the contents of the workfile.
nuke.scriptClear()
nuke.scriptReadFile(filepath)
nuke.Root()["name"].setValue(filepath)
nuke.Root()["project_directory"].setValue(os.path.dirname(filepath))
nuke.Root().setModified(False)
return True
def current_file():
current_file = nuke.root().name()
# Unsaved current file
if current_file == 'Root':
return None
return os.path.normpath(current_file).replace("\\", "/")
def work_root(session):
work_dir = session["AVALON_WORKDIR"]
scene_dir = session.get("AVALON_SCENEDIR")
if scene_dir:
path = os.path.join(work_dir, scene_dir)
else:
path = work_dir
return os.path.normpath(path).replace("\\", "/")

View file

@ -1,9 +1,12 @@
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import plugin
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import (
select_nodes,
set_avalon_knob_data
)
class CreateBackdrop(plugin.PypeCreator):
class CreateBackdrop(plugin.OpenPypeCreator):
"""Add Publishable Backdrop"""
name = "nukenodes"
@ -25,14 +28,14 @@ class CreateBackdrop(plugin.PypeCreator):
nodes = self.nodes
if len(nodes) >= 1:
anlib.select_nodes(nodes)
select_nodes(nodes)
bckd_node = autoBackdrop()
bckd_node["name"].setValue("{}_BDN".format(self.name))
bckd_node["tile_color"].setValue(int(self.node_color, 16))
bckd_node["note_font_size"].setValue(24)
bckd_node["label"].setValue("[{}]".format(self.name))
# add avalon knobs
instance = anlib.set_avalon_knob_data(bckd_node, self.data)
instance = set_avalon_knob_data(bckd_node, self.data)
return instance
else:
@ -48,6 +51,6 @@ class CreateBackdrop(plugin.PypeCreator):
bckd_node["note_font_size"].setValue(24)
bckd_node["label"].setValue("[{}]".format(self.name))
# add avalon knobs
instance = anlib.set_avalon_knob_data(bckd_node, self.data)
instance = set_avalon_knob_data(bckd_node, self.data)
return instance

View file

@ -1,9 +1,11 @@
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import plugin
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import (
set_avalon_knob_data
)
class CreateCamera(plugin.PypeCreator):
class CreateCamera(plugin.OpenPypeCreator):
"""Add Publishable Backdrop"""
name = "camera"
@ -36,7 +38,7 @@ class CreateCamera(plugin.PypeCreator):
# change node color
n["tile_color"].setValue(int(self.node_color, 16))
# add avalon knobs
anlib.set_avalon_knob_data(n, data)
set_avalon_knob_data(n, data)
return True
else:
msg = str("Please select nodes you "
@ -49,5 +51,5 @@ class CreateCamera(plugin.PypeCreator):
camera_node = nuke.createNode("Camera2")
camera_node["tile_color"].setValue(int(self.node_color, 16))
# add avalon knobs
instance = anlib.set_avalon_knob_data(camera_node, self.data)
instance = set_avalon_knob_data(camera_node, self.data)
return instance

View file

@ -1,9 +1,14 @@
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import plugin
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import (
maintained_selection,
select_nodes,
set_avalon_knob_data
)
class CreateGizmo(plugin.PypeCreator):
class CreateGizmo(plugin.OpenPypeCreator):
"""Add Publishable "gizmo" group
The name is symbolically gizmo as presumably
@ -28,13 +33,13 @@ class CreateGizmo(plugin.PypeCreator):
nodes = self.nodes
self.log.info(len(nodes))
if len(nodes) == 1:
anlib.select_nodes(nodes)
select_nodes(nodes)
node = nodes[-1]
# check if Group node
if node.Class() in "Group":
node["name"].setValue("{}_GZM".format(self.name))
node["tile_color"].setValue(int(self.node_color, 16))
return anlib.set_avalon_knob_data(node, self.data)
return set_avalon_knob_data(node, self.data)
else:
msg = ("Please select a group node "
"you wish to publish as the gizmo")
@ -42,7 +47,7 @@ class CreateGizmo(plugin.PypeCreator):
nuke.message(msg)
if len(nodes) >= 2:
anlib.select_nodes(nodes)
select_nodes(nodes)
nuke.makeGroup()
gizmo_node = nuke.selectedNode()
gizmo_node["name"].setValue("{}_GZM".format(self.name))
@ -57,16 +62,15 @@ class CreateGizmo(plugin.PypeCreator):
"- create User knobs on the group")
# add avalon knobs
return anlib.set_avalon_knob_data(gizmo_node, self.data)
return set_avalon_knob_data(gizmo_node, self.data)
else:
msg = ("Please select nodes you "
"wish to add to the gizmo")
msg = "Please select nodes you wish to add to the gizmo"
self.log.error(msg)
nuke.message(msg)
return
else:
with anlib.maintained_selection():
with maintained_selection():
gizmo_node = nuke.createNode("Group")
gizmo_node["name"].setValue("{}_GZM".format(self.name))
gizmo_node["tile_color"].setValue(int(self.node_color, 16))
@ -80,4 +84,4 @@ class CreateGizmo(plugin.PypeCreator):
"- create User knobs on the group")
# add avalon knobs
return anlib.set_avalon_knob_data(gizmo_node, self.data)
return set_avalon_knob_data(gizmo_node, self.data)

View file

@ -1,9 +1,11 @@
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import plugin
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import (
set_avalon_knob_data
)
class CreateModel(plugin.PypeCreator):
class CreateModel(plugin.OpenPypeCreator):
"""Add Publishable Model Geometry"""
name = "model"
@ -68,7 +70,7 @@ class CreateModel(plugin.PypeCreator):
# change node color
n["tile_color"].setValue(int(self.node_color, 16))
# add avalon knobs
anlib.set_avalon_knob_data(n, data)
set_avalon_knob_data(n, data)
return True
else:
msg = str("Please select nodes you "
@ -81,5 +83,5 @@ class CreateModel(plugin.PypeCreator):
model_node = nuke.createNode("WriteGeo")
model_node["tile_color"].setValue(int(self.node_color, 16))
# add avalon knobs
instance = anlib.set_avalon_knob_data(model_node, self.data)
instance = set_avalon_knob_data(model_node, self.data)
return instance

View file

@ -1,13 +1,16 @@
from collections import OrderedDict
import avalon.api
import avalon.nuke
from openpype import api as pype
from openpype.hosts.nuke.api import plugin
import nuke
import avalon.api
from openpype import api as pype
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import (
set_avalon_knob_data
)
class CrateRead(plugin.PypeCreator):
class CrateRead(plugin.OpenPypeCreator):
# change this to template preset
name = "ReadCopy"
label = "Create Read Copy"
@ -45,7 +48,7 @@ class CrateRead(plugin.PypeCreator):
continue
avalon_data = self.data
avalon_data['subset'] = "{}".format(self.name)
avalon.nuke.lib.set_avalon_knob_data(node, avalon_data)
set_avalon_knob_data(node, avalon_data)
node['tile_color'].setValue(16744935)
count_reads += 1

View file

@ -1,11 +1,12 @@
from collections import OrderedDict
from openpype.hosts.nuke.api import (
plugin,
lib)
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import create_write_node
class CreateWritePrerender(plugin.PypeCreator):
class CreateWritePrerender(plugin.OpenPypeCreator):
# change this to template preset
name = "WritePrerender"
label = "Create Write Prerender"
@ -98,7 +99,7 @@ class CreateWritePrerender(plugin.PypeCreator):
self.log.info("write_data: {}".format(write_data))
write_node = lib.create_write_node(
write_node = create_write_node(
self.data["subset"],
write_data,
input=selected_node,

View file

@ -1,11 +1,12 @@
from collections import OrderedDict
from openpype.hosts.nuke.api import (
plugin,
lib)
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import create_write_node
class CreateWriteRender(plugin.PypeCreator):
class CreateWriteRender(plugin.OpenPypeCreator):
# change this to template preset
name = "WriteRender"
label = "Create Write Render"
@ -119,7 +120,7 @@ class CreateWriteRender(plugin.PypeCreator):
}
]
write_node = lib.create_write_node(
write_node = create_write_node(
self.data["subset"],
write_data,
input=selected_node,

View file

@ -1,11 +1,12 @@
from collections import OrderedDict
from openpype.hosts.nuke.api import (
plugin,
lib)
import nuke
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import create_write_node
class CreateWriteStill(plugin.PypeCreator):
class CreateWriteStill(plugin.OpenPypeCreator):
# change this to template preset
name = "WriteStillFrame"
label = "Create Write Still Image"
@ -108,7 +109,7 @@ class CreateWriteStill(plugin.PypeCreator):
}
]
write_node = lib.create_write_node(
write_node = create_write_node(
self.name,
write_data,
input=selected_node,

View file

@ -1,7 +1,6 @@
from avalon import api, style
from avalon.nuke import lib as anlib
from openpype.api import (
Logger)
from openpype.api import Logger
from openpype.hosts.nuke.api.lib import set_avalon_knob_data
class RepairOldLoaders(api.InventoryAction):
@ -10,7 +9,7 @@ class RepairOldLoaders(api.InventoryAction):
icon = "gears"
color = style.colors.alert
log = Logger().get_logger(__name__)
log = Logger.get_logger(__name__)
def process(self, containers):
import nuke
@ -34,4 +33,4 @@ class RepairOldLoaders(api.InventoryAction):
})
node["name"].setValue(new_name)
# get data from avalon knob
anlib.set_avalon_knob_data(node, cdata)
set_avalon_knob_data(node, cdata)

View file

@ -1,4 +1,5 @@
from avalon import api
from openpype.hosts.nuke.api.commands import viewer_update_and_undo_stop
class SelectContainers(api.InventoryAction):
@ -9,11 +10,10 @@ class SelectContainers(api.InventoryAction):
def process(self, containers):
import nuke
import avalon.nuke
nodes = [nuke.toNode(i["objectName"]) for i in containers]
with avalon.nuke.viewer_update_and_undo_stop():
with viewer_update_and_undo_stop():
# clear previous_selection
[n['selected'].setValue(False) for n in nodes]
# Select tool

View file

@ -1,9 +1,18 @@
from avalon import api, style, io
import nuke
import nukescripts
from openpype.hosts.nuke.api import lib as pnlib
from avalon.nuke import lib as anlib
from avalon.nuke import containerise, update_container
from openpype.hosts.nuke.api.lib import (
find_free_space_to_paste_nodes,
maintained_selection,
reset_selection,
select_nodes,
get_avalon_knob_data,
set_avalon_knob_data
)
from openpype.hosts.nuke.api.commands import viewer_update_and_undo_stop
from openpype.hosts.nuke.api import containerise, update_container
class LoadBackdropNodes(api.Loader):
"""Loading Published Backdrop nodes (workfile, nukenodes)"""
@ -66,12 +75,12 @@ class LoadBackdropNodes(api.Loader):
# Get mouse position
n = nuke.createNode("NoOp")
xcursor, ycursor = (n.xpos(), n.ypos())
anlib.reset_selection()
reset_selection()
nuke.delete(n)
bdn_frame = 50
with anlib.maintained_selection():
with maintained_selection():
# add group from nk
nuke.nodePaste(file)
@ -81,11 +90,13 @@ class LoadBackdropNodes(api.Loader):
nodes = nuke.selectedNodes()
# get pointer position in DAG
xpointer, ypointer = pnlib.find_free_space_to_paste_nodes(nodes, direction="right", offset=200+bdn_frame)
xpointer, ypointer = find_free_space_to_paste_nodes(
nodes, direction="right", offset=200 + bdn_frame
)
# reset position to all nodes and replace inputs and output
for n in nodes:
anlib.reset_selection()
reset_selection()
xpos = (n.xpos() - xcursor) + xpointer
ypos = (n.ypos() - ycursor) + ypointer
n.setXYpos(xpos, ypos)
@ -108,7 +119,7 @@ class LoadBackdropNodes(api.Loader):
d.setInput(index, dot)
# remove Input node
anlib.reset_selection()
reset_selection()
nuke.delete(n)
continue
@ -127,15 +138,15 @@ class LoadBackdropNodes(api.Loader):
dot.setInput(0, dep)
# remove Input node
anlib.reset_selection()
reset_selection()
nuke.delete(n)
continue
else:
new_nodes.append(n)
# reselect nodes with new Dot instead of Inputs and Output
anlib.reset_selection()
anlib.select_nodes(new_nodes)
reset_selection()
select_nodes(new_nodes)
# place on backdrop
bdn = nukescripts.autoBackdrop()
@ -208,16 +219,16 @@ class LoadBackdropNodes(api.Loader):
# just in case we are in group lets jump out of it
nuke.endGroup()
with anlib.maintained_selection():
with maintained_selection():
xpos = GN.xpos()
ypos = GN.ypos()
avalon_data = anlib.get_avalon_knob_data(GN)
avalon_data = get_avalon_knob_data(GN)
nuke.delete(GN)
# add group from nk
nuke.nodePaste(file)
GN = nuke.selectedNode()
anlib.set_avalon_knob_data(GN, avalon_data)
set_avalon_knob_data(GN, avalon_data)
GN.setXYpos(xpos, ypos)
GN["name"].setValue(object_name)
@ -243,7 +254,6 @@ class LoadBackdropNodes(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -1,8 +1,15 @@
from avalon import api, io
from avalon.nuke import lib as anlib
from avalon.nuke import containerise, update_container
import nuke
from avalon import api, io
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
from openpype.hosts.nuke.api.lib import (
maintained_selection
)
class AlembicCameraLoader(api.Loader):
"""
@ -43,7 +50,7 @@ class AlembicCameraLoader(api.Loader):
# getting file path
file = self.fname.replace("\\", "/")
with anlib.maintained_selection():
with maintained_selection():
camera_node = nuke.createNode(
"Camera2",
"name {} file {} read_from_file True".format(
@ -122,7 +129,7 @@ class AlembicCameraLoader(api.Loader):
# getting file path
file = api.get_representation_path(representation).replace("\\", "/")
with anlib.maintained_selection():
with maintained_selection():
camera_node = nuke.toNode(object_name)
camera_node['selected'].setValue(True)
@ -181,7 +188,6 @@ class AlembicCameraLoader(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -3,13 +3,13 @@ from avalon.vendor import qargparse
from avalon import api, io
from openpype.hosts.nuke.api.lib import (
get_imageio_input_colorspace
get_imageio_input_colorspace,
maintained_selection
)
from avalon.nuke import (
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop,
maintained_selection
viewer_update_and_undo_stop
)
from openpype.hosts.nuke.api import plugin
@ -280,9 +280,6 @@ class LoadClip(plugin.NukeLoader):
self.set_as_member(read_node)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
read_node = nuke.toNode(container['objectName'])
assert read_node.Class() == "Read", "Must be Read"
@ -378,4 +375,4 @@ class LoadClip(plugin.NukeLoader):
"class_name": self.__class__.__name__
}
return self.node_name_template.format(**name_data)
return self.node_name_template.format(**name_data)

View file

@ -1,7 +1,12 @@
from avalon import api, style, io
import nuke
import json
from collections import OrderedDict
import nuke
from avalon import api, style, io
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class LoadEffects(api.Loader):
@ -30,9 +35,6 @@ class LoadEffects(api.Loader):
Returns:
nuke node: containerised nuke node object
"""
# import dependencies
from avalon.nuke import containerise
# get main variables
version = context['version']
version_data = version.get("data", {})
@ -138,10 +140,6 @@ class LoadEffects(api.Loader):
inputs:
"""
from avalon.nuke import (
update_container
)
# get main variables
# Get version from io
version = io.find_one({
@ -338,7 +336,6 @@ class LoadEffects(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -1,8 +1,15 @@
from avalon import api, style, io
import nuke
import json
from collections import OrderedDict
import nuke
from avalon import api, style, io
from openpype.hosts.nuke.api import lib
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class LoadEffectsInputProcess(api.Loader):
@ -30,8 +37,6 @@ class LoadEffectsInputProcess(api.Loader):
Returns:
nuke node: containerised nuke node object
"""
# import dependencies
from avalon.nuke import containerise
# get main variables
version = context['version']
@ -142,9 +147,6 @@ class LoadEffectsInputProcess(api.Loader):
"""
from avalon.nuke import (
update_container
)
# get main variables
# Get version from io
version = io.find_one({
@ -355,7 +357,6 @@ class LoadEffectsInputProcess(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -1,7 +1,15 @@
from avalon import api, style, io
import nuke
from avalon.nuke import lib as anlib
from avalon.nuke import containerise, update_container
from avalon import api, style, io
from openpype.hosts.nuke.api.lib import (
maintained_selection,
get_avalon_knob_data,
set_avalon_knob_data
)
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class LoadGizmo(api.Loader):
@ -61,7 +69,7 @@ class LoadGizmo(api.Loader):
# just in case we are in group lets jump out of it
nuke.endGroup()
with anlib.maintained_selection():
with maintained_selection():
# add group from nk
nuke.nodePaste(file)
@ -122,16 +130,16 @@ class LoadGizmo(api.Loader):
# just in case we are in group lets jump out of it
nuke.endGroup()
with anlib.maintained_selection():
with maintained_selection():
xpos = GN.xpos()
ypos = GN.ypos()
avalon_data = anlib.get_avalon_knob_data(GN)
avalon_data = get_avalon_knob_data(GN)
nuke.delete(GN)
# add group from nk
nuke.nodePaste(file)
GN = nuke.selectedNode()
anlib.set_avalon_knob_data(GN, avalon_data)
set_avalon_knob_data(GN, avalon_data)
GN.setXYpos(xpos, ypos)
GN["name"].setValue(object_name)
@ -157,7 +165,6 @@ class LoadGizmo(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -1,8 +1,16 @@
from avalon import api, style, io
import nuke
from openpype.hosts.nuke.api import lib as pnlib
from avalon.nuke import lib as anlib
from avalon.nuke import containerise, update_container
from openpype.hosts.nuke.api.lib import (
maintained_selection,
create_backdrop,
get_avalon_knob_data,
set_avalon_knob_data
)
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class LoadGizmoInputProcess(api.Loader):
@ -62,7 +70,7 @@ class LoadGizmoInputProcess(api.Loader):
# just in case we are in group lets jump out of it
nuke.endGroup()
with anlib.maintained_selection():
with maintained_selection():
# add group from nk
nuke.nodePaste(file)
@ -128,16 +136,16 @@ class LoadGizmoInputProcess(api.Loader):
# just in case we are in group lets jump out of it
nuke.endGroup()
with anlib.maintained_selection():
with maintained_selection():
xpos = GN.xpos()
ypos = GN.ypos()
avalon_data = anlib.get_avalon_knob_data(GN)
avalon_data = get_avalon_knob_data(GN)
nuke.delete(GN)
# add group from nk
nuke.nodePaste(file)
GN = nuke.selectedNode()
anlib.set_avalon_knob_data(GN, avalon_data)
set_avalon_knob_data(GN, avalon_data)
GN.setXYpos(xpos, ypos)
GN["name"].setValue(object_name)
@ -197,8 +205,12 @@ class LoadGizmoInputProcess(api.Loader):
viewer["input_process_node"].setValue(group_node_name)
# put backdrop under
pnlib.create_backdrop(label="Input Process", layer=2,
nodes=[viewer, group_node], color="0x7c7faaff")
create_backdrop(
label="Input Process",
layer=2,
nodes=[viewer, group_node],
color="0x7c7faaff"
)
return True
@ -234,7 +246,6 @@ class LoadGizmoInputProcess(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -7,6 +7,11 @@ from avalon import api, io
from openpype.hosts.nuke.api.lib import (
get_imageio_input_colorspace
)
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class LoadImage(api.Loader):
@ -46,10 +51,6 @@ class LoadImage(api.Loader):
return cls.representations + cls._representations
def load(self, context, name, namespace, options):
from avalon.nuke import (
containerise,
viewer_update_and_undo_stop
)
self.log.info("__ options: `{}`".format(options))
frame_number = options.get("frame_number", 1)
@ -154,11 +155,6 @@ class LoadImage(api.Loader):
inputs:
"""
from avalon.nuke import (
update_container
)
node = nuke.toNode(container["objectName"])
frame_number = node["first"].value()
@ -234,9 +230,6 @@ class LoadImage(api.Loader):
self.log.info("udated to version: {}".format(version.get("name")))
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
assert node.Class() == "Read", "Must be Read"

View file

@ -1,7 +1,11 @@
from avalon import api, io
from avalon.nuke import lib as anlib
from avalon.nuke import containerise, update_container
import nuke
from avalon import api, io
from openpype.hosts.nuke.api.lib import maintained_selection
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class AlembicModelLoader(api.Loader):
@ -43,7 +47,7 @@ class AlembicModelLoader(api.Loader):
# getting file path
file = self.fname.replace("\\", "/")
with anlib.maintained_selection():
with maintained_selection():
model_node = nuke.createNode(
"ReadGeo2",
"name {} file {} ".format(
@ -122,7 +126,7 @@ class AlembicModelLoader(api.Loader):
# getting file path
file = api.get_representation_path(representation).replace("\\", "/")
with anlib.maintained_selection():
with maintained_selection():
model_node = nuke.toNode(object_name)
model_node['selected'].setValue(True)
@ -181,7 +185,6 @@ class AlembicModelLoader(api.Loader):
self.update(container, representation)
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -1,6 +1,11 @@
from avalon import api, style, io
from avalon.nuke import get_avalon_knob_data
import nuke
from avalon import api, style, io
from openpype.hosts.nuke.api.lib import get_avalon_knob_data
from openpype.hosts.nuke.api import (
containerise,
update_container,
viewer_update_and_undo_stop
)
class LinkAsGroup(api.Loader):
@ -15,8 +20,6 @@ class LinkAsGroup(api.Loader):
color = style.colors.alert
def load(self, context, name, namespace, data):
from avalon.nuke import containerise
# for k, v in context.items():
# log.info("key: `{}`, value: {}\n".format(k, v))
version = context['version']
@ -103,11 +106,6 @@ class LinkAsGroup(api.Loader):
inputs:
"""
from avalon.nuke import (
update_container
)
node = nuke.toNode(container['objectName'])
root = api.get_representation_path(representation).replace("\\", "/")
@ -155,7 +153,6 @@ class LinkAsGroup(api.Loader):
self.log.info("udated to version: {}".format(version.get("name")))
def remove(self, container):
from avalon.nuke import viewer_update_and_undo_stop
node = nuke.toNode(container['objectName'])
with viewer_update_and_undo_stop():
nuke.delete(node)

View file

@ -1,9 +1,16 @@
import pyblish.api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import lib as pnlib
import nuke
import os
import nuke
import pyblish.api
import openpype
from openpype.hosts.nuke.api.lib import (
maintained_selection,
reset_selection,
select_nodes
)
class ExtractBackdropNode(openpype.api.Extractor):
"""Extracting content of backdrop nodes
@ -27,7 +34,7 @@ class ExtractBackdropNode(openpype.api.Extractor):
path = os.path.join(stagingdir, filename)
# maintain selection
with anlib.maintained_selection():
with maintained_selection():
# all connections outside of backdrop
connections_in = instance.data["nodeConnectionsIn"]
connections_out = instance.data["nodeConnectionsOut"]
@ -44,7 +51,7 @@ class ExtractBackdropNode(openpype.api.Extractor):
nodes.append(inpn)
tmp_nodes.append(inpn)
anlib.reset_selection()
reset_selection()
# connect output node
for n, output in connections_out.items():
@ -58,11 +65,11 @@ class ExtractBackdropNode(openpype.api.Extractor):
opn.autoplace()
nodes.append(opn)
tmp_nodes.append(opn)
anlib.reset_selection()
reset_selection()
# select nodes to copy
anlib.reset_selection()
anlib.select_nodes(nodes)
reset_selection()
select_nodes(nodes)
# create tmp nk file
# save file to the path
nuke.nodeCopy(path)

View file

@ -1,10 +1,12 @@
import nuke
import os
import math
from pprint import pformat
import nuke
import pyblish.api
import openpype.api
from avalon.nuke import lib as anlib
from pprint import pformat
from openpype.hosts.nuke.api.lib import maintained_selection
class ExtractCamera(openpype.api.Extractor):
@ -52,7 +54,7 @@ class ExtractCamera(openpype.api.Extractor):
filename = subset + ".{}".format(extension)
file_path = os.path.join(staging_dir, filename).replace("\\", "/")
with anlib.maintained_selection():
with maintained_selection():
# bake camera with axeses onto word coordinate XYZ
rm_n = bakeCameraWithAxeses(
nuke.toNode(instance.data["name"]), output_range)

View file

@ -1,9 +1,15 @@
import pyblish.api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import utils as pnutils
import nuke
import os
import nuke
import pyblish.api
import openpype
from openpype.hosts.nuke.api import utils as pnutils
from openpype.hosts.nuke.api.lib import (
maintained_selection,
reset_selection,
select_nodes
)
class ExtractGizmo(openpype.api.Extractor):
@ -26,17 +32,17 @@ class ExtractGizmo(openpype.api.Extractor):
path = os.path.join(stagingdir, filename)
# maintain selection
with anlib.maintained_selection():
with maintained_selection():
orig_grpn_name = orig_grpn.name()
tmp_grpn_name = orig_grpn_name + "_tmp"
# select original group node
anlib.select_nodes([orig_grpn])
select_nodes([orig_grpn])
# copy to clipboard
nuke.nodeCopy("%clipboard%")
# reset selection to none
anlib.reset_selection()
reset_selection()
# paste clipboard
nuke.nodePaste("%clipboard%")

View file

@ -1,9 +1,12 @@
import nuke
import os
from pprint import pformat
import nuke
import pyblish.api
import openpype.api
from avalon.nuke import lib as anlib
from pprint import pformat
from openpype.hosts.nuke.api.lib import (
maintained_selection,
select_nodes
)
class ExtractModel(openpype.api.Extractor):
@ -49,9 +52,9 @@ class ExtractModel(openpype.api.Extractor):
filename = subset + ".{}".format(extension)
file_path = os.path.join(staging_dir, filename).replace("\\", "/")
with anlib.maintained_selection():
with maintained_selection():
# select model node
anlib.select_nodes([model_node])
select_nodes([model_node])
# create write geo node
wg_n = nuke.createNode("WriteGeo")

View file

@ -1,6 +1,6 @@
import nuke
import pyblish.api
from avalon.nuke import maintained_selection
from openpype.hosts.nuke.api.lib import maintained_selection
class CreateOutputNode(pyblish.api.ContextPlugin):

View file

@ -1,8 +1,8 @@
import os
import pyblish.api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import plugin
import openpype
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import maintained_selection
class ExtractReviewDataLut(openpype.api.Extractor):
@ -37,7 +37,7 @@ class ExtractReviewDataLut(openpype.api.Extractor):
"StagingDir `{0}`...".format(instance.data["stagingDir"]))
# generate data
with anlib.maintained_selection():
with maintained_selection():
exporter = plugin.ExporterReviewLut(
self, instance
)

View file

@ -1,8 +1,8 @@
import os
import pyblish.api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api import plugin
import openpype
from openpype.hosts.nuke.api import plugin
from openpype.hosts.nuke.api.lib import maintained_selection
class ExtractReviewDataMov(openpype.api.Extractor):
@ -41,7 +41,7 @@ class ExtractReviewDataMov(openpype.api.Extractor):
self.log.info(self.outputs)
# generate data
with anlib.maintained_selection():
with maintained_selection():
generated_repres = []
for o_name, o_data in self.outputs.items():
f_families = o_data["filter"]["families"]

View file

@ -1,8 +1,8 @@
import os
import nuke
from avalon.nuke import lib as anlib
import pyblish.api
import openpype
from openpype.hosts.nuke.api.lib import maintained_selection
class ExtractSlateFrame(openpype.api.Extractor):
@ -25,7 +25,7 @@ class ExtractSlateFrame(openpype.api.Extractor):
else:
self.viewer_lut_raw = False
with anlib.maintained_selection():
with maintained_selection():
self.log.debug("instance: {}".format(instance))
self.log.debug("instance.data[families]: {}".format(
instance.data["families"]))

View file

@ -1,9 +1,9 @@
import sys
import os
import nuke
from avalon.nuke import lib as anlib
import pyblish.api
import openpype
from openpype.hosts.nuke.api.lib import maintained_selection
if sys.version_info[0] >= 3:
@ -30,7 +30,7 @@ class ExtractThumbnail(openpype.api.Extractor):
if "render.farm" in instance.data["families"]:
return
with anlib.maintained_selection():
with maintained_selection():
self.log.debug("instance: {}".format(instance))
self.log.debug("instance.data[families]: {}".format(
instance.data["families"]))

View file

@ -1,7 +1,10 @@
import nuke
import pyblish.api
from avalon import io, api
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api.lib import (
add_publish_knob,
get_avalon_knob_data
)
@pyblish.api.log
@ -39,7 +42,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
self.log.warning(E)
# get data from avalon knob
avalon_knob_data = anlib.get_avalon_knob_data(
avalon_knob_data = get_avalon_knob_data(
node, ["avalon:", "ak:"])
self.log.debug("avalon_knob_data: {}".format(avalon_knob_data))
@ -115,7 +118,7 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
# get publish knob value
if "publish" not in node.knobs():
anlib.add_publish_knob(node)
add_publish_knob(node)
# sync workfile version
_families_test = [family] + families

View file

@ -1,8 +1,13 @@
import nuke
import pyblish.api
import os
import nuke
import pyblish.api
import openpype.api as pype
from avalon.nuke import lib as anlib
from openpype.hosts.nuke.api.lib import (
add_publish_knob,
get_avalon_knob_data
)
class CollectWorkfile(pyblish.api.ContextPlugin):
@ -17,9 +22,9 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
current_file = os.path.normpath(nuke.root().name())
knob_data = anlib.get_avalon_knob_data(root)
knob_data = get_avalon_knob_data(root)
anlib.add_publish_knob(root)
add_publish_knob(root)
family = "workfile"
task = os.getenv("AVALON_TASK", None)

View file

@ -1,6 +1,6 @@
import pyblish
from avalon.nuke import lib as anlib
import nuke
import pyblish
from openpype.hosts.nuke.api.lib import maintained_selection
class SelectCenterInNodeGraph(pyblish.api.Action):
@ -28,7 +28,7 @@ class SelectCenterInNodeGraph(pyblish.api.Action):
all_yC = list()
# maintain selection
with anlib.maintained_selection():
with maintained_selection():
# collect all failed nodes xpos and ypos
for instance in instances:
bdn = instance[0]

View file

@ -1,6 +1,6 @@
import pyblish
from avalon.nuke import lib as anlib
import nuke
import pyblish
from openpype.hosts.nuke.api.lib import maintained_selection
class OpenFailedGroupNode(pyblish.api.Action):
@ -25,7 +25,7 @@ class OpenFailedGroupNode(pyblish.api.Action):
instances = pyblish.api.instances_by_plugin(failed, plugin)
# maintain selection
with anlib.maintained_selection():
with maintained_selection():
# collect all failed nodes xpos and ypos
for instance in instances:
grpn = instance[0]

View file

@ -6,8 +6,11 @@ import nuke
import pyblish.api
import openpype.api
import avalon.nuke.lib
import openpype.hosts.nuke.api as nuke_api
from openpype.hosts.nuke.api.lib import (
recreate_instance,
reset_selection,
select_nodes
)
class SelectInvalidInstances(pyblish.api.Action):
@ -47,12 +50,12 @@ class SelectInvalidInstances(pyblish.api.Action):
self.deselect()
def select(self, instances):
avalon.nuke.lib.select_nodes(
select_nodes(
[nuke.toNode(str(x)) for x in instances]
)
def deselect(self):
avalon.nuke.lib.reset_selection()
reset_selection()
class RepairSelectInvalidInstances(pyblish.api.Action):
@ -82,7 +85,7 @@ class RepairSelectInvalidInstances(pyblish.api.Action):
context_asset = context.data["assetEntity"]["name"]
for instance in instances:
origin_node = instance[0]
nuke_api.lib.recreate_instance(
recreate_instance(
origin_node, avalon_data={"asset": context_asset}
)

View file

@ -1,13 +1,12 @@
import toml
import os
import toml
import nuke
from avalon import api
import re
import pyblish.api
import openpype.api
from avalon.nuke import get_avalon_knob_data
from openpype.hosts.nuke.api.lib import get_avalon_knob_data
class ValidateWriteLegacy(pyblish.api.InstancePlugin):

Some files were not shown because too many files have changed in this diff Show more