Merge branch 'develop' of github.com:pypeclub/OpenPype into feature/OP-2011_Deafult-priority-configurable

This commit is contained in:
Petr Kalis 2022-04-06 17:09:11 +02:00
commit 374187da87
140 changed files with 3332 additions and 1264 deletions

View file

@ -5,15 +5,15 @@ from Qt import QtWidgets
from bson.objectid import ObjectId
import pyblish.api
import avalon.api
from avalon import io
from openpype import lib
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
import openpype.hosts.aftereffects
@ -73,7 +73,7 @@ def install():
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
@ -86,7 +86,7 @@ def install():
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):

View file

@ -5,14 +5,6 @@ from openpype.pipeline import HOST_WORKFILE_EXTENSIONS
from .launch_logic import get_stub
def _active_document():
document_name = get_stub().get_active_document_name()
if not document_name:
return None
return document_name
def file_extensions():
return HOST_WORKFILE_EXTENSIONS["aftereffects"]
@ -39,7 +31,8 @@ def current_file():
full_name = get_stub().get_active_document_full_name()
if full_name and full_name != "null":
return os.path.normpath(full_name).replace("\\", "/")
except Exception:
except ValueError:
print("Nothing opened")
pass
return None
@ -47,3 +40,15 @@ def current_file():
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
def _active_document():
# TODO merge with current_file - even in extension
document_name = None
try:
document_name = get_stub().get_active_document_name()
except ValueError:
print("Nothing opened")
pass
return document_name

View file

@ -1,12 +1,14 @@
from openpype.pipeline import create
from openpype.pipeline import CreatorError
from openpype.pipeline import (
CreatorError,
LegacyCreator
)
from openpype.hosts.aftereffects.api import (
get_stub,
list_instances
)
class CreateRender(create.LegacyCreator):
class CreateRender(LegacyCreator):
"""Render folder for publish.
Creates subsets in format 'familyTaskSubsetname',

View file

@ -29,12 +29,12 @@ def add_implementation_envs(env, _app):
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
)
for path in openpype_blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
if path:
previous_user_scripts.add(os.path.normpath(path))
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
for path in blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
if path:
previous_user_scripts.add(os.path.normpath(path))
# Remove implementation path from user script paths as is set to

View file

@ -14,9 +14,10 @@ import avalon.api
from avalon import io, schema
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.api import Logger
@ -54,7 +55,7 @@ def install():
pyblish.api.register_plugin_path(str(PUBLISH_PATH))
register_loader_plugin_path(str(LOAD_PATH))
avalon.api.register_plugin_path(LegacyCreator, str(CREATE_PATH))
register_creator_plugin_path(str(CREATE_PATH))
lib.append_user_scripts()
@ -76,7 +77,7 @@ def uninstall():
pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))
deregister_loader_plugin_path(str(LOAD_PATH))
avalon.api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH))
deregister_creator_plugin_path(str(CREATE_PATH))
if not IS_HEADLESS:
ops.unregister()

View file

@ -3,14 +3,14 @@ Basic avalon integration
"""
import os
import contextlib
from avalon import api as avalon
from pyblish import api as pyblish
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from .lib import (
@ -37,7 +37,7 @@ def install():
pyblish.register_host("flame")
pyblish.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info("OpenPype Flame plug-ins registred ...")
# register callback for switching publishable
@ -52,7 +52,7 @@ def uninstall():
log.info("Deregistering Flame plug-ins..")
pyblish.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)

View file

@ -7,14 +7,14 @@ import logging
import contextlib
import pyblish.api
import avalon.api
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
deregister_loader_plugin_path,
register_creator_plugin_path,
register_inventory_action_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
deregister_inventory_action_path,
AVALON_CONTAINER_ID,
)
@ -70,7 +70,7 @@ def install():
log.info("Registering Fusion plug-ins..")
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
register_inventory_action_path(INVENTORY_PATH)
pyblish.api.register_callback(
@ -94,7 +94,7 @@ def uninstall():
log.info("Deregistering Fusion plug-ins..")
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
pyblish.api.deregister_callback(

View file

@ -1,13 +1,13 @@
import os
from openpype.pipeline import create
from openpype.pipeline import LegacyCreator
from openpype.hosts.fusion.api import (
get_current_comp,
comp_lock_and_undo_chunk
)
class CreateOpenEXRSaver(create.LegacyCreator):
class CreateOpenEXRSaver(LegacyCreator):
name = "openexrDefault"
label = "Create OpenEXR Saver"

View file

@ -6,14 +6,14 @@ from bson.objectid import ObjectId
import pyblish.api
from avalon import io
import avalon.api
from openpype import lib
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
import openpype.hosts.harmony
@ -108,9 +108,8 @@ def check_inventory():
if not lib.any_outdated():
return
host = avalon.api.registered_host()
outdated_containers = []
for container in host.ls():
for container in ls():
representation = container['representation']
representation_doc = io.find_one(
{
@ -186,7 +185,7 @@ def install():
pyblish.api.register_host("harmony")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
# Register callbacks.
@ -200,7 +199,7 @@ def install():
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):

View file

@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
]
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
for path in old_hiero_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -5,13 +5,13 @@ import os
import contextlib
from collections import OrderedDict
from avalon import api as avalon
from avalon import schema
from pyblish import api as pyblish
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_creator_plugin_path,
register_loader_plugin_path,
deregister_creator_plugin_path,
deregister_loader_plugin_path,
AVALON_CONTAINER_ID,
)
@ -50,7 +50,7 @@ def install():
pyblish.register_host("hiero")
pyblish.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
@ -71,7 +71,7 @@ def uninstall():
pyblish.deregister_host("hiero")
pyblish.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)

View file

@ -15,7 +15,7 @@ def add_implementation_envs(env, _app):
old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or ""
for path in old_houdini_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)
@ -23,7 +23,7 @@ def add_implementation_envs(env, _app):
new_houdini_path.append(norm_path)
for path in old_houdini_menu_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -11,7 +11,7 @@ import avalon.api
from avalon.lib import find_submodule
from openpype.pipeline import (
LegacyCreator,
register_creator_plugin_path,
register_loader_plugin_path,
AVALON_CONTAINER_ID,
)
@ -54,7 +54,7 @@ def install():
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info("Installing callbacks ... ")
# register_event_callback("init", on_init)

View file

