Maya: Remove pymel dependency (#4724)

* Remove pymel from `validate_no_namespace`

* Refactor `ValidateRigOutputIds` to remove pymel dependency

* Remove pymel dependency

* Fix logic

* Remove pymel dependency

* Remove py

* Remove pymel dependency

* Remove pymel dependency

* Remove pymel dependency

* Opt-out early if possible

* Remove pymel dependency

* Remove pymel dependency

* Remove pymel dependency

* Remove pymel dependency

* Remove pymel dependency

* Remove code that should've been removed

* Cosmetics

* Shush hound

* Refactor `get_parent` -> `get_node_parent`

* Fix argument

* Refactor logic for Validate Mesh Arnold Attributes

* Fix load image plane camera transform name

* Merge remote-tracking branch 'upstream/develop' into maya_remove_pymel

# Conflicts:
#	openpype/hosts/maya/plugins/load/load_reference.py
#	openpype/hosts/maya/plugins/publish/collect_review.py

* Fix bug

* Improve labels on messagebox buttons

* Fix repair

* Create empty mesh instead of cube

* Tweak code style based on comments by @fabiaserra

---------

Co-authored-by: Jakub Ježek <jakubjezek001@gmail.com>
This commit is contained in:
Roy Nieterau 2023-04-04 11:23:19 +02:00 committed by GitHub
parent 0c66e3c566
commit a93b635618
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 474 additions and 334 deletions

View file

@ -12,4 +12,4 @@
# Apply label "release" if base matches "release/*"
'Bump Minor':
base: "release/next-minor"
base: "release/next-minor"

View file

@ -1367,6 +1367,71 @@ def set_id(node, unique_id, overwrite=False):
cmds.setAttr(attr, unique_id, type="string")
def get_attribute(plug,
asString=False,
expandEnvironmentVariables=False,
**kwargs):
"""Maya getAttr with some fixes based on `pymel.core.general.getAttr()`.
Like Pymel getAttr this applies some changes to `maya.cmds.getAttr`
- maya pointlessly returned vector results as a tuple wrapped in a list
(ex. '[(1,2,3)]'). This command unpacks the vector for you.
- when getting a multi-attr, maya would raise an error, but this will
return a list of values for the multi-attr
- added support for getting message attributes by returning the
connections instead
Note that the asString + expandEnvironmentVariables argument naming
convention matches the `maya.cmds.getAttr` arguments so that it can
act as a direct replacement for it.
Args:
plug (str): Node's attribute plug as `node.attribute`
asString (bool): Return string value for enum attributes instead
of the index. Note that the return value can be dependent on the
UI language Maya is running in.
expandEnvironmentVariables (bool): Expand any environment variable and
(tilde characters on UNIX) found in string attributes which are
returned.
Kwargs:
Supports the keyword arguments of `maya.cmds.getAttr`
Returns:
object: The value of the maya attribute.
"""
attr_type = cmds.getAttr(plug, type=True)
if asString:
kwargs["asString"] = True
if expandEnvironmentVariables:
kwargs["expandEnvironmentVariables"] = True
try:
res = cmds.getAttr(plug, **kwargs)
except RuntimeError:
if attr_type == "message":
return cmds.listConnections(plug)
node, attr = plug.split(".", 1)
children = cmds.attributeQuery(attr, node=node, listChildren=True)
if children:
return [
get_attribute("{}.{}".format(node, child))
for child in children
]
raise
# Convert vector result wrapped in tuple
if isinstance(res, list) and len(res):
if isinstance(res[0], tuple) and len(res):
if attr_type in {'pointArray', 'vectorArray'}:
return res
return res[0]
return res
def set_attribute(attribute, value, node):
"""Adjust attributes based on the value from the attribute data
@ -1881,6 +1946,12 @@ def remove_other_uv_sets(mesh):
cmds.removeMultiInstance(attr, b=True)
def get_node_parent(node):
"""Return full path name for parent of node"""
parents = cmds.listRelatives(node, parent=True, fullPath=True)
return parents[0] if parents else None
def get_id_from_sibling(node, history_only=True):
"""Return first node id in the history chain that matches this node.
@ -1904,10 +1975,6 @@ def get_id_from_sibling(node, history_only=True):
"""
def _get_parent(node):
"""Return full path name for parent of node"""
return cmds.listRelatives(node, parent=True, fullPath=True)
node = cmds.ls(node, long=True)[0]
# Find all similar nodes in history
@ -1919,8 +1986,8 @@ def get_id_from_sibling(node, history_only=True):
similar_nodes = [x for x in similar_nodes if x != node]
# The node *must be* under the same parent
parent = _get_parent(node)
similar_nodes = [i for i in similar_nodes if _get_parent(i) == parent]
parent = get_node_parent(node)
similar_nodes = [i for i in similar_nodes if get_node_parent(i) == parent]
# Check all of the remaining similar nodes and take the first one
# with an id and assume it's the original.
@ -3166,38 +3233,78 @@ def set_colorspace():
def parent_nodes(nodes, parent=None):
# type: (list, str) -> list
"""Context manager to un-parent provided nodes and return them back."""
import pymel.core as pm # noqa
parent_node = None
def _as_mdagpath(node):
"""Return MDagPath for node path."""
if not node:
return
sel = OpenMaya.MSelectionList()
sel.add(node)
return sel.getDagPath(0)
# We can only parent dag nodes so we ensure input contains only dag nodes
nodes = cmds.ls(nodes, type="dagNode", long=True)
if not nodes:
# opt-out early
yield
return
parent_node_path = None
delete_parent = False
if parent:
if not cmds.objExists(parent):
parent_node = pm.createNode("transform", n=parent, ss=False)
parent_node = cmds.createNode("transform",
name=parent,
skipSelect=False)
delete_parent = True
else:
parent_node = pm.PyNode(parent)
parent_node = parent
parent_node_path = cmds.ls(parent_node, long=True)[0]
# Store original parents
node_parents = []
for node in nodes:
n = pm.PyNode(node)
try:
root = pm.listRelatives(n, parent=1)[0]
except IndexError:
root = None
node_parents.append((n, root))
node_parent = get_node_parent(node)
node_parents.append((_as_mdagpath(node), _as_mdagpath(node_parent)))
try:
for node in node_parents:
if not parent:
node[0].setParent(world=True)
for node, node_parent in node_parents:
node_parent_path = node_parent.fullPathName() if node_parent else None # noqa
if node_parent_path == parent_node_path:
# Already a child
continue
if parent_node_path:
cmds.parent(node.fullPathName(), parent_node_path)
else:
node[0].setParent(parent_node)
cmds.parent(node.fullPathName(), world=True)
yield
finally:
for node in node_parents:
if node[1]:
node[0].setParent(node[1])
# Reparent to original parents
for node, original_parent in node_parents:
node_path = node.fullPathName()
if not node_path:
# Node must have been deleted
continue
node_parent_path = get_node_parent(node_path)
original_parent_path = None
if original_parent:
original_parent_path = original_parent.fullPathName()
if not original_parent_path:
# Original parent node must have been deleted
continue
if node_parent_path != original_parent_path:
if not original_parent_path:
cmds.parent(node_path, world=True)
else:
cmds.parent(node_path, original_parent_path)
if delete_parent:
pm.delete(parent_node)
cmds.delete(parent_node_path)
@contextlib.contextmanager

View file

@ -19,6 +19,8 @@ from maya.app.renderSetup.model.override import (
UniqueOverride
)
from openpype.hosts.maya.api.lib import get_attribute
EXACT_MATCH = 0
PARENT_MATCH = 1
CLIENT_MATCH = 2
@ -96,9 +98,6 @@ def get_attr_in_layer(node_attr, layer):
"""
# Delay pymel import to here because it's slow to load
import pymel.core as pm
def _layer_needs_update(layer):
"""Return whether layer needs updating."""
# Use `getattr` as e.g. DEFAULT_RENDER_LAYER does not have
@ -125,7 +124,7 @@ def get_attr_in_layer(node_attr, layer):
node = history_overrides[-1] if history_overrides else override
node_attr_ = node + ".original"
return pm.getAttr(node_attr_, asString=True)
return get_attribute(node_attr_, asString=True)
layer = get_rendersetup_layer(layer)
rs = renderSetup.instance()
@ -145,7 +144,7 @@ def get_attr_in_layer(node_attr, layer):
# we will let it error out.
rs.switchToLayer(current_layer)
return pm.getAttr(node_attr, asString=True)
return get_attribute(node_attr, asString=True)
overrides = get_attr_overrides(node_attr, layer)
default_layer_value = get_default_layer_value(node_attr)
@ -156,7 +155,7 @@ def get_attr_in_layer(node_attr, layer):
for match, layer_override, index in overrides:
if isinstance(layer_override, AbsOverride):
# Absolute override
value = pm.getAttr(layer_override.name() + ".attrValue")
value = get_attribute(layer_override.name() + ".attrValue")
if match == EXACT_MATCH:
# value = value
pass
@ -168,8 +167,8 @@ def get_attr_in_layer(node_attr, layer):
elif isinstance(layer_override, RelOverride):
# Relative override
# Value = Original * Multiply + Offset
multiply = pm.getAttr(layer_override.name() + ".multiply")
offset = pm.getAttr(layer_override.name() + ".offset")
multiply = get_attribute(layer_override.name() + ".multiply")
offset = get_attribute(layer_override.name() + ".offset")
if match == EXACT_MATCH:
value = value * multiply + offset

View file

@ -11,7 +11,7 @@ from openpype.pipeline import (
get_representation_path,
)
from openpype.hosts.maya.api.pipeline import containerise
from openpype.hosts.maya.api.lib import unique_namespace
from openpype.hosts.maya.api.lib import unique_namespace, get_container_members
class AudioLoader(load.LoaderPlugin):
@ -52,17 +52,15 @@ class AudioLoader(load.LoaderPlugin):
)
def update(self, container, representation):
import pymel.core as pm
audio_node = None
for node in pm.PyNode(container["objectName"]).members():
if node.nodeType() == "audio":
audio_node = node
members = get_container_members(container)
audio_nodes = cmds.ls(members, type="audio")
assert audio_node is not None, "Audio node not found."
assert audio_nodes is not None, "Audio node not found."
audio_node = audio_nodes[0]
path = get_representation_path(representation)
audio_node.filename.set(path)
cmds.setAttr("{}.filename".format(audio_node), path, type="string")
cmds.setAttr(
container["objectName"] + ".representation",
str(representation["_id"]),
@ -80,8 +78,12 @@ class AudioLoader(load.LoaderPlugin):
asset = get_asset_by_id(
project_name, subset["parent"], fields=["parent"]
)
audio_node.sourceStart.set(1 - asset["data"]["frameStart"])
audio_node.sourceEnd.set(asset["data"]["frameEnd"])
source_start = 1 - asset["data"]["frameStart"]
source_end = asset["data"]["frameEnd"]
cmds.setAttr("{}.sourceStart".format(audio_node), source_start)
cmds.setAttr("{}.sourceEnd".format(audio_node), source_end)
def switch(self, container, representation):
self.update(container, representation)

View file

@ -11,11 +11,26 @@ from openpype.pipeline import (
get_representation_path
)
from openpype.hosts.maya.api.pipeline import containerise
from openpype.hosts.maya.api.lib import unique_namespace
from openpype.hosts.maya.api.lib import (
unique_namespace,
namespaced,
pairwise,
get_container_members
)
from maya import cmds
def disconnect_inputs(plug):
overrides = cmds.listConnections(plug,
source=True,
destination=False,
plugs=True,
connections=True) or []
for dest, src in pairwise(overrides):
cmds.disconnectAttr(src, dest)
class CameraWindow(QtWidgets.QDialog):
def __init__(self, cameras):
@ -74,6 +89,7 @@ class CameraWindow(QtWidgets.QDialog):
self.camera = None
self.close()
class ImagePlaneLoader(load.LoaderPlugin):
"""Specific loader of plate for image planes on selected camera."""
@ -84,9 +100,7 @@ class ImagePlaneLoader(load.LoaderPlugin):
color = "orange"
def load(self, context, name, namespace, data, options=None):
import pymel.core as pm
new_nodes = []
image_plane_depth = 1000
asset = context['asset']['name']
namespace = namespace or unique_namespace(
@ -96,16 +110,20 @@ class ImagePlaneLoader(load.LoaderPlugin):
)
# Get camera from user selection.
camera = None
# is_static_image_plane = None
# is_in_all_views = None
if data:
camera = pm.PyNode(data.get("camera"))
camera = data.get("camera") if data else None
if not camera:
cameras = pm.ls(type="camera")
camera_names = {x.getParent().name(): x for x in cameras}
camera_names["Create new camera."] = "create_camera"
cameras = cmds.ls(type="camera")
# Cameras by names
camera_names = {}
for camera in cameras:
parent = cmds.listRelatives(camera, parent=True, path=True)[0]
camera_names[parent] = camera
camera_names["Create new camera."] = "create-camera"
window = CameraWindow(camera_names.keys())
window.exec_()
# Skip if no camera was selected (Dialog was closed)
@ -113,43 +131,48 @@ class ImagePlaneLoader(load.LoaderPlugin):
return
camera = camera_names[window.camera]
if camera == "create_camera":
camera = pm.createNode("camera")
if camera == "create-camera":
camera = cmds.createNode("camera")
if camera is None:
return
try:
camera.displayResolution.set(1)
camera.farClipPlane.set(image_plane_depth * 10)
cmds.setAttr("{}.displayResolution".format(camera), True)
cmds.setAttr("{}.farClipPlane".format(camera),
image_plane_depth * 10)
except RuntimeError:
pass
# Create image plane
image_plane_transform, image_plane_shape = pm.imagePlane(
fileName=context["representation"]["data"]["path"],
camera=camera)
image_plane_shape.depth.set(image_plane_depth)
with namespaced(namespace):
# Create inside the namespace
image_plane_transform, image_plane_shape = cmds.imagePlane(
fileName=context["representation"]["data"]["path"],
camera=camera
)
start_frame = cmds.playbackOptions(query=True, min=True)
end_frame = cmds.playbackOptions(query=True, max=True)
start_frame = pm.playbackOptions(q=True, min=True)
end_frame = pm.playbackOptions(q=True, max=True)
image_plane_shape.frameOffset.set(0)
image_plane_shape.frameIn.set(start_frame)
image_plane_shape.frameOut.set(end_frame)
image_plane_shape.frameCache.set(end_frame)
image_plane_shape.useFrameExtension.set(1)
for attr, value in {
"depth": image_plane_depth,
"frameOffset": 0,
"frameIn": start_frame,
"frameOut": end_frame,
"frameCache": end_frame,
"useFrameExtension": True
}.items():
plug = "{}.{}".format(image_plane_shape, attr)
cmds.setAttr(plug, value)
movie_representations = ["mov", "preview"]
if context["representation"]["name"] in movie_representations:
# Need to get "type" by string, because its a method as well.
pm.Attribute(image_plane_shape + ".type").set(2)
cmds.setAttr(image_plane_shape + ".type", 2)
# Ask user whether to use sequence or still image.
if context["representation"]["name"] == "exr":
# Ensure OpenEXRLoader plugin is loaded.
pm.loadPlugin("OpenEXRLoader.mll", quiet=True)
cmds.loadPlugin("OpenEXRLoader", quiet=True)
message = (
"Hold image sequence on first frame?"
@ -161,32 +184,18 @@ class ImagePlaneLoader(load.LoaderPlugin):
None,
"Frame Hold.",
message,
QtWidgets.QMessageBox.Ok,
QtWidgets.QMessageBox.Cancel
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.Ok:
# find the input and output of frame extension
expressions = image_plane_shape.frameExtension.inputs()
frame_ext_output = image_plane_shape.frameExtension.outputs()
if expressions:
# the "time1" node is non-deletable attr
# in Maya, use disconnectAttr instead
pm.disconnectAttr(expressions, frame_ext_output)
if reply == QtWidgets.QMessageBox.Yes:
frame_extension_plug = "{}.frameExtension".format(image_plane_shape) # noqa
if not image_plane_shape.frameExtension.isFreeToChange():
raise RuntimeError("Can't set frame extension for {}".format(image_plane_shape)) # noqa
# get the node of time instead and set the time for it.
image_plane_shape.frameExtension.set(start_frame)
# Remove current frame expression
disconnect_inputs(frame_extension_plug)
new_nodes.extend(
[
image_plane_transform.longName().split("|")[-1],
image_plane_shape.longName().split("|")[-1]
]
)
cmds.setAttr(frame_extension_plug, start_frame)
for node in new_nodes:
pm.rename(node, "{}:{}".format(namespace, node))
new_nodes = [image_plane_transform, image_plane_shape]
return containerise(
name=name,
@ -197,21 +206,19 @@ class ImagePlaneLoader(load.LoaderPlugin):
)
def update(self, container, representation):
import pymel.core as pm
image_plane_shape = None
for node in pm.PyNode(container["objectName"]).members():
if node.nodeType() == "imagePlane":
image_plane_shape = node
assert image_plane_shape is not None, "Image plane not found."
members = get_container_members(container)
image_planes = cmds.ls(members, type="imagePlane")
assert image_planes, "Image plane not found."
image_plane_shape = image_planes[0]
path = get_representation_path(representation)
image_plane_shape.imageName.set(path)
cmds.setAttr(
container["objectName"] + ".representation",
str(representation["_id"]),
type="string"
)
cmds.setAttr("{}.imageName".format(image_plane_shape),
path,
type="string")
cmds.setAttr("{}.representation".format(container["objectName"]),
str(representation["_id"]),
type="string")
# Set frame range.
project_name = legacy_io.active_project()
@ -227,10 +234,14 @@ class ImagePlaneLoader(load.LoaderPlugin):
start_frame = asset["data"]["frameStart"]
end_frame = asset["data"]["frameEnd"]
image_plane_shape.frameOffset.set(0)
image_plane_shape.frameIn.set(start_frame)
image_plane_shape.frameOut.set(end_frame)
image_plane_shape.frameCache.set(end_frame)
for attr, value in {
"frameOffset": 0,
"frameIn": start_frame,
"frameOut": end_frame,
"frameCache": end_frame
}:
plug = "{}.{}".format(image_plane_shape, attr)
cmds.setAttr(plug, value)
def switch(self, container, representation):
self.update(container, representation)

View file

@ -12,7 +12,8 @@ from openpype.pipeline.create import (
import openpype.hosts.maya.api.plugin
from openpype.hosts.maya.api.lib import (
maintained_selection,
get_container_members
get_container_members,
parent_nodes
)
@ -118,7 +119,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
def process_reference(self, context, name, namespace, options):
import maya.cmds as cmds
import pymel.core as pm
try:
family = context["representation"]["context"]["family"]
@ -148,7 +148,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# if there are cameras, try to lock their transforms
self._lock_camera_transforms(new_nodes)
current_namespace = pm.namespaceInfo(currentNamespace=True)
current_namespace = cmds.namespaceInfo(currentNamespace=True)
if current_namespace != ":":
group_name = current_namespace + ":" + group_name
@ -158,37 +158,29 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
self[:] = new_nodes
if attach_to_root:
group_node = pm.PyNode(group_name)
roots = set()
roots = cmds.listRelatives(group_name,
children=True,
fullPath=True) or []
for node in new_nodes:
try:
roots.add(pm.PyNode(node).getAllParents()[-2])
except: # noqa: E722
pass
if family not in {"layout", "setdress",
"mayaAscii", "mayaScene"}:
# QUESTION Why do we need to exclude these families?
with parent_nodes(roots, parent=None):
cmds.xform(group_name, zeroTransformPivots=True)
if family not in ["layout", "setdress",
"mayaAscii", "mayaScene"]:
for root in roots:
root.setParent(world=True)
group_node.zeroTransformPivots()
for root in roots:
root.setParent(group_node)
cmds.setAttr(group_name + ".displayHandle", 1)
cmds.setAttr("{}.displayHandle".format(group_name), 1)
settings = get_project_settings(os.environ['AVALON_PROJECT'])
colors = settings['maya']['load']['colors']
c = colors.get(family)
if c is not None:
group_node.useOutlinerColor.set(1)
group_node.outlinerColor.set(
(float(c[0]) / 255),
(float(c[1]) / 255),
(float(c[2]) / 255))
cmds.setAttr("{}.useOutlinerColor".format(group_name), 1)
cmds.setAttr("{}.outlinerColor".format(group_name),
(float(c[0]) / 255),
(float(c[1]) / 255),
(float(c[2]) / 255))
cmds.setAttr(group_name + ".displayHandle", 1)
cmds.setAttr("{}.displayHandle".format(group_name), 1)
# get bounding box
bbox = cmds.exactWorldBoundingBox(group_name)
# get pivot position on world space
@ -202,15 +194,16 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
cy = cy + pivot[1]
cz = cz + pivot[2]
# set selection handle offset to center of bounding box
cmds.setAttr(group_name + ".selectHandleX", cx)
cmds.setAttr(group_name + ".selectHandleY", cy)
cmds.setAttr(group_name + ".selectHandleZ", cz)
cmds.setAttr("{}.selectHandleX".format(group_name), cx)
cmds.setAttr("{}.selectHandleY".format(group_name), cy)
cmds.setAttr("{}.selectHandleZ".format(group_name), cz)
if family == "rig":
self._post_process_rig(name, namespace, context, options)
else:
if "translate" in options:
cmds.setAttr(group_name + ".t", *options["translate"])
cmds.setAttr("{}.translate".format(group_name),
*options["translate"])
return new_nodes
def switch(self, container, representation):

View file

@ -1,5 +1,4 @@
from maya import cmds, mel
import pymel.core as pm
import pyblish.api
@ -123,43 +122,42 @@ class CollectReview(pyblish.api.InstancePlugin):
# Collect audio
playback_slider = mel.eval('$tmpVar=$gPlayBackSlider')
audio_name = cmds.timeControl(playback_slider, q=True, s=True)
audio_name = cmds.timeControl(playback_slider,
query=True,
sound=True)
display_sounds = cmds.timeControl(
playback_slider, q=True, displaySound=True
playback_slider, query=True, displaySound=True
)
audio_nodes = []
def get_audio_node_data(node):
return {
"offset": cmds.getAttr("{}.offset".format(node)),
"filename": cmds.getAttr("{}.filename".format(node))
}
audio_data = []
if audio_name:
audio_nodes.append(pm.PyNode(audio_name))
audio_data.append(get_audio_node_data(audio_name))
if not audio_name and display_sounds:
start_frame = int(pm.playbackOptions(q=True, min=True))
end_frame = float(pm.playbackOptions(q=True, max=True))
frame_range = range(int(start_frame), int(end_frame))
elif display_sounds:
start_frame = int(cmds.playbackOptions(query=True, min=True))
end_frame = int(cmds.playbackOptions(query=True, max=True))
for node in pm.ls(type="audio"):
for node in cmds.ls(type="audio"):
# Check if frame range and audio range intersections,
# for whether to include this audio node or not.
start_audio = node.offset.get()
end_audio = node.offset.get() + node.duration.get()
audio_range = range(int(start_audio), int(end_audio))
duration = cmds.getAttr("{}.duration".format(node))
start_audio = cmds.getAttr("{}.offset".format(node))
end_audio = start_audio + duration
if bool(set(frame_range).intersection(audio_range)):
audio_nodes.append(node)
if start_audio <= end_frame and end_audio > start_frame:
audio_data.append(get_audio_node_data(node))
instance.data["audio"] = []
for node in audio_nodes:
instance.data["audio"].append(
{
"offset": node.offset.get(),
"filename": node.filename.get()
}
)
instance.data["audio"] = audio_data
# Collect focal length.
attr = camera + ".focalLength"
focal_length = None
if get_attribute_input(attr):
start = instance.data["frameStart"]
end = instance.data["frameEnd"] + 1

View file

@ -9,7 +9,6 @@ from openpype.pipeline import publish
from openpype.hosts.maya.api import lib
from maya import cmds
import pymel.core as pm
@contextlib.contextmanager
@ -110,11 +109,11 @@ class ExtractPlayblast(publish.Extractor):
preset["filename"] = path
preset["overwrite"] = True
pm.refresh(f=True)
cmds.refresh(force=True)
refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True))
pm.currentTime(refreshFrameInt - 1, edit=True)
pm.currentTime(refreshFrameInt, edit=True)
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
cmds.currentTime(refreshFrameInt - 1, edit=True)
cmds.currentTime(refreshFrameInt, edit=True)
# Override transparency if requested.
transparency = instance.data.get("transparency", 0)
@ -226,7 +225,7 @@ class ExtractPlayblast(publish.Extractor):
tags.append("delete")
# Add camera node name to representation data
camera_node_name = pm.ls(camera)[0].getTransform().name()
camera_node_name = cmds.listRelatives(camera, parent=True)[0]
collected_files = list(frame_collection)
# single frame file shouldn't be in list, only as a string

View file

@ -8,7 +8,6 @@ from openpype.pipeline import publish
from openpype.hosts.maya.api import lib
from maya import cmds
import pymel.core as pm
class ExtractThumbnail(publish.Extractor):
@ -99,11 +98,11 @@ class ExtractThumbnail(publish.Extractor):
preset["filename"] = path
preset["overwrite"] = True
pm.refresh(f=True)
cmds.refresh(force=True)
refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True))
pm.currentTime(refreshFrameInt - 1, edit=True)
pm.currentTime(refreshFrameInt, edit=True)
refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True))
cmds.currentTime(refreshFrameInt - 1, edit=True)
cmds.currentTime(refreshFrameInt, edit=True)
# Override transparency if requested.
transparency = instance.data.get("transparency", 0)

