Merge branch 'develop' into enhancement/add-maya2025-support

This commit is contained in:
murphy 2024-04-02 14:24:26 +02:00 committed by GitHub
commit 3f28ed1624
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 1152 additions and 500 deletions

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
import ayon_core.hosts.blender.api.action import ayon_core.hosts.blender.api.action
class ValidateMeshNoNegativeScale(pyblish.api.Validator, class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale.""" """Ensure that meshes don't have a negative scale."""

View file

@ -3,11 +3,11 @@ import sys
from pprint import pformat from pprint import pformat
class CollectCelactionCliKwargs(pyblish.api.Collector): class CollectCelactionCliKwargs(pyblish.api.ContextPlugin):
""" Collects all keyword arguments passed from the terminal """ """ Collects all keyword arguments passed from the terminal """
label = "Collect Celaction Cli Kwargs" label = "Collect Celaction Cli Kwargs"
order = pyblish.api.Collector.order - 0.1 order = pyblish.api.CollectorOrder - 0.1
def process(self, context): def process(self, context):
args = list(sys.argv[1:]) args = list(sys.argv[1:])

View file

@ -5,6 +5,8 @@ import contextlib
from ayon_core.lib import Logger from ayon_core.lib import Logger
from ayon_core.pipeline import registered_host
from ayon_core.pipeline.create import CreateContext
from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.pipeline.context_tools import get_current_project_folder
self = sys.modules[__name__] self = sys.modules[__name__]
@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True,
comp.SetAttrs(attrs) comp.SetAttrs(attrs)
def set_current_context_framerange(): def set_current_context_framerange(folder_entity=None):
"""Set Comp's frame range based on current folder.""" """Set Comp's frame range based on current folder."""
folder_entity = get_current_project_folder() if folder_entity is None:
folder_entity = get_current_project_folder(
fields={"attrib.frameStart",
"attrib.frameEnd",
"attrib.handleStart",
"attrib.handleEnd"})
folder_attributes = folder_entity["attrib"] folder_attributes = folder_entity["attrib"]
start = folder_attributes["frameStart"] start = folder_attributes["frameStart"]
end = folder_attributes["frameEnd"] end = folder_attributes["frameEnd"]
@ -65,9 +73,24 @@ def set_current_context_framerange():
handle_end=handle_end) handle_end=handle_end)
def set_current_context_resolution(): def set_current_context_fps(folder_entity=None):
"""Set Comp's frame rate (FPS) to based on current asset"""
if folder_entity is None:
folder_entity = get_current_project_folder(fields={"attrib.fps"})
fps = float(folder_entity["attrib"].get("fps", 24.0))
comp = get_current_comp()
comp.SetPrefs({
"Comp.FrameFormat.Rate": fps,
})
def set_current_context_resolution(folder_entity=None):
"""Set Comp's resolution width x height default based on current folder""" """Set Comp's resolution width x height default based on current folder"""
folder_entity = get_current_project_folder() if folder_entity is None:
folder_entity = get_current_project_folder(
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"})
folder_attributes = folder_entity["attrib"] folder_attributes = folder_entity["attrib"]
width = folder_attributes["resolutionWidth"] width = folder_attributes["resolutionWidth"]
height = folder_attributes["resolutionHeight"] height = folder_attributes["resolutionHeight"]
@ -285,3 +308,98 @@ def comp_lock_and_undo_chunk(
finally: finally:
comp.Unlock() comp.Unlock()
comp.EndUndo(keep_undo) comp.EndUndo(keep_undo)
def update_content_on_context_change():
"""Update all Creator instances to current asset"""
host = registered_host()
context = host.get_current_context()
folder_path = context["folder_path"]
task = context["task_name"]
create_context = CreateContext(host, reset=True)
for instance in create_context.instances:
instance_folder_path = instance.get("folderPath")
if instance_folder_path and instance_folder_path != folder_path:
instance["folderPath"] = folder_path
instance_task = instance.get("task")
if instance_task and instance_task != task:
instance["task"] = task
create_context.save_changes()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from ayon_core.style import load_stylesheet
from ayon_core.lib import BoolDef, UILabelDef
from qtpy import QtWidgets, QtCore
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset Comp FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset Comp start and end frame ranges",
default=True
),
BoolDef(
"resolution",
label="Comp Resolution",
tooltip="Reset Comp resolution",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowFlags(
dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
folder_entity = get_current_project_folder()
if options["frame_range"]:
set_current_context_framerange(folder_entity)
if options["fps"]:
set_current_context_fps(folder_entity)
if options["resolution"]:
set_current_context_resolution(folder_entity)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()

View file

@ -5,6 +5,7 @@ import os
import sys import sys
import logging import logging
import contextlib import contextlib
from pathlib import Path
import pyblish.api import pyblish.api
from qtpy import QtCore from qtpy import QtCore
@ -28,7 +29,8 @@ from ayon_core.tools.utils import host_tools
from .lib import ( from .lib import (
get_current_comp, get_current_comp,
validate_comp_prefs validate_comp_prefs,
prompt_reset_context
) )
log = Logger.get_logger(__name__) log = Logger.get_logger(__name__)
@ -40,6 +42,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create") CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save
_about_to_save = False
class FusionLogHandler(logging.Handler): class FusionLogHandler(logging.Handler):
# Keep a reference to fusion's Print function (Remote Object) # Keep a reference to fusion's Print function (Remote Object)
@ -103,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
# Register events # Register events
register_event_callback("open", on_after_open) register_event_callback("open", on_after_open)
register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback("save", on_save) register_event_callback("save", on_save)
register_event_callback("new", on_new) register_event_callback("new", on_new)
register_event_callback("taskChanged", on_task_changed)
# region workfile io api # region workfile io api
def has_unsaved_changes(self): def has_unsaved_changes(self):
@ -168,6 +175,19 @@ def on_save(event):
comp = event["sender"] comp = event["sender"]
validate_comp_prefs(comp) validate_comp_prefs(comp)
# We are now starting the actual save directly
global _about_to_save
_about_to_save = False
def on_task_changed():
global _about_to_save
print(f"Task changed: {_about_to_save}")
# TODO: Only do this if not headless
if _about_to_save:
# Let's prompt the user to update the context settings or not
prompt_reset_context()
def on_after_open(event): def on_after_open(event):
comp = event["sender"] comp = event["sender"]
@ -201,6 +221,28 @@ def on_after_open(event):
dialog.setStyleSheet(load_stylesheet()) dialog.setStyleSheet(load_stylesheet())
def before_workfile_save(event):
# Due to Fusion's external python process design we can't really
# detect whether the current Fusion environment matches the one the artists
# expects it to be. For example, our pipeline python process might
# have been shut down, and restarted - which will restart it to the
# environment Fusion started with; not necessarily where the artist
# is currently working.
# The `_about_to_save` var is used to detect context changes when
# saving into another asset. If we keep it False it will be ignored
# as context change. As such, before we change tasks we will only
# consider it the current filepath is within the currently known
# AVALON_WORKDIR. This way we avoid false positives of thinking it's
# saving to another context and instead sometimes just have false negatives
# where we fail to show the "Update on task change" prompt.
comp = get_current_comp()
filepath = comp.GetAttrs()["COMPS_FileName"]
workdir = os.environ.get("AYON_WORKDIR")
if Path(workdir) in Path(filepath).parents:
global _about_to_save
_about_to_save = True
def ls(): def ls():
"""List containers from active Fusion scene """List containers from active Fusion scene
@ -337,7 +379,6 @@ class FusionEventHandler(QtCore.QObject):
>>> handler = FusionEventHandler(parent=window) >>> handler = FusionEventHandler(parent=window)
>>> handler.start() >>> handler.start()
""" """
ACTION_IDS = [ ACTION_IDS = [
"Comp_Save", "Comp_Save",

View file

@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs):
pane = stateutils.activePane(kwargs) pane = stateutils.activePane(kwargs)
if isinstance(pane, hou.NetworkEditor): if isinstance(pane, hou.NetworkEditor):
pwd = pane.pwd() pwd = pane.pwd()
project_name = context.get_current_project_name(), project_name = context.get_current_project_name()
folder_path = context.get_current_folder_path() folder_path = context.get_current_folder_path()
task_name = context.get_current_task_name() task_name = context.get_current_task_name()
folder_entity = ayon_api.get_folder_by_path( folder_entity = ayon_api.get_folder_by_path(

View file

@ -39,7 +39,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save # Track whether the workfile tool is about to save
ABOUT_TO_SAVE = False _about_to_save = False
class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
@ -292,8 +292,8 @@ def ls():
def before_workfile_save(event): def before_workfile_save(event):
global ABOUT_TO_SAVE global _about_to_save
ABOUT_TO_SAVE = True _about_to_save = True
def before_save(): def before_save():
@ -307,18 +307,14 @@ def on_save():
# update houdini vars # update houdini vars
lib.update_houdini_vars_context_dialog() lib.update_houdini_vars_context_dialog()
nodes = lib.get_id_required_nodes()
for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False)
# We are now starting the actual save directly # We are now starting the actual save directly
global ABOUT_TO_SAVE global _about_to_save
ABOUT_TO_SAVE = False _about_to_save = False
def on_task_changed(): def on_task_changed():
global ABOUT_TO_SAVE global _about_to_save
if not IS_HEADLESS and ABOUT_TO_SAVE: if not IS_HEADLESS and _about_to_save:
# Let's prompt the user to update the context settings or not # Let's prompt the user to update the context settings or not
lib.prompt_reset_context() lib.prompt_reset_context()

View file

@ -4,7 +4,10 @@ from __future__ import absolute_import
import pyblish.api import pyblish.api
import ayon_api import ayon_api
from ayon_core.pipeline.publish import get_errored_instances_from_context from ayon_core.pipeline.publish import (
get_errored_instances_from_context,
get_errored_plugins_from_context
)
class GenerateUUIDsOnInvalidAction(pyblish.api.Action): class GenerateUUIDsOnInvalidAction(pyblish.api.Action):
@ -112,20 +115,25 @@ class SelectInvalidAction(pyblish.api.Action):
except ImportError: except ImportError:
raise ImportError("Current host is not Maya") raise ImportError("Current host is not Maya")
errored_instances = get_errored_instances_from_context(context,
plugin=plugin)
# Get the invalid nodes for the plug-ins # Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes..") self.log.info("Finding invalid nodes..")
invalid = list() invalid = list()
for instance in errored_instances: if issubclass(plugin, pyblish.api.ContextPlugin):
invalid_nodes = plugin.get_invalid(instance) errored_plugins = get_errored_plugins_from_context(context)
if invalid_nodes: if plugin in errored_plugins:
if isinstance(invalid_nodes, (list, tuple)): invalid = plugin.get_invalid(context)
invalid.extend(invalid_nodes) else:
else: errored_instances = get_errored_instances_from_context(
self.log.warning("Plug-in returned to be invalid, " context, plugin=plugin
"but has no selectable nodes.") )
for instance in errored_instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning("Plug-in returned to be invalid, "
"but has no selectable nodes.")
# Ensure unique (process each node only once) # Ensure unique (process each node only once)
invalid = list(set(invalid)) invalid = list(set(invalid))

View file

@ -113,7 +113,9 @@ def override_toolbox_ui():
annotation="Look Manager", annotation="Look Manager",
label="Look Manager", label="Look Manager",
image=os.path.join(icons, "lookmanager.png"), image=os.path.join(icons, "lookmanager.png"),
command=show_look_assigner, command=lambda: show_look_assigner(
parent=parent_widget
),
width=icon_size, width=icon_size,
height=icon_size, height=icon_size,
parent=parent parent=parent

View file

@ -1876,18 +1876,9 @@ def list_looks(project_name, folder_id):
list[dict[str, Any]]: List of look products. list[dict[str, Any]]: List of look products.
""" """
# # get all products with look leading in return list(ayon_api.get_products(
# the name associated with the asset project_name, folder_ids=[folder_id], product_types={"look"}
# TODO this should probably look for product type 'look' instead of ))
# checking product name that can not start with product type
product_entities = ayon_api.get_products(
project_name, folder_ids=[folder_id]
)
return [
product_entity
for product_entity in product_entities
if product_entity["name"].startswith("look")
]
def assign_look_by_version(nodes, version_id): def assign_look_by_version(nodes, version_id):
@ -1906,12 +1897,15 @@ def assign_look_by_version(nodes, version_id):
project_name = get_current_project_name() project_name = get_current_project_name()
# Get representations of shader file and relationships # Get representations of shader file and relationships
look_representation = ayon_api.get_representation_by_name( representations = list(ayon_api.get_representations(
project_name, "ma", version_id project_name=project_name,
) representation_names={"ma", "json"},
json_representation = ayon_api.get_representation_by_name( version_ids=[version_id]
project_name, "json", version_id ))
) look_representation = next(
repre for repre in representations if repre["name"] == "ma")
json_representation = next(
repre for repre in representations if repre["name"] == "json")
# See if representation is already loaded, if so reuse it. # See if representation is already loaded, if so reuse it.
host = registered_host() host = registered_host()
@ -1948,7 +1942,7 @@ def assign_look_by_version(nodes, version_id):
apply_shaders(relationships, shader_nodes, nodes) apply_shaders(relationships, shader_nodes, nodes)
def assign_look(nodes, product_name="lookDefault"): def assign_look(nodes, product_name="lookMain"):
"""Assigns a look to a node. """Assigns a look to a node.
Optimizes the nodes by grouping by folder id and finding Optimizes the nodes by grouping by folder id and finding
@ -1981,14 +1975,10 @@ def assign_look(nodes, product_name="lookDefault"):
product_entity["id"] product_entity["id"]
for product_entity in product_entities_by_folder_id.values() for product_entity in product_entities_by_folder_id.values()
} }
last_version_entities = ayon_api.get_last_versions( last_version_entities_by_product_id = ayon_api.get_last_versions(
project_name, project_name,
product_ids product_ids
) )
last_version_entities_by_product_id = {
last_version_entity["productId"]: last_version_entity
for last_version_entity in last_version_entities
}
for folder_id, asset_nodes in grouped.items(): for folder_id, asset_nodes in grouped.items():
product_entity = product_entities_by_folder_id.get(folder_id) product_entity = product_entities_by_folder_id.get(folder_id)
@ -2651,31 +2641,114 @@ def reset_scene_resolution():
set_scene_resolution(width, height, pixelAspect) set_scene_resolution(width, height, pixelAspect)
def set_context_settings(): def set_context_settings(
fps=True,
resolution=True,
frame_range=True,
colorspace=True
):
"""Apply the project settings from the project definition """Apply the project settings from the project definition
Settings can be overwritten by an folder if the folder.attrib contains Settings can be overwritten by an asset if the asset.data contains
any information regarding those settings. any information regarding those settings.
Examples of settings: Args:
fps fps (bool): Whether to set the scene FPS.
resolution resolution (bool): Whether to set the render resolution.
renderer frame_range (bool): Whether to reset the time slide frame ranges.
colorspace (bool): Whether to reset the colorspace.
Returns: Returns:
None None
""" """
# Set project fps if fps:
set_scene_fps(get_fps_for_current_context()) # Set project fps
set_scene_fps(get_fps_for_current_context())
reset_scene_resolution() if resolution:
reset_scene_resolution()
# Set frame range. # Set frame range.
reset_frame_range() if frame_range:
reset_frame_range(fps=False)
# Set colorspace # Set colorspace
set_colorspace() if colorspace:
set_colorspace()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from ayon_core.style import load_stylesheet
from ayon_core.lib import BoolDef, UILabelDef
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset workfile FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset workfile start and end frame ranges",
default=True
),
BoolDef(
"resolution",
label="Resolution",
tooltip="Reset workfile resolution",
default=True
),
BoolDef(
"colorspace",
label="Colorspace",
tooltip="Reset workfile resolution",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
with suspended_refresh():
set_context_settings(
fps=options["fps"],
resolution=options["resolution"],
frame_range=options["frame_range"],
colorspace=options["colorspace"]
)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()
# Valid FPS # Valid FPS

View file

@ -67,6 +67,9 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
AVALON_CONTAINERS = ":AVALON_CONTAINERS" AVALON_CONTAINERS = ":AVALON_CONTAINERS"
# Track whether the workfile tool is about to save
_about_to_save = False
class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "maya" name = "maya"
@ -581,6 +584,10 @@ def on_save():
for node, new_id in lib.generate_ids(nodes): for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False) lib.set_id(node, new_id, overwrite=False)
# We are now starting the actual save directly
global _about_to_save
_about_to_save = False
def on_open(): def on_open():
"""On scene open let's assume the containers have changed.""" """On scene open let's assume the containers have changed."""
@ -650,6 +657,11 @@ def on_task_changed():
lib.set_context_settings() lib.set_context_settings()
lib.update_content_on_context_change() lib.update_content_on_context_change()
global _about_to_save
if not lib.IS_HEADLESS and _about_to_save:
# Let's prompt the user to update the context settings or not
lib.prompt_reset_context()
def before_workfile_open(): def before_workfile_open():
if handle_workfile_locks(): if handle_workfile_locks():
@ -664,6 +676,9 @@ def before_workfile_save(event):
if workdir_path: if workdir_path:
create_workspace_mel(workdir_path, project_name) create_workspace_mel(workdir_path, project_name)
global _about_to_save
_about_to_save = True
def workfile_save_before_xgen(event): def workfile_save_before_xgen(event):
"""Manage Xgen external files when switching context. """Manage Xgen external files when switching context.

View file

@ -142,9 +142,21 @@ class ImagePlaneLoader(load.LoaderPlugin):
with namespaced(namespace): with namespaced(namespace):
# Create inside the namespace # Create inside the namespace
image_plane_transform, image_plane_shape = cmds.imagePlane( image_plane_transform, image_plane_shape = cmds.imagePlane(
fileName=context["representation"]["data"]["path"], fileName=self.filepath_from_context(context),
camera=camera camera=camera
) )
# Set colorspace
colorspace = self.get_colorspace(context["representation"])
if colorspace:
cmds.setAttr(
"{}.ignoreColorSpaceFileRules".format(image_plane_shape),
True
)
cmds.setAttr("{}.colorSpace".format(image_plane_shape),
colorspace, type="string")
# Set offset frame range
start_frame = cmds.playbackOptions(query=True, min=True) start_frame = cmds.playbackOptions(query=True, min=True)
end_frame = cmds.playbackOptions(query=True, max=True) end_frame = cmds.playbackOptions(query=True, max=True)
@ -216,6 +228,15 @@ class ImagePlaneLoader(load.LoaderPlugin):
repre_entity["id"], repre_entity["id"],
type="string") type="string")
colorspace = self.get_colorspace(repre_entity)
if colorspace:
cmds.setAttr(
"{}.ignoreColorSpaceFileRules".format(image_plane_shape),
True
)
cmds.setAttr("{}.colorSpace".format(image_plane_shape),
colorspace, type="string")
# Set frame range. # Set frame range.
start_frame = folder_entity["attrib"]["frameStart"] start_frame = folder_entity["attrib"]["frameStart"]
end_frame = folder_entity["attrib"]["frameEnd"] end_frame = folder_entity["attrib"]["frameEnd"]
@ -243,3 +264,12 @@ class ImagePlaneLoader(load.LoaderPlugin):
deleteNamespaceContent=True) deleteNamespaceContent=True)
except RuntimeError: except RuntimeError:
pass pass
def get_colorspace(self, representation):
data = representation.get("data", {}).get("colorspaceData", {})
if not data:
return
colorspace = data.get("colorspace")
return colorspace

