Merge branch 'develop' into enhancement/maya_load_rendersetup_mark_imported

This commit is contained in:
Roy Nieterau 2024-03-29 13:57:14 +01:00 committed by GitHub
commit bcb6ffaccc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 555 additions and 73 deletions

View file

@ -5,6 +5,8 @@ import contextlib
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
self = sys.modules[__name__]
@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True,
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."""
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"]
start = folder_attributes["frameStart"]
end = folder_attributes["frameEnd"]
@ -65,9 +73,24 @@ def set_current_context_framerange():
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"""
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"]
width = folder_attributes["resolutionWidth"]
height = folder_attributes["resolutionHeight"]
@ -285,3 +308,98 @@ def comp_lock_and_undo_chunk(
finally:
comp.Unlock()
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 logging
import contextlib
from pathlib import Path
import pyblish.api
from qtpy import QtCore
@ -28,7 +29,8 @@ from ayon_core.tools.utils import host_tools
from .lib import (
get_current_comp,
validate_comp_prefs
validate_comp_prefs,
prompt_reset_context
)
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")
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):
# Keep a reference to fusion's Print function (Remote Object)
@ -103,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
# Register events
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("new", on_new)
register_event_callback("taskChanged", on_task_changed)
# region workfile io api
def has_unsaved_changes(self):
@ -168,6 +175,19 @@ def on_save(event):
comp = event["sender"]
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):
comp = event["sender"]
@ -201,6 +221,28 @@ def on_after_open(event):
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():
"""List containers from active Fusion scene
@ -337,7 +379,6 @@ class FusionEventHandler(QtCore.QObject):
>>> handler = FusionEventHandler(parent=window)
>>> handler.start()
"""
ACTION_IDS = [
"Comp_Save",

View file

@ -2651,31 +2651,114 @@ def reset_scene_resolution():
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
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.
Examples of settings:
fps
resolution
renderer
Args:
fps (bool): Whether to set the scene FPS.
resolution (bool): Whether to set the render resolution.
frame_range (bool): Whether to reset the time slide frame ranges.
colorspace (bool): Whether to reset the colorspace.
Returns:
None
"""
# Set project fps
set_scene_fps(get_fps_for_current_context())
if fps:
# Set project fps
set_scene_fps(get_fps_for_current_context())
reset_scene_resolution()
if resolution:
reset_scene_resolution()
# Set frame range.
reset_frame_range()
if frame_range:
reset_frame_range(fps=False)
# 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

View file

@ -67,6 +67,9 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
AVALON_CONTAINERS = ":AVALON_CONTAINERS"
# Track whether the workfile tool is about to save
ABOUT_TO_SAVE = False
class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "maya"
@ -581,6 +584,10 @@ def on_save():
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
global ABOUT_TO_SAVE
ABOUT_TO_SAVE = False
def on_open():
"""On scene open let's assume the containers have changed."""
@ -650,6 +657,11 @@ def on_task_changed():
lib.set_context_settings()
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():
if handle_workfile_locks():
@ -664,6 +676,9 @@ def before_workfile_save(event):
if workdir_path:
create_workspace_mel(workdir_path, project_name)
global ABOUT_TO_SAVE
ABOUT_TO_SAVE = True
def workfile_save_before_xgen(event):
"""Manage Xgen external files when switching context.

View file

@ -142,9 +142,21 @@ class ImagePlaneLoader(load.LoaderPlugin):
with namespaced(namespace):
# Create inside the namespace
image_plane_transform, image_plane_shape = cmds.imagePlane(
fileName=context["representation"]["data"]["path"],
fileName=self.filepath_from_context(context),
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)
end_frame = cmds.playbackOptions(query=True, max=True)
@ -216,6 +228,15 @@ class ImagePlaneLoader(load.LoaderPlugin):
repre_entity["id"],
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.
start_frame = folder_entity["attrib"]["frameStart"]
end_frame = folder_entity["attrib"]["frameEnd"]
@ -243,3 +264,12 @@ class ImagePlaneLoader(load.LoaderPlugin):
deleteNamespaceContent=True)
except RuntimeError:
pass
def get_colorspace(self, representation):
data = representation.get("data", {}).get("colorspaceData", {})
if not data:
return
colorspace = data.get("colorspace")
return colorspace

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 (
RepairAction,
ValidateContentsOrder,
PublishValidationError,
PublishXmlValidationError,
OptionalPyblishPluginMixin,
get_plugin_settings,
apply_plugin_settings_automatically
@ -56,40 +56,39 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin,
# if a deformer has been created on the shape
invalid = self.get_invalid(instance)
if invalid:
# TODO: Message formatting can be improved
raise PublishValidationError("Nodes found with mismatching "
"IDs: {0}".format(invalid),
title="Invalid node ids")
# Use the short names
invalid = cmds.ls(invalid)
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
def get_invalid(cls, instance):
"""Get all nodes which do not match the criteria"""
invalid = []
types_to_skip = ["locator"]
types = ["mesh", "nurbsCurve", "nurbsSurface"]
# get asset id
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
if cmds.referenceQuery(node, isNodeReferenced=True):
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
node_id = lib.get_id(node)
if not node_id:
invalid.append(node)
continue
history_id = lib.get_id_from_sibling(node)
if history_id is not None and node_id != history_id:

View file

@ -1,14 +1,99 @@
from maya import cmds
from maya import cmds, mel
import pyblish.api
import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import (
ValidateMeshOrder,
PublishValidationError,
PublishXmlValidationError,
RepairAction,
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"):
"""Return list as bullet point list for a report"""
if not values:
@ -29,7 +114,8 @@ class ValidateMeshNonManifold(pyblish.api.InstancePlugin,
hosts = ['maya']
families = ['model']
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
@staticmethod
@ -39,9 +125,11 @@ class ValidateMeshNonManifold(pyblish.api.InstancePlugin,
invalid = []
for mesh in meshes:
if (cmds.polyInfo(mesh, nonManifoldVertices=True) or
cmds.polyInfo(mesh, nonManifoldEdges=True)):
invalid.append(mesh)
components = cmds.polyInfo(mesh,
nonManifoldVertices=True,
nonManifoldEdges=True)
if components:
invalid.extend(components)
return invalid
@ -49,12 +137,34 @@ class ValidateMeshNonManifold(pyblish.api.InstancePlugin,
"""Process all the nodes in the instance 'objectSet'"""
if not self.is_active(instance.data):
return
invalid = self.get_invalid(instance)
if invalid:
raise PublishValidationError(
"Meshes found with non-manifold edges/vertices:\n\n{0}".format(
_as_report_list(sorted(invalid))
),
title="Non-Manifold Edges/Vertices"
# Report only the meshes instead of all component indices
invalid_meshes = {
component.split(".", 1)[0] for component in invalid
}
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

@ -19,22 +19,17 @@ def _as_report_list(values, prefix="- ", suffix="\n"):
def has_shape_children(node):
# Check if any descendants
allDescendents = cmds.listRelatives(node,
allDescendents=True,
fullPath=True)
if not allDescendents:
all_descendents = cmds.listRelatives(node,
allDescendents=True,
fullPath=True)
if not all_descendents:
return False
# 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:
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

View file

@ -6,6 +6,7 @@ import ayon_core.hosts.maya.api.action
from ayon_core.pipeline.publish import (
RepairAction,
ValidateMeshOrder,
PublishValidationError,
OptionalPyblishPluginMixin
)
@ -20,7 +21,6 @@ class ValidateShapeRenderStats(pyblish.api.InstancePlugin,
label = 'Shape Default Render Stats'
actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction,
RepairAction]
optional = True
defaults = {'castsShadows': 1,
'receiveShadows': 1,
@ -37,14 +37,13 @@ class ValidateShapeRenderStats(pyblish.api.InstancePlugin,
# It seems the "surfaceShape" and those derived from it have
# `renderStat` attributes.
shapes = cmds.ls(instance, long=True, type='surfaceShape')
invalid = []
invalid = set()
for shape in shapes:
_iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items)
for attr, default_value in _iteritems():
for attr, default_value in cls.defaults.items():
if cmds.attributeQuery(attr, node=shape, exists=True):
value = cmds.getAttr('{}.{}'.format(shape, attr))
if value != default_value:
invalid.append(shape)
invalid.add(shape)
return invalid
@ -52,17 +51,36 @@ class ValidateShapeRenderStats(pyblish.api.InstancePlugin,
if not self.is_active(instance.data):
return
invalid = self.get_invalid(instance)
if not invalid:
return
if invalid:
raise ValueError("Shapes with non-default renderStats "
"found: {0}".format(invalid))
defaults_str = "\n".join(
"- {}: {}\n".format(key, value)
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
def repair(cls, instance):
for shape in cls.get_invalid(instance):
_iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items)
for attr, default_value in _iteritems():
for attr, default_value in cls.defaults.items():
if cmds.attributeQuery(attr, node=shape, exists=True):
plug = '{0}.{1}'.format(shape, attr)
value = cmds.getAttr(plug)

View file

@ -1,5 +1,6 @@
from maya import cmds
import inspect
from maya import cmds
import pyblish.api
import ayon_core.hosts.maya.api.action
@ -57,7 +58,7 @@ class ValidateTransformZero(pyblish.api.InstancePlugin,
if ('_LOC' in transform) or ('_loc' in transform):
continue
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)):
invalid.append(transform)
@ -69,14 +70,24 @@ class ValidateTransformZero(pyblish.api.InstancePlugin,
return
invalid = self.get_invalid(instance)
if invalid:
names = "<br>".join(
" - {}".format(node) for node in invalid
)
raise PublishValidationError(
title="Transform Zero",
description=self.get_description(),
message="The model publish allows no transformations. You must"
" <b>freeze transformations</b> to continue.<br><br>"
"Nodes found with transform values: "
"Nodes found with transform values:<br>"
"{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.
""")