@ -9,7 +9,7 @@ def add_implementation_envs(env, _app):
]
old_python_path = env.get("PYTHONPATH") or ""
for path in old_python_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
"""Tools to work with FBX."""
import logging
from pyblish.api import Instance
from maya import cmds # noqa
import maya.mel as mel # noqa
class FBXExtractor:
"""Extract FBX from Maya.
This extracts reproducible FBX exports ignoring any of the settings set
on the local machine in the FBX export options window.
All export settings are applied with the `FBXExport*` commands prior
to the `FBXExport` call itself. The options can be overridden with
their
nice names as seen in the "options" property on this class.
For more information on FBX exports see:
- https://knowledge.autodesk.com/support/maya/learn-explore/caas
/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4
-9CB19C28F4E0-htm.html
- http://forums.cgsociety.org/archive/index.php?t-1032853.html
- https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE
/LKs9hakE28kJ
"""
@property
def options(self):
"""Overridable options for FBX Export
Given in the following format
- {NAME: EXPECTED TYPE}
If the overridden option's type does not match,
the option is not included and a warning is logged.
"""
return {
"cameras": bool,
"smoothingGroups": bool,
"hardEdges": bool,
"tangents": bool,
"smoothMesh": bool,
"instances": bool,
# "referencedContainersContent": bool, # deprecated in Maya 2016+
"bakeComplexAnimation": int,
"bakeComplexStart": int,
"bakeComplexEnd": int,
"bakeComplexStep": int,
"bakeResampleAnimation": bool,
"animationOnly": bool,
"useSceneName": bool,
"quaternion": str, # "euler"
"shapes": bool,
"skins": bool,
"constraints": bool,
"lights": bool,
"embeddedTextures": bool,
"inputConnections": bool,
"upAxis": str, # x, y or z,
"triangulate": bool
}
@property
def default_options(self):
"""The default options for FBX extraction.
This includes shapes, skins, constraints, lights and incoming
connections and exports with the Y-axis as up-axis.
By default this uses the time sliders start and end time.
"""
start_frame = int(cmds.playbackOptions(query=True,
animationStartTime=True))
end_frame = int(cmds.playbackOptions(query=True,
animationEndTime=True))
return {
"cameras": False,
"smoothingGroups": True,
"hardEdges": False,
"tangents": False,
"smoothMesh": True,
"instances": False,
"bakeComplexAnimation": True,
"bakeComplexStart": start_frame,
"bakeComplexEnd": end_frame,
"bakeComplexStep": 1,
"bakeResampleAnimation": True,
"animationOnly": False,
"useSceneName": False,
"quaternion": "euler",
"shapes": True,
"skins": True,
"constraints": False,
"lights": True,
"embeddedTextures": False,
"inputConnections": True,
"upAxis": "y",
"triangulate": False
}
def __init__(self, log=None):
# Ensure FBX plug-in is loaded
self.log = log or logging.getLogger(self.__class__.__name__)
cmds.loadPlugin("fbxmaya", quiet=True)
def parse_overrides(self, instance, options):
"""Inspect data of instance to determine overridden options
An instance may supply any of the overridable options
as data, the option is then added to the extraction.
"""
for key in instance.data:
if key not in self.options:
continue
# Ensure the data is of correct type
value = instance.data[key]
if not isinstance(value, self.options[key]):
self.log.warning(
"Overridden attribute {key} was of "
"the wrong type: {invalid_type} "
"- should have been {valid_type}".format(
key=key,
invalid_type=type(value).__name__,
valid_type=self.options[key].__name__))
continue
options[key] = value
return options
def set_options_from_instance(self, instance):
# type: (Instance) -> None
"""Sets FBX export options from data in the instance.
Args:
instance (Instance): Instance data.
"""
# Parse export options
options = self.default_options
options = self.parse_overrides(instance, options)
self.log.info("Export options: {0}".format(options))
# Collect the start and end including handles
start = instance.data.get("frameStartHandle") or \
instance.context.data.get("frameStartHandle")
end = instance.data.get("frameEndHandle") or \
instance.context.data.get("frameEndHandle")
options['bakeComplexStart'] = start
options['bakeComplexEnd'] = end
# First apply the default export settings to be fully consistent
# each time for successive publishes
mel.eval("FBXResetExport")
# Apply the FBX overrides through MEL since the commands
# only work correctly in MEL according to online
# available discussions on the topic
_iteritems = getattr(options, "iteritems", options.items)
for option, value in _iteritems():
key = option[0].upper() + option[1:] # uppercase first letter
# Boolean must be passed as lower-case strings
# as to MEL standards
if isinstance(value, bool):
value = str(value).lower()
template = "FBXExport{0} {1}" if key == "UpAxis" else \
"FBXExport{0} -v {1}" # noqa
cmd = template.format(key, value)
self.log.info(cmd)
mel.eval(cmd)
# Never show the UI or generate a log
mel.eval("FBXExportShowUI -v false")
mel.eval("FBXExportGenerateLog -v false")
@staticmethod
def export(members, path):
# type: (list, str) -> None
"""Export members as FBX with given path.
Args:
members (list): List of members to export.
path (str): Path to use for export.
"""
cmds.select(members, r=True, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))

View file

@ -3138,11 +3138,20 @@ def set_colorspace():
@contextlib.contextmanager
def root_parent(nodes):
# type: (list) -> list
def parent_nodes(nodes, parent=None):
# type: (list, str) -> list
"""Context manager to un-parent provided nodes and return them back."""
import pymel.core as pm # noqa
parent_node = None
delete_parent = False
if parent:
if not cmds.objExists(parent):
parent_node = pm.createNode("transform", n=parent, ss=False)
delete_parent = True
else:
parent_node = pm.PyNode(parent)
node_parents = []
for node in nodes:
n = pm.PyNode(node)
@ -3153,9 +3162,14 @@ def root_parent(nodes):
node_parents.append((n, root))
try:
for node in node_parents:
node[0].setParent(world=True)
if not parent:
node[0].setParent(world=True)
else:
node[0].setParent(parent_node)
yield
finally:
for node in node_parents:
if node[1]:
node[0].setParent(node[1])
if delete_parent:
pm.delete(parent_node)

View file

@ -9,8 +9,6 @@ import maya.api.OpenMaya as om
import pyblish.api
import avalon.api
from avalon.lib import find_submodule
import openpype.hosts.maya
from openpype.tools.utils import host_tools
from openpype.lib import (
@ -20,11 +18,12 @@ from openpype.lib import (
)
from openpype.lib.path_tools import HostDirmap
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_inventory_action_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_inventory_action_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.hosts.maya.lib import copy_workspace_mel
@ -60,7 +59,7 @@ def install():
pyblish.api.register_host("maya")
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
register_inventory_action_path(INVENTORY_PATH)
log.info(PUBLISH_PATH)
@ -189,7 +188,7 @@ def uninstall():
pyblish.api.deregister_host("maya")
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
menu.uninstall()
@ -268,21 +267,8 @@ def ls():
"""
container_names = _ls()
has_metadata_collector = False
config_host = find_submodule(avalon.api.registered_config(), "maya")
if hasattr(config_host, "collect_container_metadata"):
has_metadata_collector = True
for container in sorted(container_names):
data = parse_container(container)
# Collect custom data if attribute is present
if has_metadata_collector:
metadata = config_host.collect_container_metadata(container)
data.update(metadata)
yield data
yield parse_container(container)
def containerise(name,

View file

@ -22,4 +22,6 @@ class CreateLook(plugin.Creator):
self.data["maketx"] = self.make_tx
# Enable users to force a copy.
# - on Windows is "forceCopy" always changed to `True` because of
# windows implementation of hardlinks
self.data["forceCopy"] = False

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""Creator for Unreal Skeletal Meshes."""
from openpype.hosts.maya.api import plugin, lib
from avalon.api import Session
from maya import cmds # noqa
class CreateUnrealSkeletalMesh(plugin.Creator):
"""Unreal Static Meshes with collisions."""
name = "staticMeshMain"
label = "Unreal - Skeletal Mesh"
family = "skeletalMesh"
icon = "thumbs-up"
dynamic_subset_keys = ["asset"]
joint_hints = []
def __init__(self, *args, **kwargs):
"""Constructor."""
super(CreateUnrealSkeletalMesh, self).__init__(*args, **kwargs)
@classmethod
def get_dynamic_data(
cls, variant, task_name, asset_id, project_name, host_name
):
dynamic_data = super(CreateUnrealSkeletalMesh, cls).get_dynamic_data(
variant, task_name, asset_id, project_name, host_name
)
dynamic_data["asset"] = Session.get("AVALON_ASSET")
return dynamic_data
def process(self):
self.name = "{}_{}".format(self.family, self.name)
with lib.undo_chunk():
instance = super(CreateUnrealSkeletalMesh, self).process()
content = cmds.sets(instance, query=True)
# empty set and process its former content
cmds.sets(content, rm=instance)
geometry_set = cmds.sets(name="geometry_SET", empty=True)
joints_set = cmds.sets(name="joints_SET", empty=True)
cmds.sets([geometry_set, joints_set], forceElement=instance)
members = cmds.ls(content) or []
for node in members:
if node in self.joint_hints:
cmds.sets(node, forceElement=joints_set)
else:
cmds.sets(node, forceElement=geometry_set)

View file

@ -10,7 +10,7 @@ class CreateUnrealStaticMesh(plugin.Creator):
"""Unreal Static Meshes with collisions."""
name = "staticMeshMain"
label = "Unreal - Static Mesh"
family = "unrealStaticMesh"
family = "staticMesh"
icon = "cube"
dynamic_subset_keys = ["asset"]
@ -28,10 +28,10 @@ class CreateUnrealStaticMesh(plugin.Creator):
variant, task_name, asset_id, project_name, host_name
)
dynamic_data["asset"] = Session.get("AVALON_ASSET")
return dynamic_data
def process(self):
self.name = "{}_{}".format(self.family, self.name)
with lib.undo_chunk():
instance = super(CreateUnrealStaticMesh, self).process()
content = cmds.sets(instance, query=True)