View file

@ -1,24 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Collect render data. """Collect render data.
This collector will go through render layers in maya and prepare all data This collector will go through renderlayer instances and prepare all data
needed to create instances and their representations for submission and needed to detect the expected rendered files for a layer, with resolution,
publishing on farm. frame ranges and collects the data needed for publishing on the farm.
Requires: Requires:
instance -> families instance -> families
instance -> setMembers
instance -> folderPath
context -> currentFile context -> currentFile
context -> workspaceDir
context -> user context -> user
Optional:
Provides: Provides:
instance -> label instance -> label
instance -> productName instance -> subset
instance -> attachTo instance -> attachTo
instance -> setMembers instance -> setMembers
instance -> publish instance -> publish
@ -26,6 +21,8 @@ Provides:
instance -> frameEnd instance -> frameEnd
instance -> byFrameStep instance -> byFrameStep
instance -> renderer instance -> renderer
instance -> family
instance -> asset
instance -> time instance -> time
instance -> author instance -> author
instance -> source instance -> source
@ -71,8 +68,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
# TODO: Re-add force enable of workfile instance? # TODO: Re-add force enable of workfile instance?
# TODO: Re-add legacy layer support with LAYER_ prefix but in Creator # TODO: Re-add legacy layer support with LAYER_ prefix but in Creator
# TODO: Set and collect active state of RenderLayer in Creator using
# renderlayer.isRenderable()
context = instance.context context = instance.context
layer = instance.data["transientData"]["layer"] layer = instance.data["transientData"]["layer"]
@ -112,7 +107,13 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
except UnsupportedRendererException as exc: except UnsupportedRendererException as exc:
raise KnownPublishError(exc) raise KnownPublishError(exc)
render_products = layer_render_products.layer_data.products render_products = layer_render_products.layer_data.products
assert render_products, "no render products generated" if not render_products:
self.log.error(
"No render products generated for '%s'. You might not have "
"any render camera in the renderlayer or render end frame is "
"lower than start frame.",
instance.name
)
expected_files = [] expected_files = []
multipart = False multipart = False
for product in render_products: for product in render_products:
@ -130,16 +131,21 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
}) })
has_cameras = any(product.camera for product in render_products) has_cameras = any(product.camera for product in render_products)
assert has_cameras, "No render cameras found." if render_products and not has_cameras:
self.log.error(
self.log.debug("multipart: {}".format( "No render cameras found for: %s",
multipart)) instance
assert expected_files, "no file names were generated, this is a bug"
self.log.debug(
"expected files: {}".format(
json.dumps(expected_files, indent=4, sort_keys=True)
) )
) if not expected_files:
self.log.warning(
"No file names were generated, this is a bug.")
for render_product in render_products:
self.log.debug(render_product)
self.log.debug("multipart: {}".format(multipart))
self.log.debug("expected files: {}".format(
json.dumps(expected_files, indent=4, sort_keys=True)
))
# if we want to attach render to product, check if we have AOV's # if we want to attach render to product, check if we have AOV's
# in expectedFiles. If so, raise error as we cannot attach AOV # in expectedFiles. If so, raise error as we cannot attach AOV
@ -151,14 +157,14 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
) )
# append full path # append full path
aov_dict = {}
image_directory = os.path.join( image_directory = os.path.join(
cmds.workspace(query=True, rootDirectory=True), cmds.workspace(query=True, rootDirectory=True),
cmds.workspace(fileRuleEntry="images") cmds.workspace(fileRuleEntry="images")
) )
# replace relative paths with absolute. Render products are # replace relative paths with absolute. Render products are
# returned as list of dictionaries. # returned as list of dictionaries.
publish_meta_path = None publish_meta_path = "NOT-SET"
aov_dict = {}
for aov in expected_files: for aov in expected_files:
full_paths = [] full_paths = []
aov_first_key = list(aov.keys())[0] aov_first_key = list(aov.keys())[0]
@ -169,14 +175,6 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
publish_meta_path = os.path.dirname(full_path) publish_meta_path = os.path.dirname(full_path)
aov_dict[aov_first_key] = full_paths aov_dict[aov_first_key] = full_paths
full_exp_files = [aov_dict] full_exp_files = [aov_dict]
self.log.debug(full_exp_files)
if publish_meta_path is None:
raise KnownPublishError("Unable to detect any expected output "
"images for: {}. Make sure you have a "
"renderable camera and a valid frame "
"range set for your renderlayer."
"".format(instance.name))
frame_start_render = int(self.get_render_attribute( frame_start_render = int(self.get_render_attribute(
"startFrame", layer=layer_name)) "startFrame", layer=layer_name))
@ -222,7 +220,8 @@ class CollectMayaRender(pyblish.api.InstancePlugin):
common_publish_meta_path = "/" + common_publish_meta_path common_publish_meta_path = "/" + common_publish_meta_path
self.log.debug( self.log.debug(
"Publish meta path: {}".format(common_publish_meta_path)) "Publish meta path: {}".format(common_publish_meta_path)
)
# Get layer specific settings, might be overrides # Get layer specific settings, might be overrides
colorspace_data = lib.get_color_management_preferences() colorspace_data = lib.get_color_management_preferences()

View file

@ -5,7 +5,8 @@ from maya import cmds
from ayon_core.pipeline import publish from ayon_core.pipeline import publish
class ExtractGPUCache(publish.Extractor): class ExtractGPUCache(publish.Extractor,
publish.OptionalPyblishPluginMixin):
"""Extract the content of the instance to a GPU cache file.""" """Extract the content of the instance to a GPU cache file."""
label = "GPU Cache" label = "GPU Cache"
@ -20,6 +21,9 @@ class ExtractGPUCache(publish.Extractor):
useBaseTessellation = True useBaseTessellation = True
def process(self, instance): def process(self, instance):
if not self.is_active(instance.data):
return
cmds.loadPlugin("gpuCache", quiet=True) cmds.loadPlugin("gpuCache", quiet=True)
staging_dir = self.staging_dir(instance) staging_dir = self.staging_dir(instance)

View file

@ -26,6 +26,10 @@ class ExtractAlembic(publish.Extractor):
families = ["pointcache", "model", "vrayproxy.alembic"] families = ["pointcache", "model", "vrayproxy.alembic"]
targets = ["local", "remote"] targets = ["local", "remote"]
# From settings
bake_attributes = []
bake_attribute_prefixes = []
def process(self, instance): def process(self, instance):
if instance.data.get("farm"): if instance.data.get("farm"):
self.log.debug("Should be processed on farm, skipping.") self.log.debug("Should be processed on farm, skipping.")
@ -40,10 +44,12 @@ class ExtractAlembic(publish.Extractor):
attrs = instance.data.get("attr", "").split(";") attrs = instance.data.get("attr", "").split(";")
attrs = [value for value in attrs if value.strip()] attrs = [value for value in attrs if value.strip()]
attrs += instance.data.get("userDefinedAttributes", []) attrs += instance.data.get("userDefinedAttributes", [])
attrs += self.bake_attributes
attrs += ["cbId"] attrs += ["cbId"]
attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = instance.data.get("attrPrefix", "").split(";")
attr_prefixes = [value for value in attr_prefixes if value.strip()] attr_prefixes = [value for value in attr_prefixes if value.strip()]
attr_prefixes += self.bake_attribute_prefixes
self.log.debug("Extracting pointcache..") self.log.debug("Extracting pointcache..")
dirname = self.staging_dir(instance) dirname = self.staging_dir(instance)

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Shape IDs mismatch original shape</title>
<description>## Shapes mismatch IDs with original shape
Meshes are detected where the (deformed) mesh has a different `cbId` than
the same mesh in its deformation history.
Theses should normally be the same.
### How to repair?
By using the repair action the IDs from the shape in history will be
copied to the deformed shape. For **animation** instances using the
repair action usually is usually the correct fix.
</description>
<detail>
### How does this happen?
When a deformer is applied in the scene on a referenced mesh that had no
deformers then Maya will create a new shape node for the mesh that
does not have the original id. Then on scene save new ids get created for the
meshes lacking a `cbId` and thus the mesh then has a different `cbId` than
the mesh in the deformation history.
</detail>
</error>
</root>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Non-Manifold Edges/Vertices</title>
<description>## Non-Manifold Edges/Vertices
Meshes found with non-manifold edges or vertices.
### How to repair?
Run select invalid to select the invalid components.
You can also try the _cleanup matching polygons_ action which will perform a
cleanup like Maya's `Mesh > Cleanup...` modeling tool.
It is recommended to always select the invalid to see where the issue is
because if you run any repair on it you will need to double check the topology
is still like you wanted.
</description>
<detail>
### What is non-manifold topology?
_Non-manifold topology_ polygons have a configuration that cannot be unfolded
into a continuous flat piece, for example:
- Three or more faces share an edge
- Two or more faces share a single vertex but no edge.
- Adjacent faces have opposite normals
</detail>
</error>
</root>

View file

@ -6,7 +6,7 @@ from ayon_core.hosts.maya.api import lib
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
RepairAction, RepairAction,
ValidateContentsOrder, ValidateContentsOrder,
PublishValidationError, PublishXmlValidationError,
OptionalPyblishPluginMixin, OptionalPyblishPluginMixin,
get_plugin_settings, get_plugin_settings,
apply_plugin_settings_automatically apply_plugin_settings_automatically
@ -56,40 +56,39 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin,
# if a deformer has been created on the shape # if a deformer has been created on the shape
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
# TODO: Message formatting can be improved
raise PublishValidationError("Nodes found with mismatching " # Use the short names
"IDs: {0}".format(invalid), invalid = cmds.ls(invalid)
title="Invalid node ids") invalid.sort()
# Construct a human-readable list
invalid = "\n".join("- {}".format(node) for node in invalid)
raise PublishXmlValidationError(
plugin=self,
message=(
"Nodes have different IDs than their input "
"history: \n{0}".format(invalid)
)
)
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):
"""Get all nodes which do not match the criteria""" """Get all nodes which do not match the criteria"""
invalid = [] invalid = []
types_to_skip = ["locator"] types = ["mesh", "nurbsCurve", "nurbsSurface"]
# get asset id # get asset id
nodes = instance.data.get("out_hierarchy", instance[:]) nodes = instance.data.get("out_hierarchy", instance[:])
for node in nodes: for node in cmds.ls(nodes, type=types, long=True):
# We only check when the node is *not* referenced # We only check when the node is *not* referenced
if cmds.referenceQuery(node, isNodeReferenced=True): if cmds.referenceQuery(node, isNodeReferenced=True):
continue continue
# Check if node is a shape as deformers only work on shapes
obj_type = cmds.objectType(node, isAType="shape")
if not obj_type:
continue
# Skip specific types
if cmds.objectType(node) in types_to_skip:
continue
# Get the current id of the node # Get the current id of the node
node_id = lib.get_id(node) node_id = lib.get_id(node)
if not node_id:
invalid.append(node)
continue
history_id = lib.get_id_from_sibling(node) history_id = lib.get_id_from_sibling(node)
if history_id is not None and node_id != history_id: if history_id is not None and node_id != history_id:

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
) )
class ValidateColorSets(pyblish.api.Validator, class ValidateColorSets(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Validate all meshes in the instance have unlocked normals """Validate all meshes in the instance have unlocked normals

