From 92f897288bae621e2fbc5a3e8b2a05069f35322b Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Sun, 12 Sep 2021 18:29:17 +0100 Subject: [PATCH 1/9] Implement undoable animation changes --- cmdx.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/cmdx.py b/cmdx.py index d7fa12a..6c784ea 100644 --- a/cmdx.py +++ b/cmdx.py @@ -2295,14 +2295,26 @@ def key(self, time, value, interpolation=Linear): else: self._fna.addKey(time, value, interpolation, interpolation) - def keys(self, times, values, interpolation=Linear): + def keys(self, times, values, interpolation=Linear, change=None): times = list(map( lambda t: Seconds(t) if isinstance(t, (float, int)) else t, times )) try: - self._fna.addKeys(times, values) + keepExistingKeys = False # Default + args = [ + times, + values, + oma.MFnAnimCurve.kTangentGlobal, + oma.MFnAnimCurve.kTangentGlobal, + keepExistingKeys, + ] + + if change is not None: + args += [change] + + self._fna.addKeys(*args) except RuntimeError: # The error provided by Maya aren't very descriptive, @@ -4468,6 +4480,15 @@ def isEquivalent(self, other, tolerance=om.MEulerRotation.kTolerance): kZYX: 'zyx' } + enumToOrder = [ + kXYZ, + kXZY, + kYXZ, + kYZX, + kZXY, + kZYX + ] + if ENABLE_PEP8: as_quaternion = asQuaternion as_matrix = asMatrix @@ -4922,21 +4943,22 @@ 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) + curve = plug.input(type=curve_typ) - if not anim: + if not curve: if isinstance(mod, DGModifier): - anim = mod.createNode(curve_typ) + curve = mod.createNode(curve_typ) else: # The DagModifier can't create DG nodes with DGModifier() as dgmod: - anim = dgmod.createNode(curve_typ) + curve = dgmod.createNode(curve_typ) - mod.connect(anim["output"]._mplug, plug._mplug) + mod.connect(curve["output"]._mplug, plug._mplug) - anim.keys(times, values) + change = oma.MAnimCurveChange() + curve.keys(times, values, change=change) - return True + return change mplug = plug._mplug @@ -5078,6 +5100,14 @@ def encode(path): # type: (str) -> Node return Node(mobj) +def find(path, default=None): + """Find node at `path` or return `default`""" + try: + return encode(path) + except ExistError: + return default + + def fromHash(code, default=None): """Get existing node from MObjectHandle.hashCode()""" try: @@ -5341,7 +5371,19 @@ def __exit__(self, exc_type, exc_value, tb): # Make our commit within the current undo chunk, # but *before* we call any maya.cmds as it may # otherwise confuse the chunk - commit(self._modifier.undoIt, self._modifier.doIt) + def undoit(): + for change in self._animChanges: + change.undoIt() + + self._modifier.undoIt() + + def redoit(): + self._modifier.doIt() + + for change in self._animChanges: + change.redoIt() + + commit(undoit, redoit) # These all involve calling on cmds, # which manages undo on its own. @@ -5381,6 +5423,7 @@ def __init__(self, self._lockAttrs = [] self._keyableAttrs = [] self._niceNames = [] + self._animChanges = [] # Undo self._doneLockAttrs = [] @@ -5772,7 +5815,10 @@ def setAttr(self, plug, value): if isinstance(value, om.MPlug): value = Plug(value.node(), value).read() - _python_to_mod(value, plug, self._modifier) + result = _python_to_mod(value, plug, self._modifier) + + if isinstance(result, oma.MAnimCurveChange): + self._animChanges += [result] if SAFE_MODE: self._modifier.doIt() From 11e1b2c5b35627012e54f9020238d15385ada7cf Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Tue, 16 Nov 2021 19:58:49 +0000 Subject: [PATCH 2/9] A few minor tweaks, see below. - Support for tangents and AnimCurve - AnimCurve.key(interpolation) was renmed tangents - Support for `plug.connection(plugs=("name1", "name2")` to match by name - Add Tm.setRotatePivot and setRotatePivotTranslate - Add Tm.setScalePivot and setScaleivotTranslate - More accurate cmdx.lookAt --- cmdx.py | 223 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 156 insertions(+), 67 deletions(-) diff --git a/cmdx.py b/cmdx.py index 6c784ea..9071711 100644 --- a/cmdx.py +++ b/cmdx.py @@ -124,7 +124,7 @@ First = 0 Last = -1 -# Animation curve interpolation, from MFnAnimCurve::TangentType +# Animation curve tangents, from MFnAnimCurve::TangentType Stepped = 5 Linear = 2 Smooth = 4 @@ -839,6 +839,7 @@ def isA(self, type): True """ + if isinstance(type, om.MTypeId): return type == self._fn.typeId elif isinstance(type, string_types): @@ -847,7 +848,9 @@ def isA(self, type): return any(self.isA(t) for t in type) elif isinstance(type, int): return self._mobject.hasFn(type) + cmds.warning("Unsupported argument passed to isA('%s')" % type) + return False def lock(self, value=True): @@ -1271,6 +1274,7 @@ def connections(self, plugs=plugs, source=source, destination=destination): + if connections: yield connection, plug if plugs else self else: @@ -2284,7 +2288,7 @@ def __init__(self, mobj, exists=True): super(AnimCurve, self).__init__(mobj, exists) self._fna = oma.MFnAnimCurve(mobj) - def key(self, time, value, interpolation=Linear): + def key(self, time, value, tangents=Linear): if isinstance(time, (float, int)): time = Seconds(time) @@ -2293,21 +2297,24 @@ def key(self, time, value, interpolation=Linear): if index: self._fna.setValue(index, value) else: - self._fna.addKey(time, value, interpolation, interpolation) + self._fna.addKey(time, value, tangents, tangents) - def keys(self, times, values, interpolation=Linear, change=None): + def keys(self, times, values, tangents=None, change=None): times = list(map( lambda t: Seconds(t) if isinstance(t, (float, int)) else t, times )) + if tangents is None: + tangents = oma.MFnAnimCurve.kTangentGlobal + try: keepExistingKeys = False # Default args = [ times, values, - oma.MFnAnimCurve.kTangentGlobal, - oma.MFnAnimCurve.kTangentGlobal, + tangents, + tangents, keepExistingKeys, ] @@ -3714,7 +3721,7 @@ def read(self, unit=None, time=None): raise if __maya_version__ > 2015: - def animate(self, values, interpolation=None): + def animate(self, values, tangents=None): """Treat values as time:value pairs and animate this attribute Example: @@ -3741,7 +3748,7 @@ def animate(self, values, interpolation=None): anim = createNode(typ) anim["output"] >> self - anim.keys(times, values, interpolation=Linear) + anim.keys(times, values, tangents=Linear) def write(self, value): if isinstance(value, dict) and __maya_version__ > 2015: @@ -3847,6 +3854,21 @@ def connections(self, """ + def _name(plug): + return plug.partialName( + False, # includeNodeName + False, # includeNonMandatoryIndices + False, # includeInstancedIndices + False, # useAlias + False, # useFullAttributePath + True # useLongNames + ) + + plug_names = [] + if isinstance(plugs, (list, tuple)): + plug_names = plugs + plugs = True + for plug in self._mplug.connectedTo(source, destination): mobject = plug.node() node = Node(mobject) @@ -3882,6 +3904,9 @@ def connections(self, else: plug = node.findPlug(plug.partialName()) + if plug_names and _name(plug) not in plug_names: + continue + yield Plug(node, plug, unit) else: yield node @@ -3893,6 +3918,10 @@ def connection(self, plug=False, unit=None): """Return first connection from :func:`connections()`""" + + if isinstance(plug, string_types): + plug = (plug,) + return next(self.connections(type=type, source=source, destination=destination, @@ -3904,11 +3933,11 @@ def input(self, plug=False, unit=None): """Return input connection from :func:`connections()`""" - return next(self.connections(type=type, - source=True, - destination=False, - plugs=plug, - unit=unit), None) + return self.connection(type=type, + source=True, + destination=False, + plug=plug, + unit=unit) def outputs(self, type=None, @@ -3926,11 +3955,11 @@ def output(self, plug=False, unit=None): """Return first output connection from :func:`connections()`""" - return next(self.connections(type=type, - source=False, - destination=True, - plugs=plug, - unit=unit), None) + return self.connection(type=type, + source=False, + destination=True, + plug=plug, + unit=unit) def source(self, unit=None): cls = self.__class__ @@ -4089,30 +4118,42 @@ 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): + def rotatePivotTranslation(self, space=sTransform): """This method does not typically support optional arguments""" - space = space or sTransform return Vector( super(TransformationMatrix, self).rotatePivotTranslation(space) ) - def scalePivot(self, space=None): + def scalePivot(self, space=sTransform): """This method does not typically support optional arguments""" - space = space or sTransform return Vector(super(TransformationMatrix, self).scalePivot(space)) - def scalePivotTranslation(self, space=None): + def scalePivotTranslation(self, space=sTransform): """This method does not typically support optional arguments""" - space = space or sTransform return Vector( super(TransformationMatrix, self).scalePivotTranslation(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 setRotatePivotTranslation(self, pivot, space=sTransform): + pivot = pivot if isinstance(pivot, om.MVector) else om.MVector(pivot) + return super(TransformationMatrix, self).setRotatePivotTranslation( + pivot, space) + + def setScalePivot(self, pivot, space=sTransform, balance=False): + pivot = pivot if isinstance(pivot, om.MPoint) else om.MPoint(pivot) + return super(TransformationMatrix, self).setScalePivot( + pivot, space, balance) + + def setScalePivotTranslation(self, pivot, space=sTransform): + pivot = pivot if isinstance(pivot, om.MVector) else om.MVector(pivot) + return super(TransformationMatrix, self).setScalePivotTranslation( + pivot, space) + def translation(self, space=None): # type: (om.MSpace) -> om.MVector """This method does not typically support optional arguments""" space = space or sTransform @@ -4940,6 +4981,8 @@ def _python_to_mod(value, plug, mod): """ + assert isinstance(plug, Plug), "plug must be of type cmdx.Plug" + if isinstance(value, dict) and __maya_version__ > 2015: times, values = map(UiUnit(), value.keys()), value.values() curve_typ = _find_curve_type(plug) @@ -4955,8 +4998,16 @@ def _python_to_mod(value, plug, mod): mod.connect(curve["output"]._mplug, plug._mplug) + tangents = None + + # Unit can also be Time and other unrelated types + if plug._unit in (Stepped, Linear, Smooth): + tangents = plug._unit + change = oma.MAnimCurveChange() - curve.keys(times, values, change=change) + curve.keys(times, values, + tangents=tangents, + change=change) return change @@ -5670,10 +5721,17 @@ def deleteNode(self, node): """ + mobj = node + if isinstance(node, Node): + mobj = node._mobject + + assert isinstance(mobj, om.MObject), ( + "%s was not an MObject" % node + ) + # This is one picky s-o-b, let's not give it the # satisfaction of ever erroring out on us. Performance # is of less importance here, as deletion is not time-cricital - mobj = node._mobject if not _isalive(mobj): raise ExistError @@ -5685,10 +5743,13 @@ def deleteNode(self, node): @record_history def renameNode(self, node, name): + if isinstance(node, Node): + node = node._mobject + if SAFE_MODE: - assert _isalive(node._mobject) + assert _isalive(node) - return self._modifier.renameNode(node._mobject, name) + return self._modifier.renameNode(node, name) @record_history def addAttr(self, node, attr): @@ -6268,6 +6329,7 @@ def disconnect(self, a, b=None, source=True, destination=True): rename = renameNode if ENABLE_PEP8: + commit = doIt do_it = doIt undo_it = undoIt create_node = createNode @@ -6280,6 +6342,7 @@ def disconnect(self, a, b=None, source=True, destination=True): smart_set_attr = smartSetAttr delete_attr = deleteAttr reset_attr = resetAttr + lock_attr = setLocked try_connect = tryConnect connect_attr = connectAttr connect_attrs = connectAttrs @@ -6436,13 +6499,35 @@ def currentTime(time=None): if time is None: return oma.MAnimControl.currentTime() else: + if not isinstance(time, om.MTime): + time = om.MTime(time, TimeUiUnit()) + return oma.MAnimControl.setCurrentTime(time) +def selectedTime(): + """Return currently selected time range in MTime format""" + from maya import mel + + try: + control = mel.eval('$tmpVar=$gPlayBackSlider') + time_range = cmds.timeControl(control, q=True, rangeArray=True) + time_range = list(om.MTime(t, TimeUiUnit()) for t in time_range) + + except RuntimeError: + # Only relevant in interactive mode + return (minTime(), minTime() + 1) + + return time_range + + def animationStartTime(time=None): if time is None: return oma.MAnimControl.animationStartTime() else: + if not isinstance(time, om.MTime): + time = om.MTime(time, TimeUiUnit()) + return oma.MAnimControl.setAnimationStartTime(time) @@ -6450,6 +6535,9 @@ def animationEndTime(time=None): if time is None: return oma.MAnimControl.animationEndTime() else: + if not isinstance(time, om.MTime): + time = om.MTime(time, TimeUiUnit()) + return oma.MAnimControl.setAnimationEndTime(time) @@ -6457,6 +6545,9 @@ def minTime(time=None): if time is None: return oma.MAnimControl.minTime() else: + if not isinstance(time, om.MTime): + time = om.MTime(time, TimeUiUnit()) + return oma.MAnimControl.setMinTime(time) @@ -6464,6 +6555,9 @@ def maxTime(time=None): if time is None: return oma.MAnimControl.maxTime() else: + if not isinstance(time, om.MTime): + time = om.MTime(time, TimeUiUnit()) + return oma.MAnimControl.setMaxTime(time) @@ -6896,6 +6990,7 @@ def setUpAxis(axis=Y): max_time = maxTime animation_start_time = animationStartTime animation_end_time = animationEndTime + selected_time = selectedTime up_axis = upAxis set_up_axis = setUpAxis @@ -6945,7 +7040,7 @@ def redo(): return encode(shapeFn.fullPathName()) -def curve(parent, points, degree=1, form=kOpen): +def curve(parent, points, degree=1, form=kOpen, mod=None): """Create a NURBS curve from a series of points Arguments: @@ -6953,6 +7048,7 @@ def curve(parent, points, degree=1, form=kOpen): points (list): One tuples per point, with 3 floats each degree (int, optional): Degree of curve, 1 is linear form (int, optional): Whether to close the curve or not + mod (DagModifier, optional): Use this for undo/redo Example: >>> parent = createNode("transform") @@ -6969,47 +7065,40 @@ def curve(parent, points, degree=1, form=kOpen): "parent must be of type cmdx.DagNode" ) - # Superimpose end knots - # startpoints = [points[0]] * (degree - 1) - # endpoints = [points[-1]] * (degree - 1) - # points = startpoints + list(points) + endpoints - degree = min(3, max(1, degree)) - cvs = om1.MPointArray() - knots = om1.MDoubleArray() - curveFn = om1.MFnNurbsCurve() - knotcount = len(points) - degree + 2 * degree - 1 - for point in points: - cvs.append(om1.MPoint(*point)) - - for index in range(knotcount): - knots.append(index) + cvs = [p for p in points] + knots = [i for i in range(knotcount)] + curveFn = om.MFnNurbsCurve() mobj = curveFn.create(cvs, knots, degree, form, False, True, - _encode1(parent.path())) + parent.object()) - mod = om1.MDagModifier() - mod.renameNode(mobj, parent.name(namespace=True) + "Shape") - mod.doIt() + if mod: + mod.rename(mobj, parent.name(namespace=True) + "Shape") - def undo(): - mod.deleteNode(mobj) + else: + mod = DagModifier() + mod.rename(mobj, parent.name(namespace=True) + "Shape") mod.doIt() - def redo(): - mod.undoIt() + def undo(): + mod.deleteNode(mobj) + mod.doIt() - commit(undo, redo) + def redo(): + mod.undoIt() - shapeFn = om1.MFnDagNode(mobj) + commit(undo, redo) + + shapeFn = om.MFnDagNode(mobj) return encode(shapeFn.fullPathName()) @@ -7018,12 +7107,12 @@ def lookAt(origin, center, up=None): See glm::glc::matrix_transform::lookAt for reference - + Z (up) - / - / + + Z (up) + / + / (origin) o------ + X (center) - \ - + Y + \ + + Y Arguments: origin (Vector): Starting position @@ -7058,10 +7147,10 @@ def lookAt(origin, center, up=None): z = x ^ y return MatrixType(( - x[0], x[1], x[2], 0, - y[0], y[1], y[2], 0, - z[0], z[1], z[2], 0, - 0, 0, 0, 0 + x[0], x[1], x[2], 1, + y[0], y[1], y[2], 1, + z[0], z[1], z[2], 1, + 0, 0, 0, 1 )) From d728bf388c452dcb735e4f8f3c6d6748a725e2a8 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Tue, 16 Nov 2021 19:59:48 +0000 Subject: [PATCH 3/9] Increment version --- cmdx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdx.py b/cmdx.py index 9071711..bdadd9e 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.1" +__version__ = "0.6.2" PY3 = sys.version_info[0] == 3 From 3e05aecec22e17100e1127c990ee3042bbab55c5 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Tue, 16 Nov 2021 20:31:16 +0000 Subject: [PATCH 4/9] Fix broken documentation build --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b082880..ee06ce2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,6 +50,7 @@ jobs: nose-exclude==0.5.0 \ coverage==5.5 \ flaky==3.7.0 \ + docutils<0.18 \ sphinx==1.8.5 \ sphinxcontrib-napoleon==0.7 From df0be2d19d8c5d89758f92203f0fa53582a3e293 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Tue, 16 Nov 2021 20:32:53 +0000 Subject: [PATCH 5/9] Update link to PyPI release --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ea495..d43be72 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ On average, `cmdx` is **140x faster** than [PyMEL](https://github.com/LumaPictur ##### Status -[![](https://img.shields.io/pypi/v/cmdx?color=steelblue&label=PyPI)](https://github.com/mottosso/cmdx/) [![](https://img.shields.io/pypi/pyversions/cmdx?color=steelblue)](https://pypi.org/project/cmdx) +[![](https://img.shields.io/pypi/v/cmdx?color=steelblue&label=PyPI)](https://pypi.org/project/cmdx/) [![](https://img.shields.io/pypi/pyversions/cmdx?color=steelblue)](https://pypi.org/project/cmdx) | Maya | Status |:----------|:---- From 88718b3f7a6d8b4c77e5dd86b2f8cf516e5ebe8f Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Wed, 17 Nov 2021 06:47:58 +0000 Subject: [PATCH 6/9] Fix broken pip --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee06ce2..4c830bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: nose-exclude==0.5.0 \ coverage==5.5 \ flaky==3.7.0 \ - docutils<0.18 \ + docutils==17.1 \ sphinx==1.8.5 \ sphinxcontrib-napoleon==0.7 From 2d1759dd580cc1ee110ec113783e96bd1a841411 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Wed, 17 Nov 2021 07:28:54 +0000 Subject: [PATCH 7/9] Fix broken pip --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c830bc..1e65c83 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: nose-exclude==0.5.0 \ coverage==5.5 \ flaky==3.7.0 \ - docutils==17.1 \ + docutils==0.17.1 \ sphinx==1.8.5 \ sphinxcontrib-napoleon==0.7 From a8603a0546f045ba968c1696da211c7ae39713c0 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Sat, 22 Jan 2022 09:55:14 +0000 Subject: [PATCH 8/9] - More friendly cmdx-as-plug-in mechanic - Add cmdx.time() and cmdx.frame() conversion functions, like math.degrees() and math.radians() - Support matching array-plugs by name - Repair cmdx.lookAt() --- README.md | 45 +++++++++++++++++++++ cmdx.py | 119 ++++++++++++++++++++---------------------------------- 2 files changed, 88 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index d43be72..396931e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,37 @@ $ pip install cmdx
+### Vendoring + +> Note: Advanced topic, you can skip this + +Unlike PyMEL and cmds, `cmdx` is designed to be distributed alongside your tool. That means multiple copies of `cmdx` can coincide within the same Maya/Python session. But because the way [Undo/Redo](#undo) is handled, the `cmdx.py` module is also loaded as a Maya command plug-in. + +You can either ignore this, things to look out for is errors during undo coming from another tool or global module directory, even though the command came from your tool. Alternatively, you can follow this recommendation. + +```bash +mytool/ + vendor/ + __init__.py + cmdx_mytool.py +``` + +From here, you can either `from .vendor import cmdx_mytool as cmdx` or you can put the following into the `__init__.py` of the `vendor/` package. + +```py +from . import cmdx_mytool as cmdx +``` + +This would then allow your users to call.. + +```py +from mytool.vendor import cmdx +``` + +..as though the module was called just `cmdx.py`. + +
+ ### What is novel? With [so many options](#comparison) for interacting with Maya, when or why should you choose `cmdx`? @@ -1093,6 +1124,20 @@ v = Vector(1, 2, 3) assert isinstance(q * v, Vector) ``` +##### Conversions + +Python's `math` library provides a few convenience functions for converting `math.degrees` to `math.radians`. `cmdx` extends this with `cmdx.time` and `cmdx.frame`. + +```py +radians = cmdx.radians(5) +degrees = cmdx.degrees(radians) +assert degrees = 5 + +time = cmdx.time(frame=10) +frame = cmdx.frame(time=time) +assert frame == 10 +``` + ##### Available types - `asDouble()` -> `float` diff --git a/cmdx.py b/cmdx.py index bdadd9e..e2df926 100644 --- a/cmdx.py +++ b/cmdx.py @@ -4,11 +4,10 @@ import os import sys import json -import time +import time as time_ import math import types import logging -import getpass import operator import traceback import collections @@ -19,7 +18,9 @@ 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__ PY3 = sys.version_info[0] == 3 @@ -169,12 +170,12 @@ def timings_decorator(func): @wraps(func) def func_wrapper(*args, **kwargs): - t0 = time.clock() + t0 = time_.clock() try: return func(*args, **kwargs) finally: - t1 = time.clock() + t1 = time_.clock() duration = (t1 - t0) * 10 ** 6 # microseconds Stats.LastTiming = duration @@ -2933,7 +2934,7 @@ def nextAvailableIndex(self, startIndex=0): return 0 def pull(self): - """Pull on a plug, without seriasing any value. For performance""" + """Pull on a plug, without serialising any value. For performance.""" self._mplug.asMObject() def append(self, value, autofill=False): @@ -3862,11 +3863,16 @@ def _name(plug): False, # useAlias False, # useFullAttributePath True # useLongNames - ) + ).split("[", 1)[0] + # Support search of exact plug names plug_names = [] if isinstance(plugs, (list, tuple)): plug_names = plugs + + # Convert `plugName[2]` to `plugName` + plug_names = [name.split("[", 1)[0] for name in plug_names] + plugs = True for plug in self._mplug.connectedTo(source, destination): @@ -5242,6 +5248,16 @@ def asHex(mobj): pi = math.pi +def time(frame): + assert isinstance(frame, int), "%s was not an int" % frame + return om.MTime(frame, TimeUiUnit()) + + +def frame(time): + assert isinstance(time, om.MTime), "%s was not an om.MTime" % time + return int(time.value) + + def meters(cm): """Centimeters (Maya's default unit) to Meters @@ -6497,7 +6513,7 @@ def connect(a, b): def currentTime(time=None): """Set or return current time in MTime format""" if time is None: - return oma.MAnimControl.currentTime() + return HashableTime(oma.MAnimControl.currentTime()) else: if not isinstance(time, om.MTime): time = om.MTime(time, TimeUiUnit()) @@ -7142,14 +7158,14 @@ def lookAt(origin, center, up=None): up = up or Vector(0, 1, 0) - x = (center - origin).normalize() - y = ((center - origin) ^ (center - up)).normalize() + x = (center - origin).normal() + y = ((center - origin) ^ (center - up)).normal() z = x ^ y return MatrixType(( - x[0], x[1], x[2], 1, - y[0], y[1], y[2], 1, - z[0], z[1], z[2], 1, + x[0], x[1], x[2], 0, + y[0], y[1], y[2], 0, + z[0], z[1], z[2], 0, 0, 0, 0, 1 )) @@ -7808,15 +7824,6 @@ class Distance4(Compound): # -------------------------------------------------------- -# 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(".", "_") @@ -7877,13 +7884,8 @@ def install(): Inception time! :) In order to facilitate undo, we need a custom command registered - with Maya's native plug-in system. To do that, we need a dedicated - file. We *could* register ourselves as that file, but what we need - is a unique instance of said command per distribution of cmdx. - - Per distribution? Yes, because cmdx.py can be vendored with any - library, and we don't want cmdx.py from one vendor to interfere - with one from another. + with Maya's native plug-in system. To do that, we will register + ourselves as a Maya command plug-in. Maya uses (pollutes) global memory in two ways that matter to us here. @@ -7904,56 +7906,21 @@ def install(): *is* no __name__. Instead, we'll rely on each version being unique and consistent. - """ - - import errno - import shutil - - # E.g. c:\users\marcus\Documents\maya - tempdir = os.path.expanduser("~/maya/plug-ins") + Vendoring + --------- + If you vendored cmdx, you'll need to take into account that Maya + plug-ins are registered by *name*. And there can only ever be a + single plug-in with a given name. - try: - os.makedirs(tempdir) + So rename your `cmdx.py` to something like `cmdx_mytool.py` and from + your `vendor` package use e.g. `from . import cmdx_mytool as cmdx` - except OSError as e: - if e.errno == errno.EEXIST: - # This is fine - pass + This will enable you to `from vendor import cmdx` whilst at the same + time allowing Maya to use the unique name for plug-in purposes. Win-win. - 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) - - 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] + """ - 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! - cmds.loadPlugin(tempfname, quiet=True) + cmds.loadPlugin(__file__, quiet=True) self.installed = True @@ -7965,7 +7932,7 @@ def uninstall(): # therefore cannot be unloaded until flushed. clear() - cmds.unloadPlugin(unique_plugin) + cmds.unloadPlugin(__file__) self.installed = False From b8906c278f2c37bc96cf81e8b883fe4a78abeccb Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Sat, 22 Jan 2022 10:18:52 +0000 Subject: [PATCH 9/9] Correct version increment --- cmdx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdx.py b/cmdx.py index e2df926..26c55f4 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.3" +__version__ = "0.6.2" IS_VENDORED = "." in __name__