Merge branch 'develop' into bugfix/OP-8005_thumbnail_duration

This commit is contained in:
Jakub Ježek 2024-03-06 10:40:11 +01:00 committed by GitHub
commit 0c7028c19e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
769 changed files with 10638 additions and 20268 deletions

View file

@ -1,6 +1,6 @@
name: Bug Report
description: File a bug report
title: 'Bug: '
title: ''
labels:
- 'type: bug'
body:

View file

@ -1,6 +1,6 @@
name: Enhancement Request
description: Create a report to help us enhance a particular feature
title: "Enhancement: "
title: ""
labels:
- "type: enhancement"
body:

102
.github/pr-glob-labeler.yml vendored Normal file
View file

@ -0,0 +1,102 @@
# Add type: unittest label if any changes in tests folders
'type: unittest':
- '*/*tests*/**/*'
# any changes in documentation structure
'type: documentation':
- '*/**/*website*/**/*'
- '*/**/*docs*/**/*'
# hosts triage
'host: Nuke':
- '*/**/*nuke*'
- '*/**/*nuke*/**/*'
'host: Photoshop':
- '*/**/*photoshop*'
- '*/**/*photoshop*/**/*'
'host: Harmony':
- '*/**/*harmony*'
- '*/**/*harmony*/**/*'
'host: UE':
- '*/**/*unreal*'
- '*/**/*unreal*/**/*'
'host: Houdini':
- '*/**/*houdini*'
- '*/**/*houdini*/**/*'
'host: Maya':
- '*/**/*maya*'
- '*/**/*maya*/**/*'
'host: Resolve':
- '*/**/*resolve*'
- '*/**/*resolve*/**/*'
'host: Blender':
- '*/**/*blender*'
- '*/**/*blender*/**/*'
'host: Hiero':
- '*/**/*hiero*'
- '*/**/*hiero*/**/*'
'host: Fusion':
- '*/**/*fusion*'
- '*/**/*fusion*/**/*'
'host: Flame':
- '*/**/*flame*'
- '*/**/*flame*/**/*'
'host: TrayPublisher':
- '*/**/*traypublisher*'
- '*/**/*traypublisher*/**/*'
'host: 3dsmax':
- '*/**/*max*'
- '*/**/*max*/**/*'
'host: TV Paint':
- '*/**/*tvpaint*'
- '*/**/*tvpaint*/**/*'
'host: CelAction':
- '*/**/*celaction*'
- '*/**/*celaction*/**/*'
'host: After Effects':
- '*/**/*aftereffects*'
- '*/**/*aftereffects*/**/*'
'host: Substance Painter':
- '*/**/*substancepainter*'
- '*/**/*substancepainter*/**/*'
# modules triage
'module: Deadline':
- '*/**/*deadline*'
- '*/**/*deadline*/**/*'
'module: RoyalRender':
- '*/**/*royalrender*'
- '*/**/*royalrender*/**/*'
'module: Sitesync':
- '*/**/*sync_server*'
- '*/**/*sync_server*/**/*'
'module: Ftrack':
- '*/**/*ftrack*'
- '*/**/*ftrack*/**/*'
'module: Shotgrid':
- '*/**/*shotgrid*'
- '*/**/*shotgrid*/**/*'
'module: Kitsu':
- '*/**/*kitsu*'
- '*/**/*kitsu*/**/*'

View file

@ -0,0 +1,28 @@
name: Sync Issues to ClickUp [trigger]
on:
workflow_dispatch:
inputs:
issue-number:
required: true
issues:
types: [labeled]
jobs:
call-ci-tools-issue-sync:
if: github.event.inputs.issue-number != '' || github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'backlog')
uses: ynput/ci-tools/.github/workflows/issue_to_clickup_ref.yml@main
with:
# issue number should be taken either from inputs or from the event
issue-number: ${{ github.event.inputs.issue-number || github.event.issue.number }}
repo-owner: ${{ github.event.repository.owner.login }}
repo-name: ${{ github.event.repository.name }}
secrets:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
cu_api_key: ${{ secrets.CLICKUP_API_KEY }}
cu_team_id: ${{ secrets.CLICKUP_TEAM_ID }}
cu_folder_id: ${{ secrets.CLICKUP_FOLDER_ID }}
cu_list_id: ${{ secrets.CLICKUP_LIST_ID }}
cu_field_domain_id: ${{ secrets.CLICKUP_DOMAIN_FIELD_ID }}
cu_field_type_id: ${{ secrets.CLICKUP_ISSUETYPE_FIELD_ID }}

View file

@ -7,3 +7,6 @@ AYON_CORE_ROOT = os.path.dirname(os.path.abspath(__file__))
PACKAGE_DIR = AYON_CORE_ROOT
PLUGINS_DIR = os.path.join(AYON_CORE_ROOT, "plugins")
AYON_SERVER_ENABLED = True
# Indicate if AYON entities should be used instead of OpenPype entities
USE_AYON_ENTITIES = False

View file

@ -31,7 +31,7 @@ AYON addons should contain separated logic of specific kind of implementation, s
- addon must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"`
- each key may contain list or string with a path to directory with plugins
## ITrayModule
## ITrayAddon
- addon has more logic when used in a tray
- it is possible that addon can be used only in the tray
- abstract methods
@ -46,7 +46,7 @@ AYON addons should contain separated logic of specific kind of implementation, s
- if addon has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
### ITrayService
- inherits from `ITrayModule` and implements `tray_menu` method for you
- inherits from `ITrayAddon` and implements `tray_menu` method for you
- adds action to submenu "Services" in tray widget menu with icon and label
- abstract attribute `label`
- label shown in menu
@ -57,7 +57,7 @@ AYON addons should contain separated logic of specific kind of implementation, s
- these states must be set by addon itself `set_service_running` is default state on initialization
### ITrayAction
- inherits from `ITrayModule` and implements `tray_menu` method for you
- inherits from `ITrayAddon` and implements `tray_menu` method for you
- adds action to tray widget menu with label
- abstract attribute `label`
- label shown in menu
@ -89,4 +89,4 @@ AYON addons should contain separated logic of specific kind of implementation, s
### TrayAddonsManager
- inherits from `AddonsManager`
- has specific implementation for Pype Tray tool and handle `ITrayModule` methods
- has specific implementation for Pype Tray tool and handle `ITrayAddon` methods

View file

@ -15,13 +15,9 @@ from abc import ABCMeta, abstractmethod
import six
import appdirs
from ayon_core.lib import Logger
from ayon_core.lib import Logger, is_dev_mode_enabled
from ayon_core.client import get_ayon_server_api_connection
from ayon_core.settings import get_system_settings
from ayon_core.settings.ayon_settings import (
is_dev_mode_enabled,
get_ayon_settings,
)
from ayon_core.settings import get_studio_settings
from .interfaces import (
IPluginPaths,
@ -648,7 +644,6 @@ class AddonsManager:
def __init__(self, settings=None, initialize=True):
self._settings = settings
self._system_settings = None
self._addons = []
self._addons_by_id = {}
@ -738,14 +733,9 @@ class AddonsManager:
# Prepare settings for addons
settings = self._settings
if settings is None:
settings = get_ayon_settings()
settings = get_studio_settings()
# OpenPype settings
system_settings = self._system_settings
if system_settings is None:
system_settings = get_system_settings()
modules_settings = system_settings["modules"]
modules_settings = {}
report = {}
time_start = time.time()
@ -788,6 +778,7 @@ class AddonsManager:
addon_classes.append(modules_item)
aliased_names = []
for addon_cls in addon_classes:
name = addon_cls.__name__
if issubclass(addon_cls, OpenPypeModule):
@ -807,6 +798,13 @@ class AddonsManager:
self._addons.append(addon)
self._addons_by_id[addon.id] = addon
self._addons_by_name[addon.name] = addon
# NOTE This will be removed with release 1.0.0 of ayon-core
# please use carefully.
# Gives option to use alias name for addon for cases when
# name in OpenPype was not the same as in AYON.
name_alias = getattr(addon, "openpype_alias", None)
if name_alias:
aliased_names.append((name_alias, addon))
enabled_str = "X"
if not addon.enabled:
enabled_str = " "
@ -822,6 +820,17 @@ class AddonsManager:
exc_info=True
)
for item in aliased_names:
name_alias, addon = item
if name_alias not in self._addons_by_name:
self._addons_by_name[name_alias] = addon
continue
self.log.warning(
"Alias name '{}' of addon '{}' is already assigned.".format(
name_alias, addon.name
)
)
if self._report is not None:
report[self._report_total_key] = time.time() - time_start
self._report["Initialization"] = report

View file

