From 574d0affacaea94b6a929de75150436152afb9b9 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Sat, 17 Apr 2021 10:49:41 +0100 Subject: [PATCH 1/6] Implement clone And fix a few minor issues too --- cmdx.py | 355 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 314 insertions(+), 41 deletions(-) diff --git a/cmdx.py b/cmdx.py index 0750e88..eaf725e 100644 --- a/cmdx.py +++ b/cmdx.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import re import os import sys import json @@ -216,6 +217,29 @@ def _isalive(mobj): return True +def _camel_to_title(text): + """Convert camelCase `text` to Title Case + + Example: + >>> _camel_to_title("mixedCase") + 'Mixed Case' + >>> _camel_to_title("myName") + 'My Name' + >>> _camel_to_title("you") + 'You' + >>> _camel_to_title("You") + 'You' + >>> _camel_to_title("This is That") + 'This Is That' + + """ + + return re.sub( + r"((?<=[a-z])[A-Z]|(?>> parent = createNode("transform") + >>> cam = createNode("camera", parent=parent) + >>> original = cam["focalLength"] + >>> clone = original.clone("focalClone") + + # Original setup is preserved + >>> clone["min"] + 2.5 + >>> clone["max"] + 100000.0 + + >>> cam.addAttr(clone) + >>> cam["focalClone"].read() + 35.0 + + # Also works with modifiers + >>> with DagModifier() as mod: + ... clone = cam["fStop"].clone("fClone") + ... _ = mod.addAttr(cam, clone) + ... + >>> cam["fClone"].read() + 5.6 + + """ + + assert isinstance(self, Plug) + + if self.isArray: + raise TypeError("Array plugs are unsupported") + + if self.isCompound: + raise TypeError("Compound plugs are unsupported") + + niceName = niceName or self.niceName + + # There is no way to tell whether the niceName of + # a plug is automatically generated by Maya or if + # it is overridden by the user (if there is, please let me know!) + # So we'll wing it, and assume the user has not given + # the same nice name as would have been generated from + # the long name. + if niceName == _camel_to_title(name): + niceName = None + + cls = self.typeClass() + fn = self.fn() + attr = cls( + name, + default=self.default, + label=niceName, + shortName=shortName, + writable=fn.writable, + readable=fn.readable, + cached=fn.cached, + storable=fn.storable, + keyable=fn.keyable, + hidden=fn.hidden, + channelBox=fn.channelBox, + affectsAppearance=fn.affectsAppearance, + affectsWorldSpace=fn.affectsWorldSpace, + array=fn.array, + indexMatters=fn.indexMatters, + connectable=fn.connectable, + disconnectBehavior=fn.disconnectBehavior, + ) + + if hasattr(fn, "getMin") and fn.hasMin(): + attr["min"] = fn.getMin() + + if hasattr(fn, "getMax") and fn.hasMax(): + attr["max"] = fn.getMax() + + return attr + def plug(self): """Return the MPlug of this cmdx.Plug""" return self._mplug @@ -2876,6 +3009,7 @@ def asEulerRotation(self, order=kXYZ, time=None): def asQuaternion(self, time=None): value = self.read(time=time) value = Euler(value).asQuaternion() + return value def asVector(self, time=None): assert self.isArray or self.isCompound, "'%s' not an array" % self @@ -3164,6 +3298,35 @@ def default(self): """Return default value of plug""" return _plug_to_default(self._mplug) + def fn(self): + """Return the correct function set for the plug attribute MObject""" + + attr = self._mplug.attribute() + typ = attr.apiType() + + if typ == om.MFn.kNumericAttribute: + fn = om.MFnNumericAttribute(attr) + + elif typ == om.MFn.kUnitAttribute: + fn = om.MFnUnitAttribute(attr) + + elif typ == om.MFn.kTypedAttribute: + fn = om.MFnTypedAttribute(attr) + + elif typ in (om.MFn.kMatrixAttribute, + om.MFn.kFloatMatrixAttribute): + fn = om.MFnMatrixAttribute(attr) + + elif typ == om.MFn.kMessageAttribute: + fn = om.MFnMessageAttribute(attr) + + else: + raise TypeError( + "Couldn't figure out function set for %s" % self + ) + + return fn + def reset(self): """Restore plug to default value""" @@ -3219,63 +3382,121 @@ def type(self): return self._mplug.attribute().apiTypeStr def typeClass(self): - """Retrieve cmdx type of plug + """Retrieve cmdx Attribute class of plug, e.g. Double + + This reverse-engineers the plug attribute into the + attribute class used to originally create it. It'll + work on attributes not created with cmdx as well, but + won't catch'em'all (see below) + + Examples: + >>> victim = createNode("transform") + >>> victim["enumAttr"] = Enum() + >>> victim["stringAttr"] = String() + >>> victim["messageAttr"] = Message() + >>> victim["matrixAttr"] = Matrix() + >>> victim["longAttr"] = Long() + >>> victim["doubleAttr"] = Double() + >>> victim["floatAttr"] = Float() + >>> victim["double3Attr"] = Double3() + >>> victim["booleanAttr"] = Boolean() + >>> victim["angleAttr"] = Angle() + >>> victim["timeAttr"] = Time() + >>> victim["distanceAttr"] = Distance() + >>> victim["compoundAttr"] = Compound(children=[Double("Temp")]) + + >>> assert victim["enumAttr"].typeClass() == Enum + >>> assert victim["stringAttr"].typeClass() == String + >>> assert victim["messageAttr"].typeClass() == Message + >>> assert victim["matrixAttr"].typeClass() == Matrix + >>> assert victim["longAttr"].typeClass() == Long + >>> assert victim["doubleAttr"].typeClass() == Double + >>> assert victim["floatAttr"].typeClass() == Float + >>> assert victim["double3Attr"].typeClass() == Double3 + >>> assert victim["booleanAttr"].typeClass() == Boolean + >>> assert victim["angleAttr"].typeClass() == Angle + >>> assert victim["timeAttr"].typeClass() == Time + >>> assert victim["distanceAttr"].typeClass() == Distance + >>> assert victim["compoundAttr"].typeClass() == Compound + + # Unsupported + # >>> victim["dividerAttr"] = Divider() + # >>> victim["double2Attr"] = Double2() + # >>> victim["double4Attr"] = Double4() + # >>> victim["angle2Attr"] = Angle2() + # >>> victim["angle3Attr"] = Angle3() + # >>> victim["distance2Attr"] = Distance2() + # >>> victim["distance3Attr"] = Distance3() + # >>> victim["distance4Attr"] = Distance4() """ attr = self._mplug.attribute() - k = attr.apiType() + typ = attr.apiType() - if k == om.MFn.kAttribute3Double: + if typ == om.MFn.kAttribute3Double: return Double3 - elif k == om.MFn.kNumericAttribute: - k = om.MFnNumericAttribute(attr).numericType() - if k == om.MFnNumericData.kBoolean: + elif typ == om.MFn.kNumericAttribute: + typ = om.MFnNumericAttribute(attr).numericType() + if typ == om.MFnNumericData.kBoolean: return Boolean - elif k in (om.MFnNumericData.kLong, - om.MFnNumericData.kInt): + + elif typ in (om.MFnNumericData.kLong, + om.MFnNumericData.kInt): return Long - elif k == om.MFnNumericData.kDouble: + + elif typ == om.MFnNumericData.kDouble: return Double - elif k in (om.MFn.kDoubleAngleAttribute, - om.MFn.kFloatAngleAttribute): + elif typ == om.MFnNumericData.kFloat: + return Float + + elif typ in (om.MFn.kDoubleAngleAttribute, + om.MFn.kFloatAngleAttribute): return Angle - elif k in (om.MFn.kDoubleLinearAttribute, - om.MFn.kFloatLinearAttribute): + + elif typ in (om.MFn.kDoubleLinearAttribute, + om.MFn.kFloatLinearAttribute): return Distance - elif k == om.MFn.kTimeAttribute: + + elif typ == om.MFn.kTimeAttribute: return Time - elif k == om.MFn.kEnumAttribute: + + elif typ == om.MFn.kEnumAttribute: return Enum - elif k == om.MFn.kUnitAttribute: - k = om.MFnUnitAttribute(attr).unitType() - if k == om.MFnUnitAttribute.kAngle: + elif typ == om.MFn.kUnitAttribute: + typ = om.MFnUnitAttribute(attr).unitType() + if typ == om.MFnUnitAttribute.kAngle: return Angle - elif k == om.MFnUnitAttribute.kDistance: + + elif typ == om.MFnUnitAttribute.kDistance: return Distance - elif k == om.MFnUnitAttribute.kTime: + + elif typ == om.MFnUnitAttribute.kTime: return Time - elif k == om.MFn.kTypedAttribute: - k = om.MFnTypedAttribute(attr).attrType() - if k == om.MFnData.kString: + elif typ == om.MFn.kTypedAttribute: + typ = om.MFnTypedAttribute(attr).attrType() + if typ == om.MFnData.kString: return String - elif k == om.MFnData.kMatrix: + + elif typ == om.MFnData.kMatrix: return Matrix - elif k == om.MFn.kCompoundAttribute: + elif typ == om.MFn.kCompoundAttribute: return Compound - elif k in (om.Mfn.kMatrixAttribute, - om.MFn.kFloatMatrixAttribute): + + elif typ in (om.MFn.kMatrixAttribute, + om.MFn.kFloatMatrixAttribute): return Matrix - elif k == om.MFn.kMessageAttribute: + + elif typ == om.MFn.kMessageAttribute: return Message - t = self._mplug.attribute().apiTypeStr - log.warning('{} is not implemented'.format(t)) + apitype = self._mplug.attribute().apiTypeStr + raise TypeError('%s is not implemented' % apitype) def path(self, full=False): """Return path to attribute, including node path @@ -3945,6 +4166,14 @@ def length(self): def isNormalised(self, tol=0.0001): return abs(self.length() - 1.0) < tol + def asMatrix(self): + return Matrix4(super(Quaternion, self).asMatrix()) + + if ENABLE_PEP8: + as_matrix = asMatrix + is_normalised = isNormalised + length_squared = lengthSquared + # Alias Quat = Quaternion @@ -3977,10 +4206,10 @@ def twistSwingToQuaternion(ts): class EulerRotation(om.MEulerRotation): def asQuaternion(self): - return super(EulerRotation, self).asQuaternion() + return Quaternion(super(EulerRotation, self).asQuaternion()) def asMatrix(self): - return MatrixType(super(EulerRotation, self).asMatrix()) + return Matrix4(super(EulerRotation, self).asMatrix()) order = { 'xyz': kXYZ, @@ -4056,16 +4285,16 @@ def read(self): return self._value -def _plug_to_default(plug): - """Find default value from plug, regardless of attribute type""" +def _plug_to_default(mplug): + """Find default value from mplug, regardless of attribute type""" - if plug.isArray: + if mplug.isArray: raise TypeError("Array plugs are unsupported") - if plug.isCompound: + if mplug.isCompound: raise TypeError("Compound plugs are unsupported") - attr = plug.attribute() + attr = mplug.attribute() type = attr.apiType() if type == om.MFn.kTypedAttribute: @@ -5011,7 +5240,13 @@ def _doNiceNames(self): elements = plug if plug.isArray or plug.isCompound else [plug] for el in elements: - cmds.addAttr(el.path(), edit=True, niceName=value) + if el._mplug.isDynamic: + # Use setAttr as isKeyable doesn't + # persist on scene save for dynamic attributes. + cmds.addAttr(el.path(), edit=True, niceName=value) + else: + fn = om.MFnAttribute(el._mplug.attribute()) + fn.setNiceNameOverride(value) def doIt(self): try: @@ -6581,6 +6816,7 @@ def __init__(self, name, default=None, label=None, + shortName=None, writable=None, readable=None, @@ -6617,13 +6853,41 @@ def __init__(self, # Filled in on creation self["mobject"] = None - # MyName -> myName - self["shortName"] = self["name"][0].lower() + self["name"][1:] + self["shortName"] = ( + args.pop("shortName") or + + # MyName -> myName + self["name"][0].lower() + self["name"][1:] + ) for key, value in args.items(): default = getattr(self, key[0].upper() + key[1:]) self[key] = value if value is not None else default + def dumps(self): + """Return a block of text representing the config of this attribute""" + + result = ["type: %s" % type(self).__name__] + + for key, value in self.items(): + if key == "disconnectBehavior": + value = { + kNothing: "kNothing", + kReset: "kReset", + kDelete: "kDelete" + }[value] + + if key == "label": + key = "niceName" + + # Internal + if key == "mobject": + continue + + result += ["%s: %s" % (key, value)] + + return "\n".join(result) + def default(self, cls=None): """Return one of three available values @@ -6797,6 +7061,15 @@ def read(self, data): return data.inputValue(self["mobject"]).asDouble() +class Float(_AbstractAttribute): + Fn = om.MFnNumericAttribute + Type = om.MFnNumericData.kFloat + Default = 0.0 + + def read(self, data): + return data.inputValue(self["mobject"]).asFloat() + + class Double3(_AbstractAttribute): Fn = om.MFnNumericAttribute Type = None From 8a99bb0f0805bbac6b1141195cca012771421539 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Mon, 19 Apr 2021 14:00:02 +0100 Subject: [PATCH 2/6] Pretty-print for the matrix type --- cmdx.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cmdx.py b/cmdx.py index eaf725e..8bb8967 100644 --- a/cmdx.py +++ b/cmdx.py @@ -3976,6 +3976,25 @@ def asMatrixInverse(self): # type: () -> MatrixType class MatrixType(om.MMatrix): + def __str__(self): + fmt = ( + "%.2f %.2f %.2f %.2f\n" + "%.2f %.2f %.2f %.2f\n" + "%.2f %.2f %.2f %.2f\n" + "%.2f %.2f %.2f %.2f" + ) + + return fmt % ( + self(0, 0), self(0, 1), self(0, 2), self(0, 3), + self(1, 0), self(1, 1), self(1, 2), self(1, 3), + self(2, 0), self(2, 1), self(2, 2), self(2, 3), + self(3, 0), self(3, 1), self(3, 2), self(3, 3), + ) + + def __repr__(self): + value = "\n".join(" " + line for line in str(self).split("\n")) + return "%s.Matrix4(\n%s\n)" % (__name__, value) + def __call__(self, *item): """Native API 2.0 MMatrix does not support indexing From 253a57c91477f98db7dc5f92c6066f90b1946e84 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Tue, 20 Apr 2021 14:10:58 +0100 Subject: [PATCH 3/6] Pretty-print Tm --- cmdx.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/cmdx.py b/cmdx.py index 8bb8967..5c5dae5 100644 --- a/cmdx.py +++ b/cmdx.py @@ -1600,6 +1600,9 @@ def transform(self, space=sObject, time=None): plug = self["worldMatrix"][0] if space == sWorld else self["matrix"] return TransformationMatrix(plug.asMatrix(time)) + def transformation(self): + return TransformationMatrix(self._tfn.transformation()) + def translation(self, space=sObject, time=None): """Convenience method for transform(space).translation() @@ -3813,6 +3816,27 @@ class TransformationMatrix(om.MTransformationMatrix): """ + def __repr__(self): + return ( + "MTransformationMatrix(\n" + " translate: {t}\n" + " rotate: {r}\n" + " scale: {s}\n" + " rotatePivot: {rp}\n" + " rotatePivotTranslation: {rpt}\n" + " scalePivot: {sp}\n" + " scalePivotTranslation: {spt}\n" + ")" + ).format( + t=self.translation(), + r=self.rotation(), + s=self.scale(), + rp=self.rotatePivot(), + rpt=self.rotatePivotTranslation(), + sp=self.scalePivot(), + spt=self.scalePivotTranslation(), + ) + def __init__(self, matrix=None, translate=None, rotate=None, scale=None): # It doesn't like being handed `None` @@ -3902,6 +3926,25 @@ def rotatePivot(self, space=None): space = space or sTransform return Vector(super(TransformationMatrix, self).rotatePivot(space)) + def rotatePivotTranslation(self, space=None): + """This method does not typically support optional arguments""" + space = space or sTransform + return Vector( + super(TransformationMatrix, self).rotatePivotTranslation(space) + ) + + def scalePivot(self, space=None): + """This method does not typically support optional arguments""" + space = space or sTransform + return Vector(super(TransformationMatrix, self).scalePivot(space)) + + def scalePivotTranslation(self, space=None): + """This method does not typically support optional arguments""" + space = space or sTransform + return Vector( + super(TransformationMatrix, self).scalePivotTranslation(space) + ) + def translation(self, space=None): # type: (om.MSpace) -> om.MVector """This method does not typically support optional arguments""" space = space or sTransform From d32d8b3d510a2dd4e2d8726b1771e23e980617fc Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Fri, 23 Apr 2021 13:18:08 +0100 Subject: [PATCH 4/6] Implement asTime and improve undo/redo for locking of attributes This could *sometimes* throw an error when creating a new node and locking its attributes in the same dg context. --- cmdx.py | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/cmdx.py b/cmdx.py index 5c5dae5..16f0857 100644 --- a/cmdx.py +++ b/cmdx.py @@ -3018,6 +3018,19 @@ def asVector(self, time=None): assert self.isArray or self.isCompound, "'%s' not an array" % self return Vector(self.read(time=time)) + def asTime(self, time=None): + attr = self._mplug.attribute() + type = attr.apiType() + + if type != om.MFn.kTimeAttribute: + raise TypeError("%s is not a time attribute" % self.path()) + + kwargs = {} + if time is not None: + kwargs["context"] = DGContext(time=time) + + return self._mplug.asMTime(**kwargs) + @property def connected(self): """Return whether or not this attribute has an input connection @@ -3792,6 +3805,7 @@ def node(self): as_euler = asEuler as_quaternion = asQuaternion as_vector = asVector + as_time = asTime channel_box = channelBox lock_and_hide = lockAndHide array_indices = arrayIndices @@ -4400,6 +4414,7 @@ def _plug_to_python(plug, unit=None, context=None): """ + assert isinstance(plug, om.MPlug), "'%r' was not an MPlug" % plug assert not plug.isNull, "'%s' was null" % plug kwargs = dict() @@ -5115,20 +5130,22 @@ def __exit__(self, exc_type, exc_value, tb): try: self.redoIt() + if self._opts["undoable"]: + # 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) + # These all involve calling on cmds, # which manages undo on its own. - self._doLockAttrs() self._doKeyableAttrs() self._doNiceNames() + self._doLockAttrs() finally: - if self._opts["undoable"]: - - # Make our commit within the current undo chunk, - # to combine the commands from the above special - # attribute edits which happen via maya.cmds - commit(self._modifier.undoIt, self._modifier.doIt) + # Ensure we close the undo chunk no matter what + if self._opts["undoable"]: cmds.undoInfo(chunkName="%x" % id(self), closeChunk=True) def __init__(self, @@ -5681,7 +5698,7 @@ def connect(self, src, dst, force=True): # NOTE: This is bad, the user should be in control of when # the modifier is actually being called. Especially if we # want to avoid calling it altogether in case of an exception - self.doIt() + self._modifier.doIt() self._modifier.connect(src, dst) @@ -6156,9 +6173,12 @@ def connect(a, b): mod.connect(a, b) -def currentTime(): - """Return current time in MTime format""" - return oma.MAnimControl.currentTime() +def currentTime(time=None): + """Set or return current time in MTime format""" + if time is None: + return oma.MAnimControl.currentTime() + else: + return oma.MAnimControl.setCurrentTime(time) class DGContext(om.MDGContext): From d1b39a1819421097f1e9f9a410eb37cb1ac6e679 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Mon, 7 Jun 2021 07:33:41 +0100 Subject: [PATCH 5/6] Lots of usability updates --- cmdx.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/cmdx.py b/cmdx.py index 16f0857..d28e806 100644 --- a/cmdx.py +++ b/cmdx.py @@ -1084,6 +1084,36 @@ def dumps(self, indent=4, sort_keys=True, preserve_order=False): sort_keys=sort_keys ) + def index(self, plug): + """ Find index of `attr` in its owning node + _____________ + | | + | Friction o -> 126 + | Mass o -> 127 + | Color R o -> 128 + | Color G o -> 129 + | Color B o -> 130 + | | + |_____________| + + # TODO: This is really slow + + """ + + assert isinstance(plug, Plug), "%r was not a cmdx.Plug" % plug + + node = plug.node() + + for i in range(node._fn.attributeCount()): + attr = node._fn.attribute(i) + fn = om.MFnAttribute(attr) + + if fn.shortName == plug.name(long=False): + return i + + # Can happen if asking for an index of a plug from another node + raise ValueError("Index of '%s' not found" % plug.name()) + def type(self): """Return type name @@ -1680,7 +1710,7 @@ def mapTo(self, other, time=None): # Alias root = assembly - def parent(self, type=None): + def parent(self, type=None, filter=None): """Return parent of node Arguments: @@ -1694,6 +1724,13 @@ def parent(self, type=None): >>> not child.parent(type="camera") True >>> parent.parent() + >>> child.parent(filter=om.MFn.kTransform) == parent + True + >>> child.parent(filter=om.MFn.kJoint) is None + True + + Returns: + parent (Node): If any, else None """ @@ -1704,11 +1741,14 @@ def parent(self, type=None): cls = self.__class__ + if filter is not None and not mobject.hasFn(filter): + return None + if not type or type == self._fn.__class__(mobject).typeName: return cls(mobject) @protected - def lineage(self, type=None): + def lineage(self, type=None, filter=None): """Yield parents all the way up a hierarchy Example: @@ -1730,7 +1770,10 @@ def lineage(self, type=None): parent = self.parent(type) while parent is not None: yield parent - parent = parent.parent(type) + parent = parent.parent(type, filter) + + # Alias + parenthood = lineage @protected def children(self, @@ -2740,7 +2783,10 @@ def clone(self, name, shortName=None, niceName=None): if self.isCompound: raise TypeError("Compound plugs are unsupported") - niceName = niceName or self.niceName + if niceName is False: + niceName = None + else: + niceName = niceName or self.niceName # There is no way to tell whether the niceName of # a plug is automatically generated by Maya or if @@ -2864,6 +2910,10 @@ def nextAvailableIndex(self, startIndex=0): # No connections means the first index is available return 0 + def pull(self): + """Pull on a plug, without seriasing any value. For performance""" + self._mplug.asMObject() + def append(self, value, autofill=False): """Add `value` to end of self, which is an array @@ -3005,7 +3055,7 @@ def asTransformationMatrix(self, time=None): def asEulerRotation(self, order=kXYZ, time=None): value = self.read(time=time) - return om.MEulerRotation(value, order) + return Euler(om.MEulerRotation(value, order)) asEuler = asEulerRotation @@ -3018,6 +3068,10 @@ def asVector(self, time=None): assert self.isArray or self.isCompound, "'%s' not an array" % self return Vector(self.read(time=time)) + def asPoint(self, time=None): + assert self.isArray or self.isCompound, "'%s' not an array" % self + return Point(self.read(time=time)) + def asTime(self, time=None): attr = self._mplug.attribute() type = attr.apiType() @@ -3050,6 +3104,30 @@ def connected(self): return self.connection(destination=False) is not None + def animated(self, recursive=True): + """Return whether this attribute is connected to an animCurve + + Arguments: + recursive (bool, optional): Should I travel to connected + attributes in search of an animCurve, or only look to + the immediate connection? + + """ + + other = self.connection(destination=False, plug=True) + while other is not None: + + node = other.node() + if node.object().hasFn(om.MFn.kAnimCurve): + return True + + if not recursive: + break + + other = other.connection(destination=False, plug=True) + + return False + def lock(self): """Convenience function for plug.locked = True @@ -3326,6 +3404,12 @@ def fn(self): elif typ == om.MFn.kUnitAttribute: fn = om.MFnUnitAttribute(attr) + elif typ in (om.MFn.kDoubleLinearAttribute, + om.MFn.kFloatLinearAttribute, + om.MFn.kDoubleAngleAttribute, + om.MFn.kFloatAngleAttribute): + return om.MFnUnitAttribute(attr) + elif typ == om.MFn.kTypedAttribute: fn = om.MFnTypedAttribute(attr) @@ -3338,7 +3422,9 @@ def fn(self): else: raise TypeError( - "Couldn't figure out function set for %s" % self + "Couldn't figure out function set for '%s.%s'" % ( + self.path(), attr.apiTypeStr + ) ) return fn @@ -3805,6 +3891,7 @@ def node(self): as_euler = asEuler as_quaternion = asQuaternion as_vector = asVector + as_point = asPoint as_time = asTime channel_box = channelBox lock_and_hide = lockAndHide @@ -4148,7 +4235,7 @@ def __add__(self, value): self.z + value, ) - return super(Vector, self).__add__(value) + return Vector(super(Vector, self).__add__(value)) def __iadd__(self, value): if isinstance(value, (int, float)): @@ -4158,13 +4245,13 @@ def __iadd__(self, value): self.z + value, ) - return super(Vector, self).__iadd__(value) + return Vector(super(Vector, self).__iadd__(value)) def dot(self, value): - return super(Vector, self).__mul__(value) + return Vector(super(Vector, self).__mul__(value)) def cross(self, value): - return super(Vector, self).__xor__(value) + return Vector(super(Vector, self).__xor__(value)) # Alias, it can't take anything other than values @@ -4824,6 +4911,9 @@ def _python_to_mod(value, plug, mod): value = om.MAngle(value, om.MAngle.kRadians) _python_to_mod(value, plug[index], mod) + elif isinstance(value, om.MQuaternion): + _python_to_mod(value.asEulerRotation(), plug, mod) + else: raise TypeError( "Unsupported plug type for modifier: %s" % type(value) @@ -5209,7 +5299,6 @@ def setNiceName(self, plug, value=True): plug = Plug(Node(plug.node()), plug) assert isinstance(plug, Plug), "%s was not a plug" % plug - assert plug._mplug.isDynamic, "%s was not a dynamic attribute" % plug self._niceNames.append((plug, value)) @record_history @@ -6132,8 +6221,11 @@ def createNode(self, type, name=None, parent=None): try: mobj = self._modifier.createNode(type, parent) - except TypeError: - raise TypeError("'%s' is not a valid node type" % type) + except TypeError as e: + if e.message == "parent is not a transform type": + raise TypeError("'%s' is not a transform type," % parent) + else: + raise TypeError("'%s' is not a valid node type," % type) template = self._opts["template"] if name or template: @@ -6156,6 +6248,9 @@ def parent(self, node, parent=None): if SAFE_MODE: self._modifier.doIt() + reparent = parent + reparentNode = parent + if ENABLE_PEP8: create_node = createNode @@ -7083,8 +7178,12 @@ def __init__(self, label, **kwargs): kwargs.pop("fields", None) kwargs.pop("label", None) + # Account for spaces in label + # E.g. "Hard Pin" -> "hardPin" + name = label[0].lower() + label[1:].replace(" ", "") + super(Divider, self).__init__( - label, fields=(label,), label=" ", **kwargs + name, fields=(label,), label=" ", **kwargs ) @@ -7557,6 +7656,9 @@ def __init__(self, *args, **kwargs): super(_apiUndo, self).__init__(*args, **kwargs) _apiUndo._aliveCount += 1 + self.undoId = None + self.redoId = None + def __del__(self): _apiUndo._aliveCount -= 1 From ac4a0f83d3bbbf102bdb893ff94a0b592c98f279 Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Mon, 7 Jun 2021 07:40:04 +0100 Subject: [PATCH 6/6] Fix Python 3 issue --- cmdx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdx.py b/cmdx.py index d28e806..4a91609 100644 --- a/cmdx.py +++ b/cmdx.py @@ -6222,7 +6222,7 @@ def createNode(self, type, name=None, parent=None): try: mobj = self._modifier.createNode(type, parent) except TypeError as e: - if e.message == "parent is not a transform type": + if str(e) == "parent is not a transform type": raise TypeError("'%s' is not a transform type," % parent) else: raise TypeError("'%s' is not a valid node type," % type)