View file

@ -22,7 +22,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"camera",
"rig",
"camerarig",
"xgen"]
"xgen",
"staticMesh"]
representations = ["ma", "abc", "fbx", "mb"]
label = "Reference"

View file

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
"""Cleanup leftover nodes."""
from maya import cmds # noqa
import pyblish.api
class CleanNodesUp(pyblish.api.InstancePlugin):
"""Cleans up the staging directory after a successful publish.
This will also clean published renders and delete their parent directories.
"""
order = pyblish.api.IntegratorOrder + 10
label = "Clean Nodes"
optional = True
active = True
def process(self, instance):
if not instance.data.get("cleanNodes"):
self.log.info("Nothing to clean.")
return
nodes_to_clean = instance.data.pop("cleanNodes", [])
self.log.info("Removing {} nodes".format(len(nodes_to_clean)))
for node in nodes_to_clean:
try:
cmds.delete(node)
except ValueError:
# object might be already deleted, don't complain about it
pass

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from maya import cmds # noqa
import pyblish.api
class CollectUnrealSkeletalMesh(pyblish.api.InstancePlugin):
"""Collect Unreal Skeletal Mesh."""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Unreal Skeletal Meshes"
families = ["skeletalMesh"]
def process(self, instance):
frame = cmds.currentTime(query=True)
instance.data["frameStart"] = frame
instance.data["frameEnd"] = frame
geo_sets = [
i for i in instance[:]
if i.lower().startswith("geometry_set")
]
joint_sets = [
i for i in instance[:]
if i.lower().startswith("joints_set")
]
instance.data["geometry"] = []
instance.data["joints"] = []
for geo_set in geo_sets:
geo_content = cmds.ls(cmds.sets(geo_set, query=True), long=True)
if geo_content:
instance.data["geometry"] += geo_content
for join_set in joint_sets:
join_content = cmds.ls(cmds.sets(join_set, query=True), long=True)
if join_content:
instance.data["joints"] += join_content

View file

@ -1,38 +1,36 @@
# -*- coding: utf-8 -*-
from maya import cmds
from maya import cmds # noqa
import pyblish.api
from pprint import pformat
class CollectUnrealStaticMesh(pyblish.api.InstancePlugin):
"""Collect Unreal Static Mesh
Ensures always only a single frame is extracted (current frame). This
also sets correct FBX options for later extraction.
"""
"""Collect Unreal Static Mesh."""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Unreal Static Meshes"
families = ["unrealStaticMesh"]
families = ["staticMesh"]
def process(self, instance):
# add fbx family to trigger fbx extractor
instance.data["families"].append("fbx")
# take the name from instance (without the `S_` prefix)
instance.data["staticMeshCombinedName"] = instance.name[2:]
geometry_set = [i for i in instance if i == "geometry_SET"]
instance.data["membersToCombine"] = cmds.sets(
geometry_set = [
i for i in instance
if i.startswith("geometry_SET")
]
instance.data["geometryMembers"] = cmds.sets(
geometry_set, query=True)
collision_set = [i for i in instance if i == "collisions_SET"]
self.log.info("geometry: {}".format(
pformat(instance.data.get("geometryMembers"))))
collision_set = [
i for i in instance
if i.startswith("collisions_SET")
]
instance.data["collisionMembers"] = cmds.sets(
collision_set, query=True)
# set fbx overrides on instance
instance.data["smoothingGroups"] = True
instance.data["smoothMesh"] = True
instance.data["triangulate"] = True
self.log.info("collisions: {}".format(
pformat(instance.data.get("collisionMembers"))))
frame = cmds.currentTime(query=True)
instance.data["frameStart"] = frame

View file

@ -5,152 +5,29 @@ from maya import cmds # noqa
import maya.mel as mel # noqa
import pyblish.api
import openpype.api
from openpype.hosts.maya.api.lib import (
root_parent,
maintained_selection
)
from openpype.hosts.maya.api.lib import maintained_selection
from openpype.hosts.maya.api import fbx
class ExtractFBX(openpype.api.Extractor):
"""Extract FBX from Maya.
This extracts reproducible FBX exports ignoring any of the settings set
on the local machine in the FBX export options window.
All export settings are applied with the `FBXExport*` commands prior
to the `FBXExport` call itself. The options can be overridden with their
nice names as seen in the "options" property on this class.
For more information on FBX exports see:
- https://knowledge.autodesk.com/support/maya/learn-explore/caas
/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-6CCE943A-2ED4-4CEE-96D4
-9CB19C28F4E0-htm.html
- http://forums.cgsociety.org/archive/index.php?t-1032853.html
- https://groups.google.com/forum/#!msg/python_inside_maya/cLkaSo361oE
/LKs9hakE28kJ
This extracts reproducible FBX exports ignoring any of the
settings set on the local machine in the FBX export options window.
"""
order = pyblish.api.ExtractorOrder
label = "Extract FBX"
families = ["fbx"]
@property
def options(self):
"""Overridable options for FBX Export
Given in the following format
- {NAME: EXPECTED TYPE}
If the overridden option's type does not match,
the option is not included and a warning is logged.
"""
return {
"cameras": bool,
"smoothingGroups": bool,
"hardEdges": bool,
"tangents": bool,
"smoothMesh": bool,
"instances": bool,
# "referencedContainersContent": bool, # deprecated in Maya 2016+
"bakeComplexAnimation": int,
"bakeComplexStart": int,
"bakeComplexEnd": int,
"bakeComplexStep": int,
"bakeResampleAnimation": bool,
"animationOnly": bool,
"useSceneName": bool,
"quaternion": str, # "euler"
"shapes": bool,
"skins": bool,
"constraints": bool,
"lights": bool,
"embeddedTextures": bool,
"inputConnections": bool,
"upAxis": str, # x, y or z,
"triangulate": bool
}
@property
def default_options(self):
"""The default options for FBX extraction.
This includes shapes, skins, constraints, lights and incoming
connections and exports with the Y-axis as up-axis.
By default this uses the time sliders start and end time.
"""
start_frame = int(cmds.playbackOptions(query=True,
animationStartTime=True))
end_frame = int(cmds.playbackOptions(query=True,
animationEndTime=True))
return {
"cameras": False,
"smoothingGroups": False,
"hardEdges": False,
"tangents": False,
"smoothMesh": False,
"instances": False,
"bakeComplexAnimation": True,
"bakeComplexStart": start_frame,
"bakeComplexEnd": end_frame,
"bakeComplexStep": 1,
"bakeResampleAnimation": True,
"animationOnly": False,
"useSceneName": False,
"quaternion": "euler",
"shapes": True,
"skins": True,
"constraints": False,
"lights": True,
"embeddedTextures": True,
"inputConnections": True,
"upAxis": "y",
"triangulate": False
}
def parse_overrides(self, instance, options):
"""Inspect data of instance to determine overridden options
An instance may supply any of the overridable options
as data, the option is then added to the extraction.
"""
for key in instance.data:
if key not in self.options:
continue
# Ensure the data is of correct type
value = instance.data[key]
if not isinstance(value, self.options[key]):
self.log.warning(
"Overridden attribute {key} was of "
"the wrong type: {invalid_type} "
"- should have been {valid_type}".format(
key=key,
invalid_type=type(value).__name__,
valid_type=self.options[key].__name__))
continue
options[key] = value
return options
def process(self, instance):
# Ensure FBX plug-in is loaded
cmds.loadPlugin("fbxmaya", quiet=True)
fbx_exporter = fbx.FBXExtractor(log=self.log)
# Define output path
stagingDir = self.staging_dir(instance)
staging_dir = self.staging_dir(instance)
filename = "{0}.fbx".format(instance.name)
path = os.path.join(stagingDir, filename)
path = os.path.join(staging_dir, filename)
# The export requires forward slashes because we need
# to format it into a string in a mel expression
@ -162,54 +39,13 @@ class ExtractFBX(openpype.api.Extractor):
self.log.info("Members: {0}".format(members))
self.log.info("Instance: {0}".format(instance[:]))
# Parse export options
options = self.default_options
options = self.parse_overrides(instance, options)
self.log.info("Export options: {0}".format(options))
# Collect the start and end including handles
start = instance.data["frameStartHandle"]
end = instance.data["frameEndHandle"]
options['bakeComplexStart'] = start
options['bakeComplexEnd'] = end
# First apply the default export settings to be fully consistent
# each time for successive publishes
mel.eval("FBXResetExport")
# Apply the FBX overrides through MEL since the commands
# only work correctly in MEL according to online
# available discussions on the topic
_iteritems = getattr(options, "iteritems", options.items)
for option, value in _iteritems():
key = option[0].upper() + option[1:] # uppercase first letter
# Boolean must be passed as lower-case strings
# as to MEL standards
if isinstance(value, bool):
value = str(value).lower()
template = "FBXExport{0} {1}" if key == "UpAxis" else "FBXExport{0} -v {1}" # noqa
cmd = template.format(key, value)
self.log.info(cmd)
mel.eval(cmd)
# Never show the UI or generate a log
mel.eval("FBXExportShowUI -v false")
mel.eval("FBXExportGenerateLog -v false")
fbx_exporter.set_options_from_instance(instance)
# Export
if "unrealStaticMesh" in instance.data["families"]:
with maintained_selection():
with root_parent(members):
self.log.info("Un-parenting: {}".format(members))
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
else:
with maintained_selection():
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
with maintained_selection():
fbx_exporter.export(members, path)
cmds.select(members, r=1, noExpand=True)
mel.eval('FBXExport -f "{}" -s'.format(path))
if "representations" not in instance.data:
instance.data["representations"] = []
@ -218,7 +54,7 @@ class ExtractFBX(openpype.api.Extractor):
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": stagingDir,
"stagingDir": staging_dir,
}
instance.data["representations"].append(representation)

