From 2a61a451a75676ac2b6a8c28b3a5fef7a5f35406 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Wed, 16 Jun 2021 10:09:15 +0100 Subject: [PATCH 01/10] Fool-proof plug-in load --- cmdx.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/cmdx.py b/cmdx.py index 4a91609..cccc475 100644 --- a/cmdx.py +++ b/cmdx.py @@ -8,6 +8,7 @@ import math import types import logging +import getpass import operator import traceback import collections @@ -4374,7 +4375,7 @@ def asQuaternion(self): def asMatrix(self): return Matrix4(super(EulerRotation, self).asMatrix()) - order = { + strToOrder = { 'xyz': kXYZ, 'xzy': kXZY, 'yxz': kYXZ, @@ -4383,6 +4384,15 @@ def asMatrix(self): 'zyx': kZYX } + orderToStr = { + kXYZ: 'xyz', + kXZY: 'xzy', + kYXZ: 'yxz', + kYZX: 'yzx', + kZXY: 'zxy', + kZYX: 'zyx' + } + if ENABLE_PEP8: as_quaternion = asQuaternion as_matrix = asMatrix @@ -7520,8 +7530,14 @@ class Distance4(Compound): # -------------------------------------------------------- -# E.g. ragdoll.vendor.cmdx => ragdoll_vendor_cmdx_plugin.py -unique_plugin = "cmdx_%s_plugin.py" % __version__.replace(".", "_") +# E.g. cmdx => cmdx_0_6_0_plugin_username0.py +unique_plugin = "cmdx_%s_plugin_%s.py" % ( + __version__.replace(".", "_"), + + # Include username, in case two + # users occupy the same machine + getpass.getuser() +) # Support for multiple co-existing versions of apiundo. unique_command = "cmdx_%s_command" % __version__.replace(".", "_") @@ -7612,18 +7628,51 @@ def install(): """ + import errno import shutil - import tempfile - tempdir = tempfile.gettempdir() + # E.g. c:\users\marcus\Documents\maya + tempdir = os.path.expanduser("~/maya/plug-ins") + + try: + print("Making %s" % tempdir) + os.makedirs(tempdir) + + except OSError as e: + if e.errno == errno.EEXIST: + # This is fine + pass + + else: + # Can't think of a reason why this would ever + # happen but you can never be too careful.. + log.debug("Could not create %s" % tempdir) + + import tempfile + tempdir = tempfile.gettempdir() + tempfname = os.path.join(tempdir, unique_plugin) - # We can't know whether we're a .pyc or .py file, - # but we need to copy the .py file *only* - fname = os.path.splitext(__file__)[0] + ".py" + if not os.path.exists(tempfname): + # We can't know whether we're a .pyc or .py file, + # but we need to copy the .py file *only* + fname = os.path.splitext(__file__)[0] - # Copy *and overwrite* - shutil.copy(fname, tempfname) + try: + shutil.copyfile(fname + ".py", tempfname) + + except OSError: + # This could never really happen, but you never know. + # In which case, use the file as-is. This should work + # for a majority of cases and only really conflict when/if + # the undo mechanism of cmdx changes, which is exceedingly + # rare. The actual functionality of cmdx is independent of + # this plug-in and will still pick up the appropriate + # vendored module. + log.debug("Could not generate unique cmdx.py") + log.debug("Undo may still work, but cmdx may conflict\n" + "with other instances of it.") + tempfname = __file__ # Now we're guaranteed to not interfere # with other versions of cmdx. Win! From aed63190cd599097173677a6c3337d2131f21284 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Wed, 16 Jun 2021 10:18:53 +0100 Subject: [PATCH 02/10] Increment version --- cmdx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdx.py b/cmdx.py index cccc475..a6cb3e2 100644 --- a/cmdx.py +++ b/cmdx.py @@ -19,7 +19,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.0" +__version__ = "0.6.1" PY3 = sys.version_info[0] == 3 From 1b8195c7054057bb43c20b51e1ba091b2a826ed9 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Thu, 17 Jun 2021 16:09:46 +0100 Subject: [PATCH 03/10] Add strict=True to cmdx.exists --- cmdx.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cmdx.py b/cmdx.py index a6cb3e2..db635be 100644 --- a/cmdx.py +++ b/cmdx.py @@ -4932,15 +4932,24 @@ def _python_to_mod(value, plug, mod): return True -def exists(path): - """Return whether any node at `path` exists""" +def exists(path, strict=True): + """Return whether any node at `path` exists - selectionList = om.MSelectionList() + Arguments: + path (str): Full or partial path to node + strict (bool, optional): Error if the path isn't a full match, + including namespace. + + """ try: - selectionList.add(path) + node = encode(path) except RuntimeError: return False + + if strict: + return node.path(namespace=True) == path + return True @@ -7635,7 +7644,6 @@ def install(): tempdir = os.path.expanduser("~/maya/plug-ins") try: - print("Making %s" % tempdir) os.makedirs(tempdir) except OSError as e: From 0b60e9c112bb438776220c32f3e052cb2cf7c243 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Fri, 18 Jun 2021 08:45:35 +0100 Subject: [PATCH 04/10] Handle attribute elements, this was a bug! --- cmdx.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/cmdx.py b/cmdx.py index db635be..96f8ed1 100644 --- a/cmdx.py +++ b/cmdx.py @@ -2652,13 +2652,13 @@ def __getitem__(self, logicalIndex): >>> plug[1] << tm["parentMatrix"][0] >>> plug[2] << tm["worldMatrix"][0] - >>> plug[2].connection(plug=True) == tm["worldMatrix"] + >>> plug[2].connection(plug=True) == tm["worldMatrix"][0] True # Notice how index 2 remains index 2 even on disconnect # The physical index moves to 1. >>> plug[1].disconnect() - >>> plug[2].connection(plug=True) == tm["worldMatrix"] + >>> plug[2].connection(plug=True) == tm["worldMatrix"][0] True """ @@ -3804,6 +3804,12 @@ def connections(self, True >>> a["ihi"] 2 + >>> b["arrayAttr"] = Long(array=True) + >>> b["arrayAttr"][0] >> a["ihi"] + >>> a["ihi"].connection() == b + True + >>> a["ihi"].connection(plug=True) == b["arrayAttr"][0] + True """ @@ -3820,7 +3826,27 @@ def connections(self, # sometimes, we have to convert them before using them. # https://forums.autodesk.com/t5/maya-programming/maya-api-what-is-a-networked-plug-and-do-i-want-it-or-not/td-p/7182472 if plug.isNetworked: - plug = node.findPlug(plug.partialName()) + + if plug.isElement: + # Name would be e.g. 'myArrayAttr[0]' + # raise TypeError(plug.partialName() + "\n") + name = plug.partialName() + + if name.endswith("]"): + name, index = name.rsplit("[", 1) + index = int(index.rstrip("]")) + + else: + # E.g. worldMatrix[0] -> wm + index = 0 + + plug = node.findPlug(name) + + # The index returned is *logical*, not physical. + plug = plug.elementByLogicalIndex(index) + + else: + plug = node.findPlug(plug.partialName()) yield Plug(node, plug, unit) else: @@ -4948,7 +4974,7 @@ def exists(path, strict=True): return False if strict: - return node.path(namespace=True) == path + return node.path() == path return True From 3dc9d75dd7b4234b4f9531f905e7647065f68b8f Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Thu, 24 Jun 2021 10:16:41 +0100 Subject: [PATCH 05/10] Some Black formatter issues --- cmdx.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cmdx.py b/cmdx.py index 96f8ed1..983f323 100644 --- a/cmdx.py +++ b/cmdx.py @@ -268,16 +268,16 @@ def add_metaclass(metaclass): def wrapper(cls): orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') + slots = orig_vars.get("__slots__") if slots is not None: if isinstance(slots, str): slots = [slots] for slots_var in slots: orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - if hasattr(cls, '__qualname__'): - orig_vars['__qualname__'] = cls.__qualname__ + orig_vars.pop("__dict__", None) + orig_vars.pop("__weakref__", None) + if hasattr(cls, "__qualname__"): + orig_vars["__qualname__"] = cls.__qualname__ return metaclass(cls.__name__, cls.__bases__, orig_vars) return wrapper @@ -2843,7 +2843,7 @@ def isArray(self): @property def arrayIndices(self): if not self._mplug.isArray: - raise TypeError('{} is not an array'.format(self.path())) + raise TypeError("{} is not an array".format(self.path())) # Convert from `p_OpenMaya_py2.rItemNot3Strs` to list return list(self._mplug.getExistingArrayAttributeIndices()) @@ -3806,10 +3806,15 @@ def connections(self, 2 >>> b["arrayAttr"] = Long(array=True) >>> b["arrayAttr"][0] >> a["ihi"] + >>> b["arrayAttr"][1] >> a["visibility"] >>> a["ihi"].connection() == b True >>> a["ihi"].connection(plug=True) == b["arrayAttr"][0] True + >>> a["visibility"].connection(plug=True) == b["arrayAttr"][1] + True + >>> b["arrayAttr"][1].connection(plug=True) == a["visibility"] + True """ From a503eeaacb1416d44415a6b3cf1e1f882e3cb24e Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Thu, 24 Jun 2021 10:17:05 +0100 Subject: [PATCH 06/10] Remove impossible method An attribute can only ever have a single input, plural makes no sense. --- cmdx.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cmdx.py b/cmdx.py index 983f323..566c892 100644 --- a/cmdx.py +++ b/cmdx.py @@ -1290,19 +1290,6 @@ def connection(self, destination=destination, connections=connection), None) - def inputs(self, - type=None, - unit=None, - plugs=False, - connections=False): - """Return input connections from :func:`connections()`""" - return self.connections(type=type, - unit=unit, - plugs=plugs, - source=True, - destination=False, - connections=connections) - def input(self, type=None, unit=None, From 3f71d70ce77be351852f57c81c14877b84470eef Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Mon, 5 Jul 2021 07:52:09 +0100 Subject: [PATCH 07/10] Return cmdx type for rotation --- cmdx.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmdx.py b/cmdx.py index 566c892..d041a45 100644 --- a/cmdx.py +++ b/cmdx.py @@ -4097,7 +4097,8 @@ def setScale(self, seq, space=None): return super(TransformationMatrix, self).setScale(seq, space) def rotation(self, asQuaternion=False): - return super(TransformationMatrix, self).rotation(asQuaternion) + rotation = super(TransformationMatrix, self).rotation(True) + return Quaternion(rotation) if asQuaternion else Euler(rotation) def setRotation(self, rot): """Interpret three values as an euler rotation""" @@ -6369,7 +6370,8 @@ def ls(*args, **kwargs): def selection(*args, **kwargs): - return list(map(encode, cmds.ls(*args, selection=True, **kwargs))) + kwargs["selection"] = True + return ls(*args, **kwargs) def createNode(type, name=None, parent=None): From 0cb789281430ddb8d4b1c192105224b1e9f4de58 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Mon, 5 Jul 2021 07:52:12 +0100 Subject: [PATCH 08/10] Revert "Remove impossible method" This reverts commit a503eeaacb1416d44415a6b3cf1e1f882e3cb24e. --- cmdx.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cmdx.py b/cmdx.py index d041a45..50f9318 100644 --- a/cmdx.py +++ b/cmdx.py @@ -1290,6 +1290,19 @@ def connection(self, destination=destination, connections=connection), None) + def inputs(self, + type=None, + unit=None, + plugs=False, + connections=False): + """Return input connections from :func:`connections()`""" + return self.connections(type=type, + unit=unit, + plugs=plugs, + source=True, + destination=False, + connections=connections) + def input(self, type=None, unit=None, From 62fa229b038ee55b146798d3f6ab36e4aaa096ba Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Mon, 5 Jul 2021 07:57:53 +0100 Subject: [PATCH 09/10] Fix typo --- cmdx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdx.py b/cmdx.py index 50f9318..bf9116f 100644 --- a/cmdx.py +++ b/cmdx.py @@ -4110,7 +4110,7 @@ def setScale(self, seq, space=None): return super(TransformationMatrix, self).setScale(seq, space) def rotation(self, asQuaternion=False): - rotation = super(TransformationMatrix, self).rotation(True) + rotation = super(TransformationMatrix, self).rotation(asQuaternion) return Quaternion(rotation) if asQuaternion else Euler(rotation) def setRotation(self, rot): From 984156bb59cffd988e92fd51adef812e2e9333a0 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Sat, 4 Sep 2021 11:02:31 +0100 Subject: [PATCH 10/10] Minor tweaks, see below. - Return native Quaternion types for asQuaternion() and inverse() - Make Divider attributes non-keyable per default - Append to existing animation cuves if they exist - Add minTime(), maxTime(), animationStartTime(), animationEndTime() - Add Vector.isEquivalent - Add Quaternion.asEulerRotation - Add Quaternion.inverse - Add TransformationMatrix.setRotatePivot - Print rotation of TransformationMatrix in degrees rather than radians --- cmdx.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 12 deletions(-) diff --git a/cmdx.py b/cmdx.py index bf9116f..8db3ca6 100644 --- a/cmdx.py +++ b/cmdx.py @@ -3063,7 +3063,7 @@ def asEulerRotation(self, order=kXYZ, time=None): def asQuaternion(self, time=None): value = self.read(time=time) value = Euler(value).asQuaternion() - return value + return Quaternion(value) def asVector(self, time=None): assert self.isArray or self.isCompound, "'%s' not an array" % self @@ -3710,9 +3710,14 @@ def animate(self, values, interpolation=None): """ times, values = list(map(UiUnit(), values.keys())), values.values() - anim = createNode(_find_curve_type(self)) + typ = _find_curve_type(self) + anim = self.input(type=typ) + + if not anim: + anim = createNode(typ) + anim["output"] >> self + anim.keys(times, values, interpolation=Linear) - anim["output"] >> self def write(self, value): if isinstance(value, dict) and __maya_version__ > 2015: @@ -3962,7 +3967,8 @@ def __repr__(self): ")" ).format( t=self.translation(), - r=self.rotation(), + r="(%.2f, %.2f, %.2f)" % tuple( + degrees(v) for v in self.rotation()), s=self.scale(), rp=self.rotatePivot(), rpt=self.rotatePivotTranslation(), @@ -4059,6 +4065,11 @@ def rotatePivot(self, space=None): space = space or sTransform return Vector(super(TransformationMatrix, self).rotatePivot(space)) + def setRotatePivot(self, pivot, space=sTransform, balance=False): + pivot = pivot if isinstance(pivot, om.MPoint) else om.MPoint(pivot) + return super(TransformationMatrix, self).setRotatePivot( + pivot, space, balance) + def rotatePivotTranslation(self, space=None): """This method does not typically support optional arguments""" space = space or sTransform @@ -4286,6 +4297,12 @@ def dot(self, value): def cross(self, value): return Vector(super(Vector, self).__xor__(value)) + def isEquivalent(self, other, tolerance=om.MVector.kTolerance): + return super(Vector, self).isEquivalent(other, tolerance) + + if ENABLE_PEP8: + is_equivalent = isEquivalent + # Alias, it can't take anything other than values # and yet it isn't explicit in its name. @@ -4346,7 +4363,7 @@ def __mul__(self, other): return Vector(other.rotateBy(self)) else: - return super(Quaternion, self).__mul__(other) + return Quaternion(super(Quaternion, self).__mul__(other)) def lengthSquared(self): return ( @@ -4362,6 +4379,12 @@ def length(self): def isNormalised(self, tol=0.0001): return abs(self.length() - 1.0) < tol + def asEulerRotation(self): + return Euler(super(Quaternion, self).asEulerRotation()) + + def inverse(self): + return Quaternion(super(Quaternion, self).inverse()) + def asMatrix(self): return Matrix4(super(Quaternion, self).asMatrix()) @@ -4369,6 +4392,8 @@ def asMatrix(self): as_matrix = asMatrix is_normalised = isNormalised length_squared = lengthSquared + as_euler_rotation = asEulerRotation + as_euler = asEulerRotation # Alias @@ -4407,6 +4432,12 @@ def asQuaternion(self): def asMatrix(self): return Matrix4(super(EulerRotation, self).asMatrix()) + def isEquivalent(self, other, tolerance=om.MEulerRotation.kTolerance): + return super(EulerRotation, self).isEquivalent(other, tolerance) + + if ENABLE_PEP8: + is_equivalent = isEquivalent + strToOrder = { 'xyz': kXYZ, 'xzy': kXZY, @@ -4879,17 +4910,19 @@ def _python_to_mod(value, plug, mod): if isinstance(value, dict) and __maya_version__ > 2015: times, values = map(UiUnit(), value.keys()), value.values() curve_typ = _find_curve_type(plug) + anim = plug.input(type=curve_typ) - if isinstance(mod, DGModifier): - anim = mod.createNode(curve_typ) + if not anim: + if isinstance(mod, DGModifier): + anim = mod.createNode(curve_typ) + else: + # The DagModifier can't create DG nodes + with DGModifier() as dgmod: + anim = dgmod.createNode(curve_typ) - else: - # The DagModifier can't create DG nodes - with DGModifier() as dgmod: - anim = dgmod.createNode(curve_typ) + mod.connect(anim["output"]._mplug, plug._mplug) anim.keys(times, values) - mod.connect(anim["output"]._mplug, plug._mplug) return True @@ -4967,11 +5000,32 @@ def _python_to_mod(value, plug, mod): def exists(path, strict=True): """Return whether any node at `path` exists + This will return True for nodes that are *about to* be + created via a DG or Dag modifier. + Arguments: path (str): Full or partial path to node strict (bool, optional): Error if the path isn't a full match, including namespace. + Examples: + >>> _new() + >>> exists("uniqueName", strict=False) + False + >>> _ = createNode("transform", name="uniqueName") + >>> exists("uniqueName", strict=False) + True + + # Note that even to-be-created nodes also register as existing + >>> exists("uniqueName2", strict=False) + False + >>> with DagModifier() as mod: + ... _ = mod.createNode("transform", name="uniqueName2") + ... assert exists("uniqueName2", strict=False) + ... + >>> exists("uniqueName2", strict=False) + True + """ try: @@ -6327,6 +6381,34 @@ def currentTime(time=None): return oma.MAnimControl.setCurrentTime(time) +def animationStartTime(time=None): + if time is None: + return oma.MAnimControl.animationStartTime() + else: + return oma.MAnimControl.setAnimationStartTime(time) + + +def animationEndTime(time=None): + if time is None: + return oma.MAnimControl.animationEndTime() + else: + return oma.MAnimControl.setAnimationEndTime(time) + + +def minTime(time=None): + if time is None: + return oma.MAnimControl.minTime() + else: + return oma.MAnimControl.setMinTime(time) + + +def maxTime(time=None): + if time is None: + return oma.MAnimControl.maxTime() + else: + return oma.MAnimControl.setMaxTime(time) + + class DGContext(om.MDGContext): """Context for evaluating the Maya DG @@ -6752,6 +6834,10 @@ def setUpAxis(axis=Y): connect_attr = connectAttr obj_exists = objExists current_time = currentTime + min_time = minTime + max_time = maxTime + animation_start_time = animationStartTime + animation_end_time = animationEndTime up_axis = upAxis set_up_axis = setUpAxis @@ -7225,6 +7311,9 @@ def read(self, data): class Divider(Enum): """Visual divider in channel box""" + ChannelBox = True + Keyable = False + def __init__(self, label, **kwargs): kwargs.pop("name", None) kwargs.pop("fields", None)