@ -15,7 +15,7 @@ method to convert 'click_wrap' object to 'click' object.
Before
```python
import click
from ayon_core.modules import AYONAddon
from ayon_core.addon import AYONAddon
class ExampleAddon(AYONAddon):
@ -40,7 +40,7 @@ def mycommand(arg1, arg2):
Now
```
from ayon_core import click_wrap
from ayon_core.modules import AYONAddon
from ayon_core.addon import AYONAddon
class ExampleAddon(AYONAddon):
@ -72,7 +72,7 @@ Added small enhancements:
Example:
```python
from ayon_core import click_wrap
from ayon_core.modules import AYONAddon
from ayon_core.addon import AYONAddon
class ExampleAddon(AYONAddon):

View file

@ -73,6 +73,20 @@ class Commands:
import pyblish.api
import pyblish.util
# Fix older jobs
for src_key, dst_key in (
("AVALON_PROJECT", "AYON_PROJECT_NAME"),
("AVALON_ASSET", "AYON_FOLDER_PATH"),
("AVALON_TASK", "AYON_TASK_NAME"),
("AVALON_WORKDIR", "AYON_WORKDIR"),
("AVALON_APP_NAME", "AYON_APP_NAME"),
("AVALON_APP", "AYON_HOST_NAME"),
):
if src_key in os.environ and dst_key not in os.environ:
os.environ[dst_key] = os.environ[src_key]
# Remove old keys, so we're sure they're not used
os.environ.pop(src_key, None)
log = Logger.get_logger("CLI-publish")
install_ayon_plugins()
@ -87,12 +101,12 @@ class Commands:
if not any(paths):
raise RuntimeError("No publish paths specified")
app_full_name = os.getenv("AVALON_APP_NAME")
app_full_name = os.getenv("AYON_APP_NAME")
if app_full_name:
context = get_global_context()
env = get_app_environments_for_context(
context["project_name"],
context["asset_name"],
context["folder_path"],
context["task_name"],
app_full_name,
launch_type=LaunchTypes.farm_publish,

View file

@ -27,7 +27,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"tvpaint",
"substancepainter",
"aftereffects",
"wrap"
"wrap",
"openrv"
}
launch_types = {LaunchTypes.local}

View file

@ -54,7 +54,7 @@ class CopyTemplateWorkfile(PreLaunchHook):
self.log.info("Last workfile does not exist.")
project_name = self.data["project_name"]
asset_name = self.data["asset_name"]
asset_name = self.data["folder_path"]
task_name = self.data["task_name"]
host_name = self.application.host_name

View file

@ -21,7 +21,7 @@ class CreateWorkdirExtraFolders(PreLaunchHook):
return
env = self.data.get("env") or {}
workdir = env.get("AVALON_WORKDIR")
workdir = env.get("AYON_WORKDIR")
if not workdir or not os.path.exists(workdir):
return

View file

@ -22,7 +22,7 @@ class GlobalHostDataHook(PreLaunchHook):
app = self.launch_context.application
temp_data = EnvironmentPrepData({
"project_name": self.data["project_name"],
"asset_name": self.data["asset_name"],
"folder_path": self.data["folder_path"],
"task_name": self.data["task_name"],
"app": app,
@ -66,7 +66,7 @@ class GlobalHostDataHook(PreLaunchHook):
project_doc = get_project(project_name)
self.data["project_doc"] = project_doc
asset_name = self.data.get("asset_name")
asset_name = self.data.get("folder_path")
if not asset_name:
self.log.warning(
"Asset name was not set. Skipping asset document query."

View file

@ -19,6 +19,7 @@ class OCIOEnvHook(PreLaunchHook):
"nuke",
"hiero",
"resolve",
"openrv"
}
launch_types = set()
@ -27,10 +28,10 @@ class OCIOEnvHook(PreLaunchHook):
template_data = get_template_data_with_names(
project_name=self.data["project_name"],
asset_name=self.data["asset_name"],
asset_name=self.data["folder_path"],
task_name=self.data["task_name"],
host_name=self.host_name,
system_settings=self.data["system_settings"]
settings=self.data["project_settings"]
)
config_data = get_imageio_config(

View file

@ -92,8 +92,8 @@ class HostDirmap(object):
self.on_enable_dirmap()
for k, sp in enumerate(mapping["source-path"]):
dst = mapping["destination-path"][k]
for k, sp in enumerate(mapping["source_path"]):
dst = mapping["destination_path"][k]
try:
# add trailing slash if missing
sp = os.path.join(sp, '')
@ -116,7 +116,7 @@ class HostDirmap(object):
continue
def get_mappings(self):
"""Get translation from source-path to destination-path.
"""Get translation from source_path to destination_path.
It checks if Site Sync is enabled and user chose to use local
site, in that case configuration in Local Settings takes precedence
@ -138,8 +138,8 @@ class HostDirmap(object):
if (
not mapping
or not mapping.get("destination-path")
or not mapping.get("source-path")
or not mapping.get("destination_path")
or not mapping.get("source_path")
):
return {}
self.log.info("Processing directory mapping ...")
@ -154,7 +154,7 @@ class HostDirmap(object):
in Local Settings.
Returns:
dict : { "source-path": [XXX], "destination-path": [YYYY]}
dict : { "source_path": [XXX], "destination_path": [YYYY]}
"""
project_name = self.project_name
@ -181,6 +181,10 @@ class HostDirmap(object):
exclude_locals=False,
cached=False)
# TODO implement
# Dirmap is dependent on 'get_site_local_overrides' which
# is not implemented in AYON. The mapping should be received
# from sitesync addon.
active_overrides = get_site_local_overrides(
project_name, active_site)
remote_overrides = get_site_local_overrides(
@ -210,13 +214,13 @@ class HostDirmap(object):
continue
if os.path.isdir(active_site_dir):
if "destination-path" not in mapping:
mapping["destination-path"] = []
mapping["destination-path"].append(active_site_dir)
if "destination_path" not in mapping:
mapping["destination_path"] = []
mapping["destination_path"].append(active_site_dir)
if "source-path" not in mapping:
mapping["source-path"] = []
mapping["source-path"].append(remote_site_dir)
if "source_path" not in mapping:
mapping["source_path"] = []
mapping["source_path"].append(remote_site_dir)
self.log.debug("local sync mapping:: {}".format(mapping))
return mapping

View file

@ -49,7 +49,6 @@ class HostBase(object):
Todo:
- move content of 'install_host' as method of this class
- register host object
- install legacy_io
- install global plugin paths
- store registered plugin paths to this object
- handle current context (project, asset, task)
@ -107,7 +106,7 @@ class HostBase(object):
Union[str, None]: Current project name.
"""
return os.environ.get("AVALON_PROJECT")
return os.environ.get("AYON_PROJECT_NAME")
def get_current_asset_name(self):
"""
@ -115,7 +114,7 @@ class HostBase(object):
Union[str, None]: Current asset name.
"""
return os.environ.get("AVALON_ASSET")
return os.environ.get("AYON_FOLDER_PATH")
def get_current_task_name(self):
"""
@ -123,7 +122,7 @@ class HostBase(object):
Union[str, None]: Current task name.
"""
return os.environ.get("AVALON_TASK")
return os.environ.get("AYON_TASK_NAME")
def get_current_context(self):
"""Get current context information.
@ -133,16 +132,14 @@ class HostBase(object):
can be opened multiple workfiles at one moment and change of context
can't be caught properly.
Default implementation returns values from 'legacy_io.Session'.
Returns:
Dict[str, Union[str, None]]: Context with 3 keys 'project_name',
'asset_name' and 'task_name'. All of them can be 'None'.
'folder_path' and 'task_name'. All of them can be 'None'.
"""
return {
"project_name": self.get_current_project_name(),
"asset_name": self.get_current_asset_name(),
"folder_path": self.get_current_asset_name(),
"task_name": self.get_current_task_name()
}
@ -164,7 +161,7 @@ class HostBase(object):
# Use current context to fill the context title
current_context = self.get_current_context()
project_name = current_context["project_name"]
asset_name = current_context["asset_name"]
asset_name = current_context["folder_path"]
task_name = current_context["task_name"]
items = []
if project_name:

View file

@ -234,7 +234,7 @@ class IWorkfileHost:
str: Path to new workdir.
"""
return session["AVALON_WORKDIR"]
return session["AYON_WORKDIR"]
# --- Deprecated method names ---
def file_extensions(self):

View file

@ -1,13 +1,10 @@
from ayon_core.modules import OpenPypeModule, IHostAddon
from ayon_core.addon import AYONAddon, IHostAddon
class AfterEffectsAddon(OpenPypeModule, IHostAddon):
class AfterEffectsAddon(AYONAddon, IHostAddon):
name = "aftereffects"
host_name = "aftereffects"
def initialize(self, module_settings):
self.enabled = True
def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
defaults = {

View file

@ -15,9 +15,8 @@ from wsrpc_aiohttp import (
from qtpy import QtCore
from ayon_core.lib import Logger
from ayon_core.tests.lib import is_in_tests
from ayon_core.pipeline import install_host, legacy_io
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
@ -298,14 +297,11 @@ class AfterEffectsRoute(WebSocketRoute):
log.info("Setting context change")
log.info("project {} asset {} ".format(project, asset))
if project:
legacy_io.Session["AVALON_PROJECT"] = project
os.environ["AVALON_PROJECT"] = project
os.environ["AYON_PROJECT_NAME"] = project
if asset:
legacy_io.Session["AVALON_ASSET"] = asset
os.environ["AVALON_ASSET"] = asset
os.environ["AYON_FOLDER_PATH"] = asset
if task:
legacy_io.Session["AVALON_TASK"] = task
os.environ["AVALON_TASK"] = task
os.environ["AYON_TASK_NAME"] = task
async def read(self):
log.debug("aftereffects.read client calls server server calls "

View file

@ -128,7 +128,7 @@ def set_settings(frames, resolution, comp_ids=None, print_msg=True):
current_context = get_current_context()
asset_doc = get_asset_by_name(current_context["project_name"],
current_context["asset_name"])
current_context["folder_path"])
settings = get_asset_settings(asset_doc)
msg = ''

View file

@ -9,6 +9,8 @@ from ayon_core.pipeline import (
register_loader_plugin_path,
register_creator_plugin_path,
AVALON_CONTAINER_ID,
AVALON_INSTANCE_ID,
AYON_INSTANCE_ID,
)
from ayon_core.hosts.aftereffects.api.workfile_template_builder import (
AEPlaceholderLoadPlugin,
@ -142,7 +144,9 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
layers_meta = stub.get_metadata()
for instance in layers_meta:
if instance.get("id") == "pyblish.avalon.instance":
if instance.get("id") in {
AYON_INSTANCE_ID, AVALON_INSTANCE_ID
}:
instances.append(instance)
return instances

View file

@ -11,7 +11,7 @@ from ayon_core.pipeline import (
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 SUBSET_NAME_ALLOWED_SYMBOLS
from ayon_core.pipeline.create import PRODUCT_NAME_ALLOWED_SYMBOLS
class RenderCreator(Creator):
@ -22,7 +22,7 @@ class RenderCreator(Creator):
"""
identifier = "render"
label = "Render"
family = "render"
product_type = "render"
description = "Render creator"
create_allow_context_change = True
@ -31,7 +31,7 @@ class RenderCreator(Creator):
mark_for_review = True
force_setting_values = True
def create(self, subset_name_from_ui, data, pre_create_data):
def create(self, product_name, data, pre_create_data):
stub = api.get_stub() # only after After Effects is up
try:
@ -58,33 +58,37 @@ class RenderCreator(Creator):
len(comps) > 1)
for comp in comps:
composition_name = re.sub(
"[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
"[^{}]+".format(PRODUCT_NAME_ALLOWED_SYMBOLS),
"",
comp.name
)
if use_composition_name:
if "{composition}" not in subset_name_from_ui.lower():
subset_name_from_ui += "{Composition}"
if "{composition}" not in product_name.lower():
product_name += "{Composition}"
dynamic_fill = prepare_template_data({"composition":
composition_name})
subset_name = subset_name_from_ui.format(**dynamic_fill)
comp_product_name = product_name.format(**dynamic_fill)
data["composition_name"] = composition_name
else:
subset_name = subset_name_from_ui
subset_name = re.sub(r"\{composition\}", '', subset_name,
flags=re.IGNORECASE)
comp_product_name = re.sub(
r"\{composition\}",
"",
product_name,
flags=re.IGNORECASE
)
for inst in self.create_context.instances:
if subset_name == inst.subset_name:
if comp_product_name == inst.product_name:
raise CreatorError("{} already exists".format(
inst.subset_name))
inst.product_name))
data["members"] = [comp.id]
data["orig_comp_name"] = composition_name
new_instance = CreatedInstance(self.family, subset_name, data,
self)
new_instance = CreatedInstance(
self.product_type, comp_product_name, data, self
)
if "farm" in pre_create_data:
use_farm = pre_create_data["farm"]
new_instance.creator_attributes["farm"] = use_farm
@ -96,7 +100,7 @@ class RenderCreator(Creator):
new_instance.data_to_store())
self._add_instance_to_context(new_instance)
stub.rename_item(comp.id, subset_name)
stub.rename_item(comp.id, comp_product_name)
if self.force_setting_values:
set_settings(True, True, [comp.id], print_msg=False)
@ -107,7 +111,7 @@ class RenderCreator(Creator):
"selected by default.",
default=True, label="Use selection"),
BoolDef("use_composition_name",
label="Use composition name in subset"),
label="Use composition name in product"),
UISeparatorDef(),
BoolDef("farm", label="Render on farm"),
BoolDef(
@ -133,9 +137,14 @@ class RenderCreator(Creator):
def collect_instances(self):
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", ''))
# legacy instances have product_type=='render' or 'renderLocal', use them
creator_id = instance_data.get("creator_identifier")
if not creator_id:
# NOTE this is for backwards compatibility but probably can be
# removed
creator_id = instance_data.get("family", "")
creator_id = creator_id.replace("Local", "")
if creator_id == self.identifier:
instance_data = self._handle_legacy(instance_data)
instance = CreatedInstance.from_existing(
@ -147,10 +156,10 @@ class RenderCreator(Creator):
for created_inst, _changes in update_list:
api.get_stub().imprint(created_inst.get("instance_id"),
created_inst.data_to_store())
subset_change = _changes.get("subset")
if subset_change:
name_change = _changes.get("productName")
if name_change:
api.get_stub().rename_item(created_inst.data["members"][0],
subset_change.new_value)
name_change.new_value)
def remove_instances(self, instances):
"""Removes metadata and renames to original comp name if available."""
@ -183,33 +192,34 @@ class RenderCreator(Creator):
def get_detail_description(self):
return """Creator for Render instances
Main publishable item in AfterEffects will be of `render` family.
Main publishable item in AfterEffects will be of `render` product type.
Result of this item (instance) is picture sequence or video that could
be a final delivery product or loaded and used in another DCCs.
Select single composition and create instance of 'render' family or
turn off 'Use selection' to create instance for all compositions.
Select single composition and create instance of 'render' product type
or turn off 'Use selection' to create instance for all compositions.
'Use composition name in subset' allows to explicitly add composition
name into created subset name.
'Use composition name in product' allows to explicitly add composition
name into created product name.
Position of composition name could be set in
`project_settings/global/tools/creator/subset_name_profiles` with some
form of '{composition}' placeholder.
`project_settings/global/tools/creator/product_name_profiles` with
some form of '{composition}' placeholder.
Composition name will be used implicitly if multiple composition should
be handled at same time.
If {composition} placeholder is not us 'subset_name_profiles'
composition name will be capitalized and set at the end of subset name
if necessary.
If {composition} placeholder is not us 'product_name_profiles'
composition name will be capitalized and set at the end of
product name if necessary.
If composition name should be used, it will be cleaned up of characters
that would cause an issue in published file names.
"""
def get_dynamic_data(self, variant, task_name, asset_doc,
project_name, host_name, instance):
def get_dynamic_data(
self, project_name, asset_doc, task_name, variant, host_name, instance
):
dynamic_data = {}
if instance is not None:
composition_name = instance.get("composition_name")
@ -234,9 +244,9 @@ class RenderCreator(Creator):
instance_data["task"] = self.create_context.get_current_task_name()
if not instance_data.get("creator_attributes"):
is_old_farm = instance_data["family"] != "renderLocal"
is_old_farm = instance_data.get("family") != "renderLocal"
instance_data["creator_attributes"] = {"farm": is_old_farm}
instance_data["family"] = self.family
instance_data["productType"] = self.product_type
if instance_data["creator_attributes"].get("mark_for_review") is None:
instance_data["creator_attributes"]["mark_for_review"] = True

View file

@ -9,7 +9,7 @@ from ayon_core.hosts.aftereffects.api.pipeline import cache_and_get_instances
class AEWorkfileCreator(AutoCreator):
identifier = "workfile"
family = "workfile"
product_type = "workfile"
default_variant = "Main"
@ -20,9 +20,9 @@ class AEWorkfileCreator(AutoCreator):
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"]
product_name = instance_data["productName"]
instance = CreatedInstance(
self.family, subset_name, instance_data, self
self.product_type, product_name, instance_data, self
)
self._add_instance_to_context(instance)
@ -33,7 +33,7 @@ class AEWorkfileCreator(AutoCreator):
def create(self, options=None):
existing_instance = None
for instance in self.create_context.instances:
if instance.family == self.family:
if instance.product_type == self.product_type:
existing_instance = instance
break
@ -49,9 +49,12 @@ class AEWorkfileCreator(AutoCreator):
if existing_instance is None:
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
self.default_variant, task_name, asset_doc,
project_name, host_name
product_name = self.get_product_name(
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
)
data = {
"folderPath": asset_name,
@ -59,12 +62,16 @@ class AEWorkfileCreator(AutoCreator):
"variant": self.default_variant,
}
data.update(self.get_dynamic_data(
self.default_variant, task_name, asset_doc,
project_name, host_name, None
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
None,
))
new_instance = CreatedInstance(
self.family, subset_name, data, self
self.product_type, product_name, data, self
)
self._add_instance_to_context(new_instance)
@ -76,10 +83,13 @@ class AEWorkfileCreator(AutoCreator):
or existing_instance["task"] != task_name
):
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
self.default_variant, task_name, asset_doc,
project_name, host_name
product_name = self.get_product_name(
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
)
existing_instance["folderPath"] = asset_name
existing_instance["task"] = task_name
existing_instance["subset"] = subset_name
existing_instance["productName"] = product_name

View file

@ -11,7 +11,7 @@ from ayon_core.hosts.aftereffects.api.lib import (
class BackgroundLoader(api.AfterEffectsLoader):
"""
Load images from Background family
Load images from Background product type
Creates for each background separate folder with all imported images
from background json AND automatically created composition with layers,
each layer for separate image.
@ -56,16 +56,21 @@ class BackgroundLoader(api.AfterEffectsLoader):
self.__class__.__name__
)
def update(self, container, representation):
def update(self, container, context):
""" Switch asset or change version """
stub = self.get_stub()
context = representation.get("context", {})
asset_doc = context["asset"]
subset_doc = context["subset"]
repre_doc = context["representation"]
folder_name = asset_doc["name"]
product_name = subset_doc["name"]
_ = container.pop("layer")
# without iterator number (_001, 002...)
namespace_from_container = re.sub(r'_\d{3}$', '',
container["namespace"])
comp_name = "{}_{}".format(context["asset"], context["subset"])
comp_name = "{}_{}".format(folder_name, product_name)
# switching assets
if namespace_from_container != comp_name:
@ -73,11 +78,11 @@ class BackgroundLoader(api.AfterEffectsLoader):
existing_items = [layer.name for layer in items]
comp_name = get_unique_layer_name(
existing_items,
"{}_{}".format(context["asset"], context["subset"]))
"{}_{}".format(folder_name, product_name))
else: # switching version - keep same name
comp_name = container["namespace"]
path = get_representation_path(representation)
path = get_representation_path(repre_doc)
layers = get_background_layers(path)
comp = stub.reload_background(container["members"][1],
@ -85,8 +90,8 @@ class BackgroundLoader(api.AfterEffectsLoader):
layers)
# update container
container["representation"] = str(representation["_id"])
container["name"] = context["subset"]
container["representation"] = str(repre_doc["_id"])
container["name"] = product_name
container["namespace"] = comp_name
container["members"] = comp.members
@ -104,5 +109,5 @@ class BackgroundLoader(api.AfterEffectsLoader):
stub.imprint(layer.id, {})
stub.delete_item(layer.id)
def switch(self, container, representation):
self.update(container, representation)
def switch(self, container, context):
self.update(container, context)

View file

@ -64,31 +64,36 @@ class FileLoader(api.AfterEffectsLoader):
self.__class__.__name__
)
def update(self, container, representation):
def update(self, container, context):
""" Switch asset or change version """
stub = self.get_stub()
layer = container.pop("layer")
context = representation.get("context", {})
asset_doc = context["asset"]
subset_doc = context["subset"]
repre_doc = context["representation"]
folder_name = asset_doc["name"]
product_name = subset_doc["name"]
namespace_from_container = re.sub(r'_\d{3}$', '',
container["namespace"])
layer_name = "{}_{}".format(context["asset"], context["subset"])
layer_name = "{}_{}".format(folder_name, product_name)
# switching assets
if namespace_from_container != layer_name:
layers = stub.get_items(comps=True)
existing_layers = [layer.name for layer in layers]
layer_name = get_unique_layer_name(
existing_layers,
"{}_{}".format(context["asset"], context["subset"]))
"{}_{}".format(folder_name, product_name))
else: # switching version - keep same name
layer_name = container["namespace"]
path = get_representation_path(representation)
path = get_representation_path(repre_doc)
# with aftereffects.maintained_selection(): # TODO
stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name)
stub.imprint(
layer.id, {"representation": str(representation["_id"]),
"name": context["subset"],
layer.id, {"representation": str(repre_doc["_id"]),
"name": product_name,
"namespace": layer_name}
)
@ -103,5 +108,5 @@ class FileLoader(api.AfterEffectsLoader):
stub.imprint(layer.id, {})
stub.delete_item(layer.id)
def switch(self, container, representation):
self.update(container, representation)
def switch(self, container, context):
self.update(container, context)

View file

@ -60,8 +60,8 @@ class CollectAERender(publish.AbstractCollectRender):
if not inst.data.get("active", True):
continue
family = inst.data["family"]
if family not in ["render", "renderLocal"]: # legacy
product_type = inst.data["productType"]
if product_type not in ["render", "renderLocal"]: # legacy
continue
comp_id = int(inst.data["members"][0])
@ -81,29 +81,32 @@ class CollectAERender(publish.AbstractCollectRender):
fps = comp_info.frameRate
# TODO add resolution when supported by extension
task_name = inst.data.get("task") # legacy
task_name = inst.data.get("task")
render_q = CollectAERender.get_stub().get_render_info(comp_id)
if not render_q:
raise ValueError("No file extension set in Render Queue")
render_item = render_q[0]
product_type = "render"
instance_families = inst.data.get("families", [])
subset_name = inst.data["subset"]
instance_families.append(product_type)
product_name = inst.data["productName"]
instance = AERenderInstance(
family="render",
productType=product_type,
family=product_type,
families=instance_families,
version=version,
time="",
source=current_file,
label="{} - {}".format(subset_name, family),
subset=subset_name,
asset=inst.data["asset"],
label="{} - {}".format(product_name, product_type),
productName=product_name,
folderPath=inst.data["folderPath"],
task=task_name,
attachTo=False,
setMembers='',
publish=True,
name=subset_name,
name=product_name,
resolutionWidth=render_item.width,
resolutionHeight=render_item.height,
pixelAspect=1,
@ -175,8 +178,8 @@ class CollectAERender(publish.AbstractCollectRender):
version_str = "v{:03d}".format(render_instance.version)
if "#" not in file_name: # single frame (mov)W
path = os.path.join(base_dir, "{}_{}_{}.{}".format(
render_instance.asset,
render_instance.subset,
render_instance.folderPath,
render_instance.productName,
version_str,
ext
))
@ -184,8 +187,8 @@ class CollectAERender(publish.AbstractCollectRender):
else:
for frame in range(start, end + 1):
path = os.path.join(base_dir, "{}_{}_{}.{}.{}".format(
render_instance.asset,
render_instance.subset,
render_instance.folderPath,
render_instance.productName,
version_str,
str(frame).zfill(self.padding_width),
ext

View file

@ -3,7 +3,7 @@ Requires:
None
Provides:
instance -> family ("review")
instance -> families ("review")
"""
import pyblish.api

View file

@ -2,9 +2,6 @@ import os
import pyblish.api
from ayon_core.client import get_asset_name_identifier
from ayon_core.pipeline.create import get_subset_name
class CollectWorkfile(pyblish.api.ContextPlugin):
""" Adds the AE render instances """
@ -15,86 +12,24 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
default_variant = "Main"
def process(self, context):
existing_instance = None
workfile_instance = None
for instance in context:
if instance.data["family"] == "workfile":
self.log.debug("Workfile instance found, won't create new")
existing_instance = instance
if instance.data["productType"] == "workfile":
self.log.debug("Workfile instance found")
workfile_instance = instance
break
current_file = context.data["currentFile"]
staging_dir = os.path.dirname(current_file)
scene_file = os.path.basename(current_file)
if existing_instance is None: # old publish
instance = self._get_new_instance(context, scene_file)
else:
instance = existing_instance
if workfile_instance is None:
self.log.debug("Workfile instance not found. Skipping")
return
# creating representation
representation = {
'name': 'aep',
'ext': 'aep',
'files': scene_file,
workfile_instance.data["representations"].append({
"name": "aep",
"ext": "aep",
"files": scene_file,
"stagingDir": staging_dir,
}
if not instance.data.get("representations"):
instance.data["representations"] = []
instance.data["representations"].append(representation)
instance.data["publish"] = instance.data["active"] # for DL
def _get_new_instance(self, context, scene_file):
task = context.data["task"]
version = context.data["version"]
asset_entity = context.data["assetEntity"]
project_entity = context.data["projectEntity"]
asset_name = get_asset_name_identifier(asset_entity)
instance_data = {
"active": True,
"asset": asset_name,
"task": task,
"frameStart": context.data['frameStart'],
"frameEnd": context.data['frameEnd'],
"handleStart": context.data['handleStart'],
"handleEnd": context.data['handleEnd'],
"fps": asset_entity["data"]["fps"],
"resolutionWidth": asset_entity["data"].get(
"resolutionWidth",
project_entity["data"]["resolutionWidth"]),
"resolutionHeight": asset_entity["data"].get(
"resolutionHeight",
project_entity["data"]["resolutionHeight"]),
"pixelAspect": 1,
"step": 1,
"version": version
}
# workfile instance
family = "workfile"
subset = get_subset_name(
family,
self.default_variant,
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"],
project_settings=context.data["project_settings"]
)
# Create instance
instance = context.create_instance(subset)
# creating instance data
instance.data.update({
"subset": subset,
"label": scene_file,
"family": family,
"families": [family],
"representations": list()
})
instance.data.update(instance_data)
return instance

View file

@ -3,9 +3,9 @@
<error id="main">
<title>Subset context</title>
<description>
## Invalid subset context
## Invalid product context
Context of the given subset doesn't match your current scene.
Context of the given product doesn't match your current scene.
### How to repair?
@ -15,7 +15,7 @@ You can fix this with "repair" button on the right and refresh Publish at the bo
### __Detailed Info__ (optional)
This might happen if you are reuse old workfile and open it in different context.
(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.)
(Eg. you created product name "renderCompositingDefault" from folder "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing product for "Robot" asset stayed in the workfile.)
</detail>
</error>
</root>

View file

@ -1,54 +0,0 @@
import json
import pyblish.api
from ayon_core.hosts.aftereffects.api import AfterEffectsHost
class PreCollectRender(pyblish.api.ContextPlugin):
"""
Checks if render instance is of old type, adds to families to both
existing collectors work same way.
Could be removed in the future when no one uses old publish.
"""
label = "PreCollect Render"
order = pyblish.api.CollectorOrder + 0.400
hosts = ["aftereffects"]
family_remapping = {
"render": ("render.farm", "farm"), # (family, label)
"renderLocal": ("render.local", "local")
}
def process(self, context):
if context.data.get("newPublishing"):
self.log.debug("Not applicable for New Publisher, skip")
return
for inst in AfterEffectsHost().list_instances():
if inst.get("creator_attributes"):
raise ValueError("Instance created in New publisher, "
"cannot be published in Pyblish.\n"
"Please publish in New Publisher "
"or recreate instances with legacy Creators")
if inst["family"] not in self.family_remapping.keys():
continue
if not inst["members"]:
raise ValueError("Couldn't find id, unable to publish. " +
"Please recreate instance.")
instance = context.create_instance(inst["subset"])
inst["families"] = [self.family_remapping[inst["family"]][0]]
instance.data.update(inst)
self._debug_log(instance)
def _debug_log(self, instance):
def _default_json(value):
return str(value)
self.log.info(
json.dumps(instance.data, indent=4, default=_default_json)
)

View file

@ -30,7 +30,7 @@ class ValidateInstanceAssetRepair(pyblish.api.Action):
for instance in instances:
data = stub.read(instance[0])
data["asset"] = get_current_asset_name()
data["folderPath"] = get_current_asset_name()
stub.imprint(instance[0].instance_id, data)
@ -53,7 +53,7 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin):
order = ValidateContentsOrder
def process(self, instance):
instance_asset = instance.data["asset"]
instance_asset = instance.data["folderPath"]
current_asset = get_current_asset_name()
msg = (
f"Instance asset {instance_asset} is not the same "

View file

@ -1,16 +1,13 @@
import os
from ayon_core.modules import OpenPypeModule, IHostAddon
from ayon_core.addon import AYONAddon, IHostAddon
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
class BlenderAddon(OpenPypeModule, IHostAddon):
class BlenderAddon(AYONAddon, IHostAddon):
name = "blender"
host_name = "blender"
def initialize(self, module_settings):
self.enabled = True
def add_implementation_envs(self, env, _app):
"""Modify environments to contain all required for implementation."""
# Prepare path to implementation script

View file

@ -19,7 +19,6 @@ from ayon_core.host import (
from ayon_core.client import get_asset_by_name
from ayon_core.pipeline import (
schema,
legacy_io,
get_current_project_name,
get_current_asset_name,
register_loader_plugin_path,
@ -27,6 +26,7 @@ from ayon_core.pipeline import (
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
AYON_CONTAINER_ID,
)
from ayon_core.lib import (
Logger,
@ -273,7 +273,7 @@ def set_resolution(data):
def on_new():
project = os.environ.get("AVALON_PROJECT")
project = os.environ.get("AYON_PROJECT_NAME")
settings = get_project_settings(project).get("blender")
set_resolution_startup = settings.get("set_resolution_startup")
@ -294,7 +294,7 @@ def on_new():
def on_open():
project = os.environ.get("AVALON_PROJECT")
project = os.environ.get("AYON_PROJECT_NAME")
settings = get_project_settings(project).get("blender")
set_resolution_startup = settings.get("set_resolution_startup")
@ -380,7 +380,7 @@ def _on_task_changed():
# `directory` attribute, so it opens in that directory (does it?).
# https://docs.blender.org/api/blender2.8/bpy.types.Operator.html#calling-a-file-selector
# https://docs.blender.org/api/blender2.8/bpy.types.WindowManager.html#bpy.types.WindowManager.fileselect_add
workdir = legacy_io.Session["AVALON_WORKDIR"]
workdir = os.getenv("AYON_WORKDIR")
log.debug("New working directory: %s", workdir)
@ -564,8 +564,9 @@ def ls() -> Iterator:
called containers.
"""
for container in lib.lsattr("id", AVALON_CONTAINER_ID):
yield parse_container(container)
for id_type in {AYON_CONTAINER_ID, AVALON_CONTAINER_ID}:
for container in lib.lsattr("id", id_type):
yield parse_container(container)
def publish():

View file

@ -10,6 +10,8 @@ from ayon_core.pipeline import (
Creator,
CreatedInstance,
LoaderPlugin,
AVALON_INSTANCE_ID,
AYON_INSTANCE_ID,
)
from ayon_core.lib import BoolDef
@ -28,13 +30,13 @@ VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"]
def prepare_scene_name(
asset: str, subset: str, namespace: Optional[str] = None
folder_name: str, product_name: str, namespace: Optional[str] = None
) -> str:
"""Return a consistent name for an asset."""
name = f"{asset}"
name = f"{folder_name}"
if namespace:
name = f"{name}_{namespace}"
name = f"{name}_{subset}"
name = f"{name}_{product_name}"
# Blender name for a collection or object cannot be longer than 63
# characters. If the name is longer, it will raise an error.
@ -45,7 +47,7 @@ def prepare_scene_name(
def get_unique_number(
asset: str, subset: str
folder_name: str, product_name: str
) -> str:
"""Return a unique number based on the asset name."""
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
@ -62,10 +64,10 @@ def get_unique_number(
if c.get(AVALON_PROPERTY)}
container_names = obj_group_names.union(coll_group_names)
count = 1
name = f"{asset}_{count:0>2}_{subset}"
name = f"{folder_name}_{count:0>2}_{product_name}"
while name in container_names:
count += 1
name = f"{asset}_{count:0>2}_{subset}"
name = f"{folder_name}_{count:0>2}_{product_name}"
return f"{count:0>2}"
@ -159,24 +161,22 @@ class BaseCreator(Creator):
create_as_asset_group = False
@staticmethod
def cache_subsets(shared_data):
def cache_instance_data(shared_data):
"""Cache instances for Creators shared data.
Create `blender_cached_subsets` key when needed in shared data and
Create `blender_cached_instances` key when needed in shared data and
fill it with all collected instances from the scene under its
respective creator identifiers.
If legacy instances are detected in the scene, create
`blender_cached_legacy_subsets` key and fill it with
all legacy subsets from this family as a value. # key or value?
`blender_cached_legacy_instances` key and fill it with
all legacy products from this family as a value. # key or value?
Args:
shared_data(Dict[str, Any]): Shared data.
Return:
Dict[str, Any]: Shared data with cached subsets.
"""
if not shared_data.get('blender_cached_subsets'):
if not shared_data.get('blender_cached_instances'):
cache = {}
cache_legacy = {}
@ -193,7 +193,9 @@ class BaseCreator(Creator):
if not avalon_prop:
continue
if avalon_prop.get('id') != 'pyblish.avalon.instance':
if avalon_prop.get('id') not in {
AYON_INSTANCE_ID, AVALON_INSTANCE_ID
}:
continue
creator_id = avalon_prop.get('creator_identifier')
@ -206,19 +208,19 @@ class BaseCreator(Creator):
# Legacy creator instance
cache_legacy.setdefault(family, []).append(obj_or_col)
shared_data["blender_cached_subsets"] = cache
shared_data["blender_cached_legacy_subsets"] = cache_legacy
shared_data["blender_cached_instances"] = cache
shared_data["blender_cached_legacy_instances"] = cache_legacy
return shared_data
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
"""Override abstract method from Creator.
Create new instance and store it.
Args:
subset_name(str): Subset name of created instance.
product_name(str): Subset name of created instance.
instance_data(dict): Instance base data.
pre_create_data(dict): Data based on pre creation attributes.
Those may affect how creator works.
@ -232,7 +234,7 @@ class BaseCreator(Creator):
# Create asset group
asset_name = instance_data["folderPath"].split("/")[-1]
name = prepare_scene_name(asset_name, subset_name)
name = prepare_scene_name(asset_name, product_name)
if self.create_as_asset_group:
# Create instance as empty
instance_node = bpy.data.objects.new(name=name, object_data=None)
@ -243,10 +245,10 @@ class BaseCreator(Creator):
instance_node = bpy.data.collections.new(name=name)
instances.children.link(instance_node)
self.set_instance_data(subset_name, instance_data)
self.set_instance_data(product_name, instance_data)
instance = CreatedInstance(
self.family, subset_name, instance_data, self
self.product_type, product_name, instance_data, self
)
instance.transient_data["instance_node"] = instance_node
self._add_instance_to_context(instance)
@ -259,18 +261,18 @@ class BaseCreator(Creator):
"""Override abstract method from BaseCreator.
Collect existing instances related to this creator plugin."""
# Cache subsets in shared data
self.cache_subsets(self.collection_shared_data)
# Cache instances in shared data
self.cache_instance_data(self.collection_shared_data)
# Get cached subsets
cached_subsets = self.collection_shared_data.get(
"blender_cached_subsets"
# Get cached instances
cached_instances = self.collection_shared_data.get(
"blender_cached_instances"
)
if not cached_subsets:
if not cached_instances:
return
# Process only instances that were created by this creator
for instance_node in cached_subsets.get(self.identifier, []):
for instance_node in cached_instances.get(self.identifier, []):
property = instance_node.get(AVALON_PROPERTY)
# Create instance object from existing data
instance = CreatedInstance.from_existing(
@ -302,16 +304,17 @@ class BaseCreator(Creator):
)
return
# Rename the instance node in the scene if subset or asset changed.
# Rename the instance node in the scene if product
# or folder changed.
# Do not rename the instance if the family is workfile, as the
# workfile instance is included in the AVALON_CONTAINER collection.
if (
"subset" in changes.changed_keys
"productName" in changes.changed_keys
or "folderPath" in changes.changed_keys
) and created_instance.family != "workfile":
) and created_instance.product_type != "workfile":
asset_name = data["folderPath"].split("/")[-1]
name = prepare_scene_name(
asset=asset_name, subset=data["subset"]
asset_name, data["productName"]
)
node.name = name
@ -337,13 +340,13 @@ class BaseCreator(Creator):
def set_instance_data(
self,
subset_name: str,
product_name: str,
instance_data: dict
):
"""Fill instance data with required items.
Args:
subset_name(str): Subset name of created instance.
product_name(str): Subset name of created instance.
instance_data(dict): Instance base data.
instance_node(bpy.types.ID): Instance node in blender scene.
"""
@ -352,9 +355,9 @@ class BaseCreator(Creator):
instance_data.update(
{
"id": "pyblish.avalon.instance",
"id": AVALON_INSTANCE_ID,
"creator_identifier": self.identifier,
"subset": subset_name,
"productName": product_name,
}
)
@ -462,14 +465,14 @@ class AssetLoader(LoaderPlugin):
filepath = self.filepath_from_context(context)
assert Path(filepath).exists(), f"{filepath} doesn't exist."
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
unique_number = get_unique_number(
asset, subset
folder_name, product_name
)
namespace = namespace or f"{asset}_{unique_number}"
namespace = namespace or f"{folder_name}_{unique_number}"
name = name or prepare_scene_name(
asset, subset, unique_number
folder_name, product_name, unique_number
)
nodes = self.process_asset(
@ -495,21 +498,21 @@ class AssetLoader(LoaderPlugin):
# loader=self.__class__.__name__,
# )
# asset = context["asset"]["name"]
# subset = context["subset"]["name"]
# folder_name = context["asset"]["name"]
# product_name = context["subset"]["name"]
# instance_name = prepare_scene_name(
# asset, subset, unique_number
# folder_name, product_name, unique_number
# ) + '_CON'
# return self._get_instance_collection(instance_name, nodes)
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Must be implemented by a sub-class"""
raise NotImplementedError("Must be implemented by a sub-class")
def update(self, container: Dict, representation: Dict):
def update(self, container: Dict, context: Dict):
""" Run the update on Blender main thread"""
mti = MainThreadItem(self.exec_update, container, representation)
mti = MainThreadItem(self.exec_update, container, context)
execute_in_main_thread(mti)
def exec_remove(self, container: Dict) -> bool:

View file

@ -47,6 +47,22 @@ def get_multilayer(settings):
["multilayer_exr"])
def get_renderer(settings):
"""Get renderer from blender settings."""
return (settings["blender"]
["RenderSettings"]
["renderer"])
def get_compositing(settings):
"""Get compositing from blender settings."""
return (settings["blender"]
["RenderSettings"]
["compositing"])
def get_render_product(output_path, name, aov_sep):
"""
Generate the path to the render product. Blender interprets the `#`
@ -91,66 +107,120 @@ def set_render_format(ext, multilayer):
image_settings.file_format = "TIFF"
def set_render_passes(settings):
aov_list = (settings["blender"]
["RenderSettings"]
["aov_list"])
custom_passes = (settings["blender"]
["RenderSettings"]
["custom_passes"])
def set_render_passes(settings, renderer):
aov_list = set(settings["blender"]["RenderSettings"]["aov_list"])
custom_passes = settings["blender"]["RenderSettings"]["custom_passes"]
# Common passes for both renderers
vl = bpy.context.view_layer
# Data Passes
vl.use_pass_combined = "combined" in aov_list
vl.use_pass_z = "z" in aov_list
vl.use_pass_mist = "mist" in aov_list
vl.use_pass_normal = "normal" in aov_list
# Light Passes
vl.use_pass_diffuse_direct = "diffuse_light" in aov_list
vl.use_pass_diffuse_color = "diffuse_color" in aov_list
vl.use_pass_glossy_direct = "specular_light" in aov_list
vl.use_pass_glossy_color = "specular_color" in aov_list
vl.eevee.use_pass_volume_direct = "volume_light" in aov_list
vl.use_pass_emit = "emission" in aov_list
vl.use_pass_environment = "environment" in aov_list
vl.use_pass_shadow = "shadow" in aov_list
vl.use_pass_ambient_occlusion = "ao" in aov_list
cycles = vl.cycles
# Cryptomatte Passes
vl.use_pass_cryptomatte_object = "cryptomatte_object" in aov_list
vl.use_pass_cryptomatte_material = "cryptomatte_material" in aov_list
vl.use_pass_cryptomatte_asset = "cryptomatte_asset" in aov_list
cycles.denoising_store_passes = "denoising" in aov_list
cycles.use_pass_volume_direct = "volume_direct" in aov_list
cycles.use_pass_volume_indirect = "volume_indirect" in aov_list
if renderer == "BLENDER_EEVEE":
# Eevee exclusive passes
eevee = vl.eevee
# Light Passes
vl.use_pass_shadow = "shadow" in aov_list
eevee.use_pass_volume_direct = "volume_light" in aov_list
# Effects Passes
eevee.use_pass_bloom = "bloom" in aov_list
eevee.use_pass_transparent = "transparent" in aov_list
# Cryptomatte Passes
vl.use_pass_cryptomatte_accurate = "cryptomatte_accurate" in aov_list
elif renderer == "CYCLES":
# Cycles exclusive passes
cycles = vl.cycles
# Data Passes
vl.use_pass_position = "position" in aov_list
vl.use_pass_vector = "vector" in aov_list
vl.use_pass_uv = "uv" in aov_list
cycles.denoising_store_passes = "denoising" in aov_list
vl.use_pass_object_index = "object_index" in aov_list
vl.use_pass_material_index = "material_index" in aov_list
cycles.pass_debug_sample_count = "sample_count" in aov_list
# Light Passes
vl.use_pass_diffuse_indirect = "diffuse_indirect" in aov_list
vl.use_pass_glossy_indirect = "specular_indirect" in aov_list
vl.use_pass_transmission_direct = "transmission_direct" in aov_list
vl.use_pass_transmission_indirect = "transmission_indirect" in aov_list
vl.use_pass_transmission_color = "transmission_color" in aov_list
cycles.use_pass_volume_direct = "volume_light" in aov_list
cycles.use_pass_volume_indirect = "volume_indirect" in aov_list
cycles.use_pass_shadow_catcher = "shadow" in aov_list
aovs_names = [aov.name for aov in vl.aovs]
for cp in custom_passes:
cp_name = cp[0]
cp_name = cp["attribute"]
if cp_name not in aovs_names:
aov = vl.aovs.add()
aov.name = cp_name
else:
aov = vl.aovs[cp_name]
aov.type = cp[1].get("type", "VALUE")
aov.type = cp["value"]
return aov_list, custom_passes
return list(aov_list), custom_passes
def set_node_tree(output_path, name, aov_sep, ext, multilayer):
def _create_aov_slot(name, aov_sep, slots, rpass_name, multi_exr, output_path):
filename = f"{name}{aov_sep}{rpass_name}.####"
slot = slots.new(rpass_name if multi_exr else filename)
filepath = str(output_path / filename.lstrip("/"))
return slot, filepath
def set_node_tree(
output_path, render_product, name, aov_sep, ext, multilayer, compositing
):
# Set the scene to use the compositor node tree to render
bpy.context.scene.use_nodes = True
tree = bpy.context.scene.node_tree
# Get the Render Layers node
rl_node = None
comp_layer_type = "CompositorNodeRLayers"
output_type = "CompositorNodeOutputFile"
compositor_type = "CompositorNodeComposite"
# Get the Render Layer, Composite and the previous output nodes
render_layer_node = None
composite_node = None
old_output_node = None
for node in tree.nodes:
if node.bl_idname == "CompositorNodeRLayers":
rl_node = node
if node.bl_idname == comp_layer_type:
render_layer_node = node
elif node.bl_idname == compositor_type:
composite_node = node
elif node.bl_idname == output_type and "AYON" in node.name:
old_output_node = node
if render_layer_node and composite_node and old_output_node:
break
# If there's not a Render Layers node, we create it
if not rl_node:
rl_node = tree.nodes.new("CompositorNodeRLayers")
if not render_layer_node:
render_layer_node = tree.nodes.new(comp_layer_type)
# Get the enabled output sockets, that are the active passes for the
# render.
@ -158,48 +228,81 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer):
exclude_sockets = ["Image", "Alpha", "Noisy Image"]
passes = [
socket
for socket in rl_node.outputs
for socket in render_layer_node.outputs
if socket.enabled and socket.name not in exclude_sockets
]
# Remove all output nodes
for node in tree.nodes:
if node.bl_idname == "CompositorNodeOutputFile":
tree.nodes.remove(node)
# Create a new output node
output = tree.nodes.new("CompositorNodeOutputFile")
output = tree.nodes.new(output_type)
image_settings = bpy.context.scene.render.image_settings
output.format.file_format = image_settings.file_format
slots = None
# In case of a multilayer exr, we don't need to use the output node,
# because the blender render already outputs a multilayer exr.
if ext == "exr" and multilayer:
output.layer_slots.clear()
return []
multi_exr = ext == "exr" and multilayer
slots = output.layer_slots if multi_exr else output.file_slots
output.base_path = render_product if multi_exr else str(output_path)
output.file_slots.clear()
output.base_path = str(output_path)
slots.clear()
aov_file_products = []
old_links = {
link.from_socket.name: link for link in tree.links
if link.to_node == old_output_node}
# Create a new socket for the beauty output
pass_name = "rgba" if multi_exr else "beauty"
slot, _ = _create_aov_slot(
name, aov_sep, slots, pass_name, multi_exr, output_path)
tree.links.new(render_layer_node.outputs["Image"], slot)
if compositing:
# Create a new socket for the composite output
pass_name = "composite"
comp_socket, filepath = _create_aov_slot(
name, aov_sep, slots, pass_name, multi_exr, output_path)
aov_file_products.append(("Composite", filepath))
# For each active render pass, we add a new socket to the output node
# and link it
for render_pass in passes:
filepath = f"{name}{aov_sep}{render_pass.name}.####"
for rpass in passes:
slot, filepath = _create_aov_slot(
name, aov_sep, slots, rpass.name, multi_exr, output_path)
aov_file_products.append((rpass.name, filepath))
output.file_slots.new(filepath)
# If the rpass was not connected with the old output node, we connect
# it with the new one.
if not old_links.get(rpass.name):
tree.links.new(rpass, slot)
filename = str(output_path / filepath.lstrip("/"))
for link in list(old_links.values()):
# Check if the socket is still available in the new output node.
socket = output.inputs.get(link.to_socket.name)
# If it is, we connect it with the new output node.
if socket:
tree.links.new(link.from_socket, socket)
# Then, we remove the old link.
tree.links.remove(link)
aov_file_products.append((render_pass.name, filename))
# If there's a composite node, we connect its input with the new output
if compositing and composite_node:
for link in tree.links:
if link.to_node == composite_node:
tree.links.new(link.from_socket, comp_socket)
break
node_input = output.inputs[-1]
if old_output_node:
output.location = old_output_node.location
tree.nodes.remove(old_output_node)
tree.links.new(render_pass, node_input)
output.name = "AYON File Output"
output.label = "AYON File Output"
return aov_file_products
return [] if multi_exr else aov_file_products
def imprint_render_settings(node, data):
@ -228,17 +331,23 @@ def prepare_rendering(asset_group):
aov_sep = get_aov_separator(settings)
ext = get_image_format(settings)
multilayer = get_multilayer(settings)
renderer = get_renderer(settings)
compositing = get_compositing(settings)
set_render_format(ext, multilayer)
aov_list, custom_passes = set_render_passes(settings)
bpy.context.scene.render.engine = renderer
aov_list, custom_passes = set_render_passes(settings, renderer)
output_path = Path.joinpath(dirpath, render_folder, file_name)
render_product = get_render_product(output_path, name, aov_sep)
aov_file_product = set_node_tree(
output_path, name, aov_sep, ext, multilayer)
output_path, render_product, name, aov_sep,
ext, multilayer, compositing)
bpy.context.scene.render.filepath = render_product
# Clear the render filepath, so that the output is handled only by the
# output node in the compositor.
bpy.context.scene.render.filepath = ""
render_settings = {
"render_folder": render_folder,

View file

@ -82,7 +82,7 @@ def file_extensions() -> List[str]:
def work_root(session: dict) -> str:
"""Return the default root to browse for work files."""
work_dir = session["AVALON_WORKDIR"]
work_dir = session["AYON_WORKDIR"]
scene_dir = session.get("AVALON_SCENEDIR")
if scene_dir:
return str(Path(work_dir, scene_dir))

View file

@ -1,24 +1,24 @@
# -*- coding: utf-8 -*-
"""Converter for legacy Houdini subsets."""
"""Converter for legacy Houdini products."""
from ayon_core.pipeline.create.creator_plugins import SubsetConvertorPlugin
from ayon_core.hosts.blender.api.lib import imprint
class BlenderLegacyConvertor(SubsetConvertorPlugin):
"""Find and convert any legacy subsets in the scene.
"""Find and convert any legacy products in the scene.
This Converter will find all legacy subsets in the scene and will
transform them to the current system. Since the old subsets doesn't
This Converter will find all legacy products in the scene and will
transform them to the current system. Since the old products doesn't
retain any information about their original creators, the only mapping
we can do is based on their families.
we can do is based on their product types.
Its limitation is that you can have multiple creators creating subset
of the same family and there is no way to handle it. This code should
nevertheless cover all creators that came with OpenPype.
Its limitation is that you can have multiple creators creating product
of the same product type and there is no way to handle it. This code
should nevertheless cover all creators that came with OpenPype.
"""
identifier = "io.openpype.creators.blender.legacy"
family_to_id = {
product_type_to_id = {
"action": "io.openpype.creators.blender.action",
"camera": "io.openpype.creators.blender.camera",
"animation": "io.openpype.creators.blender.animation",
@ -33,42 +33,42 @@ class BlenderLegacyConvertor(SubsetConvertorPlugin):
def __init__(self, *args, **kwargs):
super(BlenderLegacyConvertor, self).__init__(*args, **kwargs)
self.legacy_subsets = {}
self.legacy_instances = {}
def find_instances(self):
"""Find legacy subsets in the scene.
"""Find legacy products in the scene.
Legacy subsets are the ones that doesn't have `creator_identifier`
Legacy products are the ones that doesn't have `creator_identifier`
parameter on them.
This is using cached entries done in
:py:meth:`~BaseCreator.cache_subsets()`
:py:meth:`~BaseCreator.cache_instance_data()`
"""
self.legacy_subsets = self.collection_shared_data.get(
"blender_cached_legacy_subsets")
if not self.legacy_subsets:
self.legacy_instances = self.collection_shared_data.get(
"blender_cached_legacy_instances")
if not self.legacy_instances:
return
self.add_convertor_item(
"Found {} incompatible subset{}".format(
len(self.legacy_subsets),
"s" if len(self.legacy_subsets) > 1 else ""
"Found {} incompatible product{}".format(
len(self.legacy_instances),
"s" if len(self.legacy_instances) > 1 else ""
)
)
def convert(self):
"""Convert all legacy subsets to current.
"""Convert all legacy products to current.
It is enough to add `creator_identifier` and `instance_node`.
"""
if not self.legacy_subsets:
if not self.legacy_instances:
return
for family, instance_nodes in self.legacy_subsets.items():
if family in self.family_to_id:
for product_type, instance_nodes in self.legacy_instances.items():
if product_type in self.product_type_to_id:
for instance_node in instance_nodes:
creator_identifier = self.family_to_id[family]
creator_identifier = self.product_type_to_id[product_type]
self.log.info(
"Converting {} to {}".format(instance_node.name,
creator_identifier)

View file

@ -10,19 +10,21 @@ class CreateAction(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.action"
label = "Action"
family = "action"
product_type = "action"
icon = "male"
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
subset_name, instance_data, pre_create_data
product_name, instance_data, pre_create_data
)
# Get instance name
name = plugin.prepare_scene_name(instance_data["asset"], subset_name)
name = plugin.prepare_scene_name(
instance_data["folderPath"], product_name
)
if pre_create_data.get("use_selection"):
for obj in lib.get_selection():

View file

@ -8,15 +8,15 @@ class CreateAnimation(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.animation"
label = "Animation"
family = "animation"
product_type = "animation"
icon = "male"
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
subset_name, instance_data, pre_create_data
product_name, instance_data, pre_create_data
)
if pre_create_data.get("use_selection"):

View file

@ -10,16 +10,16 @@ class CreateBlendScene(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.blendscene"
label = "Blender Scene"
family = "blendScene"
product_type = "blendScene"
icon = "cubes"
maintain_selection = False
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
instance_node = super().create(subset_name,
instance_node = super().create(product_name,
instance_data,
pre_create_data)

View file

@ -11,16 +11,16 @@ class CreateCamera(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.camera"
label = "Camera"
family = "camera"
product_type = "camera"
icon = "video-camera"
create_as_asset_group = True
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
asset_group = super().create(subset_name,
asset_group = super().create(product_name,
instance_data,
pre_create_data)
@ -30,8 +30,8 @@ class CreateCamera(plugin.BaseCreator):
obj.parent = asset_group
else:
plugin.deselect_all()
camera = bpy.data.cameras.new(subset_name)
camera_obj = bpy.data.objects.new(subset_name, camera)
camera = bpy.data.cameras.new(product_name)
camera_obj = bpy.data.objects.new(product_name, camera)
instances = bpy.data.collections.get(AVALON_INSTANCES)
instances.objects.link(camera_obj)

View file

@ -10,16 +10,16 @@ class CreateLayout(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.layout"
label = "Layout"
family = "layout"
product_type = "layout"
icon = "cubes"
create_as_asset_group = True
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
asset_group = super().create(subset_name,
asset_group = super().create(product_name,
instance_data,
pre_create_data)

View file

@ -10,15 +10,15 @@ class CreateModel(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.model"
label = "Model"
family = "model"
product_type = "model"
icon = "cube"
create_as_asset_group = True
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
asset_group = super().create(subset_name,
asset_group = super().create(product_name,
instance_data,
pre_create_data)

View file

@ -8,15 +8,15 @@ class CreatePointcache(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.pointcache"
label = "Point Cache"
family = "pointcache"
product_type = "pointcache"
icon = "gears"
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
subset_name, instance_data, pre_create_data
product_name, instance_data, pre_create_data
)
if pre_create_data.get("use_selection"):

View file

@ -1,8 +1,10 @@
"""Create render."""
import bpy
from ayon_core.lib import version_up
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.render_lib import prepare_rendering
from ayon_core.hosts.blender.api.workio import save_file
class CreateRenderlayer(plugin.BaseCreator):
@ -10,16 +12,16 @@ class CreateRenderlayer(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.render"
label = "Render"
family = "render"
product_type = "render"
icon = "eye"
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
try:
# Run parent create method
collection = super().create(
subset_name, instance_data, pre_create_data
product_name, instance_data, pre_create_data
)
prepare_rendering(collection)
@ -37,6 +39,7 @@ class CreateRenderlayer(plugin.BaseCreator):
# settings. Even the validator to check that the file is saved will
# detect the file as saved, even if it isn't. The only solution for
# now it is to force the file to be saved.
bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
filepath = version_up(bpy.data.filepath)
save_file(filepath, copy=False)
return collection

View file

@ -8,15 +8,15 @@ class CreateReview(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.review"
label = "Review"
family = "review"
product_type = "review"
icon = "video-camera"
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
# Run parent create method
collection = super().create(
subset_name, instance_data, pre_create_data
product_name, instance_data, pre_create_data
)
if pre_create_data.get("use_selection"):

View file

@ -10,15 +10,15 @@ class CreateRig(plugin.BaseCreator):
identifier = "io.openpype.creators.blender.rig"
label = "Rig"
family = "rig"
product_type = "rig"
icon = "wheelchair"
create_as_asset_group = True
def create(
self, subset_name: str, instance_data: dict, pre_create_data: dict
self, product_name: str, instance_data: dict, pre_create_data: dict
):
asset_group = super().create(subset_name,
asset_group = super().create(product_name,
instance_data,
pre_create_data)

View file

@ -19,7 +19,7 @@ class CreateWorkfile(BaseCreator, AutoCreator):
"""
identifier = "io.openpype.creators.blender.workfile"
label = "Workfile"
family = "workfile"
product_type = "workfile"
icon = "fa5.file"
def create(self):
@ -43,8 +43,12 @@ class CreateWorkfile(BaseCreator, AutoCreator):
if not workfile_instance:
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
task_name, task_name, asset_doc, project_name, host_name
product_name = self.get_product_name(
project_name,
asset_doc,
task_name,
task_name,
host_name,
)
data = {
"folderPath": asset_name,
@ -53,17 +57,17 @@ class CreateWorkfile(BaseCreator, AutoCreator):
}
data.update(
self.get_dynamic_data(
task_name,
task_name,
asset_doc,
project_name,
asset_doc,
task_name,
task_name,
host_name,
workfile_instance,
)
)
self.log.info("Auto-creating workfile instance...")
workfile_instance = CreatedInstance(
self.family, subset_name, data, self
self.product_type, product_name, data, self
)
self._add_instance_to_context(workfile_instance)
@ -73,13 +77,17 @@ class CreateWorkfile(BaseCreator, AutoCreator):
):
# Update instance context if it's different
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
task_name, task_name, asset_doc, project_name, host_name
product_name = self.get_product_name(
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
)
workfile_instance["folderPath"] = asset_name
workfile_instance["task"] = task_name
workfile_instance["subset"] = subset_name
workfile_instance["productName"] = product_name
instance_node = bpy.data.collections.get(AVALON_CONTAINERS)
if not instance_node:

View file

@ -4,10 +4,10 @@ from ayon_core.hosts.blender.api import plugin
def append_workfile(context, fname, do_import):
asset = context['asset']['name']
subset = context['subset']['name']
folder_name = context['asset']['name']
product_name = context['subset']['name']
group_name = plugin.prepare_scene_name(asset, subset)
group_name = plugin.prepare_scene_name(folder_name, product_name)
# We need to preserve the original names of the scenes, otherwise,
# if there are duplicate names in the current workfile, the imported

View file

@ -134,13 +134,15 @@ class CacheModelLoader(plugin.AssetLoader):
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
containers = bpy.data.collections.get(AVALON_CONTAINERS)
if not containers:
@ -159,6 +161,7 @@ class CacheModelLoader(plugin.AssetLoader):
self._link_objects(objects, asset_group, containers, asset_group)
product_type = context["subset"]["data"]["family"]
asset_group[AVALON_PROPERTY] = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
@ -169,14 +172,14 @@ class CacheModelLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": product_type,
"objectName": group_name
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
@ -188,15 +191,16 @@ class CacheModelLoader(plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
repre_doc = context["representation"]
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert asset_group, (
@ -241,7 +245,7 @@ class CacheModelLoader(plugin.AssetLoader):
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["representation"] = str(repre_doc["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.

View file

@ -44,11 +44,11 @@ class BlendActionLoader(plugin.AssetLoader):
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = plugin.prepare_scene_name(asset, subset)
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
lib_container = plugin.prepare_scene_name(folder_name, product_name)
container_name = plugin.prepare_scene_name(
asset, subset, namespace
folder_name, product_name, namespace
)
container = bpy.data.collections.new(lib_container)
@ -114,7 +114,7 @@ class BlendActionLoader(plugin.AssetLoader):
self[:] = nodes
return nodes
def update(self, container: Dict, representation: Dict):
def update(self, container: Dict, context: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
@ -126,18 +126,18 @@ class BlendActionLoader(plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
repre_doc = context["representation"]
collection = bpy.data.collections.get(
container["objectName"]
)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
logger.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert collection, (
@ -241,7 +241,7 @@ class BlendActionLoader(plugin.AssetLoader):
# Save the list of objects in the metadata container
collection_metadata["objects"] = objects_list
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
collection_metadata["representation"] = str(repre_doc["_id"])
bpy.ops.object.select_all(action='DESELECT')

View file

@ -39,13 +39,15 @@ class AudioLoader(plugin.AssetLoader):
options: Additional settings dictionary
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
@ -85,7 +87,7 @@ class AudioLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name,
"audio": audio
}
@ -94,7 +96,7 @@ class AudioLoader(plugin.AssetLoader):
self[:] = objects
return [objects]
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Update an audio strip in the sequence editor.
Arguments:
@ -103,14 +105,15 @@ class AudioLoader(plugin.AssetLoader):
representation (openpype:representation-1.0): Representation to
update, from `host.ls()`.
"""
repre_doc = context["representation"]
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert asset_group, (
@ -173,8 +176,8 @@ class AudioLoader(plugin.AssetLoader):
window_manager.windows[-1].screen.areas[0].type = old_type
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["parent"] = str(representation["parent"])
metadata["representation"] = str(repre_doc["_id"])
metadata["parent"] = str(repre_doc["parent"])
metadata["audio"] = new_audio
def exec_remove(self, container: Dict) -> bool:

View file

@ -127,20 +127,22 @@ class BlendLoader(plugin.AssetLoader):
options: Additional settings dictionary
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
try:
family = context["representation"]["context"]["family"]
product_type = context["subset"]["data"]["family"]
except ValueError:
family = "model"
product_type = "model"
representation = str(context["representation"]["_id"])
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
@ -149,8 +151,8 @@ class BlendLoader(plugin.AssetLoader):
container, members = self._process_data(libpath, group_name)
if family == "layout":
self._post_process_layout(container, asset, representation)
if product_type == "layout":
self._post_process_layout(container, folder_name, representation)
avalon_container.objects.link(container)
@ -164,7 +166,7 @@ class BlendLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name,
"members": members,
}
@ -179,13 +181,14 @@ class BlendLoader(plugin.AssetLoader):
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""
Update the loaded asset.
"""
repre_doc = context["representation"]
group_name = container["objectName"]
asset_group = bpy.data.objects.get(group_name)
libpath = Path(get_representation_path(representation)).as_posix()
libpath = Path(get_representation_path(repre_doc)).as_posix()
assert asset_group, (
f"The asset is not loaded: {container['objectName']}"
@ -232,8 +235,8 @@ class BlendLoader(plugin.AssetLoader):
new_data = {
"libpath": libpath,
"representation": str(representation["_id"]),
"parent": str(representation["parent"]),
"representation": str(repre_doc["_id"]),
"parent": str(repre_doc["parent"]),
"members": members,
}

View file

@ -34,7 +34,7 @@ class BlendSceneLoader(plugin.AssetLoader):
return None
def _process_data(self, libpath, group_name, family):
def _process_data(self, libpath, group_name, product_type):
# Append all the data from the .blend file
with bpy.data.libraries.load(
libpath, link=False, relative=False
@ -82,25 +82,29 @@ class BlendSceneLoader(plugin.AssetLoader):
options: Additional settings dictionary
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
try:
family = context["representation"]["context"]["family"]
product_type = context["subset"]["data"]["family"]
except ValueError:
family = "model"
product_type = "model"
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
bpy.context.scene.collection.children.link(avalon_container)
container, members = self._process_data(libpath, group_name, family)
container, members = self._process_data(
libpath, group_name, product_type
)
avalon_container.children.link(container)
@ -114,7 +118,7 @@ class BlendSceneLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name,
"members": members,
}
@ -129,13 +133,14 @@ class BlendSceneLoader(plugin.AssetLoader):
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""
Update the loaded asset.
"""
repre_doc = context["representation"]
group_name = container["objectName"]
asset_group = bpy.data.collections.get(group_name)
libpath = Path(get_representation_path(representation)).as_posix()
libpath = Path(get_representation_path(repre_doc)).as_posix()
assert asset_group, (
f"The asset is not loaded: {container['objectName']}"
@ -167,8 +172,12 @@ class BlendSceneLoader(plugin.AssetLoader):
self.exec_remove(container)
family = container["family"]
asset_group, members = self._process_data(libpath, group_name, family)
product_type = container.get("productType")
if product_type is None:
product_type = container["family"]
asset_group, members = self._process_data(
libpath, group_name, product_type
)
for member in members:
if member.name in collection_parents:
@ -193,8 +202,8 @@ class BlendSceneLoader(plugin.AssetLoader):
new_data = {
"libpath": libpath,
"representation": str(representation["_id"]),
"parent": str(representation["parent"]),
"representation": str(repre_doc["_id"]),
"parent": str(repre_doc["parent"]),
"members": members,
}

View file

@ -84,13 +84,15 @@ class AbcCameraLoader(plugin.AssetLoader):
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
@ -121,14 +123,14 @@ class AbcCameraLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name,
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
@ -140,15 +142,16 @@ class AbcCameraLoader(plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
repre_doc = context["representation"]
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert asset_group, (
@ -183,7 +186,7 @@ class AbcCameraLoader(plugin.AssetLoader):
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["representation"] = str(repre_doc["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.

View file

@ -87,13 +87,15 @@ class FbxCameraLoader(plugin.AssetLoader):
options: Additional settings dictionary
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
@ -124,14 +126,14 @@ class FbxCameraLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
@ -143,15 +145,16 @@ class FbxCameraLoader(plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
repre_doc = context["representation"]
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert asset_group, (
@ -193,7 +196,7 @@ class FbxCameraLoader(plugin.AssetLoader):
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["representation"] = str(repre_doc["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.

View file

@ -131,13 +131,15 @@ class FbxModelLoader(plugin.AssetLoader):
options: Additional settings dictionary
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
@ -168,14 +170,14 @@ class FbxModelLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
@ -187,15 +189,16 @@ class FbxModelLoader(plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
repre_doc = context["representation"]
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert asset_group, (
@ -248,7 +251,7 @@ class FbxModelLoader(plugin.AssetLoader):
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["representation"] = str(repre_doc["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.

View file

@ -50,11 +50,11 @@ class JsonLayoutLoader(plugin.AssetLoader):
if anim_collection:
bpy.data.collections.remove(anim_collection)
def _get_loader(self, loaders, family):
def _get_loader(self, loaders, product_type):
name = ""
if family == 'rig':
if product_type == 'rig':
name = "BlendRigLoader"
elif family == 'model':
elif product_type == 'model':
name = "BlendModelLoader"
if name == "":
@ -76,10 +76,12 @@ class JsonLayoutLoader(plugin.AssetLoader):
for element in data:
reference = element.get('reference')
family = element.get('family')
product_type = element.get("product_type")
if product_type is None:
product_type = element.get("family")
loaders = loaders_from_representation(all_loaders, reference)
loader = self._get_loader(loaders, family)
loader = self._get_loader(loaders, product_type)
if not loader:
continue
@ -95,7 +97,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
'parent': asset_group,
'transform': element.get('transform'),
'action': action,
'create_animation': True if family == 'rig' else False,
'create_animation': True if product_type == 'rig' else False,
'animation_asset': asset
}
@ -127,7 +129,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
# legacy_create(
# creator_plugin,
# name="camera",
# # name=f"{unique_number}_{subset}_animation",
# # name=f"{unique_number}_{product[name]}_animation",
# asset=asset,
# options={"useSelection": False}
# # data={"dependencies": str(context["representation"]["_id"])}
@ -146,13 +148,15 @@ class JsonLayoutLoader(plugin.AssetLoader):
options: Additional settings dictionary
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
asset_name = plugin.prepare_scene_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.prepare_scene_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
asset_name = plugin.prepare_scene_name(folder_name, product_name)
unique_number = plugin.get_unique_number(folder_name, product_name)
group_name = plugin.prepare_scene_name(
folder_name, product_name, unique_number
)
namespace = namespace or f"{folder_name}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
@ -177,14 +181,14 @@ class JsonLayoutLoader(plugin.AssetLoader):
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"productType": context["subset"]["data"]["family"],
"objectName": group_name
}
self[:] = asset_group.children
return asset_group.children
def exec_update(self, container: Dict, representation: Dict):
def exec_update(self, container: Dict, context: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
@ -193,15 +197,16 @@ class JsonLayoutLoader(plugin.AssetLoader):
will not be removed, only unlinked. Normally this should not be the
case though.
"""
repre_doc = context["representation"]
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(get_representation_path(representation))
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert asset_group, (
@ -239,7 +244,10 @@ class JsonLayoutLoader(plugin.AssetLoader):
for obj in asset_group.children:
obj_meta = obj.get(AVALON_PROPERTY)
if obj_meta.get('family') == 'rig':
product_type = obj_meta.get("productType")
if product_type is None:
product_type = obj_meta.get("family")
if product_type == "rig":
rig = None
for child in obj.children:
if child.type == 'ARMATURE':
@ -262,7 +270,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["representation"] = str(repre_doc["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.

View file

@ -93,18 +93,18 @@ class BlendLookLoader(plugin.AssetLoader):
"""
libpath = self.filepath_from_context(context)
asset = context["asset"]["name"]
subset = context["subset"]["name"]
folder_name = context["asset"]["name"]
product_name = context["subset"]["name"]
lib_container = plugin.prepare_scene_name(
asset, subset
folder_name, product_name
)
unique_number = plugin.get_unique_number(
asset, subset
folder_name, product_name
)
namespace = namespace or f"{asset}_{unique_number}"
namespace = namespace or f"{folder_name}_{unique_number}"
container_name = plugin.prepare_scene_name(
asset, subset, unique_number
folder_name, product_name, unique_number
)
container = bpy.data.collections.new(lib_container)
@ -131,22 +131,23 @@ class BlendLookLoader(plugin.AssetLoader):
metadata["materials"] = materials
metadata["parent"] = str(context["representation"]["parent"])
metadata["family"] = context["representation"]["context"]["family"]
metadata["product_type"] = context["subset"]["data"]["family"]
nodes = list(container.objects)
nodes.append(container)
self[:] = nodes
return nodes
def update(self, container: Dict, representation: Dict):
def update(self, container: Dict, context: Dict):
collection = bpy.data.collections.get(container["objectName"])
libpath = Path(get_representation_path(representation))
repre_doc = context["representation"]
libpath = Path(get_representation_path(repre_doc))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
pformat(repre_doc, indent=2),
)
assert collection, (
@ -201,7 +202,7 @@ class BlendLookLoader(plugin.AssetLoader):
collection_metadata["objects"] = objects
collection_metadata["materials"] = materials
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
collection_metadata["representation"] = str(repre_doc["_id"])
def remove(self, container: Dict) -> bool:
collection = bpy.data.collections.get(container["objectName"])

View file

@ -25,7 +25,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin):
members.extend(instance_node.children)
# Special case for animation instances, include armatures
if instance.data["family"] == "animation":
if instance.data["productType"] == "animation":
for obj in instance_node.objects:
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
members.extend(

View file

@ -19,9 +19,9 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.abc"
filepath = os.path.join(stagingdir, filename)

View file

@ -23,9 +23,9 @@ class ExtractAnimationABC(
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.abc"
filepath = os.path.join(stagingdir, filename)

View file

@ -20,9 +20,9 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin):
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.blend"
filepath = os.path.join(stagingdir, filename)

View file

@ -23,9 +23,9 @@ class ExtractBlendAnimation(
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.blend"
filepath = os.path.join(stagingdir, filename)

View file

@ -21,9 +21,9 @@ class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.abc"
filepath = os.path.join(stagingdir, filename)

View file

@ -20,9 +20,9 @@ class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin):
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.fbx"
filepath = os.path.join(stagingdir, filename)

View file

@ -21,9 +21,9 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):
# Define extract output file path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
filename = f"{instance_name}.fbx"
filepath = os.path.join(stagingdir, filename)

View file

@ -145,9 +145,9 @@ class ExtractAnimationFBX(
root.select_set(True)
armature.select_set(True)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
fbx_filename = f"{instance_name}_{armature.name}.fbx"
filepath = os.path.join(stagingdir, fbx_filename)

View file

@ -133,7 +133,7 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
fbx_count = 0
project_name = instance.context.data["projectEntity"]["name"]
project_name = instance.context.data["projectName"]
for asset in asset_group.children:
metadata = asset.get(AVALON_PROPERTY)
if not metadata:
@ -147,7 +147,9 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
continue
version_id = metadata["parent"]
family = metadata["family"]
product_type = metadata.get("product_type")
if product_type is None:
product_type = metadata["family"]
self.log.debug("Parent: {}".format(version_id))
# Get blend reference
@ -179,7 +181,8 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
json_element["reference_fbx"] = str(fbx_id)
if abc_id:
json_element["reference_abc"] = str(abc_id)
json_element["family"] = family
json_element["product_type"] = product_type
json_element["instance_name"] = asset.name
json_element["asset_name"] = metadata["asset_name"]
json_element["file_path"] = metadata["libpath"]
@ -215,7 +218,7 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
]
# Extract the animation as well
if family == "rig":
if product_type == "rig":
f, n = self._export_animation(
asset, instance, stagingdir, fbx_count)
if f:
@ -225,9 +228,9 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin):
json_data.append(json_element)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
instance_name = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
instance_name = f"{folder_name}_{product_name}"
json_filename = f"{instance_name}.json"
json_path = os.path.join(stagingdir, json_filename)

View file

@ -1,9 +1,11 @@
import os
import json
import clique
import pyblish.api
import bpy
import pyblish.api
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import capture
from ayon_core.hosts.blender.api.lib import maintained_time
@ -23,6 +25,8 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
optional = True
order = pyblish.api.ExtractorOrder + 0.01
presets = "{}"
def process(self, instance):
if not self.is_active(instance.data):
return
@ -51,16 +55,15 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin):
# get output path
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
filename = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
filename = f"{folder_name}_{product_name}"
path = os.path.join(stagingdir, filename)
self.log.debug(f"Outputting images to {path}")
project_settings = instance.context.data["project_settings"]["blender"]
presets = project_settings["publish"]["ExtractPlayblast"]["presets"]
presets = json.loads(self.presets)
preset = presets.get("default")
preset.update({
"camera": camera,

View file

@ -1,5 +1,6 @@
import os
import glob
import json
import pyblish.api
from ayon_core.pipeline import publish
@ -21,7 +22,7 @@ class ExtractThumbnail(publish.Extractor):
hosts = ["blender"]
families = ["review"]
order = pyblish.api.ExtractorOrder + 0.01
presets = {}
presets = "{}"
def process(self, instance):
self.log.debug("Extracting capture..")
@ -31,9 +32,9 @@ class ExtractThumbnail(publish.Extractor):
return
stagingdir = self.staging_dir(instance)
asset_name = instance.data["assetEntity"]["name"]
subset = instance.data["subset"]
filename = f"{asset_name}_{subset}"
folder_name = instance.data["assetEntity"]["name"]
product_name = instance.data["productName"]
filename = f"{folder_name}_{product_name}"
path = os.path.join(stagingdir, filename)
@ -41,10 +42,11 @@ class ExtractThumbnail(publish.Extractor):
camera = instance.data.get("review_camera", "AUTO")
start = instance.data.get("frameStart", bpy.context.scene.frame_start)
family = instance.data.get("family")
product_type = instance.data["productType"]
isolate = instance.data("isolate", None)
preset = self.presets.get(family, {})
presets = json.loads(self.presets)
preset = presets.get(product_type, {})
preset.update({
"camera": camera,

View file

@ -28,25 +28,26 @@ class IntegrateAnimation(
# Update the json file for the setdress to add the published
# representations of the animations
for json_dict in data:
json_product_name = json_dict["productName"]
i = None
for elem in instance.context:
if elem.data.get('subset') == json_dict['subset']:
if elem.data["productName"] == json_product_name:
i = elem
break
if not i:
continue
rep = None
pub_repr = i.data.get('published_representations')
pub_repr = i.data["published_representations"]
for elem in pub_repr:
if pub_repr.get(elem).get('representation').get('name') == "fbx":
rep = pub_repr.get(elem)
if pub_repr[elem]["representation"]["name"] == "fbx":
rep = pub_repr[elem]
break
if not rep:
continue
obj_id = rep.get('representation').get('_id')
obj_id = rep["representation"]["_id"]
if obj_id:
json_dict['_id'] = str(obj_id)
json_dict["representation_id"] = str(obj_id)
with open(json_path, "w") as file:
json.dump(data, fp=file, indent=2)

View file

@ -28,15 +28,27 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
def process(self, instance):
if not self.is_active(instance.data):
return
tree = bpy.context.scene.node_tree
output_type = "CompositorNodeOutputFile"
output_node = None
# Remove all output nodes that inlcude "AYON" in the name.
# There should be only one.
for node in tree.nodes:
if node.bl_idname == output_type and "AYON" in node.name:
output_node = node
break
if not output_node:
raise PublishValidationError(
"No output node found in the compositor tree."
)
filepath = bpy.data.filepath
file = os.path.basename(filepath)
filename, ext = os.path.splitext(file)
if filename not in bpy.context.scene.render.filepath:
if filename not in output_node.base_path:
raise PublishValidationError(
"Render output folder "
"doesn't match the blender scene name! "
"Use Repair action to "
"fix the folder file path."
"Render output folder doesn't match the blender scene name! "
"Use Repair action to fix the folder file path."
)
@classmethod

View file

@ -26,6 +26,7 @@ class ValidateFileSaved(pyblish.api.ContextPlugin,
hosts = ["blender"]
label = "Validate File Saved"
optional = False
# TODO rename to 'exclude_product_types'
exclude_families = []
actions = [SaveWorkfileAction]
@ -41,8 +42,8 @@ class ValidateFileSaved(pyblish.api.ContextPlugin,
# Do not validate workfile has unsaved changes if only instances
# present of families that should be excluded
families = {
instance.data["family"] for instance in context
product_types = {
instance.data["productType"] for instance in context
# Consider only enabled instances
if instance.data.get("publish", True)
and instance.data.get("active", True)
@ -52,7 +53,7 @@ class ValidateFileSaved(pyblish.api.ContextPlugin,
return any(family in exclude_family
for exclude_family in self.exclude_families)
if all(is_excluded(family) for family in families):
if all(is_excluded(product_type) for product_type in product_types):
self.log.debug("Only excluded families found, skipping workfile "
"unsaved changes validation..")
return

View file

@ -1,16 +1,13 @@
import os
from ayon_core.modules import OpenPypeModule, IHostAddon
from ayon_core.addon import AYONAddon, IHostAddon
CELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
class CelactionAddon(OpenPypeModule, IHostAddon):
class CelactionAddon(AYONAddon, IHostAddon):
name = "celaction"
host_name = "celaction"
def initialize(self, module_settings):
self.enabled = True
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []

View file

@ -22,7 +22,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
asset_name = get_asset_name_identifier(asset_entity)
shared_instance_data = {
"asset": asset_name,
"folderPath": asset_name,
"frameStart": asset_entity["data"]["frameStart"],
"frameEnd": asset_entity["data"]["frameEnd"],
"handleStart": asset_entity["data"]["handleStart"],
@ -46,17 +46,18 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
shared_instance_data.update(celaction_kwargs)
# workfile instance
family = "workfile"
subset = family + task.capitalize()
product_type = "workfile"
product_name = product_type + task.capitalize()
# Create instance
instance = context.create_instance(subset)
instance = context.create_instance(product_name)
# creating instance data
instance.data.update({
"subset": subset,
"label": scene_file,
"family": family,
"families": [],
"productName": product_name,
"productType": product_type,
"family": product_type,
"families": [product_type],
"representations": []
})
@ -76,17 +77,19 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin):
self.log.info('Publishing Celaction workfile')
# render instance
subset = f"render{task}Main"
instance = context.create_instance(name=subset)
product_name = f"render{task}Main"
product_type = "render.farm"
instance = context.create_instance(name=product_name)
# getting instance state
instance.data["publish"] = True
# add assetEntity data into instance
instance.data.update({
"label": "{} - farm".format(subset),
"family": "render.farm",
"families": [],
"subset": subset
"label": "{} - farm".format(product_name),
"productType": product_type,
"family": product_type,
"families": [product_type],
"productName": product_name
})
# adding basic script data

View file

@ -19,12 +19,14 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
anatomy = instance.context.data["anatomy"]
anatomy_data = copy.deepcopy(instance.data["anatomyData"])
padding = anatomy.templates.get("frame_padding", 4)
product_type = "render"
anatomy_data.update({
"frame": f"%0{padding}d",
"family": "render",
"family": product_type,
"representation": self.output_extension,
"ext": self.output_extension
})
anatomy_data["product"]["type"] = product_type
anatomy_filled = anatomy.format(anatomy_data)

View file

@ -1,16 +1,13 @@
import os
from ayon_core.modules import OpenPypeModule, IHostAddon
from ayon_core.addon import AYONAddon, IHostAddon
HOST_DIR = os.path.dirname(os.path.abspath(__file__))
class FlameAddon(OpenPypeModule, IHostAddon):
class FlameAddon(AYONAddon, IHostAddon):
name = "flame"
host_name = "flame"
def initialize(self, module_settings):
self.enabled = True
def add_implementation_envs(self, env, _app):
# Add requirements to DL_PYTHON_HOOK_PATH
env["DL_PYTHON_HOOK_PATH"] = os.path.join(HOST_DIR, "startup")

View file

@ -147,8 +147,8 @@ def imprint(segment, data=None):
Examples:
data = {
'asset': 'sq020sh0280',
'family': 'render',
'subset': 'subsetMain'
'productType': 'render',
'productName': 'productMain'
}
"""
data = data or {}

View file

@ -353,9 +353,9 @@ class PublishableClip:
rename_default = False
hierarchy_default = "{_folder_}/{_sequence_}/{_track_}"
clip_name_default = "shot_{_trackIndex_:0>3}_{_clipIndex_:0>4}"
subset_name_default = "[ track name ]"
review_track_default = "[ none ]"
subset_family_default = "plate"
base_product_name_default = "[ track name ]"
base_product_type_default = "plate"
count_from_default = 10
count_steps_default = 10
vertical_sync_default = False
@ -368,7 +368,7 @@ class PublishableClip:
def __init__(self, segment, **kwargs):
self.rename_index = kwargs["rename_index"]
self.family = kwargs["family"]
self.product_type = kwargs["family"]
self.log = kwargs["log"]
# get main parent objects
@ -486,10 +486,10 @@ class PublishableClip:
"countFrom", {}).get("value") or self.count_from_default
self.count_steps = self.ui_inputs.get(
"countSteps", {}).get("value") or self.count_steps_default
self.subset_name = self.ui_inputs.get(
"subsetName", {}).get("value") or self.subset_name_default
self.subset_family = self.ui_inputs.get(
"subsetFamily", {}).get("value") or self.subset_family_default
self.base_product_name = self.ui_inputs.get(
"productName", {}).get("value") or self.base_product_name_default
self.base_product_type = self.ui_inputs.get(
"productType", {}).get("value") or self.base_product_type_default
self.vertical_sync = self.ui_inputs.get(
"vSyncOn", {}).get("value") or self.vertical_sync_default
self.driving_layer = self.ui_inputs.get(
@ -509,12 +509,14 @@ class PublishableClip:
or self.retimed_framerange_default
)
# build subset name from layer name
if self.subset_name == "[ track name ]":
self.subset_name = self.track_name
# build product name from layer name
if self.base_product_name == "[ track name ]":
self.base_product_name = self.track_name
# create subset for publishing
self.subset = self.subset_family + self.subset_name.capitalize()
# create product for publishing
self.product_name = (
self.base_product_type + self.base_product_name.capitalize()
)
def _replace_hash_to_expression(self, name, text):
""" Replace hash with number in correct padding. """
@ -608,14 +610,14 @@ class PublishableClip:
_hero_data = deepcopy(hero_data)
_hero_data.update({"heroTrack": False})
if _in <= self.clip_in and _out >= self.clip_out:
data_subset = hero_data["subset"]
data_product_name = hero_data["productName"]
# add track index in case duplicity of names in hero data
if self.subset in data_subset:
_hero_data["subset"] = self.subset + str(
if self.product_name in data_product_name:
_hero_data["productName"] = self.product_name + str(
self.track_index)
# in case track name and subset name is the same then add
if self.subset_name == self.track_name:
_hero_data["subset"] = self.subset
# in case track name and product name is the same then add
if self.base_product_name == self.track_name:
_hero_data["productName"] = self.product_name
# assign data to return hierarchy data to tag
tag_hierarchy_data = _hero_data
break
@ -637,9 +639,9 @@ class PublishableClip:
"hierarchy": hierarchy_filled,
"parents": self.parents,
"hierarchyData": hierarchy_formatting_data,
"subset": self.subset,
"family": self.subset_family,
"families": [self.family]
"productName": self.product_name,
"productType": self.base_product_type,
"families": [self.base_product_type, self.product_type]
}
def _convert_to_entity(self, type, template):
@ -704,7 +706,7 @@ class ClipLoader(LoaderPlugin):
_mapping = None
_host_settings = None
def apply_settings(cls, project_settings, system_settings):
def apply_settings(cls, project_settings):
plugin_type_settings = (
project_settings

View file

@ -34,4 +34,4 @@ def current_file():
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
return os.path.normpath(session["AYON_WORKDIR"]).replace("\\", "/")

View file

@ -6,7 +6,7 @@ class CreateShotClip(opfapi.Creator):
"""Publishable clip"""
label = "Create Publishable Clip"
family = "clip"
product_type = "clip"
icon = "film"
defaults = ["Main"]
@ -32,7 +32,7 @@ class CreateShotClip(opfapi.Creator):
# open widget for plugins inputs
results_back = self.create_widget(
"Pype publish attributes creator",
"AYON publish attributes creator",
"Define sequential rename and fill hierarchy data.",
gui_inputs
)
@ -62,7 +62,7 @@ class CreateShotClip(opfapi.Creator):
"log": self.log,
"ui_inputs": results_back,
"avalon": self.data,
"family": self.data["family"]
"product_type": self.data["productType"]
}
for i, segment in enumerate(sorted_selected_segments):
@ -203,19 +203,19 @@ class CreateShotClip(opfapi.Creator):
"target": "ui",
"order": 3,
"value": {
"subsetName": {
"productName": {
"value": ["[ track name ]", "main", "bg", "fg", "bg",
"animatic"],
"type": "QComboBox",
"label": "Subset Name",
"target": "ui",
"toolTip": "chose subset name pattern, if [ track name ] is selected, name of track layer will be used", # noqa
"toolTip": "chose product name pattern, if [ track name ] is selected, name of track layer will be used", # noqa
"order": 0},
"subsetFamily": {
"productType": {
"value": ["plate", "take"],
"type": "QComboBox",
"label": "Subset Family",
"target": "ui", "toolTip": "What use of this subset is for", # noqa
"target": "ui", "toolTip": "What use of this product is for", # noqa
"order": 1},
"reviewTrack": {
"value": ["< none >"] + gui_tracks,
@ -229,7 +229,7 @@ class CreateShotClip(opfapi.Creator):
"type": "QCheckBox",
"label": "Include audio",
"target": "tag",
"toolTip": "Process subsets with corresponding audio", # noqa
"toolTip": "Process products with corresponding audio", # noqa
"order": 3},
"sourceResolution": {
"value": False,

View file

@ -11,7 +11,7 @@ from ayon_core.lib.transcoding import (
class LoadClip(opfapi.ClipLoader):
"""Load a subset to timeline as clip
"""Load a product to timeline as clip
Place clip to timeline on its asset origin timings collected
during conforming to project
@ -31,14 +31,14 @@ class LoadClip(opfapi.ClipLoader):
# settings
reel_group_name = "OpenPype_Reels"
reel_name = "Loaded"
clip_name_template = "{asset}_{subset}<_{output}>"
clip_name_template = "{folder[name]}_{product[name]}<_{output}>"
""" Anatomy keys from version context data and dynamically added:
- {layerName} - original layer name token
- {layerUID} - original layer UID token
- {originalBasename} - original clip name taken from file
"""
layer_rename_template = "{asset}_{subset}<_{output}>"
layer_rename_template = "{folder[name]}_{product[name]}<_{output}>"
layer_rename_patterns = []
def load(self, context, name, namespace, options):
@ -70,7 +70,7 @@ class LoadClip(opfapi.ClipLoader):
self.log.info("Loading with colorspace: `{}`".format(colorspace))
# create workfile path
workfile_dir = os.environ["AVALON_WORKDIR"]
workfile_dir = os.environ["AYON_WORKDIR"]
openclip_dir = os.path.join(
workfile_dir, clip_name
)
@ -180,27 +180,27 @@ class LoadClip(opfapi.ClipLoader):
# unwrapping segment from input clip
pass
# def switch(self, container, representation):
# self.update(container, representation)
# def switch(self, container, context):
# self.update(container, context)
# def update(self, container, representation):
# def update(self, container, context):
# """ Updating previously loaded clips
# """
# # load clip to timeline and get main variables
# repre_doc = context['representation']
# name = container['name']
# namespace = container['namespace']
# track_item = phiero.get_track_items(
# track_item_name=namespace)
# version = io.find_one({
# "type": "version",
# "_id": representation["parent"]
# "_id": repre_doc["parent"]
# })
# version_data = version.get("data", {})
# version_name = version.get("name", None)
# colorspace = version_data.get("colorspace", None)
# object_name = "{}_{}".format(name, namespace)
# file = get_representation_path(representation).replace("\\", "/")
# file = get_representation_path(repre_doc).replace("\\", "/")
# clip = track_item.source()
# # reconnect media to new path
@ -225,7 +225,7 @@ class LoadClip(opfapi.ClipLoader):
# # add variables related to version context
# data_imprint.update({
# "representation": str(representation["_id"]),
# "representation": str(repre_doc["_id"]),
# "version": version_name,
# "colorspace": colorspace,
# "objectName": object_name

View file

@ -10,7 +10,7 @@ from ayon_core.lib.transcoding import (
)
class LoadClipBatch(opfapi.ClipLoader):
"""Load a subset to timeline as clip
"""Load a product to timeline as clip
Place clip to timeline on its asset origin timings collected
during conforming to project
@ -29,14 +29,14 @@ class LoadClipBatch(opfapi.ClipLoader):
# settings
reel_name = "OP_LoadedReel"
clip_name_template = "{batch}_{asset}_{subset}<_{output}>"
clip_name_template = "{batch}_{folder[name]}_{product[name]}<_{output}>"
""" Anatomy keys from version context data and dynamically added:
- {layerName} - original layer name token
- {layerUID} - original layer UID token
- {originalBasename} - original clip name taken from file
"""
layer_rename_template = "{asset}_{subset}<_{output}>"
layer_rename_template = "{folder[name]}_{product[name]}<_{output}>"
layer_rename_patterns = []
def load(self, context, name, namespace, options):
@ -50,17 +50,33 @@ class LoadClipBatch(opfapi.ClipLoader):
version_name = version.get("name", None)
colorspace = self.get_colorspace(context)
clip_name_template = self.clip_name_template
layer_rename_template = self.layer_rename_template
# in case output is not in context replace key to representation
if not context["representation"]["context"].get("output"):
self.clip_name_template = self.clip_name_template.replace(
clip_name_template = clip_name_template.replace(
"output", "representation")
self.layer_rename_template = self.layer_rename_template.replace(
layer_rename_template = layer_rename_template.replace(
"output", "representation")
asset_doc = context["asset"]
subset_doc = context["subset"]
formatting_data = deepcopy(context["representation"]["context"])
formatting_data["batch"] = self.batch.name.get_value()
formatting_data.update({
"asset": asset_doc["name"],
"folder": {
"name": asset_doc["name"],
},
"subset": subset_doc["name"],
"family": subset_doc["data"]["family"],
"product": {
"name": subset_doc["name"],
"type": subset_doc["data"]["family"],
}
})
clip_name = StringTemplate(self.clip_name_template).format(
clip_name = StringTemplate(clip_name_template).format(
formatting_data)
# convert colorspace with ocio to flame mapping
@ -69,7 +85,7 @@ class LoadClipBatch(opfapi.ClipLoader):
self.log.info("Loading with colorspace: `{}`".format(colorspace))
# create workfile path
workfile_dir = options.get("workdir") or os.environ["AVALON_WORKDIR"]
workfile_dir = options.get("workdir") or os.environ["AYON_WORKDIR"]
openclip_dir = os.path.join(
workfile_dir, clip_name
)
@ -86,7 +102,7 @@ class LoadClipBatch(opfapi.ClipLoader):
"path": path.replace("\\", "/"),
"colorspace": colorspace,
"version": "v{:0>3}".format(version_name),
"layer_rename_template": self.layer_rename_template,
"layer_rename_template": layer_rename_template,
"layer_rename_patterns": self.layer_rename_patterns,
"context_data": formatting_data
}

View file

@ -59,6 +59,6 @@ class CollectTestSelection(pyblish.api.ContextPlugin):
opfapi.imprint(segment, {
'asset': segment.name.get_value(),
'family': 'render',
'subset': 'subsetMain'
'productType': 'render',
'productName': 'productMain'
})

View file

@ -3,6 +3,7 @@ from types import NoneType
import pyblish
import ayon_core.hosts.flame.api as opfapi
from ayon_core.hosts.flame.otio import flame_export
from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID
from ayon_core.pipeline.editorial import (
is_overlapping_otio_ranges,
get_media_range_with_retimes
@ -47,7 +48,9 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
if not marker_data:
continue
if marker_data.get("id") != "pyblish.avalon.instance":
if marker_data.get("id") not in {
AYON_INSTANCE_ID, AVALON_INSTANCE_ID
}:
continue
self.log.debug("__ segment.name: {}".format(
@ -107,24 +110,25 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
# add ocio_data to instance data
inst_data.update(otio_data)
asset = marker_data["asset"]
subset = marker_data["subset"]
folder_path = marker_data["folderPath"]
folder_name = folder_path.rsplit("/")[-1]
product_name = marker_data["productName"]
# insert family into families
family = marker_data["family"]
# insert product type into families
product_type = marker_data["productType"]
families = [str(f) for f in marker_data["families"]]
families.insert(0, str(family))
families.insert(0, str(product_type))
# form label
label = asset
if asset != clip_name:
label = folder_name
if folder_name != clip_name:
label += " ({})".format(clip_name)
label += " {} [{}]".format(subset, ", ".join(families))
label += " {} [{}]".format(product_name, ", ".join(families))
inst_data.update({
"name": "{}_{}".format(asset, subset),
"name": "{}_{}".format(folder_name, product_name),
"label": label,
"asset": asset,
"folderPath": folder_path,
"item": segment,
"families": families,
"publish": marker_data["publish"],
@ -332,26 +336,28 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
if not hierarchy_data:
return
asset = data["asset"]
subset = "shotMain"
folder_path = data["folderPath"]
folder_name = folder_path.rsplit("/")[-1]
product_name = "shotMain"
# insert family into families
family = "shot"
# insert product type into families
product_type = "shot"
# form label
label = asset
if asset != clip_name:
label = folder_name
if folder_name != clip_name:
label += " ({}) ".format(clip_name)
label += " {}".format(subset)
label += " [{}]".format(family)
label += " {}".format(product_name)
label += " [{}]".format(product_type)
data.update({
"name": "{}_{}".format(asset, subset),
"name": "{}_{}".format(folder_name, product_name),
"label": label,
"subset": subset,
"asset": asset,
"family": family,
"families": []
"productName": product_name,
"folderPath": folder_path,
"productType": product_type,
"family": product_type,
"families": [product_type]
})
instance = context.create_instance(**data)

View file

@ -3,7 +3,7 @@ import pyblish.api
from ayon_core.client import get_asset_name_identifier
import ayon_core.hosts.flame.api as opfapi
from ayon_core.hosts.flame.otio import flame_export
from ayon_core.pipeline.create import get_subset_name
from ayon_core.pipeline.create import get_product_name
class CollecTimelineOTIO(pyblish.api.ContextPlugin):
@ -14,7 +14,7 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin):
def process(self, context):
# plugin defined
family = "workfile"
product_type = "workfile"
variant = "otioTimeline"
# main
@ -23,29 +23,30 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin):
project = opfapi.get_current_project()
sequence = opfapi.get_current_sequence(opfapi.CTX.selection)
# create subset name
subset_name = get_subset_name(
family,
variant,
task_name,
asset_doc,
# create product name
product_name = get_product_name(
context.data["projectName"],
asset_doc,
task_name,
context.data["hostName"],
product_type,
variant,
project_settings=context.data["project_settings"]
)
asset_name = get_asset_name_identifier(asset_doc)
folder_path = get_asset_name_identifier(asset_doc)
# adding otio timeline to context
with opfapi.maintained_segment_selection(sequence) as selected_seg:
otio_timeline = flame_export.create_otio_timeline(sequence)
instance_data = {
"name": subset_name,
"asset": asset_name,
"subset": subset_name,
"family": "workfile",
"families": []
"name": product_name,
"folderPath": folder_path,
"productName": product_name,
"productType": product_type,
"family": product_type,
"families": [product_type]
}
# create instance with workfile

View file

@ -1,6 +1,5 @@
import os
import re
import tempfile
from copy import deepcopy
import pyblish.api
@ -15,12 +14,12 @@ from ayon_core.pipeline.editorial import (
import flame
class ExtractSubsetResources(publish.Extractor):
class ExtractProductResources(publish.Extractor):
"""
Extractor for transcoding files from Flame clip
"""
label = "Extract subset resources"
label = "Extract product resources"
order = pyblish.api.ExtractorOrder
families = ["clip"]
hosts = ["flame"]
@ -47,7 +46,7 @@ class ExtractSubsetResources(publish.Extractor):
hide_ui_on_process = True
# settings
export_presets_mapping = {}
export_presets_mapping = []
def process(self, instance):
if not self.keep_original_representation:
@ -56,7 +55,7 @@ class ExtractSubsetResources(publish.Extractor):
# flame objects
segment = instance.data["item"]
asset_name = instance.data["asset"]
folder_path = instance.data["folderPath"]
segment_name = segment.name.get_value()
clip_path = instance.data["path"]
sequence_clip = instance.context.data["flameSequence"]
@ -146,15 +145,21 @@ class ExtractSubsetResources(publish.Extractor):
# append staging dir for later cleanup
instance.context.data["cleanupFullPaths"].append(staging_dir)
export_presets_mapping = {}
for preset_mapping in deepcopy(self.export_presets_mapping):
name = preset_mapping.pop("name")
export_presets_mapping[name] = preset_mapping
# add default preset type for thumbnail and reviewable video
# update them with settings and override in case the same
# are found in there
_preset_keys = [k.split('_')[0] for k in self.export_presets_mapping]
_preset_keys = [k.split('_')[0] for k in export_presets_mapping]
export_presets = {
k: v for k, v in deepcopy(self.default_presets).items()
k: v
for k, v in deepcopy(self.default_presets).items()
if k not in _preset_keys
}
export_presets.update(self.export_presets_mapping)
export_presets.update(export_presets_mapping)
if not instance.data.get("versionData"):
instance.data["versionData"] = {}
@ -244,7 +249,7 @@ class ExtractSubsetResources(publish.Extractor):
out_mark = in_mark + source_duration_handles
exporting_clip = self.import_clip(clip_path)
exporting_clip.name.set_value("{}_{}".format(
asset_name, segment_name))
folder_path, segment_name))
# add xml tags modifications
modify_xml_data.update({

View file

@ -44,8 +44,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin):
))
# load plate to batch group
self.log.info("Loading subset `{}` into batch `{}`".format(
instance.data["subset"], bgroup.name.get_value()
self.log.info("Loading product `{}` into batch `{}`".format(
instance.data["productName"], bgroup.name.get_value()
))
self._load_clip_to_context(instance, bgroup)
@ -168,10 +168,10 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin):
handle_start = instance.data["handleStart"]
handle_end = instance.data["handleEnd"]
frame_duration = (frame_end - frame_start) + 1
asset_name = instance.data["asset"]
folder_path = instance.data["folderPath"]
task_name = task_data["name"]
batchgroup_name = "{}_{}".format(asset_name, task_name)
batchgroup_name = "{}_{}".format(folder_path, task_name)
batch_data = {
"shematic_reels": [

View file

@ -1,6 +1,6 @@
import os
import re
from ayon_core.modules import OpenPypeModule, IHostAddon
from ayon_core.addon import AYONAddon, IHostAddon
from ayon_core.lib import Logger
FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
@ -22,7 +22,7 @@ def get_fusion_version(app_name):
The function is triggered by the prelaunch hooks to get the fusion version.
`app_name` is obtained by prelaunch hooks from the
`launch_context.env.get("AVALON_APP_NAME")`.
`launch_context.env.get("AYON_APP_NAME")`.
To get a correct Fusion version, a version number should be present
in the `applications/fusion/variants` key
@ -48,13 +48,10 @@ def get_fusion_version(app_name):
)
class FusionAddon(OpenPypeModule, IHostAddon):
class FusionAddon(AYONAddon, IHostAddon):
name = "fusion"
host_name = "fusion"
def initialize(self, module_settings):
self.enabled = True
def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []

View file

@ -135,7 +135,7 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
return current_filepath
def work_root(self, session):
work_dir = session["AVALON_WORKDIR"]
work_dir = session["AYON_WORKDIR"]
scene_dir = session.get("AVALON_SCENEDIR")
if scene_dir:
return os.path.join(work_dir, scene_dir)

View file

@ -11,9 +11,10 @@ from ayon_core.lib import (
EnumDef,
)
from ayon_core.pipeline import (
legacy_io,
Creator,
CreatedInstance
CreatedInstance,
AVALON_INSTANCE_ID,
AYON_INSTANCE_ID,
)
@ -32,14 +33,16 @@ class GenericCreateSaver(Creator):
# TODO: This should be renamed together with Nuke so it is aligned
temp_rendering_path_template = (
"{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}")
"{workdir}/renders/fusion/{product[name]}/"
"{product[name]}.{frame}.{ext}"
)
def create(self, subset_name, instance_data, pre_create_data):
def create(self, product_name, instance_data, pre_create_data):
self.pass_pre_attributes_to_instance(instance_data, pre_create_data)
instance = CreatedInstance(
family=self.family,
subset_name=subset_name,
product_type=self.product_type,
product_name=product_name,
data=instance_data,
creator=self,
)
@ -110,23 +113,23 @@ class GenericCreateSaver(Creator):
tool.SetData(f"openpype.{key}", value)
def _update_tool_with_data(self, tool, data):
"""Update tool node name and output path based on subset data"""
if "subset" not in data:
"""Update tool node name and output path based on product data"""
if "productName" not in data:
return
original_subset = tool.GetData("openpype.subset")
original_product_name = tool.GetData("openpype.productName")
original_format = tool.GetData(
"openpype.creator_attributes.image_format"
)
subset = data["subset"]
product_name = data["productName"]
if (
original_subset != subset
original_product_name != product_name
or original_format != data["creator_attributes"]["image_format"]
):
self._configure_saver_tool(data, tool, subset)
self._configure_saver_tool(data, tool, product_name)
def _configure_saver_tool(self, data, tool, subset):
def _configure_saver_tool(self, data, tool, product_name):
formatting_data = deepcopy(data)
# get frame padding from anatomy templates
@ -136,27 +139,50 @@ class GenericCreateSaver(Creator):
ext = data["creator_attributes"]["image_format"]
# Subset change detected
workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"])
product_type = formatting_data["productType"]
f_product_name = formatting_data["productName"]
folder_path = formatting_data["folderPath"]
folder_name = folder_path.rsplit("/", 1)[-1]
workdir = os.path.normpath(os.getenv("AYON_WORKDIR"))
formatting_data.update({
"workdir": workdir,
"frame": "0" * frame_padding,
"ext": ext,
"product": {
"name": formatting_data["subset"],
"type": formatting_data["family"],
"name": f_product_name,
"type": product_type,
},
# TODO add more variants for 'folder' and 'task'
"folder": {
"name": folder_name,
},
"task": {
"name": data["task"],
},
# Backwards compatibility
"asset": folder_name,
"subset": f_product_name,
"family": product_type,
})
# build file path to render
filepath = self.temp_rendering_path_template.format(**formatting_data)
# TODO make sure the keys are available in 'formatting_data'
temp_rendering_path_template = (
self.temp_rendering_path_template
.replace("{task}", "{task[name]}")
)
filepath = temp_rendering_path_template.format(**formatting_data)
comp = get_current_comp()
tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath))
# Rename tool
if tool.Name != subset:
print(f"Renaming {tool.Name} -> {subset}")
tool.SetAttrs({"TOOLS_Name": subset})
if tool.Name != product_name:
print(f"Renaming {tool.Name} -> {product_name}")
tool.SetAttrs({"TOOLS_Name": product_name})
def get_managed_tool_data(self, tool):
"""Return data of the tool if it matches creator identifier"""
@ -164,13 +190,13 @@ class GenericCreateSaver(Creator):
if not isinstance(data, dict):
return
required = {
"id": "pyblish.avalon.instance",
"creator_identifier": self.identifier,
}
for key, value in required.items():
if key not in data or data[key] != value:
return
if (
data.get("creator_identifier") != self.identifier
or data.get("id") not in {
AYON_INSTANCE_ID, AVALON_INSTANCE_ID
}
):
return
# Get active state from the actual tool state
attrs = tool.GetAttrs()

View file

@ -131,7 +131,7 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook):
) = self.get_copy_fusion_prefs_settings()
# Get launched application context and return correct app version
app_name = self.launch_context.env.get("AVALON_APP_NAME")
app_name = self.launch_context.env.get("AYON_APP_NAME")
app_version = get_fusion_version(app_name)
if app_version is None:
version_names = ", ".join(str(x) for x in FUSION_VERSIONS_DICT)

View file

@ -28,7 +28,7 @@ class FusionPrelaunch(PreLaunchHook):
def execute(self):
# making sure python 3 is installed at provided path
# Py 3.3-3.10 for Fusion 18+ or Py 3.6 for Fu 16-17
app_data = self.launch_context.env.get("AVALON_APP_NAME")
app_data = self.launch_context.env.get("AYON_APP_NAME")
app_version = get_fusion_version(app_data)
if not app_version:
raise ApplicationLaunchFailed(

View file

@ -17,7 +17,7 @@ class CreateImageSaver(GenericCreateSaver):
identifier = "io.openpype.creators.fusion.imagesaver"
label = "Image (saver)"
name = "image"
family = "image"
product_type = "image"
description = "Fusion Saver to generate image"
default_frame = 0

View file

@ -12,7 +12,7 @@ class CreateSaver(GenericCreateSaver):
identifier = "io.openpype.creators.fusion.saver"
label = "Render (saver)"
name = "render"
family = "render"
product_type = "render"
description = "Fusion Saver to generate image sequence"
default_frame_range_option = "asset_db"

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline import (
class FusionWorkfileCreator(AutoCreator):
identifier = "workfile"
family = "workfile"
product_type = "workfile"
label = "Workfile"
icon = "fa5.file"
@ -27,9 +27,12 @@ class FusionWorkfileCreator(AutoCreator):
if not data:
return
product_name = data.get("productName")
if product_name is None:
product_name = data["subset"]
instance = CreatedInstance(
family=self.family,
subset_name=data["subset"],
product_type=self.product_type,
product_name=product_name,
data=data,
creator=self
)
@ -59,7 +62,7 @@ class FusionWorkfileCreator(AutoCreator):
existing_instance = None
for instance in self.create_context.instances:
if instance.family == self.family:
if instance.product_type == self.product_type:
existing_instance = instance
break
@ -75,9 +78,12 @@ class FusionWorkfileCreator(AutoCreator):
if existing_instance is None:
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
self.default_variant, task_name, asset_doc,
project_name, host_name
product_name = self.get_product_name(
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
)
data = {
"folderPath": asset_name,
@ -85,12 +91,17 @@ class FusionWorkfileCreator(AutoCreator):
"variant": self.default_variant,
}
data.update(self.get_dynamic_data(
self.default_variant, task_name, asset_doc,
project_name, host_name, None
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
None
))
new_instance = CreatedInstance(
self.family, subset_name, data, self
self.product_type, product_name, data, self
)
new_instance.transient_data["comp"] = comp
self._add_instance_to_context(new_instance)
@ -100,10 +111,13 @@ class FusionWorkfileCreator(AutoCreator):
or existing_instance["task"] != task_name
):
asset_doc = get_asset_by_name(project_name, asset_name)
subset_name = self.get_subset_name(
self.default_variant, task_name, asset_doc,
project_name, host_name
product_name = self.get_product_name(
project_name,
asset_doc,
task_name,
self.default_variant,
host_name,
)
existing_instance["folderPath"] = asset_name
existing_instance["task"] = task_name
existing_instance["subset"] = subset_name
existing_instance["productName"] = product_name

View file

@ -44,23 +44,24 @@ class FusionLoadAlembicMesh(load.LoaderPlugin):
context=context,
loader=self.__class__.__name__)
def switch(self, container, representation):
self.update(container, representation)
def switch(self, container, context):
self.update(container, context)
def update(self, container, representation):
def update(self, container, context):
"""Update Alembic path"""
tool = container["_tool"]
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
comp = tool.Comp()
path = get_representation_path(representation)
repre_doc = context["representation"]
path = get_representation_path(repre_doc)
with comp_lock_and_undo_chunk(comp, "Update tool"):
tool["Filename"] = path
# Update the imprinted representation
tool.SetData("avalon.representation", str(representation["_id"]))
tool.SetData("avalon.representation", str(repre_doc["_id"]))
def remove(self, container):
tool = container["_tool"]

View file

@ -59,23 +59,24 @@ class FusionLoadFBXMesh(load.LoaderPlugin):
loader=self.__class__.__name__,
)
def switch(self, container, representation):
self.update(container, representation)
def switch(self, container, context):
self.update(container, context)
def update(self, container, representation):
def update(self, container, context):
"""Update path"""
tool = container["_tool"]
assert tool.ID == self.tool_type, f"Must be {self.tool_type}"
comp = tool.Comp()
path = get_representation_path(representation)
repre_doc = context["representation"]
path = get_representation_path(repre_doc)
with comp_lock_and_undo_chunk(comp, "Update tool"):
tool["ImportFile"] = path
# Update the imprinted representation
tool.SetData("avalon.representation", str(representation["_id"]))
tool.SetData("avalon.representation", str(repre_doc["_id"]))
def remove(self, container):
tool = container["_tool"]

View file

@ -136,7 +136,7 @@ class FusionLoadSequence(load.LoaderPlugin):
"render",
"plate",
"image",
"onilne",
"online",
]
representations = ["*"]
extensions = set(
@ -175,10 +175,10 @@ class FusionLoadSequence(load.LoaderPlugin):
loader=self.__class__.__name__,
)
def switch(self, container, representation):
self.update(container, representation)
def switch(self, container, context):
self.update(container, context)
def update(self, container, representation):
def update(self, container, context):
"""Update the Loader's path
Fusion automatically tries to reset some variables when changing
@ -224,7 +224,8 @@ class FusionLoadSequence(load.LoaderPlugin):
assert tool.ID == "Loader", "Must be Loader"
comp = tool.Comp()
context = get_representation_context(representation)
repre_doc = context["representation"]
context = get_representation_context(repre_doc)
path = self.filepath_from_context(context)
# Get start frame from version data
@ -255,7 +256,7 @@ class FusionLoadSequence(load.LoaderPlugin):
)
# Update the imprinted representation
tool.SetData("avalon.representation", str(representation["_id"]))
tool.SetData("avalon.representation", str(repre_doc["_id"]))
def remove(self, container):
tool = container["_tool"]

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