View file

@ -4,6 +4,7 @@ import os
import sys
import json
import tempfile
import platform
import contextlib
import subprocess
from collections import OrderedDict
@ -334,7 +335,14 @@ class ExtractLook(openpype.api.Extractor):
transfers = []
hardlinks = []
hashes = {}
force_copy = instance.data.get("forceCopy", False)
# Temporary fix to NOT create hardlinks on windows machines
if platform.system().lower() == "windows":
self.log.info(
"Forcing copy instead of hardlink due to issues on Windows..."
)
force_copy = True
else:
force_copy = instance.data.get("forceCopy", False)
for filepath in files_metadata:

View file

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
"""Create Unreal Skeletal Mesh data to be extracted as FBX."""
import os
from contextlib import contextmanager
from maya import cmds # noqa
import pyblish.api
import openpype.api
from openpype.hosts.maya.api import fbx
@contextmanager
def renamed(original_name, renamed_name):
# type: (str, str) -> None
try:
cmds.rename(original_name, renamed_name)
yield
finally:
cmds.rename(renamed_name, original_name)
class ExtractUnrealSkeletalMesh(openpype.api.Extractor):
"""Extract Unreal Skeletal Mesh as FBX from Maya. """
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Unreal Skeletal Mesh"
families = ["skeletalMesh"]
def process(self, instance):
fbx_exporter = fbx.FBXExtractor(log=self.log)
# Define output path
staging_dir = self.staging_dir(instance)
filename = "{0}.fbx".format(instance.name)
path = os.path.join(staging_dir, filename)
geo = instance.data.get("geometry")
joints = instance.data.get("joints")
to_extract = geo + joints
# The export requires forward slashes because we need
# to format it into a string in a mel expression
path = path.replace('\\', '/')
self.log.info("Extracting FBX to: {0}".format(path))
self.log.info("Members: {0}".format(to_extract))
self.log.info("Instance: {0}".format(instance[:]))
fbx_exporter.set_options_from_instance(instance)
# This magic is done for variants. To let Unreal merge correctly
# existing data, top node must have the same name. So for every
# variant we extract we need to rename top node of the rig correctly.
# It is finally done in context manager so it won't affect current
# scene.
# we rely on hierarchy under one root.
original_parent = to_extract[0].split("|")[1]
parent_node = instance.data.get("asset")
renamed_to_extract = []
for node in to_extract:
node_path = node.split("|")
node_path[1] = parent_node
renamed_to_extract.append("|".join(node_path))
with renamed(original_parent, parent_node):
self.log.info("Extracting: {}".format(renamed_to_extract, path))
fbx_exporter.export(renamed_to_extract, path)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": staging_dir,
}
instance.data["representations"].append(representation)
self.log.info("Extract FBX successful to: {0}".format(path))

View file

@ -1,33 +1,61 @@
# -*- coding: utf-8 -*-
"""Create Unreal Static Mesh data to be extracted as FBX."""
import openpype.api
import pyblish.api
import os
from maya import cmds # noqa
import pyblish.api
import openpype.api
from openpype.hosts.maya.api.lib import (
parent_nodes,
maintained_selection
)
from openpype.hosts.maya.api import fbx
class ExtractUnrealStaticMesh(openpype.api.Extractor):
"""Extract FBX from Maya. """
"""Extract Unreal Static Mesh as FBX from Maya. """
order = pyblish.api.ExtractorOrder - 0.1
label = "Extract Unreal Static Mesh"
families = ["unrealStaticMesh"]
families = ["staticMesh"]
def process(self, instance):
to_combine = instance.data.get("membersToCombine")
static_mesh_name = instance.data.get("staticMeshCombinedName")
self.log.info(
"merging {} into {}".format(
" + ".join(to_combine), static_mesh_name))
duplicates = cmds.duplicate(to_combine, ic=True)
cmds.polyUnite(
*duplicates,
n=static_mesh_name, ch=False)
members = instance.data.get("geometryMembers", [])
if instance.data.get("collisionMembers"):
members = members + instance.data.get("collisionMembers")
if not instance.data.get("cleanNodes"):
instance.data["cleanNodes"] = []
fbx_exporter = fbx.FBXExtractor(log=self.log)
instance.data["cleanNodes"].append(static_mesh_name)
instance.data["cleanNodes"] += duplicates
# Define output path
staging_dir = self.staging_dir(instance)
filename = "{0}.fbx".format(instance.name)
path = os.path.join(staging_dir, filename)
instance.data["setMembers"] = [static_mesh_name]
instance.data["setMembers"] += instance.data["collisionMembers"]
# The export requires forward slashes because we need
# to format it into a string in a mel expression
path = path.replace('\\', '/')
self.log.info("Extracting FBX to: {0}".format(path))
self.log.info("Members: {0}".format(members))
self.log.info("Instance: {0}".format(instance[:]))
fbx_exporter.set_options_from_instance(instance)
with maintained_selection():
with parent_nodes(members):
self.log.info("Un-parenting: {}".format(members))
fbx_exporter.export(members, path)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": staging_dir,
}
instance.data["representations"].append(representation)
self.log.info("Extract FBX successful to: {0}".format(path))

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Skeletal Mesh Top Node</title>
<description>## Skeletal meshes needs common root
Skeletal meshes and their joints must be under one common root.
### How to repair?
Make sure all geometry and joints resides under same root.
</description>
</error>
</root>

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import pyblish.api
import openpype.api
from openpype.pipeline import PublishXmlValidationError
from maya import cmds
class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin):
"""Validates that nodes has common root."""
order = openpype.api.ValidateContentsOrder
hosts = ["maya"]
families = ["skeletalMesh"]
label = "Skeletal Mesh Top Node"
def process(self, instance):
geo = instance.data.get("geometry")
joints = instance.data.get("joints")
joints_parents = cmds.ls(joints, long=True)
geo_parents = cmds.ls(geo, long=True)
parents_set = {
parent.split("|")[1] for parent in (joints_parents + geo_parents)
}
if len(set(parents_set)) != 1:
raise PublishXmlValidationError(
self,
"Multiple roots on geometry or joints."
)

