Merge branch 'develop' into enhancement/AY-5648_Substance-work--publish-version-sync

This commit is contained in:
Kayla Man 2024-06-07 16:37:48 +08:00 committed by GitHub
commit b246ce9ea6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
469 changed files with 1135 additions and 780 deletions

View file

@ -49,11 +49,16 @@ IGNORED_MODULES_IN_AYON = set()
# When addon was moved from ayon-core codebase
# - this is used to log the missing addon
MOVED_ADDON_MILESTONE_VERSIONS = {
"aftereffects": VersionInfo(0, 2, 0),
"applications": VersionInfo(0, 2, 0),
"blender": VersionInfo(0, 2, 0),
"celaction": VersionInfo(0, 2, 0),
"clockify": VersionInfo(0, 2, 0),
"deadline": VersionInfo(0, 2, 0),
"flame": VersionInfo(0, 2, 0),
"fusion": VersionInfo(0, 2, 0),
"harmony": VersionInfo(0, 2, 0),
"hiero": VersionInfo(0, 2, 0),
"max": VersionInfo(0, 2, 0),
"photoshop": VersionInfo(0, 2, 0),
"traypublisher": VersionInfo(0, 2, 0),
@ -61,6 +66,7 @@ MOVED_ADDON_MILESTONE_VERSIONS = {
"maya": VersionInfo(0, 2, 0),
"nuke": VersionInfo(0, 2, 0),
"resolve": VersionInfo(0, 2, 0),
"royalrender": VersionInfo(0, 2, 0),
"substancepainter": VersionInfo(0, 2, 0),
"houdini": VersionInfo(0, 3, 0),
}

View file

@ -1,6 +0,0 @@
from .addon import BlenderAddon
__all__ = (
"BlenderAddon",
)

View file

@ -1,10 +0,0 @@
from .addon import (
HIERO_ROOT_DIR,
HieroAddon,
)
__all__ = (
"HIERO_ROOT_DIR",
"HieroAddon",
)

View file

@ -1 +0,0 @@
__version__ = "0.1.12"

View file

@ -1 +0,0 @@
__version__ = "0.1.1"

View file

@ -1,4 +1,5 @@
from .version import __version__
from .structures import HostMsgAction
from .webserver_module import (
WebServerAddon
)
@ -7,5 +8,6 @@ from .webserver_module import (
__all__ = (
"__version__",
"HostMsgAction",
"WebServerAddon",
)

View file

@ -9,22 +9,18 @@ from qtpy import QtWidgets
from ayon_core.addon import ITrayService
from ayon_core.tools.stdout_broker.window import ConsoleDialog
from .structures import HostMsgAction
log = logging.getLogger(__name__)
# Host listener icon type
class IconType:
IDLE = "idle"
RUNNING = "running"
FAILED = "failed"
class MsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"
class HostListener:
def __init__(self, webserver, module):
self._window_per_id = {}
@ -96,22 +92,22 @@ class HostListener:
if msg.type == aiohttp.WSMsgType.TEXT:
host_name, action, text = self._parse_message(msg)
if action == MsgAction.CONNECTING:
if action == HostMsgAction.CONNECTING:
self._action_per_id[host_name] = None
# must be sent to main thread, or action wont trigger
self.module.execute_in_main_thread(
lambda: self._host_is_connecting(host_name, text))
elif action == MsgAction.CLOSE:
elif action == HostMsgAction.CLOSE:
# clean close
self._close(host_name)
await ws.close()
elif action == MsgAction.INITIALIZED:
elif action == HostMsgAction.INITIALIZED:
self.module.execute_in_main_thread(
# must be queued as _host_is_connecting might not
# be triggered/finished yet
lambda: self._set_host_icon(host_name,
IconType.RUNNING))
elif action == MsgAction.ADD:
elif action == HostMsgAction.ADD:
self.module.execute_in_main_thread(
lambda: self._add_text(host_name, text))
elif msg.type == aiohttp.WSMsgType.ERROR:

View file

@ -0,0 +1,6 @@
# Host listener message actions
class HostMsgAction:
CONNECTING = "connecting"
INITIALIZED = "initialized"
ADD = "add"
CLOSE = "close"

View file

@ -11,7 +11,12 @@ from pyblish.lib import MessageHandler
from ayon_core import AYON_CORE_ROOT
from ayon_core.host import HostBase
from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event
from ayon_core.lib import (
is_in_tests,
initialize_ayon_connection,
emit_event,
version_up
)
from ayon_core.addon import load_addons, AddonsManager
from ayon_core.settings import get_project_settings
@ -21,6 +26,8 @@ from .template_data import get_template_data_with_names
from .workfile import (
get_workdir,
get_custom_workfile_template_by_string_context,
get_workfile_template_key_from_context,
get_last_workfile
)
from . import (
register_loader_plugin_path,
@ -579,3 +586,48 @@ def get_process_id():
if _process_id is None:
_process_id = str(uuid.uuid4())
return _process_id
def version_up_current_workfile():
"""Function to increment and save workfile
"""
host = registered_host()
if not host.has_unsaved_changes():
print("No unsaved changes, skipping file save..")
return
project_name = get_current_project_name()
folder_path = get_current_folder_path()
task_name = get_current_task_name()
host_name = get_current_host_name()
template_key = get_workfile_template_key_from_context(
project_name,
folder_path,
task_name,
host_name,
)
anatomy = Anatomy(project_name)
data = get_template_data_with_names(
project_name, folder_path, task_name, host_name
)
data["root"] = anatomy.roots
work_template = anatomy.get_template_item("work", template_key)
# Define saving file extension
extensions = host.get_workfile_extensions()
current_file = host.get_current_workfile()
if current_file:
extensions = [os.path.splitext(current_file)[-1]]
work_root = work_template["directory"].format_strict(data)
file_template = work_template["file"].template
last_workfile_path = get_last_workfile(
work_root, file_template, data, extensions, True
)
new_workfile_path = version_up(last_workfile_path)
if os.path.exists(new_workfile_path):
new_workfile_path = version_up(new_workfile_path)
host.save_workfile(new_workfile_path)

View file

@ -1,12 +0,0 @@
Adobe webserver
---------------
Aiohttp (Asyncio) based websocket server used for communication with host
applications, currently only for Adobe (but could be used for any non python
DCC which has websocket client).
This webserver is started in spawned Python process that opens DCC during
its launch, waits for connection from DCC and handles communication going
forward. Server is closed before Python process is killed.
(Different from `ayon_core/modules/webserver` as that one is running in Tray,
this one is running in spawn Python process.)

View file

@ -172,12 +172,30 @@ class VersionItem:
def __gt__(self, other):
if not isinstance(other, VersionItem):
return False
if (
other.version == self.version
and self.is_hero
):
# Make sure hero versions are positive
version = abs(self.version)
other_version = abs(other.version)
# Hero version is greater than non-hero
if version == other_version:
return self.is_hero
return version > other_version
def __lt__(self, other):
if not isinstance(other, VersionItem):
return True
return other.version < self.version
# Make sure hero versions are positive
version = abs(self.version)
other_version = abs(other.version)
# Non-hero version is lesser than hero
if version == other_version:
return not self.is_hero
return version < other_version
def __ge__(self, other):
return self.__eq__(other) or self.__gt__(other)
def __le__(self, other):
return self.__eq__(other) or self.__lt__(other)
def to_data(self):
return {

View file

@ -348,10 +348,18 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
return set()
if not self._loaded_products_cache.is_valid:
if isinstance(self._host, ILoadHost):
containers = self._host.get_containers()
else:
containers = self._host.ls()
try:
if isinstance(self._host, ILoadHost):
containers = self._host.get_containers()
else:
containers = self._host.ls()
except BaseException:
self.log.error(
"Failed to collect loaded products.", exc_info=True
)
containers = []
repre_ids = set()
for container in containers:
repre_id = container.get("representation")

View file

@ -321,6 +321,8 @@ class LoaderFoldersWidget(QtWidgets.QWidget):
"""
self._folders_proxy_model.setFilterFixedString(name)
if name:
self._folders_view.expandAll()
def set_merged_products_selection(self, items):
"""

View file

@ -199,7 +199,9 @@ class ProductsModel(QtGui.QStandardItemModel):
product_item = self._product_items_by_id.get(product_id)
if product_item is None:
return None
return list(product_item.version_items.values())
product_items = list(product_item.version_items.values())
product_items.sort(reverse=True)
return product_items
if role == QtCore.Qt.EditRole:
return None

View file

@ -0,0 +1,5 @@
from .broker import StdOutBroker
__all__ = (
"StdOutBroker",
)

View file

@ -1,173 +1,12 @@
import os
import sys
import threading
import collections
import websocket
import json
from datetime import datetime
import warnings
from .broker import StdOutBroker
from ayon_core.lib import Logger
from openpype_modules.webserver.host_console_listener import MsgAction
warnings.warn(
(
"Import of 'StdOutBroker' from 'ayon_core.tools.stdout_broker.app'"
" is deprecated. Please use 'ayon_core.tools.stdout_broker' instead."
),
DeprecationWarning
)
log = Logger.get_logger(__name__)
class StdOutBroker:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
MAX_LINES = 10000
TIMER_TIMEOUT = 0.200
def __init__(self, host_name):
self.host_name = host_name
self.webserver_client = None
self.original_stdout_write = None
self.original_stderr_write = None
self.log_queue = collections.deque()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host_name, date_str)
self._std_available = False
self._is_running = False
self._catch_std_outputs()
self._timer = None
@property
def send_to_tray(self):
"""Checks if connected to tray and have access to logs."""
return self.webserver_client and self._std_available
def start(self):
"""Start app, create and start timer"""
if not self._std_available or self._is_running:
return
self._is_running = True
self._create_timer()
self._connect_to_tray()
def stop(self):
"""Disconnect from Tray, process last logs"""
if not self._is_running:
return
self._is_running = False
self._process_queue()
self._disconnect_from_tray()
def host_connected(self):
"""Send to Tray console that host is ready - icon change. """
log.info("Host {} connected".format(self.host_id))
payload = {
"host": self.host_id,
"action": MsgAction.INITIALIZED,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _create_timer(self):
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
timer.start()
self._timer = timer
def _timer_callback(self):
if not self._is_running:
return
self._process_queue()
self._create_timer()
def _connect_to_tray(self):
"""Connect to Tray webserver to pass console output. """
if not self._std_available: # not content to log
return
ws = websocket.WebSocket()
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
self.webserver_client = ws
payload = {
"host": self.host_id,
"action": MsgAction.CONNECTING,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _disconnect_from_tray(self):
"""Send to Tray that host is closing - remove from Services. """
print("Host {} closing".format(self.host_name))
if not self.webserver_client:
return
payload = {
"host": self.host_id,
"action": MsgAction.CLOSE,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
self.webserver_client.close()
def _catch_std_outputs(self):
"""Redirects standard out and error to own functions"""
if sys.stdout:
self.original_stdout_write = sys.stdout.write
sys.stdout.write = self._my_stdout_write
self._std_available = True
if sys.stderr:
self.original_stderr_write = sys.stderr.write
sys.stderr.write = self._my_stderr_write
self._std_available = True
def _my_stdout_write(self, text):
"""Appends outputted text to queue, keep writing to original stdout"""
if self.original_stdout_write is not None:
self.original_stdout_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _my_stderr_write(self, text):
"""Appends outputted text to queue, keep writing to original stderr"""
if self.original_stderr_write is not None:
self.original_stderr_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _process_queue(self):
"""Sends lines and purges queue"""
if not self.send_to_tray:
return
lines = tuple(self.log_queue)
self.log_queue.clear()
if lines:
payload = {
"host": self.host_id,
"action": MsgAction.ADD,
"text": "\n".join(lines)
}
self._send(payload)
def _send(self, payload):
"""Worker method to send to existing websocket connection."""
if not self.send_to_tray:
return
try:
self.webserver_client.send(json.dumps(payload))
except ConnectionResetError: # Tray closed
self._connect_to_tray()
__all__ = ("StdOutBroker", )

View file

@ -0,0 +1,174 @@
import os
import sys
import threading
import collections
import json
from datetime import datetime
import websocket
from ayon_core.lib import Logger
from ayon_core.modules.webserver import HostMsgAction
log = Logger.get_logger(__name__)
class StdOutBroker:
"""
Application showing console in Services tray for non python hosts
instead of cmd window.
"""
MAX_LINES = 10000
TIMER_TIMEOUT = 0.200
def __init__(self, host_name):
self.host_name = host_name
self.webserver_client = None
self.original_stdout_write = None
self.original_stderr_write = None
self.log_queue = collections.deque()
date_str = datetime.now().strftime("%d%m%Y%H%M%S")
self.host_id = "{}_{}".format(self.host_name, date_str)
self._std_available = False
self._is_running = False
self._catch_std_outputs()
self._timer = None
@property
def send_to_tray(self):
"""Checks if connected to tray and have access to logs."""
return self.webserver_client and self._std_available
def start(self):
"""Start app, create and start timer"""
if not self._std_available or self._is_running:
return
self._is_running = True
self._create_timer()
self._connect_to_tray()
def stop(self):
"""Disconnect from Tray, process last logs"""
if not self._is_running:
return
self._is_running = False
self._process_queue()
self._disconnect_from_tray()
def host_connected(self):
"""Send to Tray console that host is ready - icon change. """
log.info("Host {} connected".format(self.host_id))
payload = {
"host": self.host_id,
"action": HostMsgAction.INITIALIZED,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _create_timer(self):
timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback)
timer.start()
self._timer = timer
def _timer_callback(self):
if not self._is_running:
return
self._process_queue()
self._create_timer()
def _connect_to_tray(self):
"""Connect to Tray webserver to pass console output. """
if not self._std_available: # not content to log
return
ws = websocket.WebSocket()
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
print("Unknown webserver url, cannot connect to pass log")
return
webserver_url = webserver_url.replace("http", "ws")
ws.connect("{}/ws/host_listener".format(webserver_url))
self.webserver_client = ws
payload = {
"host": self.host_id,
"action": HostMsgAction.CONNECTING,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
def _disconnect_from_tray(self):
"""Send to Tray that host is closing - remove from Services. """
print("Host {} closing".format(self.host_name))
if not self.webserver_client:
return
payload = {
"host": self.host_id,
"action": HostMsgAction.CLOSE,
"text": "Integration with {}".format(
str.capitalize(self.host_name))
}
self._send(payload)
self.webserver_client.close()
def _catch_std_outputs(self):
"""Redirects standard out and error to own functions"""
if sys.stdout:
self.original_stdout_write = sys.stdout.write
sys.stdout.write = self._my_stdout_write
self._std_available = True
if sys.stderr:
self.original_stderr_write = sys.stderr.write
sys.stderr.write = self._my_stderr_write
self._std_available = True
def _my_stdout_write(self, text):
"""Appends outputted text to queue, keep writing to original stdout"""
if self.original_stdout_write is not None:
self.original_stdout_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _my_stderr_write(self, text):
"""Appends outputted text to queue, keep writing to original stderr"""
if self.original_stderr_write is not None:
self.original_stderr_write(text)
if self.send_to_tray:
self.log_queue.append(text)
def _process_queue(self):
"""Sends lines and purges queue"""
if not self.send_to_tray:
return
lines = tuple(self.log_queue)
self.log_queue.clear()
if lines:
payload = {
"host": self.host_id,
"action": HostMsgAction.ADD,
"text": "\n".join(lines)
}
self._send(payload)
def _send(self, payload):
"""Worker method to send to existing websocket connection."""
if not self.send_to_tray:
return
try:
self.webserver_client.send(json.dumps(payload))
except ConnectionResetError: # Tray closed
self._connect_to_tray()

View file

@ -370,6 +370,8 @@ class FoldersWidget(QtWidgets.QWidget):
"""
self._folders_proxy_model.setFilterFixedString(name)
if name:
self._folders_view.expandAll()
def refresh(self):
"""Refresh folders model.

View file

@ -79,12 +79,12 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
exclude = [
"client/ayon_core/hosts/unreal/integration/*",
"client/ayon_core/hosts/aftereffects/api/extension/js/libs/*",
"client/ayon_core/hosts/hiero/api/startup/*",
"client/ayon_core/modules/deadline/repository/custom/plugins/CelAction/*",
"client/ayon_core/modules/deadline/repository/custom/plugins/HarmonyAYON/*",
"client/ayon_core/modules/click_wrap.py",
"client/ayon_core/scripts/slates/__init__.py"
"client/ayon_core/scripts/slates/__init__.py",
"server_addon/deadline/client/ayon_deadline/repository/custom/plugins/CelAction/*",
"server_addon/deadline/client/ayon_deadline/repository/custom/plugins/HarmonyAYON/*",
"server_addon/hiero/client/ayon_hiero/api/startup/*",
"server_addon/aftereffects/client/ayon_aftereffects/api/extension/js/libs/*"
]
[tool.ruff.lint.per-file-ignores]

View file

@ -798,7 +798,7 @@ class PublishPuginsModel(BaseSettingsModel):
)
ValidateOutdatedContainers: PluginStateByHostModel = SettingsField(
default_factory=PluginStateByHostModel,
title="Validate Containers"
title="Validate Outdated Containers"
)
ValidateIntent: ValidateIntentModel = SettingsField(
default_factory=ValidateIntentModel,

View file

@ -118,6 +118,15 @@ class WorkfilesLockProfile(BaseSettingsModel):
enabled: bool = SettingsField(True, title="Enabled")
class AYONMenuModel(BaseSettingsModel):
_layout = "expanded"
version_up_current_workfile: bool = SettingsField(
False,
title="Version Up Workfile",
description="Add 'Version Up Workfile' to AYON menu"
)
class WorkfilesToolModel(BaseSettingsModel):
workfile_template_profiles: list[WorkfileTemplateProfile] = SettingsField(
default_factory=list,
@ -268,6 +277,10 @@ class PublishToolModel(BaseSettingsModel):
class GlobalToolsModel(BaseSettingsModel):
ayon_menu: AYONMenuModel = SettingsField(
default_factory=AYONMenuModel,
title="AYON Menu"
)
creator: CreatorToolModel = SettingsField(
default_factory=CreatorToolModel,
title="Creator"
@ -287,6 +300,9 @@ class GlobalToolsModel(BaseSettingsModel):
DEFAULT_TOOLS_VALUES = {
"ayon_menu": {
"version_up_current_workfile": False
},
"creator": {
"product_types_smart_select": [
{

View file

@ -1,3 +1,4 @@
from .version import __version__
from .addon import (
AFTEREFFECTS_ADDON_ROOT,
AfterEffectsAddon,
@ -6,6 +7,8 @@ from .addon import (
__all__ = (
"__version__",
"AFTEREFFECTS_ADDON_ROOT",
"AfterEffectsAddon",
"get_launch_script_path",

View file

@ -2,11 +2,14 @@ import os
from ayon_core.addon import AYONAddon, IHostAddon
from .version import __version__
AFTEREFFECTS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class AfterEffectsAddon(AYONAddon, IHostAddon):
name = "aftereffects"
version = __version__
host_name = "aftereffects"
def add_implementation_envs(self, env, _app):

View file

@ -25,7 +25,7 @@ download [Anastasiys Extension Manager](https://install.anastasiy.com/)
The easiest way to get the server and After Effects launch is with:
```
python -c ^"import ayon_core.hosts.photoshop;ayon_core.hosts.aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^"
python -c ^"import ayon_core.hosts.photoshop;ayon_aftereffects.launch(""c:\Program Files\Adobe\Adobe After Effects 2020\Support Files\AfterFX.exe"")^"
```
`avalon.aftereffects.launch` launches the application and server, and also closes the server when After Effects exists.

View file

@ -18,8 +18,8 @@ from ayon_core.lib import Logger, is_in_tests
from ayon_core.pipeline import install_host
from ayon_core.addon import AddonsManager
from ayon_core.tools.utils import host_tools, get_ayon_qt_app
from ayon_core.tools.adobe_webserver.app import WebServerTool
from .webserver import WebServerTool
from .ws_stub import get_stub
from .lib import set_settings
@ -35,7 +35,7 @@ def main(*subprocess_args):
"""Main entrypoint to AE launching, called from pre hook."""
sys.excepthook = safe_excepthook
from ayon_core.hosts.aftereffects.api import AfterEffectsHost
from ayon_aftereffects.api import AfterEffectsHost
host = AfterEffectsHost()
install_host(host)
@ -355,7 +355,7 @@ class AfterEffectsRoute(WebSocketRoute):
return "nothing"
def create_placeholder_route(self):
from ayon_core.hosts.aftereffects.api.workfile_template_builder import \
from ayon_aftereffects.api.workfile_template_builder import \
create_placeholder
partial_method = functools.partial(create_placeholder)
@ -365,7 +365,7 @@ class AfterEffectsRoute(WebSocketRoute):
return "nothing"
def update_placeholder_route(self):
from ayon_core.hosts.aftereffects.api.workfile_template_builder import \
from ayon_aftereffects.api.workfile_template_builder import \
update_placeholder
partial_method = functools.partial(update_placeholder)
@ -375,7 +375,7 @@ class AfterEffectsRoute(WebSocketRoute):
return "nothing"
def build_workfile_template_route(self):
from ayon_core.hosts.aftereffects.api.workfile_template_builder import \
from ayon_aftereffects.api.workfile_template_builder import \
build_workfile_template
partial_method = functools.partial(build_workfile_template)

View file

@ -8,7 +8,7 @@ workfile or others.
import os
import sys
from ayon_core.hosts.aftereffects.api.launch_logic import main as host_main
from ayon_aftereffects.api.launch_logic import main as host_main
# Get current file to locate start point of sys.argv
CURRENT_FILE = os.path.abspath(__file__)

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -14,8 +14,6 @@ from ayon_core.pipeline import (
AYON_INSTANCE_ID,
)
from ayon_core.pipeline.load import any_outdated_containers
import ayon_core.hosts.aftereffects
from ayon_core.host import (
HostBase,
IWorkfileHost,
@ -23,6 +21,7 @@ from ayon_core.host import (
IPublishHost
)
from ayon_core.tools.utils import get_ayon_qt_app
from ayon_aftereffects import AFTEREFFECTS_ADDON_ROOT
from .launch_logic import get_stub
from .ws_stub import ConnectionNotEstablishedYet
@ -30,10 +29,7 @@ from .ws_stub import ConnectionNotEstablishedYet
log = Logger.get_logger(__name__)
HOST_DIR = os.path.dirname(
os.path.abspath(ayon_core.hosts.aftereffects.__file__)
)
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PLUGINS_DIR = os.path.join(AFTEREFFECTS_ADDON_ROOT, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")

View file

@ -1,7 +1,11 @@
"""This Webserver tool is python 3 specific.
"""Webserver for communication with AfterEffects.
Don't import directly to avalon.tools or implementation of Python 2 hosts
would break.
Aiohttp (Asyncio) based websocket server used for communication with host
application.
This webserver is started in spawned Python process that opens DCC during
its launch, waits for connection from DCC and handles communication going
forward. Server is closed before Python process is killed.
"""
import os
import logging
@ -12,9 +16,7 @@ import socket
from aiohttp import web
from wsrpc_aiohttp import (
WSRPCClient
)
from wsrpc_aiohttp import WSRPCClient
from ayon_core.pipeline import get_global_context

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.workfile.workfile_template_builder import (
PlaceholderPlugin,
PlaceholderItem
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
PLACEHOLDER_ID = "openpype.placeholder"

View file

@ -8,7 +8,8 @@ import logging
import attr
from wsrpc_aiohttp import WebSocketAsync
from ayon_core.tools.adobe_webserver.app import WebServerTool
from .webserver import WebServerTool
class ConnectionNotEstablishedYet(Exception):

View file

@ -7,7 +7,7 @@ from ayon_core.lib import (
is_using_ayon_console,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.aftereffects import get_launch_script_path
from ayon_aftereffects import get_launch_script_path
def get_launch_kwargs(kwargs):

View file

@ -2,16 +2,16 @@ import re
from ayon_core import resources
from ayon_core.lib import BoolDef, UISeparatorDef
from ayon_core.hosts.aftereffects import api
from ayon_core.pipeline import (
Creator,
CreatedInstance,
CreatorError
)
from ayon_core.hosts.aftereffects.api.pipeline import cache_and_get_instances
from ayon_core.hosts.aftereffects.api.lib import set_settings
from ayon_core.lib import prepare_template_data
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
from ayon_aftereffects import api
from ayon_aftereffects.api.pipeline import cache_and_get_instances
from ayon_aftereffects.api.lib import set_settings
class RenderCreator(Creator):

View file

@ -1,11 +1,11 @@
import ayon_api
import ayon_core.hosts.aftereffects.api as api
from ayon_core.pipeline import (
AutoCreator,
CreatedInstance
)
from ayon_core.hosts.aftereffects.api.pipeline import cache_and_get_instances
from ayon_aftereffects import api
from ayon_aftereffects.api.pipeline import cache_and_get_instances
class AEWorkfileCreator(AutoCreator):

View file

@ -1,9 +1,9 @@
import re
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.aftereffects import api
from ayon_core.hosts.aftereffects.api.lib import (
from ayon_aftereffects import api
from ayon_aftereffects.api.lib import (
get_background_layers,
get_unique_layer_name,
)

View file

@ -1,8 +1,8 @@
import re
from ayon_core.pipeline import get_representation_path
from ayon_core.hosts.aftereffects import api
from ayon_core.hosts.aftereffects.api.lib import get_unique_layer_name
from ayon_aftereffects import api
from ayon_aftereffects.api.lib import get_unique_layer_name
class FileLoader(api.AfterEffectsLoader):

View file

@ -1,6 +1,6 @@
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class AddPublishHighlight(pyblish.api.InstancePlugin):

View file

@ -2,7 +2,7 @@
"""Close AE after publish. For Webpublishing only."""
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class CloseAE(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class CollectAudio(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import pyblish.api
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class CollectCurrentFile(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import os
import re
import pyblish.api
from ayon_core.hosts.aftereffects.api import (
from ayon_aftereffects.api import (
get_stub,
get_extension_manifest_path
)

View file

@ -6,8 +6,8 @@ import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.pipeline.publish import RenderInstance
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
@attr.s
class AERenderInstance(RenderInstance):

View file

@ -14,6 +14,7 @@ class CollectReview(pyblish.api.ContextPlugin):
label = "Collect Review"
hosts = ["aftereffects"]
order = pyblish.api.CollectorOrder + 0.1
settings_category = "aftereffects"
def process(self, context):
for instance in context:

View file

@ -1,8 +1,8 @@
import os
from ayon_core.pipeline import publish
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ExtractLocalRender(publish.Extractor):
"""Render RenderQueue locally."""

View file

@ -1,7 +1,7 @@
import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ExtractSaveScene(pyblish.api.ContextPlugin):

View file

@ -2,7 +2,7 @@ import pyblish.api
from ayon_core.lib import version_up
from ayon_core.pipeline.publish import get_errored_plugins_from_context
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class IncrementWorkfile(pyblish.api.InstancePlugin):

View file

@ -1,5 +1,5 @@
from ayon_core.pipeline import publish
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class RemovePublishHighlight(publish.Extractor):

View file

@ -9,7 +9,7 @@ import pyblish.api
from ayon_core.pipeline import (
PublishXmlValidationError
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ValidateFootageItems(pyblish.api.InstancePlugin):

View file

@ -5,7 +5,7 @@ from ayon_core.pipeline.publish import (
ValidateContentsOrder,
PublishXmlValidationError,
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_aftereffects.api import get_stub
class ValidateInstanceFolderRepair(pyblish.api.Action):

View file

@ -13,7 +13,7 @@ from ayon_core.pipeline import (
PublishXmlValidationError,
OptionalPyblishPluginMixin
)
from ayon_core.hosts.aftereffects.api import get_folder_settings
from ayon_aftereffects.api import get_folder_settings
class ValidateSceneSettings(OptionalPyblishPluginMixin,
@ -60,6 +60,7 @@ class ValidateSceneSettings(OptionalPyblishPluginMixin,
label = "Validate Scene Settings"
families = ["render.farm", "render.local", "render"]
hosts = ["aftereffects"]
settings_category = "aftereffects"
optional = True
skip_timelines_check = [".*"] # * >> skip for all

View file

@ -2,9 +2,11 @@ from ayon_core.pipeline.workfile.workfile_template_builder import (
CreatePlaceholderItem,
PlaceholderCreateMixin
)
from ayon_core.hosts.aftereffects.api import get_stub
from ayon_core.hosts.aftereffects.api.lib import set_settings
import ayon_core.hosts.aftereffects.api.workfile_template_builder as wtb
from ayon_aftereffects.api import (
get_stub,
workfile_template_builder as wtb,
)
from ayon_aftereffects.api.lib import set_settings
class AEPlaceholderCreatePlugin(wtb.AEPlaceholderPlugin,

View file

@ -2,8 +2,10 @@ from ayon_core.pipeline.workfile.workfile_template_builder import (
LoadPlaceholderItem,
PlaceholderLoadMixin
)
from ayon_core.hosts.aftereffects.api import get_stub
import ayon_core.hosts.aftereffects.api.workfile_template_builder as wtb
from ayon_aftereffects.api import (
get_stub,
workfile_template_builder as wtb,
)
class AEPlaceholderLoadPlugin(wtb.AEPlaceholderPlugin, PlaceholderLoadMixin):

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'aftereffects' version."""
__version__ = "0.2.0"

View file

@ -0,0 +1,6 @@
[project]
name="aftereffects"
description="AYON AfterEffects addon."
[ayon.runtimeDependencies]
wsrpc_aiohttp = "^3.1.1" # websocket server

View file

@ -1,3 +1,10 @@
name = "aftereffects"
title = "AfterEffects"
version = "0.1.4"
version = "0.2.0"
client_dir = "ayon_aftereffects"
ayon_required_addons = {
"core": ">0.3.2",
}
ayon_compatible_addons = {}

View file

@ -0,0 +1,13 @@
from .version import __version__
from .addon import (
BlenderAddon,
BLENDER_ADDON_ROOT,
)
__all__ = (
"__version__",
"BlenderAddon",
"BLENDER_ADDON_ROOT",
)

View file

@ -1,18 +1,21 @@
import os
from ayon_core.addon import AYONAddon, IHostAddon
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
from .version import __version__
BLENDER_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
class BlenderAddon(AYONAddon, IHostAddon):
name = "blender"
version = __version__
host_name = "blender"
def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
# Prepare path to implementation script
implementation_user_script_path = os.path.join(
BLENDER_ROOT_DIR,
BLENDER_ADDON_ROOT,
"blender_addon"
)
@ -61,7 +64,7 @@ class BlenderAddon(AYONAddon, IHostAddon):
if app.host_name != self.host_name:
return []
return [
os.path.join(BLENDER_ROOT_DIR, "hooks")
os.path.join(BLENDER_ADDON_ROOT, "hooks")
]
def get_workfile_extensions(self):

View file

@ -15,7 +15,6 @@ from .pipeline import (
from .plugin import (
Creator,
Loader,
)
from .workio import (
@ -51,7 +50,6 @@ __all__ = [
"BlenderHost",
"Creator",
"Loader",
# Workfiles API
"open_file",

View file

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

Before After
Before After

View file

@ -305,7 +305,7 @@ class LaunchCreator(LaunchQtApp):
class LaunchLoader(LaunchQtApp):
"""Launch Avalon Loader."""
"""Launch AYON Loader."""
bl_idname = "wm.avalon_loader"
bl_label = "Load..."

View file

@ -5,9 +5,6 @@ from typing import Callable, Dict, Iterator, List, Optional
import bpy
from . import lib
from . import ops
import pyblish.api
import ayon_api
@ -33,8 +30,12 @@ from ayon_core.lib import (
register_event_callback,
emit_event
)
import ayon_core.hosts.blender
from ayon_core.settings import get_project_settings
from ayon_blender import BLENDER_ADDON_ROOT
from . import lib
from . import ops
from .workio import (
open_file,
save_file,
@ -44,9 +45,7 @@ from .workio import (
work_root,
)
HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.blender.__file__))
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PLUGINS_DIR = os.path.join(BLENDER_ADDON_ROOT, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")

View file

@ -4,6 +4,7 @@ import itertools
from pathlib import Path
from typing import Dict, List, Optional
import pyblish.api
import bpy
from ayon_core.pipeline import (
@ -13,6 +14,7 @@ from ayon_core.pipeline import (
AVALON_INSTANCE_ID,
AYON_INSTANCE_ID,
)
from ayon_core.pipeline.publish import Extractor
from ayon_core.lib import BoolDef
from .pipeline import (
@ -161,10 +163,23 @@ def deselect_all():
bpy.context.view_layer.objects.active = active
class BaseCreator(Creator):
class BlenderInstancePlugin(pyblish.api.InstancePlugin):
settings_category = "blender"
class BlenderContextPlugin(pyblish.api.ContextPlugin):
settings_category = "blender"
class BlenderExtractor(Extractor):
settings_category = "blender"
class BlenderCreator(Creator):
"""Base class for Blender Creator plug-ins."""
defaults = ['Main']
settings_category = "blender"
create_as_asset_group = False
@staticmethod
@ -265,7 +280,7 @@ class BaseCreator(Creator):
return instance_node
def collect_instances(self):
"""Override abstract method from BaseCreator.
"""Override abstract method from BlenderCreator.
Collect existing instances related to this creator plugin."""
# Cache instances in shared data
@ -292,7 +307,7 @@ class BaseCreator(Creator):
self._add_instance_to_context(instance)
def update_instances(self, update_list):
"""Override abstract method from BaseCreator.
"""Override abstract method from BlenderCreator.
Store changes of existing instances so they can be recollected.
Args:
@ -376,13 +391,7 @@ class BaseCreator(Creator):
]
class Loader(LoaderPlugin):
"""Base class for Loader plug-ins."""
hosts = ["blender"]
class AssetLoader(LoaderPlugin):
class BlenderLoader(LoaderPlugin):
"""A basic AssetLoader for Blender
This will implement the basic logic for linking/appending assets
@ -392,6 +401,7 @@ class AssetLoader(LoaderPlugin):
it's different for different types (e.g. model, rig, animation,
etc.).
"""
settings_category = "blender"
@staticmethod
def _get_instance_empty(instance_name: str, nodes: List) -> Optional[bpy.types.Object]:
@ -496,7 +506,7 @@ class AssetLoader(LoaderPlugin):
# Only containerise if it's not already a collection from a .blend file.
# representation = context["representation"]["name"]
# if representation != "blend":
# from ayon_core.hosts.blender.api.pipeline import containerise
# from ayon_blender.api.pipeline import containerise
# return containerise(
# name=name,
# namespace=namespace,

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