View file

@ -1,13 +1,17 @@
import pymel.core as pm
from collections import defaultdict
from maya import cmds
import pyblish.api
from openpype.hosts.maya.api.lib import set_attribute
from openpype.pipeline.publish import (
RepairContextAction,
ValidateContentsOrder,
)
class ValidateAttributes(pyblish.api.ContextPlugin):
class ValidateAttributes(pyblish.api.InstancePlugin):
"""Ensure attributes are consistent.
Attributes to validate and their values comes from the
@ -27,86 +31,80 @@ class ValidateAttributes(pyblish.api.ContextPlugin):
attributes = None
def process(self, context):
def process(self, instance):
# Check for preset existence.
if not self.attributes:
return
invalid = self.get_invalid(context, compute=True)
invalid = self.get_invalid(instance, compute=True)
if invalid:
raise RuntimeError(
"Found attributes with invalid values: {}".format(invalid)
)
@classmethod
def get_invalid(cls, context, compute=False):
invalid = context.data.get("invalid_attributes", [])
def get_invalid(cls, instance, compute=False):
if compute:
invalid = cls.get_invalid_attributes(context)
return invalid
return cls.get_invalid_attributes(instance)
else:
return instance.data.get("invalid_attributes", [])
@classmethod
def get_invalid_attributes(cls, context):
def get_invalid_attributes(cls, instance):
invalid_attributes = []
for instance in context:
# Filter publisable instances.
if not instance.data["publish"]:
# Filter families.
families = [instance.data["family"]]
families += instance.data.get("families", [])
families = set(families) & set(cls.attributes.keys())
if not families:
return []
# Get all attributes to validate.
attributes = defaultdict(dict)
for family in families:
if family not in cls.attributes:
# No attributes to validate for family
continue
# Filter families.
families = [instance.data["family"]]
families += instance.data.get("families", [])
families = list(set(families) & set(cls.attributes.keys()))
if not families:
for preset_attr, preset_value in cls.attributes[family].items():
node_name, attribute_name = preset_attr.split(".", 1)
attributes[node_name][attribute_name] = preset_value
if not attributes:
return []
# Get invalid attributes.
nodes = cmds.ls(long=True)
for node in nodes:
node_name = node.rsplit("|", 1)[-1].rsplit(":", 1)[-1]
if node_name not in attributes:
continue
# Get all attributes to validate.
attributes = {}
for family in families:
for preset in cls.attributes[family]:
[node_name, attribute_name] = preset.split(".")
try:
attributes[node_name].update(
{attribute_name: cls.attributes[family][preset]}
)
except KeyError:
attributes.update({
node_name: {
attribute_name: cls.attributes[family][preset]
}
})
for attr_name, expected in attributes.items():
# Get invalid attributes.
nodes = pm.ls()
for node in nodes:
name = node.name(stripNamespace=True)
if name not in attributes.keys():
# Skip if attribute does not exist
if not cmds.attributeQuery(attr_name, node=node, exists=True):
continue
presets_to_validate = attributes[name]
for attribute in node.listAttr():
names = [attribute.shortName(), attribute.longName()]
attribute_name = list(
set(names) & set(presets_to_validate.keys())
plug = "{}.{}".format(node, attr_name)
value = cmds.getAttr(plug)
if value != expected:
invalid_attributes.append(
{
"attribute": plug,
"expected": expected,
"current": value
}
)
if attribute_name:
expected = presets_to_validate[attribute_name[0]]
if attribute.get() != expected:
invalid_attributes.append(
{
"attribute": attribute,
"expected": expected,
"current": attribute.get()
}
)
context.data["invalid_attributes"] = invalid_attributes
instance.data["invalid_attributes"] = invalid_attributes
return invalid_attributes
@classmethod
def repair(cls, instance):
invalid = cls.get_invalid(instance)
for data in invalid:
data["attribute"].set(data["expected"])
node, attr = data["attribute"].split(".", 1)
value = data["expected"]
set_attribute(node=node, attribute=attr, value=value)

View file

@ -1,8 +1,14 @@
import pymel.core as pc
from maya import cmds
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api.lib import maintained_selection
from openpype.hosts.maya.api.lib import (
maintained_selection,
delete_after,
undo_chunk,
get_attribute,
set_attribute
)
from openpype.pipeline.publish import (
RepairAction,
ValidateMeshOrder,
@ -31,60 +37,68 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin):
else:
active = False
@classmethod
def get_default_attributes(cls):
# Get default arnold attribute values for mesh type.
defaults = {}
with delete_after() as tmp:
transform = cmds.createNode("transform")
tmp.append(transform)
mesh = cmds.createNode("mesh", parent=transform)
for attr in cmds.listAttr(mesh, string="ai*"):
plug = "{}.{}".format(mesh, attr)
try:
defaults[attr] = get_attribute(plug)
except RuntimeError:
cls.log.debug("Ignoring arnold attribute: {}".format(attr))
return defaults
@classmethod
def get_invalid_attributes(cls, instance, compute=False):
invalid = []
if compute:
# Get default arnold attributes.
temp_transform = pc.polyCube()[0]
for shape in pc.ls(instance, type="mesh"):
for attr in temp_transform.getShape().listAttr():
if not attr.attrName().startswith("ai"):
continue
meshes = cmds.ls(instance, type="mesh", long=True)
if not meshes:
return []
target_attr = pc.PyNode(
"{}.{}".format(shape.name(), attr.attrName())
)
if attr.get() != target_attr.get():
invalid.append(target_attr)
pc.delete(temp_transform)
# Compare the values against the defaults
defaults = cls.get_default_attributes()
for mesh in meshes:
for attr_name, default_value in defaults.items():
plug = "{}.{}".format(mesh, attr_name)
if get_attribute(plug) != default_value:
invalid.append(plug)
instance.data["nondefault_arnold_attributes"] = invalid
else:
invalid.extend(instance.data["nondefault_arnold_attributes"])
return invalid
return instance.data.get("nondefault_arnold_attributes", [])
@classmethod
def get_invalid(cls, instance):
invalid = []
for attr in cls.get_invalid_attributes(instance, compute=False):
invalid.append(attr.node().name())
return invalid
invalid_attrs = cls.get_invalid_attributes(instance, compute=False)
invalid_nodes = set(attr.split(".", 1)[0] for attr in invalid_attrs)
return sorted(invalid_nodes)
@classmethod
def repair(cls, instance):
with maintained_selection():
with pc.UndoChunk():
temp_transform = pc.polyCube()[0]
with undo_chunk():
defaults = cls.get_default_attributes()
attributes = cls.get_invalid_attributes(
instance, compute=False
)
for attr in attributes:
source = pc.PyNode(
"{}.{}".format(
temp_transform.getShape(), attr.attrName()
)
node, attr_name = attr.split(".", 1)
value = defaults[attr_name]
set_attribute(
node=node,
attribute=attr_name,
value=value
)
attr.set(source.get())
pc.delete(temp_transform)
def process(self, instance):

View file

@ -1,10 +1,11 @@
import pyblish.api
import openpype.hosts.maya.api.action
import math
import maya.api.OpenMaya as om
import pymel.core as pm
from six.moves import xrange
from maya import cmds
import maya.api.OpenMaya as om
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.pipeline.publish import ValidateMeshOrder
@ -185,8 +186,7 @@ class GetOverlappingUVs(object):
center, radius = self._createBoundingCircle(meshfn)
for i in xrange(meshfn.numPolygons): # noqa: F821
rayb1, face1Orig, face1Vec = self._createRayGivenFace(
meshfn, i)
rayb1, face1Orig, face1Vec = self._createRayGivenFace(meshfn, i)
if not rayb1:
continue
cui = center[2*i]
@ -206,8 +206,8 @@ class GetOverlappingUVs(object):
if (dsqr >= (ri + rj) * (ri + rj)):
continue
rayb2, face2Orig, face2Vec = self._createRayGivenFace(
meshfn, j)
rayb2, face2Orig, face2Vec = self._createRayGivenFace(meshfn,
j)
if not rayb2:
continue
# Exclude the degenerate face
@ -240,37 +240,45 @@ class ValidateMeshHasOverlappingUVs(pyblish.api.InstancePlugin):
optional = True
@classmethod
def _get_overlapping_uvs(cls, node):
""" Check if mesh has overlapping UVs.
def _get_overlapping_uvs(cls, mesh):
"""Return overlapping UVs of mesh.
Args:
mesh (str): Mesh node name
Returns:
list: Overlapping uvs for the input mesh in all uv sets.
:param node: node to check
:type node: str
:returns: True is has overlapping UVs, False otherwise
:rtype: bool
"""
ovl = GetOverlappingUVs()
# Store original uv set
original_current_uv_set = cmds.polyUVSet(mesh,
query=True,
currentUVSet=True)
overlapping_faces = []
for i, uv in enumerate(pm.polyUVSet(node, q=1, auv=1)):
pm.polyUVSet(node, cuv=1, uvSet=uv)
overlapping_faces.extend(ovl._getOverlapUVFaces(str(node)))
for uv_set in cmds.polyUVSet(mesh, query=True, allUVSets=True):
cmds.polyUVSet(mesh, currentUVSet=True, uvSet=uv_set)
overlapping_faces.extend(ovl._getOverlapUVFaces(mesh))
# Restore original uv set
cmds.polyUVSet(mesh, currentUVSet=True, uvSet=original_current_uv_set)
return overlapping_faces
@classmethod
def get_invalid(cls, instance, compute=False):
invalid = []
if compute:
instance.data["overlapping_faces"] = []
for node in pm.ls(instance, type="mesh"):
invalid = []
for node in cmds.ls(instance, type="mesh"):
faces = cls._get_overlapping_uvs(node)
invalid.extend(faces)
# Store values for later.
instance.data["overlapping_faces"].extend(faces)
else:
invalid.extend(instance.data["overlapping_faces"])
return invalid
instance.data["overlapping_faces"] = invalid
return instance.data.get("overlapping_faces", [])
def process(self, instance):