View file

@ -10,10 +10,11 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin):
order = openpype.api.ValidateMeshOrder
hosts = ["maya"]
families = ["unrealStaticMesh"]
families = ["staticMesh"]
category = "geometry"
label = "Mesh is Triangulated"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
active = False
@classmethod
def get_invalid(cls, instance):

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Validator for correct naming of Static Meshes."""
from maya import cmds # noqa
import pyblish.api
import openpype.api
@ -52,8 +52,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
optional = True
order = openpype.api.ValidateContentsOrder
hosts = ["maya"]
families = ["unrealStaticMesh"]
label = "Unreal StaticMesh Name"
families = ["staticMesh"]
label = "Unreal Static Mesh Name"
actions = [openpype.hosts.maya.api.action.SelectInvalidAction]
regex_mesh = r"(?P<renderName>.*))"
regex_collision = r"(?P<renderName>.*)"
@ -72,15 +72,13 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
["collision_prefixes"]
)
combined_geometry_name = instance.data.get(
"staticMeshCombinedName", None)
if cls.validate_mesh:
# compile regex for testing names
regex_mesh = "{}{}".format(
("_" + cls.static_mesh_prefix) or "", cls.regex_mesh
)
sm_r = re.compile(regex_mesh)
if not sm_r.match(combined_geometry_name):
if not sm_r.match(instance.data.get("subset")):
cls.log.error("Mesh doesn't comply with name validation.")
return True
@ -91,7 +89,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
cls.log.warning("No collision objects to validate.")
return False
regex_collision = "{}{}".format(
regex_collision = "{}{}_(\\d+)".format(
"(?P<prefix>({}))_".format(
"|".join("{0}".format(p) for p in collision_prefixes)
) or "", cls.regex_collision
@ -99,6 +97,9 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
cl_r = re.compile(regex_collision)
mesh_name = "{}{}".format(instance.data["asset"],
instance.data.get("variant", []))
for obj in collision_set:
cl_m = cl_r.match(obj)
if not cl_m:
@ -107,7 +108,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
else:
expected_collision = "{}_{}".format(
cl_m.group("prefix"),
combined_geometry_name
mesh_name
)
if not obj.startswith(expected_collision):
@ -116,11 +117,11 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin):
"Collision object name doesn't match "
"static mesh name"
)
cls.log.error("{}_{} != {}_{}".format(
cls.log.error("{}_{} != {}_{}*".format(
cl_m.group("prefix"),
cl_m.group("renderName"),
cl_m.group("prefix"),
combined_geometry_name,
mesh_name,
))
invalid.append(obj)

View file

@ -9,9 +9,10 @@ class ValidateUnrealUpAxis(pyblish.api.ContextPlugin):
"""Validate if Z is set as up axis in Maya"""
optional = True
active = False
order = openpype.api.ValidateContentsOrder
hosts = ["maya"]
families = ["unrealStaticMesh"]
families = ["staticMesh"]
label = "Unreal Up-Axis check"
actions = [openpype.api.RepairAction]

View file

@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
]
old_nuke_path = env.get("NUKE_PATH") or ""
for path in old_nuke_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -26,6 +26,7 @@ from openpype.tools.utils import host_tools
from openpype.lib.path_tools import HostDirmap
from openpype.settings import get_project_settings
from openpype.modules import ModulesManager
from openpype.pipeline import discover_legacy_creator_plugins
from .workio import (
save_file,
@ -1047,17 +1048,28 @@ def add_review_knob(node):
def add_deadline_tab(node):
node.addKnob(nuke.Tab_Knob("Deadline"))
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
knob.setValue(0)
node.addKnob(knob)
knob = nuke.Int_Knob("deadlinePriority", "Priority")
knob.setValue(50)
node.addKnob(knob)
knob = nuke.Int_Knob("deadlineChunkSize", "Chunk Size")
knob.setValue(0)
node.addKnob(knob)
knob = nuke.Int_Knob("deadlineConcurrentTasks", "Concurrent tasks")
# zero as default will get value from Settings during collection
# instead of being an explicit user override, see precollect_write.py
knob.setValue(0)
node.addKnob(knob)
def get_deadline_knob_names():
return ["Deadline", "deadlineChunkSize", "deadlinePriority"]
return [
"Deadline",
"deadlineChunkSize",
"deadlinePriority",
"deadlineConcurrentTasks"
]
def create_backdrop(label="", color=None, layer=0,
@ -1902,7 +1914,7 @@ def recreate_instance(origin_node, avalon_data=None):
# create new node
# get appropriate plugin class
creator_plugin = None
for Creator in api.discover(api.Creator):
for Creator in discover_legacy_creator_plugins():
if Creator.__name__ == data["creator"]:
creator_plugin = Creator
break

View file

@ -5,7 +5,6 @@ from collections import OrderedDict
import nuke
import pyblish.api
import avalon.api
import openpype
from openpype.api import (
@ -15,10 +14,11 @@ from openpype.api import (
)
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
register_inventory_action_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
deregister_inventory_action_path,
AVALON_CONTAINER_ID,
)
@ -106,7 +106,7 @@ def install():
log.info("Registering Nuke plug-ins..")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
register_inventory_action_path(INVENTORY_PATH)
# Register Avalon event for workfiles loading.
@ -132,7 +132,7 @@ def uninstall():
pyblish.deregister_host("nuke")
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
pyblish.api.deregister_callback(

View file

@ -450,6 +450,7 @@ class ExporterReviewMov(ExporterReview):
def generate_mov(self, farm=False, **kwargs):
self.publish_on_farm = farm
read_raw = kwargs["read_raw"]
reformat_node_add = kwargs["reformat_node_add"]
reformat_node_config = kwargs["reformat_node_config"]
bake_viewer_process = kwargs["bake_viewer_process"]
@ -484,6 +485,9 @@ class ExporterReviewMov(ExporterReview):
r_node["origlast"].setValue(self.last_frame)
r_node["colorspace"].setValue(self.write_colorspace)
if read_raw:
r_node["raw"].setValue(1)
# connect
self._temp_nodes[subset].append(r_node)
self.previous_node = r_node

View file

@ -1,6 +1,7 @@
import json
from collections import OrderedDict
import nuke
import six
from avalon import io
@ -333,7 +334,7 @@ class LoadEffects(load.LoaderPlugin):
for key, value in input.items()}
elif isinstance(input, list):
return [self.byteify(element) for element in input]
elif isinstance(input, str):
elif isinstance(input, six.text_type):
return str(input)
else:
return input

View file

@ -1,6 +1,6 @@
import json
from collections import OrderedDict
import six
import nuke
from avalon import io
@ -353,7 +353,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin):
for key, value in input.items()}
elif isinstance(input, list):
return [self.byteify(element) for element in input]
elif isinstance(input, str):
elif isinstance(input, six.text_type):
return str(input)
else:
return input

View file

@ -1,5 +1,5 @@
import nuke
import six
from avalon import io
from openpype.pipeline import (
@ -243,8 +243,8 @@ class LoadGizmoInputProcess(load.LoaderPlugin):
for key, value in input.items()}
elif isinstance(input, list):
return [self.byteify(element) for element in input]
elif isinstance(input, unicode):
return input.encode('utf-8')
elif isinstance(input, six.text_type):
return str(input)
else:
return input

View file

@ -0,0 +1,47 @@
import os
import pyblish.api
import openpype
from pprint import pformat
class ExtractReviewData(openpype.api.Extractor):
"""Extracts review tag into available representation
"""
order = pyblish.api.ExtractorOrder + 0.01
# order = pyblish.api.CollectorOrder + 0.499
label = "Extract Review Data"
families = ["review"]
hosts = ["nuke"]
def process(self, instance):
fpath = instance.data["path"]
ext = os.path.splitext(fpath)[-1][1:]
representations = instance.data.get("representations", [])
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviable representation if needed
if (
"render.farm" in instance.data["families"]
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")
# iterate representations and add `review` tag
for repre in representations:
if ext != repre["ext"]:
continue
if not repre.get("tags"):
repre["tags"] = []
if "review" not in repre["tags"]:
repre["tags"].append("review")
self.log.debug("Matching representation: {}".format(
pformat(repre)
))
instance.data["representations"] = representations

View file

@ -123,6 +123,7 @@ class ExtractReviewDataMov(openpype.api.Extractor):
if generated_repres:
# assign to representations
instance.data["representations"] += generated_repres
instance.data["hasReviewableRepresentations"] = True
else:
instance.data["families"].remove("review")
self.log.info((

View file

@ -128,13 +128,17 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
}
group_node = [x for x in instance if x.Class() == "Group"][0]
deadlineChunkSize = 1
dl_chunk_size = 1
if "deadlineChunkSize" in group_node.knobs():
deadlineChunkSize = group_node["deadlineChunkSize"].value()
dl_chunk_size = group_node["deadlineChunkSize"].value()
deadlinePriority = 50
dl_priority = 50
if "deadlinePriority" in group_node.knobs():
deadlinePriority = group_node["deadlinePriority"].value()
dl_priority = group_node["deadlinePriority"].value()
dl_concurrent_tasks = 0
if "deadlineConcurrentTasks" in group_node.knobs():
dl_concurrent_tasks = group_node["deadlineConcurrentTasks"].value()
instance.data.update({
"versionData": version_data,
@ -144,8 +148,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
"label": label,
"outputType": output_type,
"colorspace": colorspace,
"deadlineChunkSize": deadlineChunkSize,
"deadlinePriority": deadlinePriority
"deadlineChunkSize": dl_chunk_size,
"deadlinePriority": dl_priority,
"deadlineConcurrentTasks": dl_concurrent_tasks
})
if self.is_prerender(_families_test):

View file

@ -1,11 +1,10 @@
import os
import toml
import nuke
from avalon import api
import pyblish.api
import openpype.api
from openpype.pipeline import discover_creator_plugins
from openpype.hosts.nuke.api.lib import get_avalon_knob_data
@ -79,7 +78,7 @@ class ValidateWriteLegacy(pyblish.api.InstancePlugin):
# get appropriate plugin class
creator_plugin = None
for Creator in api.discover(api.Creator):
for Creator in discover_creator_plugins():
if Creator.__name__ != Create_name:
continue

View file

@ -9,9 +9,10 @@ from avalon import io
from openpype.api import Logger
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
import openpype.hosts.photoshop
@ -75,7 +76,7 @@ def install():
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
@ -88,7 +89,7 @@ def install():
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def ls():

View file

@ -1,9 +1,9 @@
from Qt import QtWidgets
from openpype.pipeline import create
from openpype.pipeline import LegacyCreator
from openpype.hosts.photoshop import api as photoshop
class CreateImage(create.LegacyCreator):
class CreateImage(LegacyCreator):
"""Image folder for publish."""
name = "imageDefault"

View file

@ -1,6 +1,9 @@
from avalon import api
import pyblish.api
from openpype.settings import get_project_settings
from openpype.hosts.photoshop import api as photoshop
from openpype.lib import prepare_template_data
class CollectInstances(pyblish.api.ContextPlugin):
@ -9,6 +12,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
This collector takes into account assets that are associated with
an LayerSet and marked with a unique identifier;
If no image instances are explicitly created, it looks if there is value
in `flatten_subset_template` (configurable in Settings), in that case it
produces flatten image with all visible layers.
Identifier:
id (str): "pyblish.avalon.instance"
"""
@ -19,13 +26,17 @@ class CollectInstances(pyblish.api.ContextPlugin):
families_mapping = {
"image": []
}
# configurable in Settings
flatten_subset_template = ""
def process(self, context):
stub = photoshop.stub()
layers = stub.get_layers()
layers_meta = stub.get_layers_metadata()
instance_names = []
all_layer_ids = []
for layer in layers:
all_layer_ids.append(layer.id)
layer_data = stub.read(layer, layers_meta)
# Skip layers without metadata.
@ -59,3 +70,33 @@ class CollectInstances(pyblish.api.ContextPlugin):
if len(instance_names) != len(set(instance_names)):
self.log.warning("Duplicate instances found. " +
"Remove unwanted via SubsetManager")
if len(instance_names) == 0 and self.flatten_subset_template:
project_name = context.data["projectEntity"]["name"]
variants = get_project_settings(project_name).get(
"photoshop", {}).get(
"create", {}).get(
"CreateImage", {}).get(
"defaults", [''])
family = "image"
task_name = api.Session["AVALON_TASK"]
asset_name = context.data["assetEntity"]["name"]
fill_pairs = {
"variant": variants[0],
"family": family,
"task": task_name
}
subset = self.flatten_subset_template.format(
**prepare_template_data(fill_pairs))
instance = context.create_instance(subset)
instance.data["family"] = family
instance.data["asset"] = asset_name
instance.data["subset"] = subset
instance.data["ids"] = all_layer_ids
instance.data["families"] = self.families_mapping[family]
instance.data["publish"] = True
self.log.info("flatten instance: {} ".format(instance.data))

