Merge branch 'release/3.15.x' into enhancement/tray_publisher_is_not_experimental

This commit is contained in:
Jakub Trllo 2022-11-11 16:08:46 +01:00
commit 5cca211c67
80 changed files with 1641 additions and 1213 deletions

View file

@ -2,7 +2,7 @@ name: Milestone - assign to PRs
on:
pull_request_target:
types: [opened, reopened, edited, synchronize]
types: [closed]
jobs:
run_if_release:

View file

@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.9
- name: Install Python requirements
run: pip install gitpython semver PyGithub

View file

@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.9
- name: Install Python requirements
run: pip install gitpython semver PyGithub

View file

@ -18,7 +18,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: [3.7]
python-version: [3.9]
steps:
- name: 🚛 Checkout Code
@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7]
python-version: [3.9]
steps:
- name: 🚛 Checkout Code
@ -70,7 +70,7 @@ jobs:
# runs-on: macos-latest
# strategy:
# matrix:
# python-version: [3.7]
# python-version: [3.9]
# steps:
# - name: 🚛 Checkout Code
@ -87,4 +87,4 @@ jobs:
# - name: 🔨 Build
# run: |
# ./tools/build.sh
# ./tools/build.sh

View file

@ -1,6 +1,6 @@
# Build Pype docker image
FROM ubuntu:focal AS builder
ARG OPENPYPE_PYTHON_VERSION=3.7.12
ARG OPENPYPE_PYTHON_VERSION=3.9.12
ARG BUILD_DATE
ARG VERSION

View file

@ -1,6 +1,6 @@
# Build Pype docker image
FROM centos:7 AS builder
ARG OPENPYPE_PYTHON_VERSION=3.7.12
ARG OPENPYPE_PYTHON_VERSION=3.9.12
LABEL org.opencontainers.image.name="pypeclub/openpype"
LABEL org.opencontainers.image.title="OpenPype Docker Image"
@ -96,11 +96,11 @@ RUN source $HOME/.bashrc \
RUN source $HOME/.bashrc \
&& bash ./tools/build.sh
RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \
&& cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \
&& cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib \
&& cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.7/vendor/python/PySide2/Qt/lib
RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.9/lib \
&& cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.9/lib \
&& cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.9/lib \
&& cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.9/lib \
&& cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.9/vendor/python/PySide2/Qt/lib
RUN cd /opt/openpype \
rm -rf ./vendor/bin

View file

