diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1e65c83..21b7b08 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -22,8 +22,6 @@ jobs:
matrix:
include:
- - maya: "2017"
- pip: "2.7/get-pip.py"
- maya: "2018"
pip: "2.7/get-pip.py"
- maya: "2019"
@@ -32,6 +30,10 @@ jobs:
pip: "2.7/get-pip.py"
- maya: "2022"
pip: "get-pip.py"
+ - maya: "2023"
+ pip: "get-pip.py"
+ - maya: "2024"
+ pip: "get-pip.py"
container: mottosso/maya:${{ matrix.maya }}
diff --git a/README.md b/README.md
index 396931e..f24cacd 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
A fast subset of maya.cmds
For Maya 2017-2022
+A fast subset of maya.cmds
For Maya 2018-2024
@@ -23,6 +23,7 @@ On average, `cmdx` is **140x faster** than [PyMEL](https://github.com/LumaPictur
| Date | Version | Event
|:---------|:----------|:----------
+| Dec 2023 | 0.6.3 | Cloning of attributes
| Apr 2020 | 0.6.0 | Stable Undo/Redo, dropped support for Maya 2015-2016
| Mar 2020 | 0.5.1 | Support for Maya 2022
| Mar 2020 | 0.5.0 | Stable release
@@ -528,6 +529,50 @@ The reason for this limitation is because the functions `cmds`
+### Path-like Syntax
+
+Neatly traverse a hierarchy with the `|` syntax.
+
+```py
+# Before
+group = cmdx.encode("|some_grp")
+hand = cmdx.encode(group.path() + "|hand_ctl")
+
+# After
+hand = group | "hand_ctl"
+```
+
+It can be nested too.
+
+```py
+finger = group | "hand_ctl" | "finger_ctl"
+```
+
+
+
+### setAttr
+
+Maya's `cmds.setAttr` depends on the UI settings for units.
+
+```py
+cmds.setAttr("hand_ctl.translateY", 5)
+```
+
+For a user with Maya set to `Centimeters`, this would set `translateY` to 5 centimeters. For any user with any other unit, like `Foot`, it would instead move it 5 feet. That is terrible behaviour for a script, how can you possibly define the length of something if you don't know the unit? A dog is 100 cm tall, not 100 "any unit" tall.
+
+The `cmdx.setAttr` on the other hand does what Maya's API does, which is to treat all units consistently.
+
+```py
+cmdx.setAttr("hand_ctl.translateY", 5) # centimeters, always
+```
+
+- Distance values are in `centimeters`
+- Angular values are in `radians`
+
+So the user is free to choose any unit for their UI without breaking their scripts.
+
+
+
### Units
`cmdx` takes and returns values in the units used by the UI. For example, Maya's default unit for distances, such as `translateX` is in Centimeters.
@@ -1018,6 +1063,28 @@ node["myMatrix"] = node["worldMatrix"][0].asMatrix()
+### Cloning
+
+Support for cloning enum attributes.
+
+```py
+parent = createNode("transform")
+camera = createNode("camera", parent=parent)
+
+# Make new enum attribute
+camera["myEnum"] = Enum(fields=["a", "b", "c"])
+
+# Clone it
+clone = camera["myEnum"].clone("cloneEnum")
+cam.addAttr(clone)
+
+# Compare it
+fields = camera["cloneEnum"].fields()
+assert fields == ((0, "a"), (1, "b"), (2, "c"))
+```
+
+
+
### Native Types
Maya boasts a library of classes that provide mathematical convenience functionality, such as rotating a vector, multiplying matrices or converting between Euler degrees and Quaternions.
diff --git a/build_livedocs.py b/build_livedocs.py
index 88eae6e..1c17284 100644
--- a/build_livedocs.py
+++ b/build_livedocs.py
@@ -107,6 +107,7 @@ def test_{count}_{header}():
# -*- coding: utf-8 -*-
import os
import sys
+
import nose
from nose.tools import assert_raises
@@ -115,6 +116,11 @@ def test_{count}_{header}():
from maya import standalone
standalone.initialize()
+ # For nose
+ if sys.version_info[0] == 3:
+ import collections
+ collections.Callable = collections.abc.Callable
+
from maya import cmds
import cmdx
@@ -127,6 +133,11 @@ def test_{count}_{header}():
])
result = nose.main(argv=argv, exit=False)
+
+ if os.name == "nt":
+ # Graceful exit, only Windows seems to like this consistently
+ standalone.uninitialize()
+
os._exit(0 if result.success else 1)
""")
f.write("".join(tests))
diff --git a/cmdx.py b/cmdx.py
index 853e47e..5ab6361 100644
--- a/cmdx.py
+++ b/cmdx.py
@@ -18,7 +18,7 @@
from maya.api import OpenMaya as om, OpenMayaAnim as oma, OpenMayaUI as omui
from maya import OpenMaya as om1, OpenMayaMPx as ompx1, OpenMayaUI as omui1
-__version__ = "0.6.2"
+__version__ = "0.6.3"
IS_VENDORED = "." in __name__
@@ -180,6 +180,13 @@ def function():
"""
+ try:
+ perf_counter = time_.perf_counter
+
+ # Python 2.7
+ except AttributeError:
+ perf_counter = time_.clock
+
def timings_decorator(func):
if not TIMINGS:
# Do not wrap the function.
@@ -188,12 +195,12 @@ def timings_decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
- t0 = time_.clock()
+ t0 = perf_counter()
try:
return func(*args, **kwargs)
finally:
- t1 = time_.clock()
+ t1 = perf_counter()
duration = (t1 - t0) * 10 ** 6 # microseconds
Stats.LastTiming = duration
@@ -308,6 +315,7 @@ class _Type(int):
MFn = om.MFn
kDagNode = _Type(om.MFn.kDagNode)
kShape = _Type(om.MFn.kShape)
+kMesh = _Type(om.MFn.kMesh)
kTransform = _Type(om.MFn.kTransform)
kJoint = _Type(om.MFn.kJoint)
kSet = _Type(om.MFn.kSet)
@@ -582,6 +590,8 @@ def __getitem__(self, key):
for item in items:
if isinstance(item, _Unit):
unit = item
+ elif isinstance(item, int):
+ unit = item
elif isinstance(item, _Cached):
cached = True
@@ -591,6 +601,10 @@ def __getitem__(self, key):
except KeyError:
pass
+ assert isinstance(key, str), (
+ "%s was not the name of an attribute" % key
+ )
+
try:
plug = self.findPlug(key)
except RuntimeError:
@@ -1076,7 +1090,7 @@ def dump(self, ignore_error=True, preserve_order=False):
>>> dump = node.dump()
>>> isinstance(dump, dict)
True
- >>> dump["choice1.caching"]
+ >>> dump["caching"]
False
"""
@@ -1097,7 +1111,7 @@ def dump(self, ignore_error=True, preserve_order=False):
if not ignore_error:
raise
- attrs[plug.name()] = value
+ attrs[plug.name().split(".", 1)[-1]] = value
return attrs
@@ -1500,6 +1514,34 @@ def __str__(self):
def __repr__(self):
return self.path()
+ def __or__(self, other):
+ """Syntax sugar for finding a child
+
+ Examples:
+ >>> _new()
+ >>> parent = createNode("transform", "parent")
+ >>> child = createNode("transform", "child", parent)
+ >>> parent | "child"
+ |parent|child
+
+ # Stackable too
+ >>> grand = createNode("transform", "grand", child)
+ >>> parent | "child" | "grand"
+ |parent|child|grand
+
+ """
+
+ # Handle cases where self has a namespace
+ # and no namespace is provided.
+ if ":" not in other:
+ other = "%s:%s" % (self.namespace(), other)
+
+ return encode("%s|%s" % (self.path(), other))
+
+ def __iter__(self):
+ for child in self.children():
+ yield child
+
@property
def _tfn(self):
if SAFE_MODE:
@@ -2344,7 +2386,7 @@ def keys(self, times, values, tangents=None, change=None):
except RuntimeError:
# The error provided by Maya aren't very descriptive,
- # help a brother out by look for common problems.
+ # help a brother out by looking for common problems.
if not times:
log.error("No times were provided: %s" % str(times))
@@ -2746,7 +2788,8 @@ def __getitem__(self, logicalIndex):
raise TypeError("'%s' is not a compound attribute"
% self.path())
- raise ExistError("'%s' was not found" % logicalIndex)
+ raise ExistError("'%s.%s' was not found" % (
+ self.path(), logicalIndex))
def __setitem__(self, index, value):
"""Write to child of array or compound plug
@@ -2797,10 +2840,10 @@ def clone(self, name, shortName=None, niceName=None):
>>> clone = original.clone("focalClone")
# Original setup is preserved
- >>> clone["min"]
- 2.5
- >>> clone["max"]
- 100000.0
+ >>> clone["min"] == original.fn().getMin()
+ True
+ >>> clone["max"] == original.fn().getMax()
+ True
>>> cam.addAttr(clone)
>>> cam["focalClone"].read()
@@ -2814,6 +2857,13 @@ def clone(self, name, shortName=None, niceName=None):
>>> cam["fClone"].read()
5.6
+ # Can clone enum attributes
+ >>> cam["myEnum"] = Enum(fields=["a", "b", "c"])
+ >>> clone = cam["myEnum"].clone("cloneEnum")
+ >>> cam.addAttr(clone)
+ >>> fields = cam["cloneEnum"].fields()
+ >>> assert fields == ((0, "a"), (1, "b"), (2, "c")), fields
+
"""
assert isinstance(self, Plug)
@@ -2840,8 +2890,8 @@ def clone(self, name, shortName=None, niceName=None):
cls = self.typeClass()
fn = self.fn()
- attr = cls(
- name,
+
+ kwargs = dict(
default=self.default,
label=niceName,
shortName=shortName,
@@ -2857,14 +2907,20 @@ def clone(self, name, shortName=None, niceName=None):
array=fn.array,
indexMatters=fn.indexMatters,
connectable=fn.connectable,
- disconnectBehavior=fn.disconnectBehavior,
+ disconnectBehavior=fn.disconnectBehavior
)
- if hasattr(fn, "getMin") and fn.hasMin():
- attr["min"] = fn.getMin()
+ if isinstance(fn, om.MFnEnumAttribute):
+ kwargs["fields"] = self.fields()
+
+ else:
+ if hasattr(fn, "getMin") and fn.hasMin():
+ kwargs["min"] = fn.getMin()
+
+ if hasattr(fn, "getMax") and fn.hasMax():
+ kwargs["max"] = fn.getMax()
- if hasattr(fn, "getMax") and fn.hasMax():
- attr["max"] = fn.getMax()
+ attr = cls(name, **kwargs)
return attr
@@ -2962,6 +3018,33 @@ def index(self):
"%s is not a child or element" % self.path()
)
+ def fields(self):
+ """Return fields of an Enum attribute, if any"""
+
+ fn = self.fn()
+
+ assert isinstance(fn, om.MFnEnumAttribute), (
+ "%s was not an enum attribute" % self.path()
+ )
+
+ fields = []
+
+ for index in range(fn.getMax() + 1):
+ try:
+ field = fn.fieldName(index)
+
+ except RuntimeError:
+ # Indices may not be consecutive, e.g.
+ # 0 = Off
+ # 2 = Kinematic
+ # 3 = Dynamic
+ # (missing 1!)
+ continue
+
+ else:
+ fields.append((index, field))
+
+ return tuple(fields)
def nextAvailableIndex(self, startIndex=0):
"""Find the next unconnected element in an array plug
@@ -3258,16 +3341,19 @@ def findAnimatedPlug(self):
>>> parent = createNode("transform")
>>> _ = cmds.parentConstraint(str(parent), str(node))
>>> blend = ls(type="pairBlend")[0]
- >>> node["tx"].findAnimatedPlug().path() == blend["inTranslateX1"].path()
+ >>> animPlug = node["tx"].findAnimatedPlug().path()
+ >>> animPlug == blend["inTranslateX1"].path()
True
# Animation layers
>>> layer = encode(cmds.animLayer(at=node["tx"].path()))
- >>> node["tx"].findAnimatedPlug().path() == blend["inTranslateX1"].path()
+ >>> animPlug = node["tx"].findAnimatedPlug().path()
+ >>> animPlug == blend["inTranslateX1"].path()
True
>>> cmds.animLayer(str(layer), e=True, preferred=True)
>>> animBlend = ls(type="animBlendNodeBase")[0]
- >>> node["tx"].findAnimatedPlug().path() == animBlend["inputB"].path()
+ >>> animPlug = node["tx"].findAnimatedPlug().path()
+ >>> animPlug == animBlend["inputB"].path()
True
# Animation layer then constraint
@@ -3277,10 +3363,12 @@ def findAnimatedPlug(self):
>>> parent = createNode("transform")
>>> _ = cmds.parentConstraint(str(parent), str(node))
>>> animBlend = ls(type="animBlendNodeBase")[0]
- >>> node["tx"].findAnimatedPlug().path() == animBlend["inputA"].path()
+ >>> animPlug = node["tx"].findAnimatedPlug().path()
+ >>> animPlug == animBlend["inputA"].path()
True
>>> cmds.animLayer(str(layer), e=True, preferred=True)
- >>> node["tx"].findAnimatedPlug().path() == animBlend["inputB"].path()
+ >>> animPlug = node["tx"].findAnimatedPlug().path()
+ >>> animPlug == animBlend["inputB"].path()
True
"""
@@ -3307,11 +3395,17 @@ def findAnimatedPlug(self):
plug = plug[self.index()] if plug.isCompound else plug
# Search for more pair blends or anim blends
- con = plug.input(type=AnimBlendTypes + (MFn.kPairBlend,), plug=True)
+ con = plug.input(
+ type=AnimBlendTypes + (MFn.kPairBlend,),
+ plug=True
+ )
# If no animation layers or pair blends then plug is self
plug = plug if plug is not None else self
+ # Preserve unit passed by user, e.g. Stepped
+ plug._unit = self._unit
+
return plug
def lock(self):
@@ -3606,6 +3700,9 @@ def fn(self):
elif typ == om.MFn.kMessageAttribute:
fn = om.MFnMessageAttribute(attr)
+ elif typ == om.MFn.kEnumAttribute:
+ fn = om.MFnEnumAttribute(attr)
+
else:
raise TypeError(
"Couldn't figure out function set for '%s.%s'" % (
@@ -4159,6 +4256,8 @@ def node(self):
type_class = typeClass
next_available_index = nextAvailableIndex
find_animated_plug = findAnimatedPlug
+ is_array = isArray
+ is_compound = isCompound
class TransformationMatrix(om.MTransformationMatrix):
@@ -4386,6 +4485,12 @@ def asMatrix(self): # type: () -> MatrixType
def asMatrixInverse(self): # type: () -> MatrixType
return MatrixType(super(TransformationMatrix, self).asMatrixInverse())
+ def asScaleMatrix(self): # type: () -> MatrixType
+ return MatrixType(super(TransformationMatrix, self).asScaleMatrix())
+
+ def asRotateMatrix(self): # type: () -> MatrixType
+ return MatrixType(super(TransformationMatrix, self).asRotateMatrix())
+
if ENABLE_PEP8:
x_axis = xAxis
y_axis = yAxis
@@ -4397,6 +4502,8 @@ def asMatrixInverse(self): # type: () -> MatrixType
set_scale = setScale
as_matrix = asMatrix
as_matrix_inverse = asMatrixInverse
+ as_scale_matrix = asScaleMatrix
+ as_rotate_matrix = asRotateMatrix
class MatrixType(om.MMatrix):
@@ -4486,6 +4593,12 @@ def element(self, row, col):
values = tuple(self)
return values[row * 4 + col % 4]
+ def isEquivalent(self, other, tolerance=1e-10):
+ return super(MatrixType, self).isEquivalent(other, tolerance)
+
+ if ENABLE_PEP8:
+ is_equivalent = isEquivalent
+
# Alias
Transformation = TransformationMatrix
@@ -4504,9 +4617,19 @@ class Vector(om.MVector):
0.0
>>> vec ^ Vector(0, 1, 0) # Cross product
maya.api.OpenMaya.MVector(0, 0, 1)
+ >>> Vector(0, 1, 0) * 2 # Scale
+ maya.api.OpenMaya.MVector(0, 2, 0)
"""
+ def __mul__(self, value):
+ if isinstance(value, om.MVector):
+ # Dot product
+ return super(Vector, self).__mul__(value)
+ else:
+ # Scaling
+ return Vector(super(Vector, self).__mul__(value))
+
def __add__(self, value):
if isinstance(value, (int, float)):
return type(self)(
@@ -4530,6 +4653,9 @@ def __iadd__(self, value):
def dot(self, value):
return Vector(super(Vector, self).__mul__(value))
+ def rotateBy(self, value):
+ return Vector(super(Vector, self).rotateBy(value))
+
def cross(self, value):
return Vector(super(Vector, self).__xor__(value))
@@ -4538,6 +4664,7 @@ def isEquivalent(self, other, tolerance=om.MVector.kTolerance):
if ENABLE_PEP8:
is_equivalent = isEquivalent
+ rotate_by = rotateBy
# Alias, it can't take anything other than values
@@ -4568,6 +4695,26 @@ def divide_vectors(vec1, vec2):
class Point(om.MPoint):
"""Maya's MPoint"""
+ def __init__(self, *args):
+
+ # Protect against passing an instance of a Node
+ # into here, as Maya would crash if that happens
+ if len(args) == 1:
+ assert (
+ isinstance(args[0], om.MPoint) or
+ isinstance(args[0], om.MVector)
+ ), "%s was not a MPoint or MVector" % args[0]
+
+ return super(Point, self).__init__(*args)
+
+ def distanceTo(self, other):
+ if isinstance(other, om.MVector):
+ other = Point(other)
+ return super(Point, self).distanceTo(other)
+
+ if ENABLE_PEP8:
+ distance_to = distanceTo
+
class Color(om.MColor):
"""Maya's MColor"""
@@ -4576,6 +4723,11 @@ class Color(om.MColor):
class BoundingBox(om.MBoundingBox):
"""Maya's MBoundingBox"""
+ def expand(self, position):
+ if isinstance(position, om.MVector):
+ position = Point(position)
+ return super(BoundingBox, self).expand(position)
+
def volume(self):
return self.width * self.height * self.depth
@@ -4624,12 +4776,16 @@ def inverse(self):
def asMatrix(self):
return Matrix4(super(Quaternion, self).asMatrix())
+ def isEquivalent(self, other, tolerance=om.MQuaternion.kTolerance):
+ return super(Quaternion, self).isEquivalent(other, tolerance)
+
if ENABLE_PEP8:
as_matrix = asMatrix
is_normalised = isNormalised
length_squared = lengthSquared
as_euler_rotation = asEulerRotation
as_euler = asEulerRotation
+ is_equivalent = isEquivalent
# Alias
@@ -4710,7 +4866,7 @@ def isEquivalent(self, other, tolerance=om.MEulerRotation.kTolerance):
Euler = EulerRotation
-def NurbsCurveData(points, degree=1, form=om1.MFnNurbsCurve.kOpen):
+def NurbsCurveData(points, degree=1, form=om.MFnNurbsCurve.kOpen):
"""Tuple of points to MObject suitable for nurbsCurve-typed data
Arguments:
@@ -4737,13 +4893,13 @@ def NurbsCurveData(points, degree=1, form=om1.MFnNurbsCurve.kOpen):
degree = min(3, max(1, degree))
- cvs = om1.MPointArray()
- curveFn = om1.MFnNurbsCurve()
- data = om1.MFnNurbsCurveData()
+ cvs = om.MPointArray()
+ curveFn = om.MFnNurbsCurve()
+ data = om.MFnNurbsCurveData()
mobj = data.create()
for point in points:
- cvs.append(om1.MPoint(*point))
+ cvs.append(om.MPoint(*point))
curveFn.createWithEditPoints(cvs,
degree,
@@ -5024,6 +5180,9 @@ def _python_to_plug(value, plug):
# Native Maya types
+ elif isinstance(value, om.MObject):
+ plug._mplug.setMObject(value)
+
elif isinstance(value, om1.MObject):
node = _encode1(plug._node.path())
shapeFn = om1.MFnDagNode(node)
@@ -5244,6 +5403,9 @@ def _python_to_mod(value, plug, mod):
obj = om.MFnMatrixData().create(value)
mod.newPlugValue(mplug, obj)
+ elif isinstance(value, om.MObject):
+ mod.newPlugValue(mplug, value)
+
elif isinstance(value, om.MEulerRotation):
for index, value in enumerate(value):
value = om.MAngle(value, om.MAngle.kRadians)
@@ -5418,11 +5580,12 @@ def asHex(mobj):
cos = math.cos
tan = math.tan
pi = math.pi
+sqrt = math.sqrt
def time(frame):
assert isinstance(frame, int), "%s was not an int" % frame
- return om.MTime(frame, TimeUiUnit())
+ return HashableTime(om.MTime(frame, TimeUiUnit()))
def frame(time):
@@ -5627,6 +5790,7 @@ def redoit():
# These all involve calling on cmds,
# which manages undo on its own.
self._doKeyableAttrs()
+ self._doChannelBoxAttrs()
self._doNiceNames()
self._doLockAttrs()
@@ -5661,6 +5825,7 @@ def __init__(self,
# Extras
self._lockAttrs = []
self._keyableAttrs = []
+ self._channelBoxAttrs = []
self._niceNames = []
self._animChanges = []
@@ -5785,6 +5950,42 @@ def setKeyable(self, plug, value=True):
assert isinstance(plug, Plug), "%s was not a plug" % plug
self._keyableAttrs.append((plug, value))
+ @record_history
+ def setChannelBox(self, plug, value=True):
+ """Make a plug appear in channel box
+
+ Examples:
+ >>> with DagModifier() as mod:
+ ... node = mod.createNode("transform")
+ ... mod.setChannelBox(node["rotatePivotX"])
+ ... mod.setChannelBox(node["translateX"], False)
+ ...
+ >>> node["rotatePivotX"].channelBox
+ True
+ >>> node["translateX"].channelBox
+ False
+
+ # Also works with dynamic attributes
+ >>> with DagModifier() as mod:
+ ... node = mod.createNode("transform")
+ ... _ = mod.addAttr(node, Double("myDynamic"))
+ ...
+ >>> node["myDynamic"].channelBox
+ False
+ >>> with DagModifier() as mod:
+ ... mod.setChannelBox(node["myDynamic"])
+ ...
+ >>> node["myDynamic"].channelBox
+ True
+
+ """
+
+ if isinstance(plug, om.MPlug):
+ plug = Plug(Node(plug.node()), plug)
+
+ assert isinstance(plug, Plug), "%s was not a plug" % plug
+ self._channelBoxAttrs.append((plug, value))
+
def _doLockAttrs(self):
while self._lockAttrs:
plug, value = self._lockAttrs.pop(0)
@@ -5801,6 +6002,14 @@ def _doKeyableAttrs(self):
for el in elements:
cmds.setAttr(el.path(), keyable=value)
+ def _doChannelBoxAttrs(self):
+ while self._channelBoxAttrs:
+ plug, value = self._channelBoxAttrs.pop(0)
+ elements = plug if plug.isArray or plug.isCompound else [plug]
+
+ for el in elements:
+ cmds.setAttr(el.path(), channelBox=value)
+
def _doNiceNames(self):
while self._niceNames:
plug, value = self._niceNames.pop(0)
@@ -5978,7 +6187,7 @@ def addAttr(self, node, attr):
"""
- assert isinstance(node, Node), "%s was not a cmdx.Node"
+ assert isinstance(node, Node), "%s was not a cmdx.Node" % str(node)
if SAFE_MODE:
assert _isalive(node._mobject)
@@ -6535,6 +6744,7 @@ def disconnect(self, a, b=None, source=True, destination=True):
connect_attr = connectAttr
connect_attrs = connectAttrs
set_keyable = setKeyable
+ set_channel_box = setChannelBox
set_locked = setLocked
set_nice_name = setNiceName
@@ -6675,6 +6885,10 @@ class HashableTime(om.MTime):
def __hash__(self):
return hash(self.value)
+ if ENABLE_PEP8:
+ def as_units(self, unit):
+ return self.asUnits(unit)
+
# Convenience functions
def connect(a, b):
@@ -6690,7 +6904,25 @@ def currentTime(time=None):
if not isinstance(time, om.MTime):
time = om.MTime(time, TimeUiUnit())
- return oma.MAnimControl.setCurrentTime(time)
+ cmds.currentTime(time.value)
+
+ # For whatever reason, MAnimControl.setCurrentTime
+ # interferes with threading, cause of deadlocks. So
+ # we instead rely on the trusty old cmds.currentTime
+
+
+def currentFrame(frame=None):
+ """Set or return current time as an integer"""
+ if frame is None:
+ return int(oma.MAnimControl.currentTime().value)
+ else:
+ if isinstance(time, om.MTime):
+ frame = int(frame.value)
+
+ cmds.currentTime(frame)
+
+
+ticksPerSecond = om.MTime.ticksPerSecond
def selectedTime():
@@ -6749,6 +6981,14 @@ def maxTime(time=None):
return oma.MAnimControl.setMaxTime(time)
+def isScrubbing():
+ return oma.MAnimControl.isScrubbing()
+
+
+def isPlaying():
+ return oma.MAnimControl.isPlaying()
+
+
class DGContext(om.MDGContext):
"""Context for evaluating the Maya DG
@@ -6886,7 +7126,7 @@ def getAttr(attr, type=None, time=None):
return attr.read(time=time)
-def setAttr(attr, value, type=None):
+def setAttr(attr, value, type=None, undoable=False):
"""Write `value` to `attr`
Arguments:
@@ -6900,12 +7140,19 @@ def setAttr(attr, value, type=None):
"""
+ if type == "matrix":
+ value = Matrix4(value)
+
if isinstance(attr, str):
node, attr = attr.rsplit(".", 1)
node = encode(node)
attr = node[attr]
- attr.write(value)
+ if undoable:
+ with DGModifier() as mod:
+ mod.setAttr(attr, value)
+ else:
+ attr.write(value)
def addAttr(node,
@@ -7071,9 +7318,11 @@ def delete(*nodes):
with DagModifier(undoable=False) as mod:
for node in flattened:
if isinstance(node, str):
- node, node = node.rsplit(".", 1)
- node = encode(node)
- node = node[node]
+ try:
+ node, attr = node.rsplit(".", 1)
+ node = encode(node)
+ except ExistError:
+ continue
if not node.exists:
# May have been a child of something
@@ -7143,6 +7392,14 @@ def upAxis():
return Vector(0, 0, 1)
+if __maya_version__ >= 2019:
+ isYAxisUp = om.MGlobal.isYAxisUp
+ isZAxisUp = om.MGlobal.isZAxisUp
+
+ is_y_axis_up = om.MGlobal.isYAxisUp
+ is_z_axis_up = om.MGlobal.isZAxisUp
+
+
def setUpAxis(axis=Y):
"""Set the current up-axis as Y or Z
@@ -7174,13 +7431,18 @@ def setUpAxis(axis=Y):
connect_attr = connectAttr
obj_exists = objExists
current_time = currentTime
+ current_frame = currentFrame
min_time = minTime
max_time = maxTime
+ is_scrubbing = isScrubbing
+ is_playing = isPlaying
animation_start_time = animationStartTime
animation_end_time = animationEndTime
selected_time = selectedTime
up_axis = upAxis
set_up_axis = setUpAxis
+ ticks_per_second = ticksPerSecond
+
# Special-purpose functions
@@ -7253,14 +7515,27 @@ def curve(parent, points, degree=1, form=kOpen, mod=None):
"parent must be of type cmdx.DagNode"
)
+ if isinstance(points, tuple):
+ points = list(points)
+
degree = min(3, max(1, degree))
- knotcount = len(points) - degree + 2 * degree - 1
+ if degree > 1:
+ points.insert(0, points[0])
+ points.append(points[-1])
+
+ if degree > 2:
+ points.insert(0, points[0])
+ points.append(points[-1])
+
+ spans = len(points) - degree
+ knotcount = spans + 2 * degree - 1
cvs = [p for p in points]
knots = [i for i in range(knotcount)]
curveFn = om.MFnNurbsCurve()
+
mobj = curveFn.create(cvs,
knots,
degree,
@@ -7709,6 +7984,19 @@ def read(self, data):
return data.inputValue(self["mobject"]).asLong()
+class Integer(_AbstractAttribute):
+ Fn = om.MFnNumericAttribute
+ Type = om.MFnNumericData.kInt
+ Default = 0
+
+ def read(self, data):
+ return data.inputValue(self["mobject"]).asLong()
+
+
+# Alias
+Int = Integer
+
+
class Double(_AbstractAttribute):
Fn = om.MFnNumericAttribute
Type = om.MFnNumericData.kDouble
@@ -7997,11 +8285,11 @@ class Distance4(Compound):
# Support for multiple co-existing versions of apiundo.
-unique_command = "cmdx_%s_command" % __version__.replace(".", "_")
+unique_command = "cmdx_%s_command" % (__version__.replace(".", "_"))
# This module is both a Python module and Maya plug-in.
# Data is shared amongst the two through this "module"
-unique_shared = "cmdx_%s_shared" % __version__.replace(".", "_")
+unique_shared = "cmdx_%s_shared" % (__version__.replace(".", "_"))
if unique_shared not in sys.modules:
sys.modules[unique_shared] = types.ModuleType(unique_shared)
@@ -8092,7 +8380,9 @@ def install():
"""
- cmds.loadPlugin(__file__, quiet=True)
+ plugin_name = os.path.basename(__file__).rsplit(".", 1)[0]
+ if not cmds.pluginInfo(plugin_name, query=True, loaded=True):
+ cmds.loadPlugin(__file__, quiet=True)
self.installed = True
@@ -8104,7 +8394,8 @@ def uninstall():
# therefore cannot be unloaded until flushed.
clear()
- cmds.unloadPlugin(__file__)
+ name = os.path.basename(__file__)
+ cmds.unloadPlugin(name)
self.installed = False
@@ -8172,13 +8463,24 @@ def isUndoable(self):
def initializePlugin(plugin):
- om.MFnPlugin(plugin).registerCommand(
+ if hasattr(cmds, unique_command):
+ # E.g. another cmdx from another vendored version
+ initializePlugin.installedElsewhere = True
+ return
+
+ # Only supports major.minor (no patch)
+ version = ".".join(__version__.rsplit(".")[:2])
+
+ om.MFnPlugin(plugin, "cmdx", version).registerCommand(
unique_command,
_apiUndo
)
def uninitializePlugin(plugin):
+ if hasattr(initializePlugin, "installedElsewhere"):
+ return
+
om.MFnPlugin(plugin).deregisterCommand(unique_command)
diff --git a/run_tests.py b/run_tests.py
index 249edef..09c7624 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -5,6 +5,12 @@
import nose
import flaky.flaky_nose_plugin as flaky
+# For nose
+if sys.version_info[0] == 3:
+ import collections
+ collections.Callable = collections.abc.Callable
+
+
if __name__ == "__main__":
print("Initialising Maya..")
from maya import standalone, cmds