View file

@ -2,18 +2,26 @@ import os
import pyblish.api
from openpype.lib import get_subset_name_with_asset_doc
class CollectReview(pyblish.api.ContextPlugin):
"""Gather the active document as review instance."""
label = "Review"
order = pyblish.api.CollectorOrder
order = pyblish.api.CollectorOrder + 0.1
hosts = ["photoshop"]
def process(self, context):
family = "review"
task = os.getenv("AVALON_TASK", None)
subset = family + task.capitalize()
subset = get_subset_name_with_asset_doc(
family,
"",
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"]
)
file_path = context.data["currentFile"]
base_name = os.path.basename(file_path)

View file

@ -1,6 +1,8 @@
import os
import pyblish.api
from openpype.lib import get_subset_name_with_asset_doc
class CollectWorkfile(pyblish.api.ContextPlugin):
"""Collect current script for publish."""
@ -11,8 +13,14 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
def process(self, context):
family = "workfile"
task = os.getenv("AVALON_TASK", None)
subset = family + task.capitalize()
subset = get_subset_name_with_asset_doc(
family,
"",
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"]
)
file_path = context.data["currentFile"]
staging_dir = os.path.dirname(file_path)

View file

@ -26,8 +26,10 @@ class ExtractImage(openpype.api.Extractor):
with photoshop.maintained_selection():
self.log.info("Extracting %s" % str(list(instance)))
with photoshop.maintained_visibility():
ids = set()
layer = instance.data.get("layer")
ids = set([layer.id])
if layer:
ids.add(layer.id)
add_ids = instance.data.pop("ids", None)
if add_ids:
ids.update(set(add_ids))