@ -5,7 +5,7 @@
OpenPype
====
[![documentation](https://github.com/pypeclub/pype/actions/workflows/documentation.yml/badge.svg)](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) ![GitHub VFX Platform](https://img.shields.io/badge/vfx%20platform-2021-lightgrey?labelColor=303846)
[![documentation](https://github.com/pypeclub/pype/actions/workflows/documentation.yml/badge.svg)](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) ![GitHub VFX Platform](https://img.shields.io/badge/vfx%20platform-2022-lightgrey?labelColor=303846)
@ -31,7 +31,7 @@ The main things you will need to run and build OpenPype are:
- **Terminal** in your OS
- PowerShell 5.0+ (Windows)
- Bash (Linux)
- [**Python 3.7.8**](#python) or higher
- [**Python 3.9.6**](#python) or higher
- [**MongoDB**](#database) (needed only for local development)
@ -50,13 +50,14 @@ For more details on requirements visit [requirements documentation](https://open
Building OpenPype
-------------
To build OpenPype you currently need [Python 3.7](https://www.python.org/downloads/) as we are following
To build OpenPype you currently need [Python 3.9](https://www.python.org/downloads/) as we are following
[vfx platform](https://vfxplatform.com). Because of some Linux distros comes with newer Python version
already, you need to install **3.7** version and make use of it. You can use perhaps [pyenv](https://github.com/pyenv/pyenv) for this on Linux.
already, you need to install **3.9** version and make use of it. You can use perhaps [pyenv](https://github.com/pyenv/pyenv) for this on Linux.
**Note**: We do not support 3.9.0 because of [this bug](https://github.com/python/cpython/pull/22670). Please, use higher versions of 3.9.x.
### Windows
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads).
You will need [Python >= 3.9.1](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads).
More tools might be needed for installing dependencies (for example for **OpenTimelineIO**) - mostly
development tools like [CMake](https://cmake.org/) and [Visual Studio](https://visualstudio.microsoft.com/cs/downloads/)
@ -82,7 +83,7 @@ OpenPype is build using [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest)
### macOS
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll need also other tools to build
You will need [Python >= 3.9](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll need also other tools to build
some OpenPype dependencies like [CMake](https://cmake.org/) and **XCode Command Line Tools** (or some other build system).
Easy way of installing everything necessary is to use [Homebrew](https://brew.sh):
@ -106,19 +107,19 @@ exec "$SHELL"
PATH=$(pyenv root)/shims:$PATH
```
4) Pull in required Python version 3.7.x
4) Pull in required Python version 3.9.x
```sh
# install Python build dependences
brew install openssl readline sqlite3 xz zlib
# replace with up-to-date 3.7.x version
pyenv install 3.7.9
# replace with up-to-date 3.9.x version
pyenv install 3.9.6
```
5) Set local Python version
```sh
# switch to OpenPype source directory
pyenv local 3.7.9
pyenv local 3.9.6
```
#### To build OpenPype:
@ -145,7 +146,7 @@ sudo ./tools/docker_build.sh centos7
If all is successful, you'll find built OpenPype in `./build/` folder.
#### Manual build
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled.
You will need [Python >= 3.9](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled.
To build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example).
@ -222,14 +223,14 @@ eval "$(pyenv virtualenv-init -)"
# reload shell
exec $SHELL
# install Python 3.7.9
pyenv install -v 3.7.9
# install Python 3.9.x
pyenv install -v 3.9.6
# change path to OpenPype 3
cd /path/to/openpype-3
# set local python version
pyenv local 3.7.9
pyenv local 3.9.6
```
</details>
@ -345,4 +346,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View file

@ -804,6 +804,8 @@ class BootstrapRepos:
"""
version = OpenPypeVersion.version_in_str(zip_file.name)
destination_dir = self.data_dir / f"{version.major}.{version.minor}"
if not destination_dir.exists():
destination_dir.mkdir(parents=True)
destination = destination_dir / zip_file.name
if destination.exists():

View file

@ -33,7 +33,6 @@ class InstallThread(QThread):
def __init__(self, parent=None,):
self._mongo = None
self._path = None
self._result = None
QThread.__init__(self, parent)
@ -62,143 +61,117 @@ class InstallThread(QThread):
progress_callback=self.set_progress, message=self.message)
local_version = OpenPypeVersion.get_installed_version_str()
# if user did enter nothing, we install OpenPype from local version.
# zip content of `repos`, copy it to user data dir and append
# version to it.
if not self._path:
# user did not entered url
if not self._mongo:
# it not set in environment
if not os.getenv("OPENPYPE_MONGO"):
# try to get it from settings registry
try:
self._mongo = bs.secure_registry.get_item(
"openPypeMongo")
except ValueError:
self.message.emit(
"!!! We need MongoDB URL to proceed.", True)
self._set_result(-1)
return
else:
self._mongo = os.getenv("OPENPYPE_MONGO")
else:
self.message.emit("Saving mongo connection string ...", False)
bs.secure_registry.set_item("openPypeMongo", self._mongo)
os.environ["OPENPYPE_MONGO"] = self._mongo
self.message.emit(
f"Detecting installed OpenPype versions in {bs.data_dir}",
False)
detected = bs.find_openpype(include_zips=True)
if detected:
if not OpenPypeVersion.get_installed_version().is_compatible(
detected[-1]):
self.message.emit((
f"Latest detected version {detected[-1]} "
"is not compatible with the currently running "
f"{local_version}"
), True)
self.message.emit((
"Filtering detected versions to compatible ones..."
), False)
detected = [
version for version in detected
if version.is_compatible(
OpenPypeVersion.get_installed_version())
]
if OpenPypeVersion(
version=local_version, path=Path()) < detected[-1]:
self.message.emit((
f"Latest installed version {detected[-1]} is newer "
f"then currently running {local_version}"
), False)
self.message.emit("Skipping OpenPype install ...", False)
if detected[-1].path.suffix.lower() == ".zip":
bs.extract_openpype(detected[-1])
self._set_result(0)
return
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa
self.message.emit((
f"Latest installed version is the same as "
f"currently running {local_version}"
), False)
self.message.emit("Skipping OpenPype install ...", False)
self._set_result(0)
return
self.message.emit((
"All installed versions are older then "
f"currently running one {local_version}"
), False)
else:
if getattr(sys, 'frozen', False):
self.message.emit("None detected.", True)
self.message.emit(("We will use OpenPype coming with "
"installer."), False)
openpype_version = bs.create_version_from_frozen_code()
if not openpype_version:
self.message.emit(
f"!!! Install failed - {openpype_version}", True)
self._set_result(-1)
return
self.message.emit(f"Using: {openpype_version}", False)
bs.install_version(openpype_version)
self.message.emit(f"Installed as {openpype_version}", False)
self.progress.emit(100)
self._set_result(1)
return
else:
self.message.emit("None detected.", False)
self.message.emit(
f"We will use local OpenPype version {local_version}", False)
local_openpype = bs.create_version_from_live_code()
if not local_openpype:
self.message.emit(
f"!!! Install failed - {local_openpype}", True)
self._set_result(-1)
return
# user did not entered url
if self._mongo:
self.message.emit("Saving mongo connection string ...", False)
bs.secure_registry.set_item("openPypeMongo", self._mongo)
elif os.getenv("OPENPYPE_MONGO"):
self._mongo = os.getenv("OPENPYPE_MONGO")
else:
# try to get it from settings registry
try:
bs.install_version(local_openpype)
except (OpenPypeVersionExists,
OpenPypeVersionInvalid,
OpenPypeVersionIOError) as e:
self.message.emit(f"Installed failed: ", True)
self.message.emit(str(e), True)
self._mongo = bs.secure_registry.get_item(
"openPypeMongo")
except ValueError:
self.message.emit(
"!!! We need MongoDB URL to proceed.", True)
self._set_result(-1)
return
os.environ["OPENPYPE_MONGO"] = self._mongo
self.message.emit(f"Installed as {local_openpype}", False)
self.message.emit(
f"Detecting installed OpenPype versions in {bs.data_dir}",
False)
detected = bs.find_openpype(include_zips=True)
if not detected and getattr(sys, 'frozen', False):
self.message.emit("None detected.", True)
self.message.emit(("We will use OpenPype coming with "
"installer."), False)
openpype_version = bs.create_version_from_frozen_code()
if not openpype_version:
self.message.emit(
f"!!! Install failed - {openpype_version}", True)
self._set_result(-1)
return
self.message.emit(f"Using: {openpype_version}", False)
bs.install_version(openpype_version)
self.message.emit(f"Installed as {openpype_version}", False)
self.progress.emit(100)
self._set_result(1)
return
else:
# if we have mongo connection string, validate it, set it to
# user settings and get OPENPYPE_PATH from there.
if self._mongo:
if not validate_mongo_connection(self._mongo):
self.message.emit(
f"!!! invalid mongo url {self._mongo}", True)
self._set_result(-1)
return
bs.secure_registry.set_item("openPypeMongo", self._mongo)
os.environ["OPENPYPE_MONGO"] = self._mongo
self.message.emit(f"processing {self._path}", True)
repo_file = bs.process_entered_location(self._path)
if detected and not OpenPypeVersion.get_installed_version().is_compatible(detected[-1]): # noqa: E501
self.message.emit((
f"Latest detected version {detected[-1]} "
"is not compatible with the currently running "
f"{local_version}"
), True)
self.message.emit((
"Filtering detected versions to compatible ones..."
), False)
if not repo_file:
self.message.emit("!!! Cannot install", True)
self._set_result(-1)
# filter results to get only compatible versions
detected = [
version for version in detected
if version.is_compatible(
OpenPypeVersion.get_installed_version())
]
if detected:
if OpenPypeVersion(
version=local_version, path=Path()) < detected[-1]:
self.message.emit((
f"Latest installed version {detected[-1]} is newer "
f"then currently running {local_version}"
), False)
self.message.emit("Skipping OpenPype install ...", False)
if detected[-1].path.suffix.lower() == ".zip":
bs.extract_openpype(detected[-1])
self._set_result(0)
return
if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa: E501
self.message.emit((
f"Latest installed version is the same as "
f"currently running {local_version}"
), False)
self.message.emit("Skipping OpenPype install ...", False)
self._set_result(0)
return
self.message.emit((
"All installed versions are older then "
f"currently running one {local_version}"
), False)
self.message.emit("None detected.", False)
self.message.emit(
f"We will use local OpenPype version {local_version}", False)
local_openpype = bs.create_version_from_live_code()
if not local_openpype:
self.message.emit(
f"!!! Install failed - {local_openpype}", True)
self._set_result(-1)
return
try:
bs.install_version(local_openpype)
except (OpenPypeVersionExists,
OpenPypeVersionInvalid,
OpenPypeVersionIOError) as e:
self.message.emit(f"Installed failed: ", True)
self.message.emit(str(e), True)
self._set_result(-1)
return
self.message.emit(f"Installed as {local_openpype}", False)
self.progress.emit(100)
self._set_result(1)
return
self.progress.emit(100)
self._set_result(1)
return

View file

@ -48,8 +48,8 @@ Source: "build\{#build}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdir
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\openpype_gui.exe"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\openpype_gui.exe"; Tasks: desktopicon
Name: "{autoprograms}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe"
Name: "{autodesktop}\{#MyAppName} {#AppVer}"; Filename: "{app}\openpype_gui.exe"; Tasks: desktopicon
[Run]
Filename: "{app}\openpype_gui.exe"; Description: "{cm:LaunchProgram,OpenPype}"; Flags: nowait postinstall skipifsilent

View file

@ -10,30 +10,15 @@ from .launch_logic import (
)
from .pipeline import (
AfterEffectsHost,
ls,
get_asset_settings,
install,
uninstall,
list_instances,
remove_instance,
containerise,
get_context_data,
update_context_data,
get_context_title
)
from .workio import (
file_extensions,
has_unsaved_changes,
save_file,
open_file,
current_file,
work_root,
containerise
)
from .lib import (
maintained_selection,
get_extension_manifest_path
get_extension_manifest_path,
get_asset_settings
)
from .plugin import (
@ -48,26 +33,12 @@ __all__ = [
# pipeline
"ls",
"get_asset_settings",
"install",
"uninstall",
"list_instances",
"remove_instance",
"containerise",
"get_context_data",
"update_context_data",
"get_context_title",
"file_extensions",
"has_unsaved_changes",
"save_file",
"open_file",
"current_file",
"work_root",
# lib
"maintained_selection",
"get_extension_manifest_path",
"get_asset_settings",
# plugin
"AfterEffectsLoader"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.23"
<ExtensionManifest Version="8.0" ExtensionBundleId="com.openpype.AE.panel" ExtensionBundleVersion="1.0.24"
ExtensionBundleName="openpype" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExtensionList>
<Extension Id="com.openpype.AE.panel" Version="1.0" />

View file

@ -38,17 +38,6 @@
});
</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() {
@ -82,17 +71,6 @@
});
</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() {
@ -113,11 +91,9 @@
<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>

View file

@ -284,9 +284,6 @@ class AfterEffectsRoute(WebSocketRoute):
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")
@ -294,14 +291,11 @@ class AfterEffectsRoute(WebSocketRoute):
self._tool_route("loader")
async def publish_route(self):
self._tool_route("publish")
self._tool_route("publisher")
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")

View file

@ -26,9 +26,10 @@ def safe_excepthook(*args):
def main(*subprocess_args):
sys.excepthook = safe_excepthook
from openpype.hosts.aftereffects import api
from openpype.hosts.aftereffects.api import AfterEffectsHost
install_host(api)
host = AfterEffectsHost()
install_host(host)
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
app = QtWidgets.QApplication([])
@ -133,3 +134,32 @@ def get_background_layers(file_url):
layer.get("filename")).
replace("\\", "/"))
return layers
def get_asset_settings(asset_doc):
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = asset_doc["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
}

View file

@ -1,5 +1,4 @@
import os
from Qt import QtWidgets
import pyblish.api
@ -16,6 +15,13 @@ from openpype.pipeline import (
from openpype.pipeline.load import any_outdated_containers
import openpype.hosts.aftereffects
from openpype.host import (
HostBase,
IWorkfileHost,
ILoadHost,
IPublishHost
)
from .launch_logic import get_stub, ConnectionNotEstablishedYet
log = Logger.get_logger(__name__)
@ -30,27 +36,142 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
def install():
print("Installing Pype config...")
class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "aftereffects"
pyblish.api.register_host("aftereffects")
pyblish.api.register_plugin_path(PUBLISH_PATH)
def __init__(self):
self._stub = None
super(AfterEffectsHost, self).__init__()
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
@property
def stub(self):
"""
Handle pulling stub from PS to run operations on host
Returns:
(AEServerStub) or None
"""
if self._stub:
return self._stub
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
try:
stub = get_stub() # only after Photoshop is up
except ConnectionNotEstablishedYet:
print("Not connected yet, ignoring")
return
register_event_callback("application.launched", application_launch)
if not stub.get_active_document_name():
return
self._stub = stub
return self._stub
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def install(self):
print("Installing Pype config...")
pyblish.api.register_host("aftereffects")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
register_event_callback("application.launched", application_launch)
def get_workfile_extensions(self):
return [".aep"]
def save_workfile(self, dst_path=None):
self.stub.saveAs(dst_path, True)
def open_workfile(self, filepath):
self.stub.open(filepath)
return True
def get_current_workfile(self):
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 ValueError:
print("Nothing opened")
pass
return None
def get_containers(self):
return ls()
def get_context_data(self):
meta = self.stub.get_metadata()
for item in meta:
if item.get("id") == "publish_context":
item.pop("id")
return item
return {}
def update_context_data(self, data, changes):
item = data
item["id"] = "publish_context"
self.stub.imprint(item["id"], item)
# created instances section
def list_instances(self):
"""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 = self.stub
if not stub:
return []
instances = []
layers_meta = stub.get_metadata()
for instance in layers_meta:
if instance.get("id") == "pyblish.avalon.instance":
instances.append(instance)
return instances
def remove_instance(self, 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 = self.stub
if not stub:
return
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
if not inst_id:
log.warning("No instance identifier for {}".format(instance))
return
stub.remove_instance(inst_id)
if instance.get("members"):
item = stub.get_item(instance["members"][0])
if item:
stub.rename_item(item.id,
item.name.replace(stub.PUBLISH_ICON, ''))
def application_launch():
@ -63,35 +184,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
instance[0].Visible = new_value
def get_asset_settings(asset_doc):
"""Get settings on current asset from database.
Returns:
dict: Scene data.
"""
asset_data = asset_doc["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 ls():
"""Yields containers from active AfterEffects document.
@ -191,102 +283,17 @@ def containerise(name,
return comp
# created instances section
def list_instances():
"""
List all created instances from current workfile which
will be published.
def cache_and_get_instances(creator):
"""Cache instances in shared data.
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("id") == "pyblish.avalon.instance":
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
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
if not inst_id:
log.warning("No instance identifier for {}".format(instance))
return
stub.remove_instance(inst_id)
if instance.get("members"):
item = stub.get_item(instance["members"][0])
if item:
stub.rename_item(item.id,
item.name.replace(stub.PUBLISH_ICON, ''))
# new publisher section
def get_context_data():
meta = _get_stub().get_metadata()
for item in meta:
if item.get("id") == "publish_context":
item.pop("id")
return item
return {}
def update_context_data(data, changes):
item = data
item["id"] = "publish_context"
_get_stub().imprint(item["id"], item)
def get_context_title():
"""Returns title for Creator window"""
project_name = legacy_io.Session["AVALON_PROJECT"]
asset_name = legacy_io.Session["AVALON_ASSET"]
task_name = legacy_io.Session["AVALON_TASK"]
return "{}/{}/{}".format(project_name, asset_name, task_name)
def _get_stub():
"""
Handle pulling stub from PS to run operations on host
Storing all instances as a list as legacy instances might be still present.
Args:
creator (Creator): Plugin which would like to get instances from host.
Returns:
(AEServerStub) or None
List[]: list of all instances stored in metadata
"""
try:
stub = get_stub() # only after Photoshop is up
except ConnectionNotEstablishedYet:
print("Not connected yet, ignoring")
return
if not stub.get_active_document_name():
return
return stub
shared_key = "openpype.photoshop.instances"
if shared_key not in creator.collection_shared_data:
creator.collection_shared_data[shared_key] = \
creator.host.list_instances()
return creator.collection_shared_data[shared_key]

View file

@ -1,53 +0,0 @@
"""Host API required Work Files tool"""
import os
from .launch_logic import get_stub
def file_extensions():
return [".aep"]
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 ValueError:
print("Nothing opened")
pass
return None
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
def _active_document():
# TODO merge with current_file - even in extension
document_name = None
try:
document_name = get_stub().get_active_document_name()
except ValueError:
print("Nothing opened")
pass
return document_name

View file

@ -1,13 +0,0 @@
from openpype.hosts.aftereffects.plugins.create import create_legacy_render
class CreateLocalRender(create_legacy_render.CreateRender):
""" Creator to render locally.
Created only after default render on farm. So family 'render.local' is
used for backward compatibility.
"""
name = "renderDefault"
label = "Render Locally"
family = "renderLocal"

View file

@ -1,62 +0,0 @@
from openpype.pipeline import create
from openpype.pipeline import CreatorError
from openpype.hosts.aftereffects.api import (
get_stub,
list_instances
)
class CreateRender(create.LegacyCreator):
"""Render folder for publish.
Creates subsets in format 'familyTaskSubsetname',
eg 'renderCompositingMain'.
Create only single instance from composition at a time.
"""
name = "renderDefault"
label = "Render on Farm"
family = "render"
defaults = ["Main"]
def process(self):
stub = get_stub() # only after After Effects is up
items = []
if (self.options or {}).get("useSelection"):
items = stub.get_selected_items(
comps=True, folders=False, footages=False
)
if len(items) > 1:
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."
))
existing_subsets = [
instance['subset'].lower()
for instance in list_instances()
]
item = items.pop()
if self.name.lower() in existing_subsets:
txt = "Instance with name \"{}\" already exists.".format(self.name)
raise CreatorError(txt)
self.data["members"] = [item.id]
self.data["uuid"] = item.id # for SubsetManager
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
stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"])

View file

@ -7,6 +7,7 @@ from openpype.pipeline import (
CreatorError,
legacy_io,
)
from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances
class RenderCreator(Creator):
@ -28,7 +29,7 @@ class RenderCreator(Creator):
return resources.get_openpype_splash_filepath()
def collect_instances(self):
for instance_data in api.list_instances():
for instance_data in cache_and_get_instances(self):
# legacy instances have family=='render' or 'renderLocal', use them
creator_id = (instance_data.get("creator_identifier") or
instance_data.get("family", '').replace("Local", ''))
@ -46,7 +47,7 @@ class RenderCreator(Creator):
def remove_instances(self, instances):
for instance in instances:
api.remove_instance(instance)
self.host.remove_instance(instance)
self._remove_instance_from_context(instance)
def create(self, subset_name, data, pre_create_data):

View file

@ -5,6 +5,7 @@ from openpype.pipeline import (
CreatedInstance,
legacy_io,
)
from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances
class AEWorkfileCreator(AutoCreator):
@ -17,7 +18,7 @@ class AEWorkfileCreator(AutoCreator):
return []
def collect_instances(self):
for instance_data in api.list_instances():
for instance_data in cache_and_get_instances(self):
creator_id = instance_data.get("creator_identifier")
if creator_id == self.identifier:
subset_name = instance_data["subset"]
@ -55,7 +56,7 @@ class AEWorkfileCreator(AutoCreator):
}
data.update(self.get_dynamic_data(
self.default_variant, task_name, asset_doc,
project_name, host_name
project_name, host_name, None
))
new_instance = CreatedInstance(

View file

@ -9,7 +9,7 @@ Context of the given subset doesn't match your current scene.
### How to repair?
You can fix this with "repair" button on the right.
You can fix this with "repair" button on the right and refresh Publish at the bottom right.
</description>
<detail>
### __Detailed Info__ (optional)

View file

@ -7,28 +7,15 @@ Anything that isn't defined here is INTERNAL and unreliable for external use.
from .launch_logic import stub
from .pipeline import (
PhotoshopHost,
ls,
list_instances,
remove_instance,
install,
uninstall,
containerise,
get_context_data,
update_context_data,
get_context_title
containerise
)
from .plugin import (
PhotoshopLoader,
get_unique_layer_name
)
from .workio import (
file_extensions,
has_unsaved_changes,
save_file,
open_file,
current_file,
work_root,
)
from .lib import (
maintained_selection,
@ -40,28 +27,14 @@ __all__ = [
"stub",
# pipeline
"PhotoshopHost",
"ls",
"list_instances",
"remove_instance",
"install",
"uninstall",
"containerise",
"get_context_data",
"update_context_data",
"get_context_title",
# Plugin
"PhotoshopLoader",
"get_unique_layer_name",
# workfiles
"file_extensions",
"has_unsaved_changes",
"save_file",
"open_file",
"current_file",
"work_root",
# lib
"maintained_selection",
"maintained_visibility",

View file

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<ExtensionManifest ExtensionBundleId="com.openpype.PS.panel" ExtensionBundleVersion="1.0.11" Version="7.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExtensionManifest ExtensionBundleId="com.openpype.PS.panel" ExtensionBundleVersion="1.0.12" Version="7.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ExtensionList>
<Extension Id="com.openpype.PS.panel" Version="1.0.1" />
</ExtensionList>

Binary file not shown.

View file

@ -32,17 +32,6 @@
});
</script>
<script type=text/javascript>
$(function() {
$("a#creator-button").bind("click", function() {
RPC.call('Photoshop.creator_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#loader-button").bind("click", function() {
@ -75,17 +64,6 @@
});
});
</script>
<script type=text/javascript>
$(function() {
$("a#subsetmanager-button").bind("click", function() {
RPC.call('Photoshop.subsetmanager_route').then(function (data) {
}, function (error) {
alert(error);
});
});
});
</script>
<script type=text/javascript>
$(function() {
@ -109,11 +87,9 @@
<script type="text/javascript" src="./client/client.js"></script>
<a href=# id=workfiles-button><button>Workfiles...</button></a>
<a href=# id=creator-button><button>Create...</button></a>
<a href=# id=loader-button><button>Load...</button></a>
<a href=# id=publish-button><button>Publish...</button></a>
<a href=# id=sceneinventory-button><button>Manage...</button></a>
<a href=# id=subsetmanager-button><button>Subset Manager...</button></a>
<a href=# id=experimental-button><button>Experimental Tools...</button></a>
</body>
</html>

View file

@ -334,9 +334,6 @@ class PhotoshopRoute(WebSocketRoute):
return await self.socket.call('photoshop.read')
# panel routes for tools
async def creator_route(self):
self._tool_route("creator")
async def workfiles_route(self):
self._tool_route("workfiles")
@ -344,14 +341,11 @@ class PhotoshopRoute(WebSocketRoute):
self._tool_route("loader")
async def publish_route(self):
self._tool_route("publish")
self._tool_route("publisher")
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")

View file

@ -20,9 +20,11 @@ def safe_excepthook(*args):
def main(*subprocess_args):
from openpype.hosts.photoshop import api
from openpype.hosts.photoshop.api import PhotoshopHost
host = PhotoshopHost()
install_host(host)
install_host(api)
sys.excepthook = safe_excepthook
# coloring in StdOutBroker

View file

@ -1,6 +1,5 @@
import os
from Qt import QtWidgets
import pyblish.api
from openpype.lib import register_event_callback, Logger
@ -12,6 +11,14 @@ from openpype.pipeline import (
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.host import (
HostBase,
IWorkfileHost,
ILoadHost,
IPublishHost
)
from openpype.pipeline.load import any_outdated_containers
from openpype.hosts.photoshop import PHOTOSHOP_HOST_DIR
@ -26,6 +33,140 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "photoshop"
def install(self):
"""Install Photoshop-specific functionality needed for integration.
This function is called automatically on calling
`api.install(photoshop)`.
"""
log.info("Installing OpenPype Photoshop...")
pyblish.api.register_host("photoshop")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
register_event_callback("application.launched", on_application_launch)
def current_file(self):
try:
full_name = lib.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(self, session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
def open_workfile(self, filepath):
lib.stub().open(filepath)
return True
def save_workfile(self, filepath=None):
_, ext = os.path.splitext(filepath)
lib.stub().saveAs(filepath, ext[1:], True)
def get_current_workfile(self):
return self.current_file()
def workfile_has_unsaved_changes(self):
if self.current_file():
return not lib.stub().is_saved()
return False
def get_workfile_extensions(self):
return [".psd", ".psb"]
def get_containers(self):
return ls()
def get_context_data(self):
"""Get stored values for context (validation enable/disable etc)"""
meta = _get_stub().get_layers_metadata()
for item in meta:
if item.get("id") == "publish_context":
item.pop("id")
return item
return {}
def update_context_data(self, data, changes):
"""Store value needed for context"""
item = data
item["id"] = "publish_context"
_get_stub().imprint(item["id"], item)
def get_context_title(self):
"""Returns title for Creator window"""
project_name = legacy_io.Session["AVALON_PROJECT"]
asset_name = legacy_io.Session["AVALON_ASSET"]
task_name = legacy_io.Session["AVALON_TASK"]
return "{}/{}/{}".format(project_name, asset_name, task_name)
def list_instances(self):
"""List all created instances to publish from current workfile.
Pulls from File > File Info
Returns:
(list) of dictionaries matching instances format
"""
stub = _get_stub()
if not stub:
return []
instances = []
layers_meta = stub.get_layers_metadata()
if layers_meta:
for instance in layers_meta:
if instance.get("id") == "pyblish.avalon.instance":
instances.append(instance)
return instances
def remove_instance(self, instance):
"""Remove instance from current workfile metadata.
Updates metadata of current file in File > File Info and removes
icon highlight on group layer.
Args:
instance (dict): instance representation from subsetmanager model
"""
stub = _get_stub()
if not stub:
return
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
if not inst_id:
log.warning("No instance identifier for {}".format(instance))
return
stub.remove_instance(inst_id)
if instance.get("members"):
item = stub.get_layer(instance["members"][0])
if item:
stub.rename_layer(item.id,
item.name.replace(stub.PUBLISH_ICON, ''))
def check_inventory():
if not any_outdated_containers():
return
@ -52,32 +193,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
instance[0].Visible = new_value
def install():
"""Install Photoshop-specific functionality of avalon-core.
This function is called automatically on calling `api.install(photoshop)`.
"""
log.info("Installing OpenPype Photoshop...")
pyblish.api.register_host("photoshop")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
register_event_callback("application.launched", on_application_launch)
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def ls():
"""Yields containers from active Photoshop document
@ -117,61 +232,6 @@ def ls():
yield data
def list_instances():
"""List all created instances to publish from current workfile.
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_layers_metadata()
if layers_meta:
for instance in layers_meta:
if instance.get("id") == "pyblish.avalon.instance":
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
inst_id = instance.get("instance_id") or instance.get("uuid") # legacy
if not inst_id:
log.warning("No instance identifier for {}".format(instance))
return
stub.remove_instance(inst_id)
if instance.get("members"):
item = stub.get_layer(instance["members"][0])
if item:
stub.rename_layer(item.id,
item.name.replace(stub.PUBLISH_ICON, ''))
def _get_stub():
"""Handle pulling stub from PS to run operations on host
@ -226,28 +286,17 @@ def containerise(
return layer
def get_context_data():
"""Get stored values for context (validation enable/disable etc)"""
meta = _get_stub().get_layers_metadata()
for item in meta:
if item.get("id") == "publish_context":
item.pop("id")
return item
def cache_and_get_instances(creator):
"""Cache instances in shared data.
return {}
def update_context_data(data, changes):
"""Store value needed for context"""
item = data
item["id"] = "publish_context"
_get_stub().imprint(item["id"], item)
def get_context_title():
"""Returns title for Creator window"""
project_name = legacy_io.Session["AVALON_PROJECT"]
asset_name = legacy_io.Session["AVALON_ASSET"]
task_name = legacy_io.Session["AVALON_TASK"]
return "{}/{}/{}".format(project_name, asset_name, task_name)
Storing all instances as a list as legacy instances might be still present.
Args:
creator (Creator): Plugin which would like to get instances from host.
Returns:
List[]: list of all instances stored in metadata
"""
shared_key = "openpype.photoshop.instances"
if shared_key not in creator.collection_shared_data:
creator.collection_shared_data[shared_key] = \
creator.host.list_instances()
return creator.collection_shared_data[shared_key]

View file

@ -1,49 +0,0 @@
"""Host API required Work Files tool"""
import os
from . import lib
def _active_document():
document_name = lib.stub().get_active_document_name()
if not document_name:
return None
return document_name
def file_extensions():
return [".psd", ".psb"]
def has_unsaved_changes():
if _active_document():
return not lib.stub().is_saved()
return False
def save_file(filepath):
_, ext = os.path.splitext(filepath)
lib.stub().saveAs(filepath, ext[1:], True)
def open_file(filepath):
lib.stub().open(filepath)
return True
def current_file():
try:
full_name = lib.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

@ -9,6 +9,7 @@ from openpype.pipeline import (
)
from openpype.lib import prepare_template_data
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances
class ImageCreator(Creator):
@ -19,7 +20,7 @@ class ImageCreator(Creator):
description = "Image creator"
def collect_instances(self):
for instance_data in api.list_instances():
for instance_data in cache_and_get_instances(self):
# legacy instances have family=='image'
creator_id = (instance_data.get("creator_identifier") or
instance_data.get("family"))
@ -97,6 +98,7 @@ class ImageCreator(Creator):
data.update({"subset": subset_name})
data.update({"members": [str(group.id)]})
data.update({"layer_name": layer_name})
data.update({"long_name": "_".join(layer_names_in_hierarchy)})
new_instance = CreatedInstance(self.family, subset_name, data,
@ -121,7 +123,7 @@ class ImageCreator(Creator):
def remove_instances(self, instances):
for instance in instances:
api.remove_instance(instance)
self.host.remove_instance(instance)
self._remove_instance_from_context(instance)
def get_default_variants(self):
@ -163,6 +165,11 @@ class ImageCreator(Creator):
def _clean_highlights(self, stub, item):
return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON,
'')
@classmethod
def get_dynamic_data(cls, *args, **kwargs):
def get_dynamic_data(self, variant, task_name, asset_doc,
project_name, host_name, instance):
if instance is not None:
layer_name = instance.get("layer_name")
if layer_name:
return {"layer": layer_name}
return {"layer": "{layer}"}

View file

@ -1,119 +0,0 @@
import re
from Qt import QtWidgets
from openpype.pipeline import create
from openpype.hosts.photoshop import api as photoshop
from openpype.lib import prepare_template_data
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
class CreateImage(create.LegacyCreator):
"""Image folder for publish."""
name = "imageDefault"
label = "Image"
family = "image"
defaults = ["Main"]
def process(self):
groups = []
layers = []
create_group = False
stub = photoshop.stub()
if (self.options or {}).get("useSelection"):
multiple_instances = False
selection = stub.get_selected_layers()
self.log.info("selection {}".format(selection))
if len(selection) > 1:
# Ask user whether to create one image or image per selected
# item.
msg_box = QtWidgets.QMessageBox()
msg_box.setIcon(QtWidgets.QMessageBox.Warning)
msg_box.setText(
"Multiple layers selected."
"\nDo you want to make one image per layer?"
)
msg_box.setStandardButtons(
QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No |
QtWidgets.QMessageBox.Cancel
)
ret = msg_box.exec_()
if ret == QtWidgets.QMessageBox.Yes:
multiple_instances = True
elif ret == QtWidgets.QMessageBox.Cancel:
return
if multiple_instances:
for item in selection:
if item.group:
groups.append(item)
else:
layers.append(item)
else:
group = stub.group_selected_layers(self.name)
groups.append(group)
elif len(selection) == 1:
# One selected item. Use group if its a LayerSet (group), else
# create a new group.
if selection[0].group:
groups.append(selection[0])
else:
layers.append(selection[0])
elif len(selection) == 0:
# No selection creates an empty group.
create_group = True
else:
group = stub.create_group(self.name)
groups.append(group)
if create_group:
group = stub.create_group(self.name)
groups.append(group)
for layer in layers:
stub.select_layers([layer])
group = stub.group_selected_layers(layer.name)
groups.append(group)
creator_subset_name = self.data["subset"]
layer_name = ''
for group in groups:
long_names = []
group.name = group.name.replace(stub.PUBLISH_ICON, ''). \
replace(stub.LOADED_ICON, '')
subset_name = creator_subset_name
if len(groups) > 1:
layer_name = re.sub(
"[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
"",
group.name
)
if "{layer}" not in subset_name.lower():
subset_name += "{Layer}"
layer_fill = prepare_template_data({"layer": layer_name})
subset_name = subset_name.format(**layer_fill)
if group.long_name:
for directory in group.long_name[::-1]:
name = directory.replace(stub.PUBLISH_ICON, '').\
replace(stub.LOADED_ICON, '')
long_names.append(name)
self.data.update({"subset": subset_name})
self.data.update({"uuid": str(group.id)})
self.data.update({"members": [str(group.id)]})
self.data.update({"long_name": "_".join(long_names)})
stub.imprint(group, self.data)
# reusing existing group, need to rename afterwards
if not create_group:
stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name)
@classmethod
def get_dynamic_data(cls, *args, **kwargs):
return {"layer": "{layer}"}

View file

@ -5,6 +5,7 @@ from openpype.pipeline import (
CreatedInstance,
legacy_io
)
from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances
class PSWorkfileCreator(AutoCreator):
@ -17,7 +18,7 @@ class PSWorkfileCreator(AutoCreator):
return []
def collect_instances(self):
for instance_data in api.list_instances():
for instance_data in cache_and_get_instances(self):
creator_id = instance_data.get("creator_identifier")
if creator_id == self.identifier:
subset_name = instance_data["subset"]
@ -54,7 +55,7 @@ class PSWorkfileCreator(AutoCreator):
}
data.update(self.get_dynamic_data(
self.default_variant, task_name, asset_doc,
project_name, host_name
project_name, host_name, None
))
new_instance = CreatedInstance(

View file

@ -43,7 +43,7 @@ class CollectExtensionVersion(pyblish.api.ContextPlugin):
with open(manifest_url) as fp:
content = fp.read()
found = re.findall(r'(ExtensionBundleVersion=")([0-10\.]+)(")',
found = re.findall(r'(ExtensionBundleVersion=")([0-9\.]+)(")',
content)
if found:
expected_version = found[0][1]

View file

@ -82,7 +82,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
if len(instance_names) != len(set(instance_names)):
self.log.warning("Duplicate instances found. " +
"Remove unwanted via SubsetManager")
"Remove unwanted via Publisher")
if len(instance_names) == 0 and self.flatten_subset_template:
project_name = context.data["projectEntity"]["name"]

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Asset does not match</title>
<description>
## Collected asset name is not same as in context
{msg}
### How to repair?
{repair_msg}
Refresh Publish afterwards (circle arrow at the bottom right).
If that's not correct value, close workfile and reopen via Workfiles to get
proper context asset name OR disable this validator and publish again
if you are publishing to different context deliberately.
(Context means combination of project, asset name and task name.)
</description>
</error>
</root>

View file

@ -10,7 +10,7 @@ Subset or layer name cannot contain specific characters (spaces etc) which could
### How to repair?
You can fix this with "repair" button on the right.
You can fix this with "repair" button on the right and press Refresh publishing button at the bottom right.
</description>
<detail>
### __Detailed Info__ (optional)

View file

@ -1,7 +1,11 @@
import pyblish.api
from openpype.pipeline import legacy_io
from openpype.pipeline.publish import ValidateContentsOrder
from openpype.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
OptionalPyblishPluginMixin
)
from openpype.hosts.photoshop import api as photoshop
@ -31,30 +35,38 @@ class ValidateInstanceAssetRepair(pyblish.api.Action):
stub.imprint(instance[0], data)
class ValidateInstanceAsset(pyblish.api.InstancePlugin):
class ValidateInstanceAsset(OptionalPyblishPluginMixin,
pyblish.api.InstancePlugin):
"""Validate the instance asset is the current selected context asset.
As it might happen that multiple worfiles are opened, switching
between them would mess with selected context.
In that case outputs might be output under wrong asset!
As it might happen that multiple worfiles are opened, switching
between them would mess with selected context.
In that case outputs might be output under wrong asset!
Repair action will use Context asset value (from Workfiles or Launcher)
Closing and reopening with Workfiles will refresh Context value.
Repair action will use Context asset value (from Workfiles or Launcher)
Closing and reopening with Workfiles will refresh Context value.
"""
label = "Validate Instance Asset"
hosts = ["photoshop"]
optional = True
actions = [ValidateInstanceAssetRepair]
order = ValidateContentsOrder
def process(self, instance):
instance_asset = instance.data["asset"]
current_asset = legacy_io.Session["AVALON_ASSET"]
msg = (
f"Instance asset {instance_asset} is not the same "
f"as current context {current_asset}. PLEASE DO:\n"
f"Repair with 'A' action to use '{current_asset}'.\n"
f"If that's not correct value, close workfile and "
f"reopen via Workfiles!"
)
assert instance_asset == current_asset, msg
if instance_asset != current_asset:
msg = (
f"Instance asset {instance_asset} is not the same "
f"as current context {current_asset}."
)
repair_msg = (
f"Repair with 'Repair' button to use '{current_asset}'.\n"
)
formatting_data = {"msg": msg,
"repair_msg": repair_msg}
raise PublishXmlValidationError(self, msg,
formatting_data=formatting_data)

View file

@ -84,7 +84,7 @@ class ValidateNaming(pyblish.api.InstancePlugin):
replace_char = ''
def process(self, instance):
help_msg = ' Use Repair action (A) in Pyblish to fix it.'
help_msg = ' Use Repair button to fix it and then refresh publish.'
layer = instance.data.get("layer")
if layer:

View file

@ -29,7 +29,7 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin):
for item, count in collections.Counter(subset_names).items()
if count > 1]
msg = ("Instance subset names {} are not unique. ".format(non_unique) +
"Remove duplicates via SubsetManager.")
"Remove duplicates via Publisher.")
formatting_data = {
"non_unique": ",".join(non_unique)
}

View file

@ -199,7 +199,7 @@ class FtrackTrayWrapper:
failed_count = 0
# If thread failed test Ftrack and Mongo connection
elif not self.thread_socket_server.isAlive():
elif not self.thread_socket_server.is_alive():
self.thread_socket_server.join()
self.thread_socket_server = None
ftrack_accessible = False

View file

@ -88,7 +88,10 @@ class ExperimentalTools:
"publisher",
"New publisher",
"Combined creation and publishing into one tool.",
self._show_publisher
self._show_publisher,
hosts_filter=["blender", "maya", "nuke", "celaction", "flame",
"fusion", "harmony", "hiero", "resolve",
"tvpaint", "unreal"]
)
]

View file

@ -27,6 +27,9 @@ class PluginLoadReportModel(QtGui.QStandardItemModel):
parent = self.invisibleRootItem()
parent.removeRows(0, parent.rowCount())
if report is None:
return
new_items = []
new_items_by_filepath = {}
for filepath in report.crashed_plugin_paths.keys():

View file

@ -367,6 +367,7 @@ class LoadedFilesView(QtWidgets.QTreeView):
def _on_rows_inserted(self):
header = self.header()
header.resizeSections(header.ResizeToContents)
self._update_remove_btn()
def resizeEvent(self, event):
super(LoadedFilesView, self).resizeEvent(event)

View file

@ -361,6 +361,13 @@ class PublisherWindow(QtWidgets.QDialog):
super(PublisherWindow, self).resizeEvent(event)
self._update_publish_frame_rect()
def keyPressEvent(self, event):
# Ignore escape button to close window
if event.key() == QtCore.Qt.Key_Escape:
event.accept()
return
super(PublisherWindow, self).keyPressEvent(event)
def _on_overlay_message(self, event):
self._overlay_object.add_message(
event["message"],

1049
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -28,11 +28,11 @@ packages = [
openpype = 'start:boot'
[tool.poetry.dependencies]
python = "3.7.*"
python = ">=3.9.1,<3.10"
aiohttp = "^3.7"
aiohttp_json_rpc = "*" # TVPaint server
acre = { git = "https://github.com/pypeclub/acre.git" }
opentimelineio = { version = "0.14.0.dev1", source = "openpype" }
opentimelineio = "^0.14"
appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master" }
blessed = "^1.17" # openpype terminal formatting
coolname = "*"
@ -75,7 +75,7 @@ aiohttp-middlewares = "^2.0.0"
flake8 = "^3.7"
autopep8 = "^1.4"
coverage = "*"
cx_freeze = "~6.9"
cx_freeze = "6.12.0"
GitPython = "^3.1.17"
jedi = "^0.13"
Jinja2 = "^2.11"

View file

@ -90,7 +90,7 @@ done
###############################################################################
detect_python () {
echo -e "${BIGreen}>>>${RST} Using python \c"
command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.7 installed to continue.${RST}"; return 1; }
command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.9 installed to continue.${RST}"; return 1; }
local version_command
version_command="import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))"
local python_version
@ -99,9 +99,9 @@ detect_python () {
IFS=.
set -- $python_version
IFS="$oIFS"
if [ "$1" -ge "3" ] && [ "$2" -ge "6" ] ; then
if [ "$2" -gt "7" ] ; then
echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.7.x${RST}"; return 1;
if [ "$1" -ge "3" ] && [ "$2" -ge "9" ] ; then
if [ "$2" -gt "9" ] ; then
echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}"; return 1;
else
echo -e "${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}"
fi
@ -152,7 +152,7 @@ main () {
openpype_root=$(dirname $(dirname "$(realpath ${BASH_SOURCE[0]})"))
pushd "$openpype_root" > /dev/null || return > /dev/null
version_command="import os;exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read());print(__version__);"
version_command="import os;import re;version={};exec(open(os.path.join('$openpype_root', 'openpype', 'version.py')).read(), version);print(re.search(r'(\d+\.\d+.\d+).*', version['__version__'])[1]);"
openpype_version="$(python <<< ${version_command})"
_inside_openpype_tool="1"
@ -181,7 +181,7 @@ if [ "$disable_submodule_update" == 1 ]; then
echo -e "${BIYellow}***${RST} Not updating submodules ..."
else
echo -e "${BIGreen}>>>${RST} Making sure submodules are up-to-date ..."
git submodule update --init --recursive
git submodule update --init --recursive || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return 1; }
fi
echo -e "${BIGreen}>>>${RST} Building ..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
@ -189,12 +189,21 @@ if [ "$disable_submodule_update" == 1 ]; then
elif [[ "$OSTYPE" == "darwin"* ]]; then
"$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac &> "$openpype_root/build/build.log" || { echo -e "${BIRed}------------------------------------------${RST}"; cat "$openpype_root/build/build.log"; echo -e "${BIRed}------------------------------------------${RST}"; echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; }
fi
"$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py"
"$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py" || { echo -e "${BIRed}!!!>${RST} ${BIYellow}Failed to process dependencies${RST}"; return 1; }
if [[ "$OSTYPE" == "darwin"* ]]; then
# fix cx_Freeze libs issue
echo -e "${BIGreen}>>>${RST} Fixing libs ..."
mv "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/dependencies/cx_Freeze" "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/lib/" || { echo -e "${BIRed}!!!>${RST} ${BIYellow}Can't move cx_Freeze libs${RST}"; return 1; }
# fix code signing issue
codesign --remove-signature "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/lib/Python"
echo -e "${BIGreen}>>>${RST} Fixing code signatures ...\c"
codesign --remove-signature "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/openpype_console" || { echo -e "${BIRed}FAILED${RST}"; return 1; }
codesign --remove-signature "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/openpype_gui" || { echo -e "${BIRed}FAILED${RST}"; return 1; }
echo -e "${BIGreen}DONE${RST}"
if command -v create-dmg > /dev/null 2>&1; then
echo -e "${BIGreen}>>>${RST} Creating dmg image ...\c"
create-dmg \
--volname "OpenPype $openpype_version Installer" \
--window-pos 200 120 \
@ -202,6 +211,9 @@ if [ "$disable_submodule_update" == 1 ]; then
--app-drop-link 100 50 \
"$openpype_root/build/OpenPype-Installer-$openpype_version.dmg" \
"$openpype_root/build/OpenPype $openpype_version.app"
test $? -eq 0 || { echo -e "${BIRed}FAILED${RST}"; return 1; }
echo -e "${BIGreen}DONE${RST}"
else
echo -e "${BIYellow}!!!${RST} ${BIWhite}create-dmg${RST} command is not available."
fi

View file

@ -131,15 +131,15 @@ if(-not $m) {
Set-Location -Path $current_dir
Exit-WithCode 1
}
# We are supporting python 3.7 only
if (($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
# We are supporting python 3.9
if (($matches[1] -lt 3) -or ($matches[2] -lt 9)) {
Write-Host "FAILED Version [ $p ] is old and unsupported" -ForegroundColor red
Set-Location -Path $current_dir
Exit-WithCode 1
} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 7)) {
} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 9)) {
Write-Host "WARNING Version [ $p ] is unsupported, use at your own risk." -ForegroundColor yellow
Write-Host "*** " -NoNewline -ForegroundColor yellow
Write-Host "OpenPype supports only Python 3.7" -ForegroundColor white
Write-Host "OpenPype supports only Python 3.9" -ForegroundColor white
} else {
Write-Host "OK [ $p ]" -ForegroundColor green
}

View file

@ -100,14 +100,14 @@ print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))
Set-Location -Path $current_dir
Exit-WithCode 1
}
# We are supporting python 3.7 only
if (($matches[1] -lt 3) -or ($matches[2] -lt 7)) {
# We are supporting python 3.9 only
if (($matches[1] -lt 3) -or ($matches[2] -lt 9)) {
Write-Color -Text "FAILED ", "Version ", "[", $p ,"]", "is old and unsupported" -Color Red, Yellow, Cyan, White, Cyan, Yellow
Set-Location -Path $current_dir
Exit-WithCode 1
} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 7)) {
} elseif (($matches[1] -eq 3) -and ($matches[2] -gt 9)) {
Write-Color -Text "WARNING Version ", "[", $p, "]", " is unsupported, use at your own risk." -Color Yellow, Cyan, White, Cyan, Yellow
Write-Color -Text "*** ", "OpenPype supports only Python 3.7" -Color Yellow, White
Write-Color -Text "*** ", "OpenPype supports only Python 3.9" -Color Yellow, White
} else {
Write-Color "OK ", "[", $p, "]" -Color Green, Cyan, White, Cyan
}

View file

@ -88,16 +88,16 @@ done
###############################################################################
detect_python () {
echo -e "${BIGreen}>>>${RST} Using python \c"
command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.7 installed to continue.${RST}"; return 1; }
command -v python >/dev/null 2>&1 || { echo -e "${BIRed}- NOT FOUND${RST} ${BIYellow}You need Python 3.9 installed to continue.${RST}"; return 1; }
local version_command="import sys;print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))"
local python_version="$(python <<< ${version_command})"
oIFS="$IFS"
IFS=.
set -- $python_version
IFS="$oIFS"
if [ "$1" -ge "3" ] && [ "$2" -ge "6" ] ; then
if [ "$2" -gt "7" ] ; then
echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.7.x${RST}"; return 1;
if [ "$1" -ge "3" ] && [ "$2" -ge "9" ] ; then
if [ "$2" -gt "9" ] ; then
echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}"; return 1;
else
echo -e "${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}"
fi
@ -192,8 +192,6 @@ main () {
echo -e "${BIGreen}>>>${RST} Post-venv creation fixes ..."
local openpype_index=$("$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/parse_pyproject.py" tool.poetry.source.0.url)
echo -e "${BIGreen}- ${RST} Using index: ${BIWhite}$openpype_index${RST}"
"$POETRY_HOME/bin/poetry" run pip install setuptools==49.6.0
"$POETRY_HOME/bin/poetry" run pip install --disable-pip-version-check --force-reinstall wheel
"$POETRY_HOME/bin/poetry" run python -m pip install --disable-pip-version-check --force-reinstall pip
}

View file

@ -78,9 +78,9 @@ detect_python () {
IFS=.
set -- $python_version
IFS="$oIFS"
if [ "$1" -ge "3" ] && [ "$2" -ge "6" ] ; then
if [ "$2" -gt "7" ] ; then
echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.7.x${RST}"; return 1;
if [ "$1" -ge "3" ] && [ "$2" -ge "9" ] ; then
if [ "$2" -gt "9" ] ; then
echo -e "${BIWhite}[${RST} ${BIRed}$1.$2 ${BIWhite}]${RST} - ${BIRed}FAILED${RST} ${BIYellow}Version is new and unsupported, use${RST} ${BIPurple}3.9.x${RST}"; return 1;
else
echo -e "${BIWhite}[${RST} ${BIGreen}$1.$2${RST} ${BIWhite}]${RST}"
fi

View file

@ -17,10 +17,12 @@ Distribution consists of two parts
It is self contained (frozen) software that also includes all of the OpenPype codebase with the version
from the time of the build.
Igniter package is around 500MB and preparing an updated version requires you to re-build pype. That would be
Igniter package is around 1Gb and preparing an updated version requires you to re-build pype. That would be
inconvenient for regular and quick distribution of production updates and fixes. So you can distribute those
independently, without requiring you artists to re-install every time.
You can have multiple versions installed at the same time.
### 2. OpenPype Codebase
When you upgrade your studio pype deployment to a new version or make any local code changes, you can distribute
@ -77,4 +79,6 @@ For example OpenPype will consider the versions in this order: `3.8.0-nightly` <
See https://semver.org/ for more details.
For studios customizing the source code of OpenPype, a practical approach could be to build by adding a name and a number after the PATCH and not to deploy 3.8.0 from original OpenPype repository. For example, your builds will be: `3.8.0-yourstudio.1` < `3.8.0-yourstudio.2` < `3.8.1-yourstudio.1`.
For studios customizing the source code of OpenPype, a practical approach could be to build by adding a name and a number after the PATCH and not to deploy 3.8.0 from original OpenPype repository. For example, your builds will be: `3.8.0-yourstudio.1` < `3.8.0-yourstudio.2` < `3.8.1-yourstudio.1`.
Versions of Igniter and those coming in zips are compatible if they match major and minor version - `3.13.4` is compatible with `3.13.1` but not with `3.12.2` or `3.14.0`.

View file

@ -38,34 +38,67 @@ In AfterEffects you'll find the tools in the `OpenPype` extension:
You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`.
### Create
When you have created an composition you want to publish, you will need to tag existing composition. To do this open the `Creator` through the extensions `Create` button.
![Creator](assets/aftereffects_creator.png)
Because of current rendering limitations, it is expected that only single composition will be marked for publishing!
After Creator is successfully triggered on selected composition, it will be marked with an icon and its color
will be changed.
![Highlights](assets/aftereffects_creator_after.png)
### Publish
When you are ready to share some work, you will need to publish it. This is done by opening the `Publisher` through the `Publish...` button.
There is always instance for workfile created automatically (see 'workfileCompositing' item in `Subsets to publish` column.) This allows to publish (and therefore backup)
workfile which is used to produce another publishable elements (as `image` and `review` items).
Main publishable item in AfterEffects will be of `render` family. Result of this item (instance) is picture sequence that could be a final delivery product or loaded and used in another DCCs.
First select existing composition and then press `Create >>>` in middle column of `Publisher`.
After this process you should have something like this:
![Highlights](assets/aftereffects_publish_instance.png)
Name of publishable instance (eg. subset name) could be configured with a template in `project_settings/global/tools/creator/subset_name_profiles`.
(This must be configured by admin who has access to Openpype Settings.)
Trash icon under the list of instances allows to delete any selected `render` instance.
Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item.
If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable
instances, disable them from publishing, change their task etc.
Publisher allows publishing into different context, just click on any instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button.
#### RenderQueue
AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item and single output module in the Render Queue.
AE might throw some warning windows during publishing locally, so please pay attention to them in a case publishing seems to be stuck in a `Extract Local Render`.
When you are ready to share your work, you will need to publish it. This is done by opening the `Publish` by clicking the corresponding button in the OpenPype Panel.
#### Repair Validation Issues
![Publish](assets/aftereffects_publish.png)
If you would like to run validation rules set by your Studio, click on funnel icon at the bottom right. This will run through all
enabled instances, you could see more information after clicking on `Details` tab.
This tool will run through checks to make sure the contents you are publishing is correct. Hit the "Play" button to start publishing.
If there is some issue in validator phase, you will receive something like this:
![Validation error](assets/aftereffects_publish_failed.png)
You may encounter issues with publishing which will be indicated with red squares. If these issues are within the validation section, then you can fix the issue. If there are issues outside of validation section, please let the OpenPype team know. For More details have a look at the general [Publish](artist_tools.md#publisher) documentation.
All validators will give some description about what the issue is. You can inspect this by clicking on items in the left column.
If there is an option of automatic repair, there will be `Repair` button on the right. In other case you need to fix the issue manually.
(By deleting and recreating instance, changing workfile setting etc.)
#### Render instance options
There are currently 2 options of `render` item:
- Render of farm - allows offload rendering and publishing to Deadline - requires Deadline module being enabled
- Validate Scene Settings - enables validation plugin which controls setting in DB (or asset control system like Ftrak) and scene itself
![Configuration of render instance](assets/aftereffects_render_instance.png)
#### Buttons on the bottom right are for:
- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish
- `Stop/pause publishing` - if you would like to pause publishing process at any time
- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet)
- `Publish` - standard way how to kick off full publishing process
### Load
@ -102,12 +135,19 @@ You can switch to a previous version of the image or update to the latest.
![Loader](assets/photoshop_manage_switch.gif)
![Loader](assets/photoshop_manage_update.gif)
### Subset Manager
#### Support help
If you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left:
- `Go to details` - switches into a more detailed list of published instances and plugins.
- `Copy report` - stash full publishing log to a clipboard
- `Export report` - save log into a file for sending it via mail or any communication tool
![subset_manager](assets/tools_subset_manager.png)
If you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.)
All created compositions will be shown in a simple list. If user decides, that this composition shouldn't be
published after all, right click on that item in the list and select 'Remove instance'
#### Legacy instances
Removing composition directly in the AE would result to worfile contain phantom metadata which could result in
errors during publishing!
All screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool.
New publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and
could be used right away.
If you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch.
Nuclear option is to purge workfile metadata in `Window > Metadata > Basic > Label`. This is only for most determined daredevils though!

View file

@ -22,32 +22,75 @@ When you launch Photoshop you will be met with the Workfiles app. If dont have a
In Photoshop you can find the tools in the `OpenPype` extension:
![Extension](assets/photoshop_extension.PNG) <!-- picture needs to be changed -->
![Extension](assets/photoshop_extension.png) <!-- picture needs to be changed -->
You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`.
### Create
### Publish
When you have created an image you want to publish, you will need to create special groups or tag existing groups. To do this open the `Creator` through the extensions `Create` button.
When you are ready to share some work, you will need to publish. This is done by opening the `Publisher` through the `Publish...` button.
![Creator](assets/photoshop_creator.PNG)
![Publish](assets/photoshop_publish.png)
With the `Creator` you have a variety of options to create:
There is always instance for workfile created automatically (see 'workfileArt' item in `Subsets to publish` column.) This allows to publish (and therefore backup)
workfile which is used to produce another publishable elements (as `image` and `review` items).
- Check `Use selection` (A dialog will ask whether you want to create one image per selected layer).
- Yes.
- No selection.
- This will create a single group named after the `Subset` in the `Creator`.
- Single selected layer.
- The selected layer will be grouped under a single group named after the selected layer.
- Single selected group.
- The selected group will be tagged for publishing.
- Multiple selected items.
- Each selected group will be tagged for publishing and each layer will be grouped individually.
- No.
- All selected layers will be grouped under a single group named after the `Subset` in the `Creator`.
- Uncheck `Use selection`.
- This will create a single group named after the `Subset` in the `Creator`.
#### Create
Main publishable item in Photoshop will be of `image` family. Result of this item (instance) is picture that could be loaded and used in another DCCs (for example as
single layer in composition in AfterEffects, reference in Maya etc).
There are couple of options what to publish:
- separate image per layer (or group of layers)
- all visible layers (groups) flattened into single image
In most cases you would like to keep `Create only for selected` toggled on and select what you would like to publish. Toggling this off
will allow you to create instance(s) for all visible layers without a need to select them explicitly.
For separate layers option keep `Create separate instance for each selected` toggled, select multiple layers and hit `Create >>>` button in the middle column.
This will result in:
![Image instances creates](assets/photoshop_publish_images.png)
(In Photoshop's `Layers` tab standard layers will be wrapped into group and enriched with ℗ symbol to denote publishable instance. With `Create separate instance for each selected` toggled off
it will create only single publishable instance which will wrap all visible layers.)
Name of publishable instance (eg. subset name) could be configured with a template in `project_settings/global/tools/creator/subset_name_profiles`.
(This must be configured by admin who has access to Openpype Settings.)
Trash icon under the list of instances allows to delete any selected `image` instance.
Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item.
If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable
instances, disable them from publishing, change their task etc.
Publisher allows publishing into different context, just click on any instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button.
#### Validate
If you would like to run validation rules set by your Studio, click on funnel icon at the bottom right. This will run through all
enabled instances, you could see more information after clicking on `Details` tab.
![Image instances creates](assets/photoshop_publish_validations.png)
In this dialog you could see publishable instances in left colummn, triggered plugins in the middle and logs in the right column.
In left column you could see that `review` instance was created automatically. This instance flattens all publishable instances or
all visible layers if no publishable instances were created into single image which could serve as a single reviewable element (for example in Ftrack).
Creation of Review could be disabled in `project_settings/photoshop/publish/CollectReview`.
If you are satisfied with results of validation phase (and there are no errors there), you might hit `Publish` button at bottom right.
This will run through extraction phase (it physically creates images from `image` instances, creates `review` etc) and publishes them
(eg. stores files into their final destination and stores metadata about them into DB).
This part might take a while depending on amount of layers in the workfile, amount of available memory and performance of your machine.
You may encounter issues with publishing which will be indicated with red squares. If these issues are within the validation section, then you can fix the issue. If there are issues outside of validation section, please let the OpenPype team know.
You can always start new publish run with a circle arrow button at the bottom right. You might also want to move between phases (Create, Update etc)
by clicking on available tabs at the top of the dialog.
#### Simplified publish
@ -55,39 +98,28 @@ There is a simplified workflow for simple use case where only single image shoul
No image instances must be present in a workfile and `project_settings/photoshop/publish/CollectInstances/flatten_subset_template` must be filled in Settings.
Then artists just need to hit 'Publish' button in menu.
### Publish
When you are ready to share some work, you will need to publish. This is done by opening the `Pyblish` through the extensions `Publish` button.
![Publish](assets/photoshop_publish.PNG) <!-- picture needs to be changed -->
This tool will run through checks to make sure the contents you are publishing is correct. Hit the "Play" button to start publishing.
You may encounter issues with publishing which will be indicated with red squares. If these issues are within the validation section, then you can fix the issue. If there are issues outside of validation section, please let the OpenPype team know.
#### Repair Validation Issues
All validators will give some description about what the issue is. You can inspect this by going into the validator through the arrow:
If there is some issue in validator phase, you will receive something like this:
![Inspect](assets/photoshop_publish_inspect.PNG) <!-- picture needs to be changed -->
![Validation error](assets/photoshop_publish_failed.png)
You can expand the errors by clicking on them for more details:
All validators will give some description about what the issue is. You can inspect this by clicking on items in the left column.
![Expand](assets/photoshop_publish_expand.PNG) <!-- picture needs to be changed -->
If there is an option of automatic repair, there will be `Repair` button on the right. In other case you need to fix the issue manually.
(By deleting and recreating instance etc.)
Some validator have repair actions, which will fix the issue. If you can identify validators with actions by the circle icon with an "A":
![Actions](assets/photoshop_publish_actions.PNG) <!-- picture needs to be changed -->
To access the actions, you right click on the validator. If an action runs successfully, the actions icon will turn green. Once all issues are fixed, you can just hit the "Refresh" button and try to publish again.
![Repair](assets/photoshop_publish_repair.gif) <!-- picture needs to be changed -->
#### Buttons on the bottom right are for:
- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish
- `Stop/pause publishing` - if you would like to pause publishing process at any time
- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet)
- `Publish` - standard way how to kick off full publishing process
### Load
When you want to load existing published work, you can load in smart layers through the `Loader`. You can reach the `Loader` through the extension's `Load` button.
![Loader](assets/photoshop_loader.PNG) <!-- picture needs to be changed -->
![Loader](assets/photoshop_loader.png) <!-- picture needs to be changed -->
The supported families for Photoshop are:
@ -105,7 +137,7 @@ Now that we have some images loaded, we can manage which version is loaded. This
Loaded images has to stay as smart layers in order to be updated. If you rasterize the layer, you cannot update it to a different version.
:::
![Loader](assets/photoshop_manage.PNG)
![Loader](assets/photoshop_manage.png)
You can switch to a previous version of the image or update to the latest.
@ -113,65 +145,19 @@ You can switch to a previous version of the image or update to the latest.
![Loader](assets/photoshop_manage_update.gif)
### New Publisher
All previous screenshot came from regular [pyblish](https://pyblish.com/) process, there is also a different UI available. This process extends existing implementation and adds new functionalities.
To test this in Photoshop, the artist needs first to enable experimental `New publisher` in Settings. (Tray > Settings > Experimental tools)
![Settings](assets/experimental_tools_settings.png)
New dialog opens after clicking on `Experimental tools` button in Openpype extension menu.
![Menu](assets/experimental_tools_menu.png)
After you click on this button, this dialog will show up.
![Menu](assets/artist_photoshop_new_publisher_workfile.png)
You can see the first instance, called `workfileYourTaskName`. (Name depends on studio naming convention for Photoshop's workfiles.). This instance is so called "automatic",
it was created without instigation by the artist. You shouldn't delete this instance as it might hold necessary values for future publishing, but you can choose to skip it
from publishing (by toggling the pill button inside of the rectangular object denoting instance).
New publisher allows publishing into different context, just click on a workfile instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button.
Similarly to the old publishing approach, you need to create instances for everything you want to publish. You will initiate by clicking on the '+' sign in the bottom left corner.
![Instance creator](assets/artist_photoshop_new_publisher_instance.png)
In this dialog you can select the family for the published layer or group. Currently only 'image' is implemented.
On right hand side you can see creator attributes:
- `Create only for selected` - mimics `Use selected` option of regular publish
- `Create separate instance for each selected` - if separate instance should be created for each layer if multiple selected
![Instance created](assets/artist_photoshop_new_publisher_instance_created.png)
Here you can see a newly created instance of image family. (Name depends on studio naming convention for image family.) You can disable instance from publishing in the same fashion as a workfile instance.
You could also decide delete instance by selecting it and clicking on a trashcan icon (next to plus button on left button)
Buttons on the bottom right are for:
- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish
- `Stop/pause publishing` - if you would like to pause publishing process at any time
- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet)
- `Publish` - standard way how to kick off full publishing process
In the unfortunate case of some error during publishing, you would receive this kind of error dialog.
![Publish failed](assets/artist_photoshop_new_publisher_publish_failed.png)
In this case there is an issue that you are publishing two or more instances with the same subset name ('imageMaing'). If the error is recoverable by the artist, you should
see helpful information in a `How to repair?` section or fix it automatically by clicking on a 'Wrench' button on the right if present.
If you would like to ask for help admin or support, you could use any of the three buttons on bottom left:
#### Support help
If you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left:
- `Go to details` - switches into a more detailed list of published instances and plugins.
- `Copy report` - stash full publishing log to a clipboard
- `Export and save report` - save log into a file for sending it via mail or any communication tool
- `Show details` - switches into a more detailed list of published instances and plugins. Similar to the old pyblish list.
- `Export report` - save log into a file for sending it via mail or any communication tool
If you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.)
#### Legacy instances
All screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool.
New publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and
could be used right away.
If you would create instances in a new publisher, you cannot use them in the old approach though!
If you would hit on unexpected behaviour with old instances, contact support first, then you could try some steps to recover your publish. Delete instances in New publisher UI, or try `Subset manager` in the extension menu.
If you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch.
Nuclear option is to purge workfile metadata in `File > File Info > Origin > Headline`. This is only for most determined daredevils though!

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

View file

@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem';
To build Pype you currently need (on all platforms):
- **[Python 3.7](https://www.python.org/downloads/)** as we are following [vfx platform](https://vfxplatform.com).
- **[Python 3.9](https://www.python.org/downloads/)** as we are following [vfx platform CY2022](https://vfxplatform.com).
- **[git](https://git-scm.com/downloads)**
We use [CX_Freeze](https://cx-freeze.readthedocs.io/en/latest) to freeze the code and all dependencies and
@ -114,8 +114,8 @@ To build OpenPype on Linux you will need:
- **[curl](https://curl.se)** on systems that doesn't have one preinstalled.
- **bzip2**, **readline**, **sqlite3** and other libraries.
Because some Linux distros come with newer Python version pre-installed, you might
need to install **3.7** version and make use of it explicitly.
Because some Linux distros come with older Python version pre-installed, you might
need to install **3.9** version and make use of it explicitly.
Your best bet is probably using [pyenv](https://github.com/pyenv/pyenv).
You can use your package manager to install **git** and other packages to your build
@ -136,16 +136,16 @@ $ eval "$(pyenv virtualenv-init -)"
# reload shell
$ exec $SHELL
# install Python 3.7.10
# install Python 3.9.6
# python will be downloaded and build so please make sure
# you have all necessary requirements installed (see below).
$ pyenv install -v 3.7.10
$ pyenv install -v 3.9.6
# change path to pype 3
$ cd /path/to/pype-3
# set local python version
$ pyenv local 3.7.10
$ pyenv local 3.9.6
```
:::note Install build requirements for **Ubuntu**
@ -220,19 +220,19 @@ $ exec "$SHELL"
$ PATH=$(pyenv root)/shims:$PATH
```
4) Pull in required Python version 3.7.x
4) Pull in required Python version 3.9.x
```shell
# install Python build dependences
$ brew install openssl readline sqlite3 xz zlib
# replace with up-to-date 3.7.x version
$ pyenv install 3.7.9
# replace with up-to-date 3.9.x version
$ pyenv install 3.9.6
```
5) Set local Python version
```shell
# switch to Pype source directory
$ pyenv local 3.7.9
$ pyenv local 3.9.6
```
6) Install `create-dmg`
@ -256,7 +256,7 @@ to `pyproject.toml` to `[tool.poetry.dependencies]` section.
```toml title="/pyproject.toml"
[tool.poetry.dependencies]
python = "3.7.*"
python = "3.9.*"
aiohttp = "^3.7"
aiohttp_json_rpc = "*" # TVPaint server
acre = { git = "https://github.com/pypeclub/acre.git" }

View file

@ -14,7 +14,7 @@ The main things you will need to run and build pype are:
- **Terminal** in your OS
- PowerShell 5.0+ (Windows)
- Bash (Linux)
- [**Python 3.7.9**](#python) or higher
- [**Python 3.9.x**](#python)
- [**MongoDB**](#database)
@ -55,13 +55,14 @@ To run mongoDB on server, use your server distribution tools to set it up (on Li
## Python
**Python 3.7.8** is the recommended version to use (as per [VFX platform CY2021](https://vfxplatform.com/)).
**Python 3.9.x** is the recommended version to use (as per [VFX platform CY2022](https://vfxplatform.com/)).
**Note**: We do not support 3.9.0 because of [this bug](https://github.com/python/cpython/pull/22670). Please, use higher versions of 3.9.x.
If you're planning to run openPYPE on workstations from built executables (highly recommended), you will only need python for building and development, however, if you'd like to run from source centrally, every user will need python installed.
If you're planning to run openPYPE on workstations from built executables (highly recommended), you will only need python for building and development, however, if you'd like to run from source centrally, every user will need python installed.
## Hardware
openPYPE should be installed on all workstations that need to use it, the same as any other application.
openPYPE should be installed on all workstations that need to use it, the same as any other application.
There are no specific requirements for the hardware. If the workstation can run
the major DCCs, it most probably can run openPYPE.