View file

@ -47,10 +47,18 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin,
shape, destination=True, type="shadingEngine" shape, destination=True, type="shadingEngine"
) or [] ) or []
for shading_engine in shading_engines: for shading_engine in shading_engines:
name = ( materials = cmds.listConnections(
cmds.listConnections(shading_engine + ".surfaceShader")[0] shading_engine + ".surfaceShader",
+ "SG" source=True, destination=False
) )
if not materials:
cls.log.warning(
"Shading engine '{}' has no material connected to its "
".surfaceShader attribute.".format(shading_engine))
continue
material = materials[0] # there should only ever be one input
name = material + "SG"
if shading_engine != name: if shading_engine != name:
invalid.append(shading_engine) invalid.append(shading_engine)

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
) )
class ValidateMeshNgons(pyblish.api.Validator, class ValidateMeshNgons(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure that meshes don't have ngons """Ensure that meshes don't have ngons

View file

@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values) return prefix + (suffix + prefix).join(values)
class ValidateMeshNoNegativeScale(pyblish.api.Validator, class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale. """Ensure that meshes don't have a negative scale.

View file

@ -1,14 +1,99 @@
from maya import cmds from maya import cmds, mel
import pyblish.api import pyblish.api
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
ValidateMeshOrder, ValidateMeshOrder,
PublishValidationError, PublishXmlValidationError,
RepairAction,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin
) )
def poly_cleanup(version=4,
meshes=None,
# Version 1
all_meshes=False,
select_only=False,
history_on=True,
quads=False,
nsided=False,
concave=False,
holed=False,
nonplanar=False,
zeroGeom=False,
zeroGeomTolerance=1e-05,
zeroEdge=False,
zeroEdgeTolerance=1e-05,
zeroMap=False,
zeroMapTolerance=1e-05,
# Version 2
shared_uvs=False,
non_manifold=False,
# Version 3
lamina=False,
# Version 4
invalid_components=False):
"""Wrapper around `polyCleanupArgList` mel command"""
# Get all inputs named as `dict` to easily do conversions and formatting
values = locals()
# Convert booleans to 1 or 0
for key in [
"all_meshes",
"select_only",
"history_on",
"quads",
"nsided",
"concave",
"holed",
"nonplanar",
"zeroGeom",
"zeroEdge",
"zeroMap",
"shared_uvs",
"non_manifold",
"lamina",
"invalid_components",
]:
values[key] = 1 if values[key] else 0
cmd = (
'polyCleanupArgList {version} {{ '
'"{all_meshes}",' # 0: All selectable meshes
'"{select_only}",' # 1: Only perform a selection
'"{history_on}",' # 2: Keep construction history
'"{quads}",' # 3: Check for quads polys
'"{nsided}",' # 4: Check for n-sides polys
'"{concave}",' # 5: Check for concave polys
'"{holed}",' # 6: Check for holed polys
'"{nonplanar}",' # 7: Check for non-planar polys
'"{zeroGeom}",' # 8: Check for 0 area faces
'"{zeroGeomTolerance}",' # 9: Tolerance for face areas
'"{zeroEdge}",' # 10: Check for 0 length edges
'"{zeroEdgeTolerance}",' # 11: Tolerance for edge length
'"{zeroMap}",' # 12: Check for 0 uv face area
'"{zeroMapTolerance}",' # 13: Tolerance for uv face areas
'"{shared_uvs}",' # 14: Unshare uvs that are shared
# across vertices
'"{non_manifold}",' # 15: Check for nonmanifold polys
'"{lamina}",' # 16: Check for lamina polys
'"{invalid_components}"' # 17: Remove invalid components
' }};'.format(**values)
)
mel.eval("source polyCleanupArgList")
if not all_meshes and meshes:
# Allow to specify meshes to run over by selecting them
cmds.select(meshes, replace=True)
mel.eval(cmd)
class CleanupMatchingPolygons(RepairAction):
label = "Cleanup matching polygons"
def _as_report_list(values, prefix="- ", suffix="\n"): def _as_report_list(values, prefix="- ", suffix="\n"):
"""Return list as bullet point list for a report""" """Return list as bullet point list for a report"""
if not values: if not values:
@ -16,7 +101,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values) return prefix + (suffix + prefix).join(values)
class ValidateMeshNonManifold(pyblish.api.Validator, class ValidateMeshNonManifold(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure that meshes don't have non-manifold edges or vertices """Ensure that meshes don't have non-manifold edges or vertices
@ -29,7 +114,8 @@ class ValidateMeshNonManifold(pyblish.api.Validator,
hosts = ['maya'] hosts = ['maya']
families = ['model'] families = ['model']
label = 'Mesh Non-Manifold Edges/Vertices' label = 'Mesh Non-Manifold Edges/Vertices'
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction,
CleanupMatchingPolygons]
optional = True optional = True
@staticmethod @staticmethod
@ -39,9 +125,11 @@ class ValidateMeshNonManifold(pyblish.api.Validator,
invalid = [] invalid = []
for mesh in meshes: for mesh in meshes:
if (cmds.polyInfo(mesh, nonManifoldVertices=True) or components = cmds.polyInfo(mesh,
cmds.polyInfo(mesh, nonManifoldEdges=True)): nonManifoldVertices=True,
invalid.append(mesh) nonManifoldEdges=True)
if components:
invalid.extend(components)
return invalid return invalid
@ -49,12 +137,34 @@ class ValidateMeshNonManifold(pyblish.api.Validator,
"""Process all the nodes in the instance 'objectSet'""" """Process all the nodes in the instance 'objectSet'"""
if not self.is_active(instance.data): if not self.is_active(instance.data):
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError( # Report only the meshes instead of all component indices
"Meshes found with non-manifold edges/vertices:\n\n{0}".format( invalid_meshes = {
_as_report_list(sorted(invalid)) component.split(".", 1)[0] for component in invalid
), }
title="Non-Manifold Edges/Vertices" invalid_meshes = _as_report_list(sorted(invalid_meshes))
raise PublishXmlValidationError(
plugin=self,
message=(
"Meshes found with non-manifold "
"edges/vertices:\n\n{0}".format(invalid_meshes)
)
) )
@classmethod
def repair(cls, instance):
invalid_components = cls.get_invalid(instance)
if not invalid_components:
cls.log.info("No invalid components found to cleanup.")
return
invalid_meshes = {
component.split(".", 1)[0] for component in invalid_components
}
poly_cleanup(meshes=list(invalid_meshes),
select_only=True,
non_manifold=True)

View file

@ -18,7 +18,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values) return prefix + (suffix + prefix).join(values)
class ValidateMeshNormalsUnlocked(pyblish.api.Validator, class ValidateMeshNormalsUnlocked(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Validate all meshes in the instance have unlocked normals """Validate all meshes in the instance have unlocked normals

View file

@ -16,7 +16,7 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
return prefix + (suffix + prefix).join(values) return prefix + (suffix + prefix).join(values)
class ValidateNoAnimation(pyblish.api.Validator, class ValidateNoAnimation(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure no keyframes on nodes in the Instance. """Ensure no keyframes on nodes in the Instance.

View file

@ -19,22 +19,17 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
def has_shape_children(node): def has_shape_children(node):
# Check if any descendants # Check if any descendants
allDescendents = cmds.listRelatives(node, all_descendents = cmds.listRelatives(node,
allDescendents=True, allDescendents=True,
fullPath=True) fullPath=True)
if not allDescendents: if not all_descendents:
return False return False
# Check if there are any shapes at all # Check if there are any shapes at all
shapes = cmds.ls(allDescendents, shapes=True) shapes = cmds.ls(all_descendents, shapes=True, noIntermediate=True)
if not shapes: if not shapes:
return False return False
# Check if all descendent shapes are intermediateObjects;
# if so we consider this node a null node and return False.
if all(cmds.getAttr('{0}.intermediateObject'.format(x)) for x in shapes):
return False
return True return True

View file

@ -1,4 +1,5 @@
import re import re
import inspect
import pyblish.api import pyblish.api
from maya import cmds from maya import cmds
@ -36,7 +37,10 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin,
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
raise PublishValidationError("Invalid cameras for render.") raise PublishValidationError(
"Invalid render cameras.",
description=self.get_description()
)
@classmethod @classmethod
def get_invalid(cls, instance): def get_invalid(cls, instance):
@ -51,17 +55,30 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin,
RenderSettings.get_image_prefix_attr(renderer) RenderSettings.get_image_prefix_attr(renderer)
) )
renderlayer = instance.data["renderlayer"]
if len(cameras) > 1: if len(cameras) > 1:
if re.search(cls.R_CAMERA_TOKEN, file_prefix): if re.search(cls.R_CAMERA_TOKEN, file_prefix):
# if there is <Camera> token in prefix and we have more then # if there is <Camera> token in prefix and we have more then
# 1 camera, all is ok. # 1 camera, all is ok.
return return
cls.log.error("Multiple renderable cameras found for %s: %s " % cls.log.error(
(instance.data["setMembers"], cameras)) "Multiple renderable cameras found for %s: %s ",
return [instance.data["setMembers"]] + cameras renderlayer, ", ".join(cameras))
return [renderlayer] + cameras
elif len(cameras) < 1: elif len(cameras) < 1:
cls.log.error("No renderable cameras found for %s " % cls.log.error("No renderable cameras found for %s ", renderlayer)
instance.data["setMembers"]) return [renderlayer]
return [instance.data["setMembers"]]
def get_description(self):
return inspect.cleandoc(
"""### Render Cameras Invalid
Your render cameras are misconfigured. You may have no render
camera set or have multiple cameras with a render filename
prefix that does not include the `<Camera>` token.
See the logs for more details about the cameras.
"""
)

View file

@ -6,11 +6,12 @@ import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish import (
RepairAction, RepairAction,
ValidateMeshOrder, ValidateMeshOrder,
PublishValidationError,
OptionalPyblishPluginMixin OptionalPyblishPluginMixin
) )
class ValidateShapeRenderStats(pyblish.api.Validator, class ValidateShapeRenderStats(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Ensure all render stats are set to the default values.""" """Ensure all render stats are set to the default values."""
@ -20,7 +21,6 @@ class ValidateShapeRenderStats(pyblish.api.Validator,
label = 'Shape Default Render Stats' label = 'Shape Default Render Stats'
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction,
RepairAction] RepairAction]
optional = True
defaults = {'castsShadows': 1, defaults = {'castsShadows': 1,
'receiveShadows': 1, 'receiveShadows': 1,
@ -37,14 +37,13 @@ class ValidateShapeRenderStats(pyblish.api.Validator,
# It seems the "surfaceShape" and those derived from it have # It seems the "surfaceShape" and those derived from it have
# `renderStat` attributes. # `renderStat` attributes.
shapes = cmds.ls(instance, long=True, type='surfaceShape') shapes = cmds.ls(instance, long=True, type='surfaceShape')
invalid = [] invalid = set()
for shape in shapes: for shape in shapes:
_iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) for attr, default_value in cls.defaults.items():
for attr, default_value in _iteritems():
if cmds.attributeQuery(attr, node=shape, exists=True): if cmds.attributeQuery(attr, node=shape, exists=True):
value = cmds.getAttr('{}.{}'.format(shape, attr)) value = cmds.getAttr('{}.{}'.format(shape, attr))
if value != default_value: if value != default_value:
invalid.append(shape) invalid.add(shape)
return invalid return invalid
@ -52,17 +51,36 @@ class ValidateShapeRenderStats(pyblish.api.Validator,
if not self.is_active(instance.data): if not self.is_active(instance.data):
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if not invalid:
return
if invalid: defaults_str = "\n".join(
raise ValueError("Shapes with non-default renderStats " "- {}: {}\n".format(key, value)
"found: {0}".format(invalid)) for key, value in self.defaults.items()
)
description = (
"## Shape Default Render Stats\n"
"Shapes are detected with non-default render stats.\n\n"
"To ensure a model's shapes behave like a shape would by default "
"we require the render stats to have not been altered in "
"the published models.\n\n"
"### How to repair?\n"
"You can reset the default values on the shapes by using the "
"repair action."
)
raise PublishValidationError(
"Shapes with non-default renderStats "
"found: {0}".format(", ".join(sorted(invalid))),
description=description,
detail="The expected default values "
"are:\n\n{}".format(defaults_str)
)
@classmethod @classmethod
def repair(cls, instance): def repair(cls, instance):
for shape in cls.get_invalid(instance): for shape in cls.get_invalid(instance):
_iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items) for attr, default_value in cls.defaults.items():
for attr, default_value in _iteritems():
if cmds.attributeQuery(attr, node=shape, exists=True): if cmds.attributeQuery(attr, node=shape, exists=True):
plug = '{0}.{1}'.format(shape, attr) plug = '{0}.{1}'.format(shape, attr)
value = cmds.getAttr(plug) value = cmds.getAttr(plug)

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
) )
class ValidateShapeZero(pyblish.api.Validator, class ValidateShapeZero(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Shape components may not have any "tweak" values """Shape components may not have any "tweak" values

View file

@ -1,5 +1,6 @@
from maya import cmds import inspect
from maya import cmds
import pyblish.api import pyblish.api
import ayon_core.hosts.maya.api.action import ayon_core.hosts.maya.api.action
@ -10,7 +11,7 @@ from ayon_core.pipeline.publish import (
) )
class ValidateTransformZero(pyblish.api.Validator, class ValidateTransformZero(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Transforms can't have any values """Transforms can't have any values
@ -57,7 +58,7 @@ class ValidateTransformZero(pyblish.api.Validator,
if ('_LOC' in transform) or ('_loc' in transform): if ('_LOC' in transform) or ('_loc' in transform):
continue continue
mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True) mat = cmds.xform(transform, q=1, matrix=True, objectSpace=True)
if not all(abs(x-y) < cls._tolerance if not all(abs(x - y) < cls._tolerance
for x, y in zip(cls._identity, mat)): for x, y in zip(cls._identity, mat)):
invalid.append(transform) invalid.append(transform)
@ -69,14 +70,24 @@ class ValidateTransformZero(pyblish.api.Validator,
return return
invalid = self.get_invalid(instance) invalid = self.get_invalid(instance)
if invalid: if invalid:
names = "<br>".join( names = "<br>".join(
" - {}".format(node) for node in invalid " - {}".format(node) for node in invalid
) )
raise PublishValidationError( raise PublishValidationError(
title="Transform Zero", title="Transform Zero",
description=self.get_description(),
message="The model publish allows no transformations. You must" message="The model publish allows no transformations. You must"
" <b>freeze transformations</b> to continue.<br><br>" " <b>freeze transformations</b> to continue.<br><br>"
"Nodes found with transform values: " "Nodes found with transform values:<br>"
"{0}".format(names)) "{0}".format(names))
@staticmethod
def get_description():
return inspect.cleandoc("""### Transform can't have any values
The model publish allows no transformations.
You must **freeze transformations** to continue.
""")

View file

@ -9,7 +9,7 @@ from ayon_core.pipeline.publish import (
) )
class ValidateUniqueNames(pyblish.api.Validator, class ValidateUniqueNames(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""transform names should be unique """transform names should be unique

View file

@ -10,7 +10,7 @@ from ayon_core.pipeline.publish import (
) )
class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator, class ValidateYetiRigInputShapesInInstance(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin): OptionalPyblishPluginMixin):
"""Validate if all input nodes are part of the instance's hierarchy""" """Validate if all input nodes are part of the instance's hierarchy"""

View file

@ -51,7 +51,7 @@ def assign_vrayproxy_shaders(vrayproxy, assignments):
index += 1 index += 1
def vrayproxy_assign_look(vrayproxy, product_name="lookDefault"): def vrayproxy_assign_look(vrayproxy, product_name="lookMain"):
# type: (str, str) -> None # type: (str, str) -> None
"""Assign look to vray proxy. """Assign look to vray proxy.

View file

@ -389,7 +389,13 @@ def imprint(node, data, tab=None):
""" """
for knob in create_knobs(data, tab): for knob in create_knobs(data, tab):
node.addKnob(knob) # If knob name exists we set the value. Technically there could be
# multiple knobs with the same name, but the intent is not to have
# duplicated knobs so we do not account for that.
if knob.name() in node.knobs().keys():
node[knob.name()].setValue(knob.value())
else:
node.addKnob(knob)
@deprecated @deprecated

View file

@ -2,10 +2,10 @@ import nuke
import pyblish.api import pyblish.api
class ExtractScriptSave(pyblish.api.Extractor): class ExtractScriptSave(pyblish.api.InstancePlugin):
"""Save current Nuke workfile script""" """Save current Nuke workfile script"""
label = 'Script Save' label = 'Script Save'
order = pyblish.api.Extractor.order - 0.1 order = pyblish.api.ExtractorOrder - 0.1
hosts = ['nuke'] hosts = ['nuke']
def process(self, instance): def process(self, instance):

View file

@ -18,7 +18,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin):
"""Load mesh for project""" """Load mesh for project"""
product_types = {"*"} product_types = {"*"}
representations = ["abc", "fbx", "obj", "gltf"] representations = ["abc", "fbx", "obj", "gltf", "usd", "usda", "usdc"]
label = "Load mesh" label = "Load mesh"
order = -10 order = -10

View file

@ -25,8 +25,9 @@ from ayon_core.hosts.tvpaint.lib import (
) )
class ExtractSequence(pyblish.api.Extractor): class ExtractSequence(pyblish.api.InstancePlugin):
label = "Extract Sequence" label = "Extract Sequence"
order = pyblish.api.ExtractorOrder
hosts = ["tvpaint"] hosts = ["tvpaint"]
families = ["review", "render"] families = ["review", "render"]

View file

@ -1,15 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package helping with colorizing and formatting terminal output.""" """Package helping with colorizing and formatting terminal output."""
# ::
# //. ... .. ///. //.
# ///\\\ \\\ \\ ///\\\ ///
# /// \\ \\\ \\ /// \\ /// //
# \\\ // \\\ // \\\ // \\\// ./
# \\\// \\\// \\\// \\\' //
# \\\ \\\ \\\ \\\//
# ''' ''' ''' '''
# ..---===[[ PyP3 Setup ]]===---...
#
import re import re
import time import time
import threading import threading

View file

@ -45,7 +45,7 @@ ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$")
IMAGE_EXTENSIONS = { IMAGE_EXTENSIONS = {
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave",
".cal", ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dng", ".dpx", ".ecw", ".exr",
".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc",
".icer", ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", ".icer", ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2",
".jng", ".jpeg", ".jpeg-ls", ".jpeg-hdr", ".2000", ".jpg", ".jng", ".jpeg", ".jpeg-ls", ".jpeg-hdr", ".2000", ".jpg",

View file

@ -11,19 +11,17 @@ class ClockifyStart(LauncherAction):
order = 500 order = 500
clockify_api = ClockifyAPI() clockify_api = ClockifyAPI()
def is_compatible(self, session): def is_compatible(self, selection):
"""Return whether the action is compatible with the session""" """Return whether the action is compatible with the session"""
if "AYON_TASK_NAME" in session: return selection.is_task_selected
return True
return False
def process(self, session, **kwargs): def process(self, selection, **kwargs):
self.clockify_api.set_api() self.clockify_api.set_api()
user_id = self.clockify_api.user_id user_id = self.clockify_api.user_id
workspace_id = self.clockify_api.workspace_id workspace_id = self.clockify_api.workspace_id
project_name = session["AYON_PROJECT_NAME"] project_name = selection.project_name
folder_path = session["AYON_FOLDER_PATH"] folder_path = selection.folder_path
task_name = session["AYON_TASK_NAME"] task_name = selection.task_name
description = "/".join([folder_path.lstrip("/"), task_name]) description = "/".join([folder_path.lstrip("/"), task_name])
# fetch folder entity # fetch folder entity

View file

@ -19,15 +19,18 @@ class ClockifySync(LauncherAction):
order = 500 order = 500
clockify_api = ClockifyAPI() clockify_api = ClockifyAPI()
def is_compatible(self, session): def is_compatible(self, selection):
"""Check if there's some projects to sync""" """Check if there's some projects to sync"""
if selection.is_project_selected:
return True
try: try:
next(ayon_api.get_projects()) next(ayon_api.get_projects())
return True return True
except StopIteration: except StopIteration:
return False return False
def process(self, session, **kwargs): def process(self, selection, **kwargs):
self.clockify_api.set_api() self.clockify_api.set_api()
workspace_id = self.clockify_api.workspace_id workspace_id = self.clockify_api.workspace_id
user_id = self.clockify_api.user_id user_id = self.clockify_api.user_id
@ -37,10 +40,9 @@ class ClockifySync(LauncherAction):
raise ClockifyPermissionsCheckFailed( raise ClockifyPermissionsCheckFailed(
"Current CLockify user is missing permissions for this action!" "Current CLockify user is missing permissions for this action!"
) )
project_name = session.get("AYON_PROJECT_NAME") or ""
if project_name.strip(): if selection.is_project_selected:
projects_to_sync = [ayon_api.get_project(project_name)] projects_to_sync = [selection.project_entity]
else: else:
projects_to_sync = ayon_api.get_projects() projects_to_sync = ayon_api.get_projects()

View file

@ -80,6 +80,8 @@ class AfterEffectsSubmitDeadline(
"FTRACK_API_KEY", "FTRACK_API_KEY",
"FTRACK_API_USER", "FTRACK_API_USER",
"FTRACK_SERVER", "FTRACK_SERVER",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",

View file

@ -102,6 +102,8 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
"FTRACK_API_USER", "FTRACK_API_USER",
"FTRACK_SERVER", "FTRACK_SERVER",
"OPENPYPE_SG_USER", "OPENPYPE_SG_USER",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",

View file

@ -225,6 +225,8 @@ class FusionSubmitDeadline(
"FTRACK_API_KEY", "FTRACK_API_KEY",
"FTRACK_API_USER", "FTRACK_API_USER",
"FTRACK_SERVER", "FTRACK_SERVER",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",

View file

@ -273,6 +273,8 @@ class HarmonySubmitDeadline(
"FTRACK_API_KEY", "FTRACK_API_KEY",
"FTRACK_API_USER", "FTRACK_API_USER",
"FTRACK_SERVER", "FTRACK_SERVER",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",

View file

@ -106,12 +106,14 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
"FTRACK_API_USER", "FTRACK_API_USER",
"FTRACK_SERVER", "FTRACK_SERVER",
"OPENPYPE_SG_USER", "OPENPYPE_SG_USER",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",
"AYON_WORKDIR", "AYON_WORKDIR",
"AYON_APP_NAME", "AYON_APP_NAME",
"IS_TEST" "IS_TEST",
] ]
environment = { environment = {

View file

@ -207,6 +207,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
"FTRACK_API_USER", "FTRACK_API_USER",
"FTRACK_SERVER", "FTRACK_SERVER",
"OPENPYPE_SG_USER", "OPENPYPE_SG_USER",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",

View file

@ -376,6 +376,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
keys = [ keys = [
"PYTHONPATH", "PYTHONPATH",
"PATH", "PATH",
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_PROJECT_NAME", "AYON_PROJECT_NAME",
"AYON_FOLDER_PATH", "AYON_FOLDER_PATH",
"AYON_TASK_NAME", "AYON_TASK_NAME",
@ -388,7 +390,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin,
"TOOL_ENV", "TOOL_ENV",
"FOUNDRY_LICENSE", "FOUNDRY_LICENSE",
"OPENPYPE_SG_USER", "OPENPYPE_SG_USER",
"AYON_BUNDLE_NAME",
] ]
# add allowed keys from preset if any # add allowed keys from preset if any

View file

@ -133,6 +133,9 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin,
"AYON_RENDER_JOB": "0", "AYON_RENDER_JOB": "0",
"AYON_REMOTE_PUBLISH": "0", "AYON_REMOTE_PUBLISH": "0",
"AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"],
"AYON_DEFAULT_SETTINGS_VARIANT": (
os.environ["AYON_DEFAULT_SETTINGS_VARIANT"]
),
} }
# add environments from self.environ_keys # add environments from self.environ_keys

View file

@ -210,6 +210,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin,
"AYON_RENDER_JOB": "0", "AYON_RENDER_JOB": "0",
"AYON_REMOTE_PUBLISH": "0", "AYON_REMOTE_PUBLISH": "0",
"AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"], "AYON_BUNDLE_NAME": os.environ["AYON_BUNDLE_NAME"],
"AYON_DEFAULT_SETTINGS_VARIANT": (
os.environ["AYON_DEFAULT_SETTINGS_VARIANT"]
),
} }
# add environments from self.environ_keys # add environments from self.environ_keys

View file

@ -1,4 +1,8 @@
import logging import logging
import warnings
import ayon_api
from ayon_core.pipeline.plugin_discover import ( from ayon_core.pipeline.plugin_discover import (
discover, discover,
register_plugin, register_plugin,
@ -10,6 +14,288 @@ from ayon_core.pipeline.plugin_discover import (
from .load.utils import get_representation_path_from_context from .load.utils import get_representation_path_from_context
class LauncherActionSelection:
"""Object helper to pass selection to actions.
Object support backwards compatibility for 'session' from OpenPype where
environment variable keys were used to define selection.
Args:
project_name (str): Selected project name.
folder_id (str): Selected folder id.
task_id (str): Selected task id.
folder_path (Optional[str]): Selected folder path.
task_name (Optional[str]): Selected task name.
project_entity (Optional[dict[str, Any]]): Project entity.
folder_entity (Optional[dict[str, Any]]): Folder entity.
task_entity (Optional[dict[str, Any]]): Task entity.
"""
def __init__(
self,
project_name,
folder_id,
task_id,
folder_path=None,
task_name=None,
project_entity=None,
folder_entity=None,
task_entity=None
):
self._project_name = project_name
self._folder_id = folder_id
self._task_id = task_id
self._folder_path = folder_path
self._task_name = task_name
self._project_entity = project_entity
self._folder_entity = folder_entity
self._task_entity = task_entity
def __getitem__(self, key):
warnings.warn(
(
"Using deprecated access to selection data. Please use"
" attributes and methods"
" defined by 'LauncherActionSelection'."
),
category=DeprecationWarning
)
if key in {"AYON_PROJECT_NAME", "AVALON_PROJECT"}:
return self.project_name
if key in {"AYON_FOLDER_PATH", "AVALON_ASSET"}:
return self.folder_path
if key in {"AYON_TASK_NAME", "AVALON_TASK"}:
return self.task_name
raise KeyError(f"Key: {key} not found")
def __iter__(self):
for key in self.keys():
yield key
def __contains__(self, key):
warnings.warn(
(
"Using deprecated access to selection data. Please use"
" attributes and methods"
" defined by 'LauncherActionSelection'."
),
category=DeprecationWarning
)
# Fake missing keys check for backwards compatibility
if key in {
"AYON_PROJECT_NAME",
"AVALON_PROJECT",
}:
return self._project_name is not None
if key in {
"AYON_FOLDER_PATH",
"AVALON_ASSET",
}:
return self._folder_id is not None
if key in {
"AYON_TASK_NAME",
"AVALON_TASK",
}:
return self._task_id is not None
return False
def get(self, key, default=None):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
warnings.warn(
(
"Using deprecated access to selection data. Please use"
" attributes and methods"
" defined by 'LauncherActionSelection'."
),
category=DeprecationWarning
)
try:
return self[key]
except KeyError:
return default
def items(self):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
for key, value in (
("AYON_PROJECT_NAME", self.project_name),
("AYON_FOLDER_PATH", self.folder_path),
("AYON_TASK_NAME", self.task_name),
):
if value is not None:
yield (key, value)
def keys(self):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
for key, _ in self.items():
yield key
def values(self):
"""
Deprecated:
Added for backwards compatibility with older actions.
"""
for _, value in self.items():
yield value
def get_project_name(self):
"""Selected project name.
Returns:
Union[str, None]: Selected project name.
"""
return self._project_name
def get_folder_id(self):
"""Selected folder id.
Returns:
Union[str, None]: Selected folder id.
"""
return self._folder_id
def get_folder_path(self):
"""Selected folder path.
Returns:
Union[str, None]: Selected folder path.
"""
if self._folder_id is None:
return None
if self._folder_path is None:
self._folder_path = self.folder_entity["path"]
return self._folder_path
def get_task_id(self):
"""Selected task id.
Returns:
Union[str, None]: Selected task id.
"""
return self._task_id
def get_task_name(self):
"""Selected task name.
Returns:
Union[str, None]: Selected task name.
"""
if self._task_id is None:
return None
if self._task_name is None:
self._task_name = self.task_entity["name"]
return self._task_name
def get_project_entity(self):
"""Project entity for the selection.
Returns:
Union[dict[str, Any], None]: Project entity.
"""
if self._project_name is None:
return None
if self._project_entity is None:
self._project_entity = ayon_api.get_project(self._project_name)
return self._project_entity
def get_folder_entity(self):
"""Folder entity for the selection.
Returns:
Union[dict[str, Any], None]: Folder entity.
"""
if self._project_name is None or self._folder_id is None:
return None
if self._folder_entity is None:
self._folder_entity = ayon_api.get_folder_by_id(
self._project_name, self._folder_id
)
return self._folder_entity
def get_task_entity(self):
"""Task entity for the selection.
Returns:
Union[dict[str, Any], None]: Task entity.
"""
if (
self._project_name is None
or self._task_id is None
):
return None
if self._task_entity is None:
self._task_entity = ayon_api.get_task_by_id(
self._project_name, self._task_id
)
return self._task_entity
@property
def is_project_selected(self):
"""Return whether a project is selected.
Returns:
bool: Whether a project is selected.
"""
return self._project_name is not None
@property
def is_folder_selected(self):
"""Return whether a folder is selected.
Returns:
bool: Whether a folder is selected.
"""
return self._folder_id is not None
@property
def is_task_selected(self):
"""Return whether a task is selected.
Returns:
bool: Whether a task is selected.
"""
return self._task_id is not None
project_name = property(get_project_name)
folder_id = property(get_folder_id)
task_id = property(get_task_id)
folder_path = property(get_folder_path)
task_name = property(get_task_name)
project_entity = property(get_project_entity)
folder_entity = property(get_folder_entity)
task_entity = property(get_task_entity)
class LauncherAction(object): class LauncherAction(object):
"""A custom action available""" """A custom action available"""
name = None name = None
@ -21,17 +307,23 @@ class LauncherAction(object):
log = logging.getLogger("LauncherAction") log = logging.getLogger("LauncherAction")
log.propagate = True log.propagate = True
def is_compatible(self, session): def is_compatible(self, selection):
"""Return whether the class is compatible with the Session. """Return whether the class is compatible with the Session.
Args: Args:
session (dict[str, Union[str, None]]): Session data with selection (LauncherActionSelection): Data with selection.
AYON_PROJECT_NAME, AYON_FOLDER_PATH and AYON_TASK_NAME.
"""
"""
return True return True
def process(self, session, **kwargs): def process(self, selection, **kwargs):
"""Process the action.
Args:
selection (LauncherActionSelection): Data with selection.
**kwargs: Additional arguments.
"""
pass pass

View file

@ -97,8 +97,8 @@ def install_host(host):
"""Install `host` into the running Python session. """Install `host` into the running Python session.
Args: Args:
host (module): A Python module containing the Avalon host (HostBase): A host interface object.
avalon host-interface.
""" """
global _is_installed global _is_installed
@ -154,6 +154,13 @@ def install_host(host):
def install_ayon_plugins(project_name=None, host_name=None): def install_ayon_plugins(project_name=None, host_name=None):
"""Install AYON core plugins and make sure the core is initialized.
Args:
project_name (Optional[str]): Name of project to install plugins for.
host_name (Optional[str]): Name of host to install plugins for.
"""
# Make sure global AYON connection has set site id and version # Make sure global AYON connection has set site id and version
# - this is necessary if 'install_host' is not called # - this is necessary if 'install_host' is not called
initialize_ayon_connection() initialize_ayon_connection()
@ -223,6 +230,12 @@ def install_ayon_plugins(project_name=None, host_name=None):
def install_openpype_plugins(project_name=None, host_name=None): def install_openpype_plugins(project_name=None, host_name=None):
"""Install AYON core plugins and make sure the core is initialized.
Deprecated:
Use `install_ayon_plugins` instead.
"""
install_ayon_plugins(project_name, host_name) install_ayon_plugins(project_name, host_name)
@ -281,47 +294,6 @@ def deregister_host():
_registered_host["_"] = None _registered_host["_"] = None
def debug_host():
"""A debug host, useful to debugging features that depend on a host"""
host = types.ModuleType("debugHost")
def ls():
containers = [
{
"representation": "ee-ft-a-uuid1",
"schema": "openpype:container-1.0",
"name": "Bruce01",
"objectName": "Bruce01_node",
"namespace": "_bruce01_",
"version": 3,
},
{
"representation": "aa-bc-s-uuid2",
"schema": "openpype:container-1.0",
"name": "Bruce02",
"objectName": "Bruce01_node",
"namespace": "_bruce02_",
"version": 2,
}
]
for container in containers:
yield container
host.__dict__.update({
"ls": ls,
"open_file": lambda fname: None,
"save_file": lambda fname: None,
"current_file": lambda: os.path.expanduser("~/temp.txt"),
"has_unsaved_changes": lambda: False,
"work_root": lambda: os.path.expanduser("~/temp"),
"file_extensions": lambda: ["txt"],
})
return host
def get_current_host_name(): def get_current_host_name():
"""Current host name. """Current host name.
@ -347,7 +319,8 @@ def get_global_context():
Use 'get_current_context' to make sure you'll get current host integration Use 'get_current_context' to make sure you'll get current host integration
context info. context info.
Example: Example::
{ {
"project_name": "Commercial", "project_name": "Commercial",
"folder_path": "Bunny", "folder_path": "Bunny",
@ -515,88 +488,13 @@ def get_current_context_template_data(settings=None):
) )
def get_workdir_from_session(session=None, template_key=None):
"""Template data for template fill from session keys.
Args:
session (Union[Dict[str, str], None]): The Session to use. If not
provided use the currently active global Session.
template_key (str): Prepared template key from which workdir is
calculated.
Returns:
str: Workdir path.
"""
if session is not None:
project_name = session["AYON_PROJECT_NAME"]
host_name = session["AYON_HOST_NAME"]
else:
project_name = get_current_project_name()
host_name = get_current_host_name()
template_data = get_template_data_from_session(session)
if not template_key:
task_type = template_data["task"]["type"]
template_key = get_workfile_template_key(
project_name,
task_type,
host_name,
)
anatomy = Anatomy(project_name)
template_obj = anatomy.get_template_item("work", template_key, "directory")
path = template_obj.format_strict(template_data)
if path:
path = os.path.normpath(path)
return path
def get_custom_workfile_template_from_session(
session=None, project_settings=None
):
"""Filter and fill workfile template profiles by current context.
This function cab be used only inside host where context is set.
Args:
session (Optional[Dict[str, str]]): Session from which are taken
data.
project_settings(Optional[Dict[str, Any]]): Project settings.
Returns:
str: Path to template or None if none of profiles match current
context. (Existence of formatted path is not validated.)
"""
if session is not None:
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session["AYON_TASK_NAME"]
host_name = session["AYON_HOST_NAME"]
else:
context = get_current_context()
project_name = context["project_name"]
folder_path = context["folder_path"]
task_name = context["task_name"]
host_name = get_current_host_name()
return get_custom_workfile_template_by_string_context(
project_name,
folder_path,
task_name,
host_name,
project_settings=project_settings
)
def get_current_context_custom_workfile_template(project_settings=None): def get_current_context_custom_workfile_template(project_settings=None):
"""Filter and fill workfile template profiles by current context. """Filter and fill workfile template profiles by current context.
This function can be used only inside host where context is set. This function can be used only inside host where current context is set.
Args: Args:
project_settings(Optional[Dict[str, Any]]): Project settings. project_settings (Optional[dict[str, Any]]): Project settings
Returns: Returns:
str: Path to template or None if none of profiles match current str: Path to template or None if none of profiles match current

View file

@ -8,7 +8,7 @@ Discovers Creator plugins to be able create new instances and convert existing i
Publish plugins are loaded because they can also define attributes definitions. These are less product type specific To be able define attributes Publish plugin must inherit from `AYONPyblishPluginMixin` and must override `get_attribute_defs` class method which must return list of attribute definitions. Values of publish plugin definitions are stored per plugin name under `publish_attributes`. Also can override `convert_attribute_values` class method which gives ability to modify values on instance before are used in CreatedInstance. Method `convert_attribute_values` can be also used without `get_attribute_defs` to modify values when changing compatibility (remove metadata from instance because are irrelevant). Publish plugins are loaded because they can also define attributes definitions. These are less product type specific To be able define attributes Publish plugin must inherit from `AYONPyblishPluginMixin` and must override `get_attribute_defs` class method which must return list of attribute definitions. Values of publish plugin definitions are stored per plugin name under `publish_attributes`. Also can override `convert_attribute_values` class method which gives ability to modify values on instance before are used in CreatedInstance. Method `convert_attribute_values` can be also used without `get_attribute_defs` to modify values when changing compatibility (remove metadata from instance because are irrelevant).
Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. Possible attribute definitions can be found in `ayon_core/lib/attribute_definitions.py`.
Except creating and removing instances are all changes not automatically propagated to host context (scene/workfile/...) to propagate changes call `save_changes` which trigger update of all instances in context using Creators implementation. Except creating and removing instances are all changes not automatically propagated to host context (scene/workfile/...) to propagate changes call `save_changes` which trigger update of all instances in context using Creators implementation.

View file

@ -1,36 +0,0 @@
import logging
from ayon_core.pipeline import get_current_project_name
Session = {}
log = logging.getLogger(__name__)
log.warning(
"DEPRECATION WARNING: 'legacy_io' is deprecated and will be removed in"
" future versions of ayon-core addon."
"\nReading from Session won't give you updated information and changing"
" values won't affect global state of a process."
)
def session_data_from_environment(context_keys=False):
return {}
def is_installed():
return False
def install():
pass
def uninstall():
pass
def active_project(*args, **kwargs):
return get_current_project_name()
def current_project(*args, **kwargs):
return get_current_project_name()

View file

@ -18,18 +18,14 @@ class OpenTaskPath(LauncherAction):
icon = "folder-open" icon = "folder-open"
order = 500 order = 500
def is_compatible(self, session): def is_compatible(self, selection):
"""Return whether the action is compatible with the session""" """Return whether the action is compatible with the session"""
return bool(session.get("AYON_FOLDER_PATH")) return selection.is_folder_selected
def process(self, session, **kwargs): def process(self, selection, **kwargs):
from qtpy import QtCore, QtWidgets from qtpy import QtCore, QtWidgets
project_name = session["AYON_PROJECT_NAME"] path = self._get_workdir(selection)
folder_path = session["AYON_FOLDER_PATH"]
task_name = session.get("AYON_TASK_NAME", None)
path = self._get_workdir(project_name, folder_path, task_name)
if not path: if not path:
return return
@ -60,16 +56,17 @@ class OpenTaskPath(LauncherAction):
path = path.split(field, 1)[0] path = path.split(field, 1)[0]
return path return path
def _get_workdir(self, project_name, folder_path, task_name): def _get_workdir(self, selection):
project_entity = ayon_api.get_project(project_name) data = get_template_data(
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) selection.project_entity,
task_entity = ayon_api.get_task_by_name( selection.folder_entity,
project_name, folder_entity["id"], task_name selection.task_entity
) )
data = get_template_data(project_entity, folder_entity, task_entity) anatomy = Anatomy(
selection.project_name,
anatomy = Anatomy(project_name) project_entity=selection.project_entity
)
workdir = anatomy.get_template_item( workdir = anatomy.get_template_item(
"work", "default", "folder" "work", "default", "folder"
).format(data) ).format(data)

View file

@ -194,6 +194,16 @@ class ExtractBurnin(publish.Extractor):
).format(host_name, product_type, task_name, profile)) ).format(host_name, product_type, task_name, profile))
return return
burnins_per_repres = self._get_burnins_per_representations(
instance, burnin_defs
)
if not burnins_per_repres:
self.log.debug(
"Skipped instance. No representations found matching a burnin"
"definition in: %s", burnin_defs
)
return
burnin_options = self._get_burnin_options() burnin_options = self._get_burnin_options()
# Prepare basic data for processing # Prepare basic data for processing
@ -204,9 +214,6 @@ class ExtractBurnin(publish.Extractor):
# Args that will execute the script # Args that will execute the script
executable_args = ["run", scriptpath] executable_args = ["run", scriptpath]
burnins_per_repres = self._get_burnins_per_representations(
instance, burnin_defs
)
for repre, repre_burnin_defs in burnins_per_repres: for repre, repre_burnin_defs in burnins_per_repres:
# Create copy of `_burnin_data` and `_temp_data` for repre. # Create copy of `_burnin_data` and `_temp_data` for repre.
burnin_data = copy.deepcopy(_burnin_data) burnin_data = copy.deepcopy(_burnin_data)

View file

@ -619,7 +619,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
# Prepare input and output filepaths # Prepare input and output filepaths
self.input_output_paths(new_repre, output_def, temp_data) self.input_output_paths(new_repre, output_def, temp_data)
# Set output frames len to 1 when ouput is single image # Set output frames len to 1 when output is single image
if ( if (
temp_data["output_ext_is_image"] temp_data["output_ext_is_image"]
and not temp_data["output_is_sequence"] and not temp_data["output_is_sequence"]
@ -955,7 +955,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
self.log.debug("New representation ext: `{}`".format(output_ext)) self.log.debug("New representation ext: `{}`".format(output_ext))
# Output is image file sequence witht frames # Output is image file sequence with frames
output_ext_is_image = bool(output_ext in self.image_exts) output_ext_is_image = bool(output_ext in self.image_exts)
output_is_sequence = bool( output_is_sequence = bool(
output_ext_is_image output_ext_is_image
@ -967,7 +967,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
frame_end = temp_data["output_frame_end"] frame_end = temp_data["output_frame_end"]
filename_base = "{}_{}".format(filename, filename_suffix) filename_base = "{}_{}".format(filename, filename_suffix)
# Temporary tempalte for frame filling. Example output: # Temporary template for frame filling. Example output:
# "basename.%04d.exr" when `frame_end` == 1001 # "basename.%04d.exr" when `frame_end` == 1001
repr_file = "{}.%{:0>2}d.{}".format( repr_file = "{}.%{:0>2}d.{}".format(
filename_base, len(str(frame_end)), output_ext filename_base, len(str(frame_end)), output_ext
@ -997,7 +997,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
self.log.debug("Creating dir: {}".format(dst_staging_dir)) self.log.debug("Creating dir: {}".format(dst_staging_dir))
os.makedirs(dst_staging_dir) os.makedirs(dst_staging_dir)
# Store stagingDir to representaion # Store stagingDir to representation
new_repre["stagingDir"] = dst_staging_dir new_repre["stagingDir"] = dst_staging_dir
# Store paths to temp data # Store paths to temp data
@ -1228,16 +1228,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
reformat_in_baking = bool("reformated" in new_repre["tags"]) reformat_in_baking = bool("reformated" in new_repre["tags"])
self.log.debug("reformat_in_baking: `{}`".format(reformat_in_baking)) self.log.debug("reformat_in_baking: `{}`".format(reformat_in_baking))
# Get instance data
pixel_aspect = temp_data["pixel_aspect"]
if reformat_in_baking:
self.log.debug((
"Using resolution from input. It is already "
"reformated from upstream process"
))
pixel_aspect = 1
# NOTE Skipped using instance's resolution # NOTE Skipped using instance's resolution
full_input_path_single_file = temp_data["full_input_path_single_file"] full_input_path_single_file = temp_data["full_input_path_single_file"]
try: try:
@ -1268,7 +1258,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
if reformat_in_baking: if reformat_in_baking:
self.log.debug(( self.log.debug((
"Using resolution from input. It is already " "Using resolution from input. It is already "
"reformated from upstream process" "reformatted from upstream process"
)) ))
pixel_aspect = 1 pixel_aspect = 1
output_width = input_width output_width = input_width
@ -1374,7 +1364,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
# Make sure output width and height is not an odd number # Make sure output width and height is not an odd number
# When this can happen: # When this can happen:
# - if output definition has set width and height with odd number # - if output definition has set width and height with odd number
# - `instance.data` contain width and height with odd numbeer # - `instance.data` contain width and height with odd number
if output_width % 2 != 0: if output_width % 2 != 0:
self.log.warning(( self.log.warning((
"Converting output width from odd to even number. {} -> {}" "Converting output width from odd to even number. {} -> {}"
@ -1555,7 +1545,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
custom_tags (list): Custom Tags of processed representation. custom_tags (list): Custom Tags of processed representation.
Returns: Returns:
list: Containg all output definitions matching entered tags. list: Containing all output definitions matching entered tags.
""" """
filtered_outputs = [] filtered_outputs = []
@ -1820,8 +1810,8 @@ class OverscanCrop:
""" """
# crop=width:height:x:y - explicit start x, y position # crop=width:height:x:y - explicit start x, y position
# crop=width:height - x, y are related to center by width/height # crop=width:height - x, y are related to center by width/height
# pad=width:heigth:x:y - explicit start x, y position # pad=width:height:x:y - explicit start x, y position
# pad=width:heigth - x, y are set to 0 by default # pad=width:height - x, y are set to 0 by default
width = self.width() width = self.width()
height = self.height() height = self.height()
@ -1869,7 +1859,7 @@ class OverscanCrop:
# Replace "px" (and spaces before) with single space # Replace "px" (and spaces before) with single space
string_value = re.sub(r"([ ]+)?px", " ", string_value) string_value = re.sub(r"([ ]+)?px", " ", string_value)
string_value = re.sub(r"([ ]+)%", "%", string_value) string_value = re.sub(r"([ ]+)%", "%", string_value)
# Make sure +/- sign at the beggining of string is next to number # Make sure +/- sign at the beginning of string is next to number
string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value) string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value)
# Make sure +/- sign in the middle has zero spaces before number under # Make sure +/- sign in the middle has zero spaces before number under
# which belongs # which belongs

View file

@ -10,7 +10,7 @@ Scene contains one or more outdated loaded containers, eg. versions loaded into
### How to repair? ### How to repair?
Use 'Scene Inventory' and update all highlighted old container to latest OR Use 'Scene Inventory' and update all highlighted old container to latest OR
refresh Publish and switch 'Validate Containers' toggle on 'Options' tab. refresh Publish and switch 'Validate Containers' toggle on 'Context' tab.
WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets. WARNING: Skipping this validator will result in publishing (and probably rendering) old version of loaded assets.
</description> </description>

View file

@ -5,6 +5,7 @@ from ayon_core.lib import Logger, AYONSettingsRegistry
from ayon_core.pipeline.actions import ( from ayon_core.pipeline.actions import (
discover_launcher_actions, discover_launcher_actions,
LauncherAction, LauncherAction,
LauncherActionSelection,
) )
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
@ -69,11 +70,6 @@ class ApplicationAction(LauncherAction):
project_entities = {} project_entities = {}
_log = None _log = None
required_session_keys = (
"AYON_PROJECT_NAME",
"AYON_FOLDER_PATH",
"AYON_TASK_NAME"
)
@property @property
def log(self): def log(self):
@ -81,18 +77,16 @@ class ApplicationAction(LauncherAction):
self._log = Logger.get_logger(self.__class__.__name__) self._log = Logger.get_logger(self.__class__.__name__)
return self._log return self._log
def is_compatible(self, session): def is_compatible(self, selection):
for key in self.required_session_keys: if not selection.is_task_selected:
if not session.get(key): return False
return False
project_name = session["AYON_PROJECT_NAME"] project_entity = self.project_entities[selection.project_name]
project_entity = self.project_entities[project_name]
apps = project_entity["attrib"].get("applications") apps = project_entity["attrib"].get("applications")
if not apps or self.application.full_name not in apps: if not apps or self.application.full_name not in apps:
return False return False
project_settings = self.project_settings[project_name] project_settings = self.project_settings[selection.project_name]
only_available = project_settings["applications"]["only_available"] only_available = project_settings["applications"]["only_available"]
if only_available and not self.application.find_executable(): if only_available and not self.application.find_executable():
return False return False
@ -112,7 +106,7 @@ class ApplicationAction(LauncherAction):
dialog.setDetailedText(details) dialog.setDetailedText(details)
dialog.exec_() dialog.exec_()
def process(self, session, **kwargs): def process(self, selection, **kwargs):
"""Process the full Application action""" """Process the full Application action"""
from ayon_core.lib import ( from ayon_core.lib import (
@ -120,14 +114,11 @@ class ApplicationAction(LauncherAction):
ApplicationLaunchFailed, ApplicationLaunchFailed,
) )
project_name = session["AYON_PROJECT_NAME"]
folder_path = session["AYON_FOLDER_PATH"]
task_name = session["AYON_TASK_NAME"]
try: try:
self.application.launch( self.application.launch(
project_name=project_name, project_name=selection.project_name,
folder_path=folder_path, folder_path=selection.folder_path,
task_name=task_name, task_name=selection.task_name,
**self.data **self.data
) )
@ -335,11 +326,11 @@ class ActionsModel:
""" """
not_open_workfile_actions = self._get_no_last_workfile_for_context( not_open_workfile_actions = self._get_no_last_workfile_for_context(
project_name, folder_id, task_id) project_name, folder_id, task_id)
session = self._prepare_session(project_name, folder_id, task_id) selection = self._prepare_selection(project_name, folder_id, task_id)
output = [] output = []
action_items = self._get_action_items(project_name) action_items = self._get_action_items(project_name)
for identifier, action in self._get_action_objects().items(): for identifier, action in self._get_action_objects().items():
if not action.is_compatible(session): if not action.is_compatible(selection):
continue continue
action_item = action_items[identifier] action_item = action_items[identifier]
@ -374,7 +365,7 @@ class ActionsModel:
) )
def trigger_action(self, project_name, folder_id, task_id, identifier): def trigger_action(self, project_name, folder_id, task_id, identifier):
session = self._prepare_session(project_name, folder_id, task_id) selection = self._prepare_selection(project_name, folder_id, task_id)
failed = False failed = False
error_message = None error_message = None
action_label = identifier action_label = identifier
@ -403,7 +394,7 @@ class ActionsModel:
) )
action.data["start_last_workfile"] = start_last_workfile action.data["start_last_workfile"] = start_last_workfile
action.process(session) action.process(selection)
except Exception as exc: except Exception as exc:
self.log.warning("Action trigger failed.", exc_info=True) self.log.warning("Action trigger failed.", exc_info=True)
failed = True failed = True
@ -440,29 +431,8 @@ class ActionsModel:
.get(task_id, {}) .get(task_id, {})
) )
def _prepare_session(self, project_name, folder_id, task_id): def _prepare_selection(self, project_name, folder_id, task_id):
folder_path = None return LauncherActionSelection(project_name, folder_id, task_id)
if folder_id:
folder = self._controller.get_folder_entity(
project_name, folder_id)
if folder:
folder_path = folder["path"]
task_name = None
if task_id:
task = self._controller.get_task_entity(project_name, task_id)
if task:
task_name = task["name"]
return {
"AYON_PROJECT_NAME": project_name,
"AYON_FOLDER_PATH": folder_path,
"AYON_TASK_NAME": task_name,
# Deprecated - kept for backwards compatibility
"AVALON_PROJECT": project_name,
"AVALON_ASSET": folder_path,
"AVALON_TASK": task_name,
}
def _get_discovered_action_classes(self): def _get_discovered_action_classes(self):
if self._discovered_actions is None: if self._discovered_actions is None:

View file

@ -1,33 +0,0 @@
# TODO remove - kept for kitsu addon which imported it
from qtpy import QtWidgets, QtCore, QtGui
class PressHoverButton(QtWidgets.QPushButton):
"""
Deprecated:
Use `openpype.tools.utils.PressHoverButton` instead.
"""
_mouse_pressed = False
_mouse_hovered = False
change_state = QtCore.Signal(bool)
def mousePressEvent(self, event):
self._mouse_pressed = True
self._mouse_hovered = True
self.change_state.emit(self._mouse_hovered)
super(PressHoverButton, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
self._mouse_pressed = False
self._mouse_hovered = False
self.change_state.emit(self._mouse_hovered)
super(PressHoverButton, self).mouseReleaseEvent(event)
def mouseMoveEvent(self, event):
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
under_mouse = self.rect().contains(mouse_pos)
if under_mouse != self._mouse_hovered:
self._mouse_hovered = under_mouse
self.change_state.emit(self._mouse_hovered)
super(PressHoverButton, self).mouseMoveEvent(event)

View file

@ -9,7 +9,7 @@ from ayon_server.settings import (
task_types_enum, task_types_enum,
) )
from ayon_server.types import ColorRGB_uint8, ColorRGBA_uint8 from ayon_server.types import ColorRGBA_uint8
class ValidateBaseModel(BaseSettingsModel): class ValidateBaseModel(BaseSettingsModel):
@ -221,7 +221,12 @@ class OIIOToolArgumentsModel(BaseSettingsModel):
class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
_layout = "expanded" _layout = "expanded"
name: str = SettingsField("", title="Name") name: str = SettingsField(
"",
title="Name",
description="Output name (no space)",
regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$",
)
extension: str = SettingsField("", title="Extension") extension: str = SettingsField("", title="Extension")
transcoding_type: str = SettingsField( transcoding_type: str = SettingsField(
"colorspace", "colorspace",

View file

@ -299,6 +299,16 @@ class ExtractAlembicModel(BaseSettingsModel):
families: list[str] = SettingsField( families: list[str] = SettingsField(
default_factory=list, default_factory=list,
title="Families") title="Families")
bake_attributes: list[str] = SettingsField(
default_factory=list, title="Bake Attributes",
description="List of attributes that will be included in the alembic "
"export.",
)
bake_attribute_prefixes: list[str] = SettingsField(
default_factory=list, title="Bake Attribute Prefixes",
description="List of attribute prefixes for attributes that will be "
"included in the alembic export.",
)
class ExtractObjModel(BaseSettingsModel): class ExtractObjModel(BaseSettingsModel):
@ -306,6 +316,12 @@ class ExtractObjModel(BaseSettingsModel):
optional: bool = SettingsField(title="Optional") optional: bool = SettingsField(title="Optional")
class ExtractModelModel(BaseSettingsModel):
enabled: bool = SettingsField(title="Enabled")
optional: bool = SettingsField(title="Optional")
active: bool = SettingsField(title="Active")
class ExtractMayaSceneRawModel(BaseSettingsModel): class ExtractMayaSceneRawModel(BaseSettingsModel):
"""Add loaded instances to those published families:""" """Add loaded instances to those published families:"""
enabled: bool = SettingsField(title="ExtractMayaSceneRaw") enabled: bool = SettingsField(title="ExtractMayaSceneRaw")
@ -362,7 +378,9 @@ class ExtractLookModel(BaseSettingsModel):
class ExtractGPUCacheModel(BaseSettingsModel): class ExtractGPUCacheModel(BaseSettingsModel):
enabled: bool = True enabled: bool = SettingsField(title="Enabled")
optional: bool = SettingsField(title="Optional")
active: bool = SettingsField(title="Active")
families: list[str] = SettingsField(default_factory=list, title="Families") families: list[str] = SettingsField(default_factory=list, title="Families")
step: float = SettingsField(1.0, ge=1.0, title="Step") step: float = SettingsField(1.0, ge=1.0, title="Step")
stepSave: int = SettingsField(1, ge=1, title="Step Save") stepSave: int = SettingsField(1, ge=1, title="Step Save")
@ -789,6 +807,10 @@ class PublishersModel(BaseSettingsModel):
default_factory=ExtractGPUCacheModel, default_factory=ExtractGPUCacheModel,
title="Extract GPU Cache", title="Extract GPU Cache",
) )
ExtractModel: ExtractModelModel = SettingsField(
default_factory=ExtractModelModel,
title="Extract Model (Maya Scene)"
)
DEFAULT_SUFFIX_NAMING = { DEFAULT_SUFFIX_NAMING = {
@ -1184,7 +1206,9 @@ DEFAULT_PUBLISH_SETTINGS = {
"pointcache", "pointcache",
"model", "model",
"vrayproxy.alembic" "vrayproxy.alembic"
] ],
"bake_attributes": [],
"bake_attribute_prefixes": []
}, },
"ExtractObj": { "ExtractObj": {
"enabled": False, "enabled": False,
@ -1329,6 +1353,8 @@ DEFAULT_PUBLISH_SETTINGS = {
}, },
"ExtractGPUCache": { "ExtractGPUCache": {
"enabled": False, "enabled": False,
"optional": False,
"active": True,
"families": [ "families": [
"model", "model",
"animation", "animation",
@ -1341,5 +1367,10 @@ DEFAULT_PUBLISH_SETTINGS = {
"optimizeAnimationsForMotionBlur": True, "optimizeAnimationsForMotionBlur": True,
"writeMaterials": True, "writeMaterials": True,
"useBaseTessellation": True "useBaseTessellation": True
},
"ExtractModel": {
"enabled": True,
"optional": True,
"active": True,
} }
} }

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package declaring addon version.""" """Package declaring addon version."""
__version__ = "0.1.11" __version__ = "0.1.13"

View file

@ -142,6 +142,7 @@ DEFAULT_SIMPLE_CREATORS = [
"extensions": [ "extensions": [
".exr", ".exr",
".png", ".png",
".dng",
".dpx", ".dpx",
".jpg", ".jpg",
".tiff", ".tiff",
@ -165,6 +166,7 @@ DEFAULT_SIMPLE_CREATORS = [
"extensions": [ "extensions": [
".exr", ".exr",
".png", ".png",
".dng",
".dpx", ".dpx",
".jpg", ".jpg",
".jpeg", ".jpeg",
@ -215,6 +217,7 @@ DEFAULT_SIMPLE_CREATORS = [
".exr", ".exr",
".jpg", ".jpg",
".jpeg", ".jpeg",
".dng",
".dpx", ".dpx",
".bmp", ".bmp",
".tif", ".tif",

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Package declaring addon version.""" """Package declaring addon version."""
__version__ = "0.1.3" __version__ = "0.1.4"