View file

@ -155,6 +155,9 @@ class ExtractReview(openpype.api.Extractor):
for image_instance in instance.context:
if image_instance.data["family"] != "image":
continue
if not image_instance.data.get("layer"):
# dummy instance for flatten image
continue
layers.append(image_instance.data.get("layer"))
return sorted(layers)

View file

@ -29,7 +29,8 @@ class ValidateNamingRepair(pyblish.api.Action):
stub = photoshop.stub()
for instance in instances:
self.log.info("validate_naming instance {}".format(instance))
metadata = stub.read(instance[0])
layer_item = instance.data["layer"]
metadata = stub.read(layer_item)
self.log.info("metadata instance {}".format(metadata))
layer_name = None
if metadata.get("uuid"):
@ -43,11 +44,11 @@ class ValidateNamingRepair(pyblish.api.Action):
stub.rename_layer(instance.data["uuid"], layer_name)
subset_name = re.sub(invalid_chars, replace_char,
instance.data["name"])
instance.data["subset"])
instance[0].Name = layer_name or subset_name
layer_item.name = layer_name or subset_name
metadata["subset"] = subset_name
stub.imprint(instance[0], metadata)
stub.imprint(layer_item, metadata)
return True

View file

@ -4,14 +4,17 @@ Basic avalon integration
import os
import contextlib
from collections import OrderedDict
from avalon import api as avalon
from avalon import schema
from pyblish import api as pyblish
from avalon import schema
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from . import lib
@ -46,7 +49,7 @@ def install():
log.info("Registering DaVinci Resovle plug-ins..")
register_loader_plugin_path(LOAD_PATH)
avalon.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
@ -70,7 +73,7 @@ def uninstall():
log.info("Deregistering DaVinci Resovle plug-ins..")
deregister_loader_plugin_path(LOAD_PATH)
avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""Collect original base name for use in templates."""
from pathlib import Path
import pyblish.api
class CollectOriginalBasename(pyblish.api.InstancePlugin):
"""Collect original file base name."""
order = pyblish.api.CollectorOrder + 0.498
label = "Collect Base Name"
hosts = ["standalonepublisher"]
families = ["simpleUnrealTexture"]
def process(self, instance):
file_name = Path(instance.data["representations"][0]["files"])
instance.data["originalBasename"] = file_name.stem

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Invalid texture name</title>
<description>
## Invalid file name
Submitted file has invalid name:
'{invalid_file}'
### How to repair?
Texture file must adhere to naming conventions for Unreal:
T_{asset}_*.ext
</description>
</error>
</root>

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
"""Validator for correct file naming."""
import pyblish.api
import openpype.api
import re
from openpype.pipeline import PublishXmlValidationError
class ValidateSimpleUnrealTextureNaming(pyblish.api.InstancePlugin):
label = "Validate Unreal Texture Names"
hosts = ["standalonepublisher"]
families = ["simpleUnrealTexture"]
order = openpype.api.ValidateContentsOrder
regex = "^T_{asset}.*"
def process(self, instance):
file_name = instance.data.get("originalBasename")
self.log.info(file_name)
pattern = self.regex.format(asset=instance.data.get("asset"))
if not re.match(pattern, file_name):
msg = f"Invalid file name {file_name}"
raise PublishXmlValidationError(
self, msg, formatting_data={
"invalid_file": file_name,
"asset": instance.data.get("asset")
})

View file

@ -30,7 +30,7 @@ class MyAutoCreator(AutoCreator):
def update_instances(self, update_list):
pipeline.update_instances(update_list)
def create(self, options=None):
def create(self):
existing_instance = None
for instance in self.create_context.instances:
if instance.family == self.family:

View file

@ -7,7 +7,7 @@ from avalon import io
import avalon.api
import pyblish.api
from openpype.pipeline import BaseCreator
from openpype.pipeline import register_creator_plugin_path
ROOT_DIR = os.path.dirname(os.path.dirname(
os.path.abspath(__file__)
@ -169,7 +169,7 @@ def install():
pyblish.api.register_host("traypublisher")
pyblish.api.register_plugin_path(PUBLISH_PATH)
avalon.api.register_plugin_path(BaseCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
def set_project_name(project_name):

View file

@ -1,8 +1,8 @@
from openpype.hosts.traypublisher.api import pipeline
from openpype.lib import FileDef
from openpype.pipeline import (
Creator,
CreatedInstance,
lib
CreatedInstance
)
@ -80,7 +80,7 @@ class WorkfileCreator(Creator):
def get_instance_attr_defs(self):
output = [
lib.FileDef(
FileDef(
"filepath",
folders=False,
extensions=self.extensions,

View file

@ -6,7 +6,7 @@ from openpype.pipeline import PublishValidationError
class ValidateWorkfilePath(pyblish.api.InstancePlugin):
"""Validate existence of workfile instance existence."""
label = "Collect Workfile"
label = "Validate Workfile"
order = pyblish.api.ValidatorOrder - 0.49
families = ["workfile"]
hosts = ["traypublisher"]
@ -14,11 +14,22 @@ class ValidateWorkfilePath(pyblish.api.InstancePlugin):
def process(self, instance):
filepath = instance.data["sourceFilepath"]
if not filepath:
raise PublishValidationError((
"Filepath of 'workfile' instance \"{}\" is not set"
).format(instance.data["name"]))
raise PublishValidationError(
(
"Filepath of 'workfile' instance \"{}\" is not set"
).format(instance.data["name"]),
"File not filled",
"## Missing file\nYou are supposed to fill the path."
)
if not os.path.exists(filepath):
raise PublishValidationError((
"Filepath of 'workfile' instance \"{}\" does not exist: {}"
).format(instance.data["name"], filepath))
raise PublishValidationError(
(
"Filepath of 'workfile' instance \"{}\" does not exist: {}"
).format(instance.data["name"], filepath),
"File not found",
(
"## File was not found\nFile \"{}\" was not found."
" Check if the path is still available."
).format(filepath)
)

View file

@ -15,9 +15,10 @@ from openpype.hosts import tvpaint
from openpype.api import get_current_project_settings
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
@ -82,7 +83,7 @@ def install():
pyblish.api.register_host("tvpaint")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
registered_callbacks = (
pyblish.api.registered_callbacks().get("instanceToggled") or []
@ -104,7 +105,7 @@ def uninstall():
pyblish.api.deregister_host("tvpaint")
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def containerise(

View file

@ -20,21 +20,30 @@ class CollectInstances(pyblish.api.ContextPlugin):
json.dumps(workfile_instances, indent=4)
))
filtered_instance_data = []
# Backwards compatibility for workfiles that already have review
# instance in metadata.
review_instance_exist = False
for instance_data in workfile_instances:
if instance_data["family"] == "review":
family = instance_data["family"]
if family == "review":
review_instance_exist = True
break
elif family not in ("renderPass", "renderLayer"):
self.log.info("Unknown family \"{}\". Skipping {}".format(
family, json.dumps(instance_data, indent=4)
))
continue
filtered_instance_data.append(instance_data)
# Fake review instance if review was not found in metadata families
if not review_instance_exist:
workfile_instances.append(
filtered_instance_data.append(
self._create_review_instance_data(context)
)
for instance_data in workfile_instances:
for instance_data in filtered_instance_data:
instance_data["fps"] = context.data["sceneFps"]
# Store workfile instance data to instance data
@ -42,8 +51,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Global instance data modifications
# Fill families
family = instance_data["family"]
families = [family]
if family != "review":
families.append("review")
# Add `review` family for thumbnail integration
instance_data["families"] = [family, "review"]
instance_data["families"] = families
# Instance name
subset_name = instance_data["subset"]
@ -78,7 +90,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
# Host name from environment variable
host_name = os.environ["AVALON_APP"]
host_name = context.data["hostName"]
# Use empty variant value
variant = ""
task_name = io.Session["AVALON_TASK"]
@ -106,12 +118,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance = self.create_render_pass_instance(
context, instance_data
)
else:
raise AssertionError(
"Instance with unknown family \"{}\": {}".format(
family, instance_data
)
)
if instance is None:
continue

View file

@ -0,0 +1,110 @@
import json
import copy
import pyblish.api
from avalon import io
from openpype.lib import get_subset_name_with_asset_doc
class CollectRenderScene(pyblish.api.ContextPlugin):
"""Collect instance which renders whole scene in PNG.
Creates instance with family 'renderScene' which will have all layers
to render which will be composite into one result. The instance is not
collected from scene.
Scene will be rendered with all visible layers similar way like review is.
Instance is disabled if there are any created instances of 'renderLayer'
or 'renderPass'. That is because it is expected that this instance is
used as lazy publish of TVPaint file.
Subset name is created similar way like 'renderLayer' family. It can use
`renderPass` and `renderLayer` keys which can be set using settings and
`variant` is filled using `renderPass` value.
"""
label = "Collect Render Scene"
order = pyblish.api.CollectorOrder - 0.39
hosts = ["tvpaint"]
# Value of 'render_pass' in subset name template
render_pass = "beauty"
# Settings attributes
enabled = False
# Value of 'render_layer' and 'variant' in subset name template
render_layer = "Main"
def process(self, context):
# Check if there are created instances of renderPass and renderLayer
# - that will define if renderScene instance is enabled after
# collection
any_created_instance = False
for instance in context:
family = instance.data["family"]
if family in ("renderPass", "renderLayer"):
any_created_instance = True
break
# Global instance data modifications
# Fill families
family = "renderScene"
# Add `review` family for thumbnail integration
families = [family, "review"]
# Collect asset doc to get asset id
# - not sure if it's good idea to require asset id in
# get_subset_name?
workfile_context = context.data["workfile_context"]
asset_name = workfile_context["asset"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
# Host name from environment variable
host_name = context.data["hostName"]
# Variant is using render pass name
variant = self.render_layer
dynamic_data = {
"render_layer": self.render_layer,
"render_pass": self.render_pass
}
task_name = workfile_context["task"]
subset_name = get_subset_name_with_asset_doc(
"render",
variant,
task_name,
asset_doc,
project_name,
host_name,
dynamic_data=dynamic_data
)
instance_data = {
"family": family,
"families": families,
"fps": context.data["sceneFps"],
"subset": subset_name,
"name": subset_name,
"label": "{} [{}-{}]".format(
subset_name,
context.data["sceneMarkIn"] + 1,
context.data["sceneMarkOut"] + 1
),
"active": not any_created_instance,
"publish": not any_created_instance,
"representations": [],
"layers": copy.deepcopy(context.data["layersData"]),
"asset": asset_name,
"task": task_name
}
instance = context.create_instance(**instance_data)
self.log.debug("Created instance: {}\n{}".format(
instance, json.dumps(instance.data, indent=4)
))

View file

@ -0,0 +1,99 @@
"""Plugin converting png files from ExtractSequence into exrs.
Requires:
ExtractSequence - source of PNG
ExtractReview - review was already created so we can convert to any exr
"""
import os
import json
import pyblish.api
from openpype.lib import (
get_oiio_tools_path,
run_subprocess,
)
from openpype.pipeline import KnownPublishError
class ExtractConvertToEXR(pyblish.api.InstancePlugin):
# Offset to get after ExtractSequence plugin.
order = pyblish.api.ExtractorOrder + 0.1
label = "Extract Sequence EXR"
hosts = ["tvpaint"]
families = ["render"]
enabled = False
# Replace source PNG files or just add
replace_pngs = True
# EXR compression
exr_compression = "ZIP"
def process(self, instance):
repres = instance.data.get("representations")
if not repres:
return
oiio_path = get_oiio_tools_path()
# Raise an exception when oiiotool is not available
# - this can currently happen on MacOS machines
if not os.path.exists(oiio_path):
KnownPublishError(
"OpenImageIO tool is not available on this machine."
)
new_repres = []
for repre in repres:
if repre["name"] != "png":
continue
self.log.info(
"Processing representation: {}".format(
json.dumps(repre, sort_keys=True, indent=4)
)
)
src_filepaths = set()
new_filenames = []
for src_filename in repre["files"]:
dst_filename = os.path.splitext(src_filename)[0] + ".exr"
new_filenames.append(dst_filename)
src_filepath = os.path.join(repre["stagingDir"], src_filename)
dst_filepath = os.path.join(repre["stagingDir"], dst_filename)
src_filepaths.add(src_filepath)
args = [
oiio_path, src_filepath,
"--compression", self.exr_compression,
# TODO how to define color conversion?
"--colorconvert", "sRGB", "linear",
"-o", dst_filepath
]
run_subprocess(args)
new_repres.append(
{
"name": "exr",
"ext": "exr",
"files": new_filenames,
"stagingDir": repre["stagingDir"],
"tags": list(repre["tags"])
}
)
if self.replace_pngs:
instance.data["representations"].remove(repre)
for filepath in src_filepaths:
instance.context.data["cleanupFullPaths"].append(filepath)
instance.data["representations"].extend(new_repres)
self.log.info(
"Representations: {}".format(
json.dumps(
instance.data["representations"], sort_keys=True, indent=4
)
)
)

View file

@ -12,14 +12,13 @@ from openpype.hosts.tvpaint.lib import (
fill_reference_frames,
composite_rendered_layers,
rename_filepaths_by_frame_start,
composite_images
)
class ExtractSequence(pyblish.api.Extractor):
label = "Extract Sequence"
hosts = ["tvpaint"]
families = ["review", "renderPass", "renderLayer"]
families = ["review", "renderPass", "renderLayer", "renderScene"]
# Modifiable with settings
review_bg = [255, 255, 255, 255]
@ -160,7 +159,7 @@ class ExtractSequence(pyblish.api.Extractor):
# Fill tags and new families
tags = []
if family_lowered in ("review", "renderlayer"):
if family_lowered in ("review", "renderlayer", "renderscene"):
tags.append("review")
# Sequence of one frame
@ -186,7 +185,7 @@ class ExtractSequence(pyblish.api.Extractor):
instance.data["representations"].append(new_repre)
if family_lowered in ("renderpass", "renderlayer"):
if family_lowered in ("renderpass", "renderlayer", "renderscene"):
# Change family to render
instance.data["family"] = "render"

View file

@ -8,7 +8,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin):
label = "Validate Layers Visibility"
order = pyblish.api.ValidatorOrder
families = ["review", "renderPass", "renderLayer"]
families = ["review", "renderPass", "renderLayer", "renderScene"]
def process(self, instance):
layer_names = set()

View file

@ -7,9 +7,10 @@ import pyblish.api
from avalon import api
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.tools.utils import host_tools
@ -49,7 +50,7 @@ def install():
logger.info("installing OpenPype for Unreal")
pyblish.api.register_plugin_path(str(PUBLISH_PATH))
register_loader_plugin_path(str(LOAD_PATH))
api.register_plugin_path(LegacyCreator, str(CREATE_PATH))
register_creator_plugin_path(str(CREATE_PATH))
_register_callbacks()
_register_events()
@ -58,7 +59,7 @@ def uninstall():
"""Uninstall Unreal configuration for Avalon."""
pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))
deregister_loader_plugin_path(str(LOAD_PATH))
api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH))
deregister_creator_plugin_path(str(CREATE_PATH))
def _register_callbacks():