View file

@ -1,4 +1,3 @@
import pymel.core as pm
import maya.cmds as cmds
import pyblish.api
@ -12,7 +11,7 @@ import openpype.hosts.maya.api.action
def get_namespace(node_name):
# ensure only node's name (not parent path)
node_name = node_name.rsplit("|")[-1]
node_name = node_name.rsplit("|", 1)[-1]
# ensure only namespace
return node_name.rpartition(":")[0]
@ -45,13 +44,11 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin):
invalid = cls.get_invalid(instance)
# Get nodes with pymel since we'll be renaming them
# Since we don't want to keep checking the hierarchy
# or full paths
nodes = pm.ls(invalid)
# Iterate over the nodes by long to short names to iterate the lowest
# in hierarchy nodes first. This way we avoid having renamed parents
# before renaming children nodes
for node in sorted(invalid, key=len, reverse=True):
for node in nodes:
namespace = node.namespace()
if namespace:
name = node.nodeName()
node.rename(name[len(namespace):])
node_name = node.rsplit("|", 1)[-1]
node_name_without_namespace = node_name.rsplit(":")[-1]
cmds.rename(node, node_name_without_namespace)

View file

@ -1,14 +1,22 @@
import pymel.core as pc
from collections import defaultdict
from maya import cmds
import pyblish.api
import openpype.hosts.maya.api.action
from openpype.hosts.maya.api.lib import get_id, set_id
from openpype.pipeline.publish import (
RepairAction,
ValidateContentsOrder,
)
def get_basename(node):
"""Return node short name without namespace"""
return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1]
class ValidateRigOutputIds(pyblish.api.InstancePlugin):
"""Validate rig output ids.
@ -30,43 +38,48 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
@classmethod
def get_invalid(cls, instance, compute=False):
invalid = cls.get_invalid_matches(instance, compute=compute)
return [x["node"].longName() for x in invalid]
invalid_matches = cls.get_invalid_matches(instance, compute=compute)
return list(invalid_matches.keys())
@classmethod
def get_invalid_matches(cls, instance, compute=False):
invalid = []
invalid = {}
if compute:
out_set = next(x for x in instance if x.endswith("out_SET"))
instance_nodes = pc.sets(out_set, query=True)
instance_nodes.extend(
[x.getShape() for x in instance_nodes if x.getShape()])
scene_nodes = pc.ls(type="transform") + pc.ls(type="mesh")
instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True)
instance_nodes = cmds.ls(instance_nodes, long=True)
for node in instance_nodes:
shapes = cmds.listRelatives(node, shapes=True, fullPath=True)
if shapes:
instance_nodes.extend(shapes)
scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh")
scene_nodes = set(scene_nodes) - set(instance_nodes)
scene_nodes_by_basename = defaultdict(list)
for node in scene_nodes:
basename = get_basename(node)
scene_nodes_by_basename[basename].append(node)
for instance_node in instance_nodes:
matches = []
basename = instance_node.name(stripNamespace=True)
for scene_node in scene_nodes:
if scene_node.name(stripNamespace=True) == basename:
matches.append(scene_node)
basename = get_basename(instance_node)
if basename not in scene_nodes_by_basename:
continue
if matches:
ids = [instance_node.cbId.get()]
ids.extend([x.cbId.get() for x in matches])
ids = set(ids)
matches = scene_nodes_by_basename[basename]
if len(ids) > 1:
cls.log.error(
"\"{}\" id mismatch to: {}".format(
instance_node.longName(), matches
)
)
invalid.append(
{"node": instance_node, "matches": matches}
ids = set(get_id(node) for node in matches)
ids.add(get_id(instance_node))
if len(ids) > 1:
cls.log.error(
"\"{}\" id mismatch to: {}".format(
instance_node.longName(), matches
)
)
invalid[instance_node] = matches
instance.data["mismatched_output_ids"] = invalid
else:
@ -76,19 +89,21 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin):
@classmethod
def repair(cls, instance):
invalid = cls.get_invalid_matches(instance)
invalid_matches = cls.get_invalid_matches(instance)
multiple_ids_match = []
for data in invalid:
ids = [x.cbId.get() for x in data["matches"]]
for instance_node, matches in invalid_matches.items():
ids = set(get_id(node) for node in matches)
# If there are multiple scene ids matched, and error needs to be
# raised for manual correction.
if len(ids) > 1:
multiple_ids_match.append(data)
multiple_ids_match.append({"node": instance_node,
"matches": matches})
continue
data["node"].cbId.set(ids[0])
id_to_set = next(iter(ids))
set_id(instance_node, id_to_set, overwrite=True)
if multiple_ids_match:
raise RuntimeError(

View file

@ -53,5 +53,5 @@ There are four settings available:
## Q&A
### Is it safe to rename an entity from Kitsu?
Absolutely! Entities are linked by their unique IDs between the two databases.
Absolutely! Entities are linked by their unique IDs between the two databases.
But renaming from the OP's Project Manager won't apply the change to Kitsu, it'll be overridden during the next synchronization.