Giter Club home page Giter Club logo

cmdx's Introduction

A fast subset of maya.cmds
For Maya 2018-2025


About

cmdx is a Python wrapper for the Maya Python API 2.0 and a fast subset of the maya.cmds module, with persistent references to nodes.

If you fit in either of these groups, then cmdx is for you.

  • You like cmds, but wish to type less
  • You like PyMEL, but wish it was faster

On average, cmdx is 140x faster than PyMEL, and 2.5x faster than maya.cmds at common tasks; at best, it is 1,300x faster than PyMEL.

News
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
Aug 2019 0.4.0 Public release
Feb 2018 0.1.0 Extracted into its own repository
Jun 2017 0.0.0 Starts as an internal module
Status

Maya Status
2017 cmdx-test
2018 cmdx-test
2019 cmdx-test
2020 cmdx-test
2022 cmdx-test
Usecases

cmdx was written for performance critical run-time tasks in Maya, listening to thousands of events, reading from and writing to thousands of attributes each frame, without affecting user interactivity. It doesn't capture all of cmds, but rather a small subset related to parts relevant to these types of performance critical tasks.

Usecase Description
Real-time processing Such as responding to user input without interruption
Data intensive processing Such as processing thousands of attributes on thousands of nodes at once
Plug-in creation Provides both superclasses and compatible API for performing most if not all calls in compute() or draw() using cmdx.

Install

cmdx is a single file and can either be copy/pasted into your project, downloaded as-is, cloned as-is or installed via pip.

$ pip install cmdx
  • Pro tip: Never use the latest commit for production. Instead, use the latest release. That way, when you read bug reports or make one for yourself you will be able to match a version with the problem without which you will not know which fixes apply to you nor would we be able to help you. Installing via pip or conda as above ensures you are provided the latest stable release. Unstable releases are suffixed with a .b, e.g. 0.5.0.b1.

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 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.

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.

from . import cmdx_mytool as cmdx

This would then allow your users to call..

from mytool.vendor import cmdx

..as though the module was called just cmdx.py.


What is novel?

With so many options for interacting with Maya, when or why should you choose cmdx?



Table of contents


System Requirements

cmdx runs on Maya 2017 above.

It may run on older versions too, but those are not being tested. To bypass the version check, see CMDX_IGNORE_VERSION.


Syntax

cmdx supports the legacy syntax of maya.cmds, along with an object-oriented syntax, similar to PyMEL.

Legacy

Familiar and fast.

>>> import cmdx
>>> joe = cmdx.createNode("transform", name="Joe")
>>> benji = cmdx.createNode("transform", name="myChild", parent=joe)
>>> cmdx.addAttr(joe, longName="myAttr", defaultValue=5.0, attributeType="double")
>>> cmdx.connectAttr(joe + ".myAttr", benji + ".tx")
>>> cmdx.setAttr(joe + ".myAttr", 5)
>>> cmdx.delete(joe)

Modern

Faster and most concise.

>>> import cmdx
>>> joe = cmdx.createNode("transform", name="Joe")
>>> benji = cmdx.createNode("transform", name="myChild", parent=joe)
>>> joe["myAttr"] = cmdx.Double(default=5.0)
>>> joe["myAttr"] >> benji["translateX"]
>>> joe["tx"] = 5
>>> cmdx.delete(joe)

Commands

  • createNode
  • getAttr
  • setAttr
  • addAttr
  • connectAttr
  • listRelatives
  • listConnections

Attribute Types

  • Double
  • Double3
  • Enum
  • String
  • Angle
  • Distance
  • Time
  • Message
  • Boolean
  • Divider
  • Long
  • Compound
  • NurbsCurve

Performance

cmdx is fast, faster than cmds by 2-5x and PyMEL by 5-150x, because of how it uses the Maya API 2.0, how classes are built and the (efficient) pre-processing happening on import.

See Measurements for performance statistics and comparisons between MEL, cmds, cmdx, PyMEL, API 1.0 and 2.0.

How?

The fastest you can possibly get with Python inside Maya is through the Maya Python API 2.0. cmdx is a thin wrapper around this library that provides a more accessible and readable interface, whilst avoiding as much overhead as possible.


Goals

With PyMEL as baseline, these are the primary goals of this project, in order of importance.

Goal Description
Readable For code that is read more than it is written
Fast Faster than PyMEL, and cmds
Lightweight A single Python module, implementing critical parts well, leaving the rest to cmds
Persistent References to nodes do not break
Do not crash Working with low-level Maya API calls make it susceptible to crashes; cmdx should protect against this, without sacrificing performance
No side effects Importing cmdx has no affect any other module
External Shipped alongside your code, not alongside Maya; you control the version, features and fixes.
Vendorable Embed an appropriate version of cmdx alongside your own project
PEP8 Continuous integration ensures that every commit follows the consistency of PEP8
Examples No feature is without examples

Overhead

cmdx tracks node access via a Maya API callback. This callback is called on node destruction and carries an overhead to normal Maya operation when deleting nodes, most noticeably when creating a new scene (as it causes all nodes to be destroyed at once).

In the most extreme circumstance, with 100,000 nodes tracked by cmdx, all nodes are destroyed in 4.4 seconds. Without this callback, the nodes are destroyed in 4.3 seconds.

This accounts for an overhead of 1 ms/node destroyed.

This overhead can be bypassed with Rogue Mode.

Test

To confirm this for yourself, run the below in your Script Editor; it should take about 30-60 seconds depending on your hardware.

# untested
import time
import timeit
import cmdx

import os

def setup():
   for i in range(100000):
     cmdx.createNode("transform")

def rogue():
    os.environ["CMDX_ROGUE_MODE"] = "1"
    cmds.file(new=True, force=True)
    reload(cmdx)
    setup()

def nonrogue():
    os.environ.pop("CMDX_ROGUE_MODE", None)
    cmds.file(new=True, force=True)
    reload(cmdx)
    setup()

t1 = timeit.Timer(
    lambda: cmds.file(new=True, force=True),
    setup=rogue
).repeat(repeat=2, number=2)

t2 = timeit.Timer(
    lambda: cmds.file(new=True, force=True),
    setup=nonrogue
).repeat(repeat=4, number=1)

print("rogue: %.3f ms" % (min(t1) * 1000))
print("nonrogue: %.3f ms" % (min(t2) * 1000))

Query Reduction

Beyond making queries faster is making less of them.

Any interaction with the Maya API carries the overhead of translating from Python to C++ and, most of the time, back to Python again. So in order to make cmdx fast, it must facilitate re-use of queries where re-use makes sense.

Node Reuse

Any node created or queried via cmdx is kept around until the next time the same node is returned, regardless of the exact manner in which it was queried.

For example, when encoded or returned as children of another node.

>>> import cmdx
>>> node = cmdx.createNode("transform", name="parent")
>>> cmdx.encode("|parent") is node
True

This property survives function calls too.

>>> import cmdx
>>> from maya import cmds
>>> def function1():
...   return cmdx.createNode("transform", name="parent")
...
>>> def function2():
...   return cmdx.encode("|parent")
...
>>> _ = cmds.file(new=True, force=True)
>>> function1() is function2()
True

In fact, regardless of how a node is queried, there is only ever a single instance in cmdx of it. This is great for repeated queries to nodes and means nodes can contain an additional level of state, beyond the one found in Maya. A property which is used for, amongst other things, optimising plug reuse.

Plug Reuse

node = cmdx.createNode("transform")
node["translateX"]  # Maya's API `findPlug` is called
node["translateX"]  # Previously found plug is returned
node["translateX"]  # Previously found plug is returned
node["translateX"]  # ...

Whenever an attribute is queried, a number of things happen.

  1. An MObject is retrieved via string-comparison
  2. A relevant plug is found via another string-comparison
  3. A value is retrieved, wrapped in a Maya API object, e.g. MDistance
  4. The object is cast to Python object, e.g. MDistance to float

This isn't just 4 interactions with the Maya API, it's also 3 interactions with the Maya scenegraph. An interaction of this nature triggers the propagation and handling of the dirty flag, which in turn triggers a virtually unlimited number of additional function calls; both internally to Maya - i.e. the compute() method and callbacks - and in any Python that might be listening.

With module level caching, a repeated query to either an MObject or MPlug is handled entirely in Python, saving on both time and computational resources.

Hashable References

In addition to reusing things internally, you are able to re-use things yourself by using nodes as e.g. keys to dictionaries.

>>> import cmdx
>>> from maya import cmds
>>> _ = cmds.file(new=True, force=True)
>>> node = cmdx.createNode("animCurveTA")
>>> nodes = {node: {"key": "value"}}
>>> for node in cmdx.ls(type="animCurveTA"):
...    assert node in nodes
...    assert nodes[node]["key"] == "value"
...

The hash of the node is guaranteed unique, and the aforementioned reuse mechanism ensure that however a node is referenced the same reference is returned.

Utilities

Here are some useful utilities that leverages this hash.

>>> import cmdx
>>> node = cmdx.createNode("transform")
>>> node == cmdx.fromHash(node.hashCode)
True
>>> node == cmdx.fromHex(node.hex)
True

These tap directly into the dictionary used to maintain references to each cmdx.Node. The hashCode is the one from maya.api.OpenMaya.MObjectHandle.hashCode(), which means that if you have an object from the Maya Python API 2.0, you can fetch the cmdx equivalent of it by passing its hashCode.

However keep in mind that you can only retrieve nodes that have previously been access by cmdx.

from maya.api import OpenMaya as om
import cmdx
fn = om.MFnDagNode()
mobj = fn.create("transform")
handle = om.MObjectHandle(mobj)
node = cmdx.fromHash(handle.hashCode())
assert node is None, "%s should have been None" % node
node = cmdx.Node(mobj)
node = cmdx.fromHash(handle.hashCode())

A more robust alternative is to instead pass the MObject directly.

from maya.api import OpenMaya as om
import cmdx
fn = om.MDagNode()
mobj = fn.create("transform")
node = cmdx.Node(mobj)

This will use the hash if a cmdx instance of this MObject already exist, else it will instantiate a new. The performance difference is slim and as such this is the recommended approach. The exception is if you happen to already has either an MObjectHandle or a corresponding hashCode at hand, in which case you can save a handful of cycles per call by using fromHash or fromHex.


Metadata

For persistent metadata, one practice is to use a Maya string attribute and store arbitrary data there, serialised to string.

For transient metadata however - data that doesn't need or should persist across sessions - you can rely on the node reuse mechanism of cmdx.

# Get reference to existing node
node = cmdx.encode("|myNode")

node.data["myData"] = {
  "awesome": True
}

This data is then preserved with the node for its lifetime. Once the node is destroyed - e.g. on deleting it or opening a new scene - the data is destroyed along with it.

The data is stored entirely in Python so there is no overhead of interacting with the Maya scenegraph per call or edit.

To make persistent data, you may for example associate a given ID with a file on disk or database path and automatically load the data into it on node creation.

...

Interoperability

cmdx complements cmds, but does not replace it.

Commands such as menuItem, inViewMessage and move are left out and considered a convenience; not sensitive to performance-critical tasks such as generating nodes, setting or connecting attributes etc.

Hence interoperability, where necessary, looks like this.

from maya import cmds
import cmdx

group = cmds.group(name="group", empty=True)
cmds.move(group, 0, 50, 0)
group = cmdx.encode(group)
group["rotateX", cmdx.Radians] = 3.14
cmds.select(cmdx.decode(group))

An alternative to cmdx.decode is to simply cast it to str, which will convert a cmdx node into the equivalent shortest path.

cmds.select(str(group))

Another aspect of cmdx that differ from cmds is the number arguments to functions, such as listConnections and ls.

from maya import cmds
import cmdx

node = cmdx.createNode("transform")
cmds.listConnections(str(node), source=True)
cmdx.listConnections(str(node), source=True)
TypeError: listConnections() got an unexpected keyword argument 'source'

The reason for this limitation is because the functions cmds


Path-like Syntax

Neatly traverse a hierarchy with the | syntax.

# Before
group = cmdx.encode("|some_grp")
hand = cmdx.encode(group.path() + "|hand_ctl")

# After
hand = group | "hand_ctl"

It can be nested too.

finger = group | "hand_ctl" | "finger_ctl"

setAttr

Maya's cmds.setAttr depends on the UI settings for units.

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.

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.

import cmdx

node = cmdx.createNode("transform")
node["translateX"] = 5
node["translateX"]
# 5

To return translateX in Meters, you can pass in a unit explicitly.

node["translateX", cmdx.Meters]
# 0.05

To set translateX to a value defined in Meters, you can pass that explicitly too.

node["translateX", cmdx.Meters] = 5

Or use the alternative syntax.

node["translateX"] = cmdx.Meters(5)

The following units are currently supported.

  • Angular
    • Degrees
    • Radians
    • AngularMinutes
    • AngularSeconds
  • Linear
    • Millimeters
    • Centimeters
    • Meters
    • Kilometers
    • Inches
    • Feet
    • Miles
    • Yards

Exceptions

Not all attribute editing supports units.

transform = cmdx.createNode("transform")
tm = transform["worldMatrix"][0].asTransformationMatrix()

# What unit am I?
tm.translation()

The same applies to orientation.

tm.rotation()

In circumstances without an option, cmdx takes and returns a default unit per type of plug, similar to maya.api

Defaults

Type Unit
Linear Centimeter
Angular Radian
Time Second

Limitations

All of this performance is great and all, but why hasn't anyone thought of this before? Are there no consequences?

I'm sure someone has, and yes there are.

Undo

With every command made through maya.cmds, the undo history is populated such that you can undo a block of commands all at once. cmdx doesn't do this, which is how it remains fast, but also less capable of undoing.

For undo, you've got two options.

  1. Use cmdx.DagModifier or cmdx.DGModifier for automatic undo of whatever to create or edit using these modifiers
  2. Use cmdx.commit for manual control over what happens when the user tries to undo
node = cmdx.createNode("transform")

This operation is not undoable and is intended for use with cmdx.commit and/or within a Python plug-in.

node["translateX"] = 5
node["tx"] >> node["ty"]
cmdx.delete(node)

These operations are also not undoable.

In order to edit attributes with support for undo, you must use either a modifier or call commit. This is how the Maya API normally works, for both Python and C++.

with cmdx.DagModifier() as mod:
    mod.setAttr(node["translateX"], 5)
    mod.connect(node["tx"], node["ty"])

Alternatively, call commit.

previous_value = node["translateX"].read()

def my_undo():
    node["translateX"] = previous_value
    node["ty"].disconnect()

node["translateX"] = 5
node["tx"] >> node["ty"]
cmdx.commit(my_undo)

Typically, you should prefer to use a modifier as it will manage previous values for you and ensure things are undone in the right order (e.g. no need to undo attribute changes if the node is deleted).


Undo Caveats

With this level of control, you are able to put Maya in a bad state.

a = cmdx.encode("existingNode")

with cmdx.DagModifier() as mod:
    b = mod.createNode("transform", name="newNode")

b["ty"] >> a["tx"]

Here, we are creating a new node and connecting it to a. As mentioned, connections are not undoable, so what do you think will happen when the user undos?

  1. newNode is deleted
  2. Connections are preserved

But how can that be? What is a["tx"] connected to?! You'll find that the channel is locked and connected, but the connected node is unselectable and yet visible in odd places like the Node Editor but not Outliner.

To address this, make sure that you include anything related to a block of operations in a modifier or commit. It can be multiple modifiers, that is fine, they will undo together en masse.

a = cmdx.encode("existingTransform")

with cmdx.DagModifier() as mod:
    b = mod.createNode("transform")
    mod.connect(b["ty"], a["tx"])

The user can now undo safely.


Crashes

If this happens to you, please report it along with a reproducible as that would qualify as a bug!


Node Creation

Nodes are created much like with maya.cmds.

import cmdx
cmdx.createNode("transform")

For a 5-10% performance increase, you may pass type as an object rather than string.

cmdx.createNode(cmdx.tTransform)

Only the most commonly used and performance sensitive types are available as explicit types.

  • tAddDoubleLinear
  • tAddMatrix
  • tAngleBetween
  • tMultMatrix
  • tAngleDimension
  • tBezierCurve
  • tBlendShape
  • tCamera
  • tChoice
  • tChooser
  • tCondition
  • tTransform
  • tTransformGeometry
  • tWtAddMatrix

Node Types

Unlike PyMEL and for best performance, cmdx does not wrap each node type in an individual class. However it does wrap those with a corresponding API function set.

Node Type Features
Node Lowest level superclass, this host most of the functionality of cmdx
DagNode A subclass of Node with added functinality related to hierarchy
ObjectSet A subclass of Node with added functinality related to sets

Node

Any node that isn't a DagNode or ObjectSet is wrapped in this class, which provides the basic building blocks for manipulating nodes in the Maya scenegraph, including working with attributes and connections.

import cmdx
add = cmdx.createNode("addDoubleLinear")
mult = cmdx.createNode("multDoubleLinear")
add["input1"] = 1
add["input2"] = 1
mult["input1"] = 2
mult["input2"] << add["output"]
assert mult["output"] == 4

DagNode

Any node compatible with the MFnDagNode function set is wrapped in this class and faciliates a parent/child relationship.

import cmdx
parent = cmdx.createNode("transform")
child = cmdx.createNode("transform")
parent.addChild(child)

ObjectSet

Any node compatible with the MFnSet function set is wrapped in this class and provides a Python list-like interface for working with sets.

import cmdx
objset = cmdx.createNode("objectSet")
member = cmdx.createNode("transform")
objset.append(member)

for member in objset:
  print(member)

Attribute Query and Assignment

Attributes are accessed in a dictionary-like fashion.

import cmdx
node = cmdx.createNode("transform")
node["translateX"]
# 0.0

Evaluation of an attribute is delayed until the very last minute, which means that if you don't read the attribute, then it is only accessed and not evaluated and cast to a Python type.

attr = node["rx"]

The resulting type of an attribute is cmdx.Plug

type(attr)
# <class 'cmdx.Plug'>

Which has a number of additional methods for query and assignment.

attr.read()
# 0.0
attr.write(1.0)
attr.read()
# 1.0

attr.read() is called when printing an attribute.

print(attr)
# 1.0

For familiarity, an attribute may also be accessed by string concatenation.

attr = node + ".tx"

Meta Attributes

Attributes about attributes, such as keyable and channelBox are native Python properties.

import cmdx
node = cmdx.createNode("transform")
node["translateX"].keyable = False
node["translateX"].channelBox = True

These also have convenience methods for use where it makes sense for readability.

# Hide from Channel Box
node["translateX"].hide()

Arrays

Working with arrays is akin to the native Python list.

node = createNode("transform")
node["myArray"] = Double(array=True)
node["myArray"].append(1.0)  # Explicit append
node["myArray"].extend([2.0, 3.0])  # Explicit extend
node["myArray"] += 6.0  # Append via __iadd__
node["myArray"] += [1.1, 2.3, 999.0]  # Append multiple values

Cached

Sometimes, a value is queried when you know it hasn't changed since your last query. By passing cmdx.Cached to any attribute, the previously computed value is returned, without the round-trip the the Maya API.

import cmdx
node = cmdx.createNode("transform")
node["tx"] = 5
assert node["tx"] == 5
node["tx"] = 10
assert node["tx", cmdx.Cached] == 5
assert node["tx"] == 10

Using cmdx.Cached is a lot faster than recomputing the value, sometimes by several orders of magnitude depending on the type of value being queried.

Animation

Assigning a dictionary to any numerical attribute turns those values into animation, with an appropriate curve type.

node = createNode("transform")
node["translateX"] = {1: 0.0, 5: 1.0, 10: 0.0}      # animCurveTL
node["rotateX"] = {1: 0.0, 5: 1.0, 10: 0.0}         # animCurveTA
node["scaleX"] = {1: 0.0, 5: 1.0, 10: 0.0}          # animCurveTL
node["visibility"] = {1: True, 5: False, 10: True}  # animCurveTU

# Alternatively
node["v"].animate({1: False, 5: True, 10: False})

Where the key is the frame number (can be fractional) and value is the value at that frame. Interpolation is cmdx.Linear per default, but can be customised with..

node["rotateX"].animate({1: 0.0, 5: 1.0, 10: 0.0}, cmdx.Smooth)

Currently available options:

  • cmdx.Stepped
  • cmdx.Linear
  • cmdx.Smooth

Animation is undoable if used with a modifier.

with cmdx.DagModifier() as mod:
    node = mod.createNode("transform")
    node["tx"] = {1: 0.0, 2: 5.0}

Time

The time argument of cmdx.getAttr enables a query to yield results relative a specific point in time. The time argument of Plug.read offers this same convenience, only faster.

import cmdx
from maya import cmds
node = cmdx.createNode("transform")

# Make some animation
node["tx"] = {1: 0.0, 50: 10.0, 100: 0.0}

# Query it
node = cmdx.create_node("transform")
node["tx"] << tx["output"]
node["tx"].read(time=50)
# 10.0

In Maya 2018 and above, Plug.read will yield the result based on the current evaluation context. Following on from the previous example.

from maya.api import OpenMaya as om

context = om.MDGContext(om.MTime(50, unit=om.MTime.uiUnit()))
context.makeCurrent()
node["tx"].read() # Evaluates the context at frame 50
# 10.0
om.MDGContext.kNormal.makeCurrent()

The cmdx.DGContext class is also provided to make evaluating the DG in another context simpler. When used as a context manager it will set the current context then restore the previous context upon completion.

with cmdx.DGContext(50):
    node["tx"].read()

Compound and Array Attributes

These both have children, and are accessed like a Python list.

node = cmdx.createNode("transform")
decompose = cmdx.createNode("decomposeMatrix")
node["worldMatrix"][0] >> decompose["inputMatrix"]

Array attributes are created by an additional argument.

node = cmdx.createNode("transform")
node["myArray"] = cmdx.Double(array=True)

Compound attributes are created as a group.

node = cmdx.createNode("transform")
node["myGroup"] = cmdx.Compound(children=(
  cmdx.Double("myGroupX")
  cmdx.Double("myGroupY")
  cmdx.Double("myGroupZ")
))

Both array and compound attributes can be written via index or tuple assignment.

node["myArray"] = (5, 5, 5)
node["myArray"][1] = 10
node["myArray"][2]
# 5

Matrix Attributes

Create and edit matrix attributes like any other attribute.

For example, here's how you can store a copy of the current worldmatrix of any given node.

import cmdx

node = cmdx.createNode("transform")
node["translate"] = (1, 2, 3)
node["rotate", cmdx.Degrees] = (20, 30, 40)

# Create a new matrix attribute
node["myMatrix"] = cmdx.Matrix()

# Store current world matrix in this custom attribute
node["myMatrix"] = node["worldMatrix"][0].asMatrix()

Cloning

Support for cloning enum attributes.

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.

You can access these classes via the .as* prefix of cmdx instances.

import cmdx
nodeA = cmdx.createNode("transform")
nodeB = cmdx.createNode("transform", parent=nodeA)
nodeC = cmdx.createNode("transform")

nodeA["rotate"] = (4, 8, 15)

tmA = nodeB["worldMatrix"][0].asTransformationMatrix()
nodeC["rotate"] = tmA.rotation()

Now nodeC will share the same worldspace orientation as nodeA (note that nodeB was not rotated).

Matrix Multiplication

One useful aspect of native types is that you can leverage their operators, such as multiplication.

matA = nodeA["worldMatrix"][0].asMatrix()
matB = nodeB["worldInverseMatrix"][0].asMatrix()
tm = cmdx.TransformationMatrix(matA * matB)
relativeTranslate = tm.translation()
relativeRotate = tm.rotation()
Vector Operations

Maya's MVector is exposed as cmdx.Vector.

from maya.api import OpenMaya as om
import cmdx

vec = cmdx.Vector(1, 0, 0)

# Dot product
vec * cmdx.Vector(0, 1, 0) == 0.0

# Cross product
vec ^ cmdx.Vector(0, 1, 0) == om.MVector(0, 0, 1)
EulerRotation Operations

Maya's MEulerRotation is exposed as cmdx.EulerRotation and cmdx.Euler

TransformationMatrix Operations

Maya's MTransformationMatrix is exposed as cmdx.TransformationMatrix, cmdx.Transform and cmdx.Tm.

Editing the cmdx version of a Tm is meant to be more readable and usable in maths operations.

import cmdx
from maya.api import OpenMaya as om

# Original
tm = om.MTransformationMatrix()
tm.setTranslation(om.MVector(0, 0, 0))
tm.setRotation(om.MEulerRotation(cmdx.radians(90), 0, 0, cmdx.kXYZ))

# cmdx
tm = cmdx.Tm()
tm.setTranslation((0, 0, 0))
tm.setRotation((90, 0, 0))

In this example, cmdx assumes an MVector on passing a tuple, and that when you specify a rotation you intended to use the same unit as your UI is setup to display, in most cases degrees.

In addition to the default methods, it can also do multiplication of vectors, to e.g. transform a point into the space of a given transform.

import cmdx

tm = cmdx.TransformationMatrix()
tm.setTranslation((0, 0, 0))
tm.setRotation((90, 0, 0))

pos = cmdx.Vector(0, 1, 0)

# Move a point 1 unit in Y, as though it was a child
# of a transform that is rotated 90 degrees in X,
# the resulting position should yield Z=1
newpos = tm * pos
assert newpos == cmdx.Vector(0, 0, 1)
Quaternion Operations

Maya's MQuaternion is exposed via cmdx.Quaternion

In addition to its default methods, it can also do multiplication with a vector.

q = Quaternion(0, 0, 0, 1)
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.

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
  • asMatrix() -> MMatrix
  • asTransformationMatrix() (alias asTm()) -> MTransformationMatrix
  • asQuaternion() -> MQuaternion
  • asVector -> MVector

Query

Filter children by a search query, similar to MongoDB.

cmds.file(new=True, force=True)
a = createNode("transform", "a")
b = createNode("transform", "b", parent=a)
c = createNode("transform", "c", parent=a)

b["bAttr"] = Double(default=5)
c["cAttr"] = Double(default=12)

# Return children with this attribute only
a.child(query=["bAttr"]) == b
a.child(query=["cAttr"]) == c
a.child(query=["noExist"]) is None

# Return children with this attribute *and value*
a.child(query={"bAttr": 5}) == b
a.child(query={"bAttr": 1}) is None

# Search with multiple queries
a.child(query={
  "aAttr": 12,
  "visibility": True,
  "translateX": 0.0,
}) == b

Contains

Sometimes, it only makes sense to query the children of a node for children with a shape of a particular type. For example, you may only interested in children with a shape node.

import cmdx

a = createNode("transform", "a")
b = createNode("transform", "b", parent=a)
c = createNode("transform", "c", parent=a)
d = createNode("mesh", "d", parent=c)

# Return children with a `mesh` shape
assert b.child(contains="mesh") == c

# As the parent has children, but none with a mesh
# the below would return nothing.
assert b.child(contains="nurbsCurve") != c

Geometry Types

cmdx supports reading and writing of geometry attributes via the *Data family of functions.

Drawing a line

import cmdx

parent = cmdx.createNode("transform")
shape = cmdx.createNode("nurbsCurve", parent=parent)
shape["cached"] = cmdx.NurbsCurveData(points=((0, 0, 0), (0, 1, 0), (0, 2, 0)))

This creates a new nurbsCurve shape and fills it with points.

Drawing an arc

Append the degree argument for a smooth curve.

import cmdx

parent = cmdx.createNode("transform")
shape = cmdx.createNode("nurbsCurve", parent=parent)
shape["cached"] = cmdx.NurbsCurveData(
    points=((0, 0, 0), (1, 1, 0), (0, 2, 0)),
    degree=2
)

Drawing a circle

Append the form argument for closed loop.

import cmdx

parent = cmdx.createNode("transform")
shape = cmdx.createNode("nurbsCurve", parent=parent)
shape["cached"] = cmdx.NurbsCurveData(
    points=((1, 1, 0), (-1, 1, 0), (-1, -1, 0), (1, -1, 0)),
    degree=2,
    form=cmdx.kClosed
)

Connections

Connect one attribute to another with one of two syntaxes, whichever one is the most readable.

a, b = map(cmdx.createNode, ("transform", "camera"))

# Option 1
a["translateX"] >> b["translateX"]

# Option 2
a["translateY"].connect(b["translateY"])

Legacy syntax is also supported, and is almost as fast - the overhead is one additional call to str.strip.

cmdx.connectAttr(a + ".translateX", b + ".translateX")

Plug-ins

cmdx is fast enough for use in draw() and compute() of plug-ins.

Usage

import cmdx

class MyNode(cmdx.DgNode):
    name = "myNode"
    typeid = cmdx.TypeId(0x85005)

initializePlugin2 = cmdx.initialize2(MyNode)
uninitializePlugin2 = cmdx.uninitialize2(MyNode)

Simply save this file to e.g. myNode.py and load it from within Maya like this.

from maya import cmds
cmds.loadPlugin("/path/to/myNode.py")
cmds.createNode("myNode")

See also:

Available superclasses:

  • cmdx.DgNode
  • cmdx.SurfaceShape
  • cmdx.SurfaceShapeUI
  • cmdx.LocatorNode

Keep in mind

  • Don't forget to cmds.unloadPlugin before loading it anew
  • Every Maya node requires a globally unique "TypeId"
  • You can register your own series of IDs for free, here
  • Try not to undo the creation of your custom node, as you will be unable to unload it without restarting Maya
  • If two nodes with the same ID exists in the same scene, Maya may crash and will be unable to load the file (if you are even able to save it)
  • The 2 refers to Maya API 2.0, which is the default API used by cmdx. You can alternatively define a variable or function called maya_useNewAPI and use initializePlugin without the suffix 2.
  • See the Maya API Documentation for superclass documentation, these are merely aliases for the original node types, without the prefix M.

Declarative

cmdx comes with a declarative method of writing Maya plug-ins. "Declarative" means that rather than writing instructions for your plug-in, you write a description of it.

Before

from maya.api import OpenMaya as om

class MyNode(om.MPxNode):
    name = "myNode"
    typeid = om.MTypeId(0x85006)

    @staticmethod
    def initializer():
        tAttr = om.MFnTypedAttribute()

        MyNode.myString = tAttr.create(
            "myString", "myString", om.MFnData.kString)
        tAttr.writable = True
        tAttr.storable = True
        tAttr.hidden = True
        tAttr.array = True

        mAttr = om.MFnMessageAttribute()
        MyNode.myMessage = mAttr.create("myMessage", "myMessage")
        mAttr.writable = True
        mAttr.storable = True
        mAttr.hidden = True
        mAttr.array = True

        xAttr = om.MFnMatrixAttribute()
        MyNode.myMatrix = xAttr.create("myMatrix", "myMatrix")
        xAttr.writable = True
        xAttr.storable = True
        xAttr.hidden = True
        xAttr.array = True

        uniAttr = om.MFnUnitAttribute()
        MyNode.currentTime = uniAttr.create(
            "currentTime", "ctm", om.MFnUnitAttribute.kTime, 0.0)

        MyNode.addAttribute(MyNode.myString)
        MyNode.addAttribute(MyNode.myMessage)
        MyNode.addAttribute(MyNode.myMatrix)
        MyNode.addAttribute(MyNode.currentTime)

        MyNode.attributeAffects(MyNode.myString, MyNode.myMatrix)
        MyNode.attributeAffects(MyNode.myMessage, MyNode.myMatrix)
        MyNode.attributeAffects(MyNode.currentTime, MyNode.myMatrix)

After

Here is the equivalent plug-in, written with cmdx.

import cmdx

class MyNode(cmdx.DgNode):
    name = "myNode"
    typeid = cmdx.TypeId(0x85006)

    attributes = [
        cmdx.String("myString"),
        cmdx.Message("myMessage"),
        cmdx.Matrix("myMatrix"),
        cmdx.Time("myTime", default=0.0),
    ]

    affects = [
        ("myString", "myMatrix"),
        ("myMessage", "myMatrix"),
        ("myTime", "myMatrix"),
    ]

Defaults

Defaults can either be specified as an argument to the attribute, e.g. cmdx.Double("MyAttr", default=5.0) or in a separate dictionary.

This can be useful if you need to synchronise defaults between, say, a plug-in and external physics simulation software and if you automatically generate documentation from your attributes and need to access their defaults from another environment, such as sphinx.

import cmdx
import external_library

class MyNode(cmdx.DgNode):
    name = "myNode"
    typeid = cmdx.TypeId(0x85006)

    defaults = external_library.get_defaults()

    attributes = [
        cmdx.String("myString"),
        cmdx.Message("myMessage"),
        cmdx.Matrix("myMatrix"),
        cmdx.Time("myTime"),
    ]

Where defaults is a plain dictionary.

import cmdx


class MyNode(cmdx.DgNode):
    name = "myNode"
    typeid = cmdx.TypeId(0x85006)

    defaults = {
        "myString": "myDefault",
        "myTime": 1.42,
    }

    attributes = [
        cmdx.String("myString"),
        cmdx.Message("myMessage"),
        cmdx.Matrix("myMatrix"),
        cmdx.Time("myTime"),
    ]

This can be used with libraries such as jsonschema, which is supported by other languages and libraries like C++ and sphinx.


Draw()

cmdx exposes the native math libraries of Maya, and extends these with additional functionality useful for drawing to the viewport.

import cmdx
from maya.api import OpenMaya as om
from maya import OpenMayaRender as omr1

renderer = omr1.MHardwareRenderer.theRenderer()
gl = renderer.glFunctionTable()
maya_useNewAPI = True


class MyNode(cmdx.LocatorNode):
    name = "myNode"

    classification = "drawdb/geometry/custom"
    typeid = cmdx.TypeId(0x13b992)
    attributes = [
        cmdx.Distance("Length", default=5)
    ]

    def draw(self, view, path, style, status):
        this = cmdx.Node(self.thisMObject())
        length = this["Length", cmdx.Cached].read()

        start = cmdx.Vector(0, 0, 0)
        end = cmdx.Vector(length, 0, 0)

        gl.glBegin(omr1.MGL_LINES)
        gl.glColor3f(0.1, 0.65, 0.0)
        gl.glVertex3f(start.x, start.y, start.z)
        gl.glVertex3f(end.x, end.y, end.z)
        gl.glEnd()

        view.endGL()

    def isBounded(self):
        return True

    def boundingBox(self):
        this = cmdx.Node(self.thisMObject())
        multiplier = this["Length", cmdx.Meters].read()
        corner1 = cmdx.Point(-multiplier, -multiplier, -multiplier)
        corner2 = cmdx.Point(multiplier, multiplier, multiplier)
        return cmdx.BoundingBox(corner1, corner2)


initializePlugin = cmdx.initialize(MyNode)
uninitializePlugin = cmdx.uninitialize(MyNode)

Of interest is the..

  1. cmdx.Node(self.thisMObject()) A one-off (small) cost, utilising the Node Re-use mechanism of cmdx to optimise instantiation of new objects.
  2. Attribute access via ["Length"], fast and readable compared to its OpenMaya equivalent
  3. Custom units via ["Length", cmdx.Meters]
  4. Custom vectors via cmdx.Vector()
  5. Attribute value re-use, via cmdx.Cached. boundingBox is called first, computing the value of Length, which is later re-used in draw(); saving on previous FPS

Compute()

Attribute Editor Template

Generate templates from your plug-ins automatically.


Iterators

Any method on a Node returning multiple values do so in the form of an iterator.

a = cmdx.createNode("transform")
b = cmdx.createNode("transform", parent=a)
c = cmdx.createNode("transform", parent=a)

for child in a.children():
   pass

Because it is an iterator, it is important to keep in mind that you cannot index into it, nor compare it with a list or tuple.

a.children()[0]
ERROR

a.children() == [b, c]
False  # The iterator does not equal the list, no matter the content

From a performance perspective, returning all values from an iterator is equally fast as returning them all at once, as cmds does, so you may wonder why do it this way?

It's because an iterator only spends time computing the values requested, so returning any number less than the total number yields performance benefits.

i = a.children()
assert next(i) == b
assert next(i) == c

For convenience, every iterator features a corresponding "singular" version of said iterator for readability.

assert a.child() == b

More iterators

  • a.children()
  • a.connections()
  • a.siblings()
  • a.descendents()

Transactions

cmdx supports the notion of an "atomic commit", similar to what is commonly found in database software. It means to perform a series of commands as though they were one.

The differences between an atomic and non-atomic commit with regards to cmdx is the following.

  1. Commands within an atomic commit are not executed until committed as one
  2. An atomic commit is undoable as one

(1) means that if a series of commands where to be "queued", but not committed, then the Maya scenegraph remains unspoiled. It also means that executing commands is faster, as they are merely added to the end of a series of commands that will at some point be executed by Maya, which means that if one of those commands should fail, you will know without having to wait for Maya to spend time actually performing any of the actions.

Known Issues

It's not all roses; in order of severity:

  1. Errors are not known until finalisation, which can complicate debugging
  2. Errors are generic; they don't mention what actually happened and only says RuntimeError: (kFailure): Unexpected Internal Failure #
  3. Not all attribute types can be set using a modifier
  4. Properties of future nodes are not known until finalisation, such as its name, parent or children

Modifier

Modifiers in cmdx extend the native modifiers with these extras.

  1. Automatically undoable Like cmds
  2. Atomic Changes are automatically rolled back on error, making every modifier atomic
  3. Debuggable Maya's native modifier throws an error without including what or where it happened. cmdx provides detailed diagnostics of what was supposed to happen, what happened, attempts to figure out why and what line number it occurred on.
  4. Name templates Reduce character count by delegating a "theme" of names across many new nodes.

For example.

import cmdx

with cmdx.DagModifier() as mod:
    parent = mod.createNode("transform", name="MyParent")
    child = mod.createNode("transform", parent=parent)
    mod.setAttr(parent + ".translate", (1, 2, 3))
    mod.connect(parent + ".rotate", child + ".rotate")

Now when calling undo, the above lines will be undone as you'd expect.

There is also a completely equivalent PEP8 syntax.

with cmdx.DagModifier() as mod:
    parent = mod.create_node("transform", name="MyParent")
    child = mod.create_node("transform", parent=parent)
    mod.set_attr(parent + ".translate", (1, 2, 3))
    mod.connect(parent + ".rotate", child + ".rotate")

Name templates look like this.

with cmdx.DagModifier(template="myName_{type}") as mod:
  node = mod.createNode("transform")

assert node.name() == "myName_transform"
Connect To Newly Created Attribute

Creating a new attribute returns a "promise" of that attribute being created. You can pass that to connectAttr to both create and connect attributes in the same modifier.

with cmdx.DagModifier() as mod:
    node = mod.createNode("transform")
    attr = mod.createAttr(node, cmdx.Double("myNewAttr"))
    mod.connectAttr(node["translateX"], attr)

You can even connect two previously unexisting attributes at the same time with connectAttrs.

with cmdx.DagModifier() as mod:
    node = mod.createNode("transform")
    attr1 = mod.createAttr(node, cmdx.Double("attr1"))
    attr2 = mod.createAttr(node, cmdx.Double("attr2"))
    mod.connectAttrs(node, attr1, node, attr2)
Convenience Historyically Interesting

Sometimes you're creating a series of utility nodes that you don't want visible in the channel box. So you can either go..

with cmdx.DGModifier() as mod:
    reverse = mod.createNode("reverse")
    multMatrix = mod.createNode("multMatrix")
    mod.set_attr(reverse["isHistoricallyInteresting"], False)
    mod.set_attr(multMatrix["isHistoricallyInteresting"], False)

..or use the convenience argument to make everything neat.

with cmdx.DGModifier(interesting=False) as mod:
    mod.createNode("reverse")
    mod.createNode("multMatrix")
Convenience Try Set Attr

Sometimes you aren't too concerned whether setting an attribute actually succeeds or not. Perhaps you're writing a bulk-importer, and it'll become obvious to the end-user whether attributes were set or not, or you simply could not care less.

For that, you can either..

with cmdx.DagModifier() as mod:
    try:
        mod.setAttr(node["attr1"], 5.0)
    except cmdx.LockedError:
        pass  # This is OK
    try:
        mod.setAttr(node["attr2"], 5.0)
    except cmdx.LockedError:
        pass  # This is OK
    try:
        mod.setAttr(node["attr3"], 5.0)
    except cmdx.LockedError:
        pass  # This is OK

..or you can use the convenience trySetAttr to ease up on readability.

with cmdx.DagModifier() as mod:
    mod.trySetAttr(node["attr1"], 5.0)
    mod.trySetAttr(node["attr2"], 5.0)
    mod.trySetAttr(node["attr3"], 5.0)
Convenience Set Attr

Sometimes, the attribute you're setting is connected to by another attribute. Maybe driven by some controller on a character rig?

In such cases, the attribute cannot be set, and must set whichever attribute is feeding into it instead. So you could..

with cmdx.DagModifier() as mod:
    if node["myAttr"].connected:
        other = node["myAttr"].connection(destination=False, plug=True)
        mod.setAttr(other["myAttr"], 5.0)
    else:
        mod.setAttr(node["myAttr"], 5.0)

Or, you can use the smart_set_attr to automate this process.

with cmdx.DagModifier() as mod:
    mod.smartSetAttr(node["myAttr"], 5.0)
Limitations

The modifier is quite limited in what features it provides; in general, it can only modify the scenegraph, it cannot query it.

  1. It cannot read attributes
  2. It cannot set complex attribute types, such as meshes or nurbs curves
  3. It cannot query a future hierarchy, such as asking for the parent or children of a newly created node unless you call doIt() first)

PEP8 Dual Syntax

Write in either Maya-style mixedCase or PEP8-compliant snake_case where it makes sense to do so. Every member of cmdx and its classes offer a functionally identical snake_case alternative.

Example

import cmdx

# Maya-style
cmdx.createNode("transform")

# PEP8
cmdx.create_node("transform")

When to use

Consistency aids readability and comprehension. When a majority of your application is written using mixedCase it makes sense to use it with cmdx as well. And vice versa.


Comparison

This section explores the relationship between cmdx and (1) MEL, (2) cmds, (3) PyMEL and (4) API 1/2.

MEL

Maya's Embedded Language (MEL) makes for a compact scene description format.

createNode transform -n "myNode"
  setAttr .tx 12
  setAttr .ty 9

On creation, a node is "selected" which is leveraged by subsequent commands, commands that also reference attributes via their "short" name to further reduce file sizes.

A scene description never faces naming or parenting problems the way programmers do. In a scene description, there is no need to rename nor reparent; a node is created either as a child of another, or not. It is given a name, which is unique. No ambiguity.

From there, it was given expressions, functions, branching logic and was made into a scripting language where the standard library is a scene description kit.

cmds is tedious and pymel is slow. cmds is also a victim of its own success. Like MEL, it works with relative paths and the current selection; this facilitates the compact file format, whereby a node is created, and then any references to this node is implicit in each subsequent line. Long attribute names have a short equivalent and paths need only be given at enough specificity to not be ambiguous given everything else that was previously created. Great for scene a file format, not so great for code that operates on-top of this scene file.

PyMEL

PyMEL is 31,000 lines of code, the bulk of which implements backwards compatibility to maya.cmds versions of Maya as far back as 2008, the rest reiterates the Maya API.

Line count

PyMEL has accumulated a large number of lines throughout the years.

root@0e540f42ee9d:/# git clone https://github.com/LumaPictures/pymel.git
Cloning into 'pymel'...
remote: Counting objects: 21058, done.
remote: Total 21058 (delta 0), reused 0 (delta 0), pack-reused 21058
Receiving objects: 100% (21058/21058), 193.16 MiB | 15.62 MiB/s, done.
Resolving deltas: 100% (15370/15370), done.
Checking connectivity... done.
root@0e540f42ee9d:/# cd pymel/
root@0e540f42ee9d:/pymel# ls
CHANGELOG.rst  LICENSE  README.md  docs  examples  extras  maintenance  maya  pymel  setup.py  tests
root@0e540f42ee9d:/pymel# cloc pymel/
      77 text files.
      77 unique files.
       8 files ignored.

http://cloc.sourceforge.net v 1.60  T=0.97 s (71.0 files/s, 65293.4 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Python                          67           9769          22410          31251
DOS Batch                        2              0              0              2
-------------------------------------------------------------------------------
SUM:                            69           9769          22410          31253
-------------------------------------------------------------------------------

Third-party

Another wrapping of the Maya API is MRV, written by independent developer Sebastian Thiel for Maya 8.5-2011, and Metan

Unlike cmdx and PyMEL, MRV (and seemingly Metan) exposes the Maya API as directly as possible.

See the Comparison page for more details.


YAGNI

The Maya Ascii file format consists of a limited number of MEL commands that accurately and efficiently reproduce anything you can achieve in Maya. This format consists of primarily 4 commands.

  • createNode
  • addAttr
  • setAttr
  • connectAttr

You'll notice how there aren't any calls to reparent, rename otherwise readjust created nodes. Nor are there high-level commands such as cmds.polySphere or cmds.move. These 4 commands is all there is to represent the entirety of the Maya scenegraph; including complex rigs, ugly hacks and workarounds by inexperienced and seasoned artists alike.

The members of cmdx is a reflection of this simplicity.

However, convenience members make for more readable and maintainable code, so a balance must be struck between minimalism and readability. This balance is captured in cmdx.encode and cmdx.decode which acts as a bridge between cmds and cmdx. Used effectively, you should see little to no performance impact when performing bulk-operations with cmdx and passing the resulting nodes as transient paths to cmds.


Timings

cmdx is on average 142.89x faster than PyMEL on these common tasks.

Times Task
cmdx is 2.2x faster addAttr
cmdx is 4.9x faster setAttr
cmdx is 7.5x faster createNode
cmdx is 2.6x faster connectAttr
cmdx is 50.9x faster long
cmdx is 16.6x faster getAttr
cmdx is 19.0x faster node.attr
cmdx is 11.3x faster node.attr=5
cmdx is 1285.6x faster import
cmdx is 148.7x faster listRelatives
cmdx is 22.6x faster ls

cmdx is on average 2.53x faster than cmds on these common tasks.

Times Task
cmdx is 1.4x faster addAttr
cmdx is 2.3x faster setAttr
cmdx is 4.8x faster createNode
cmdx is 2.1x faster connectAttr
cmdx is 8.0x faster long
cmdx is 1.8x faster getAttr
cmdx is 0.0x faster import
cmdx is 1.8x faster listRelatives
cmdx is 0.5x faster ls

Run plot.py to reproduce these numbers.


Measurements

Below is a performance comparisons between the available methods of manipulating the Maya scene graph.

  • MEL
  • cmds
  • cmdx
  • PyMEL
  • API 1.0
  • API 2.0

Surprisingly, MEL is typically outdone by cmds. Unsurprisingly, PyMEL performs on average 10x slower than cmds, whereas cmdx performs on average 5x faster than cmds.


Overall Performance

Shorter is better.

import

Both cmdx and PyMEL perform some amount of preprocessing on import.

createNode

getAttr

setAttr

connectAttr

allDescendents

long

Retrieving the long name of any node, e.g. cmds.ls("node", long=True).

node.attr

Both cmdx and PyMEL offer an object-oriented interface for reading and writing attributes.

# cmdx
node["tx"].read()
node["tx"].write(5)

# PyMEL
pynode.tx().get()
pynode.tx().set(5)


Evolution

cmdx started as a wrapper for cmds where instead of returning a transient path to nodes, it returned the new UUID attribute of Maya 2016 and newer. The benefit was immediate; no longer had I to worry about whether references to any node was stale. But it impacted negatively on performance. It was effectively limited to the performance of cmds plus the overhead of converting to/from the UUID of each absolute path.

The next hard decision was to pivot from being a superset of cmds to a subset; to rather than wrapping the entirety of cmds instead support a minimal set of functionality. The benefit of which is that more development and optimisation effort is spent on less functionality.


References

These are some of the resources used to create this project.


FAQ

Why is it crashing?

cmdx should never crash (if it does, please submit a bug report!), but the cost of performance is safety. maya.cmds rarely causes a crash because it has safety procedures built in. It double checks to ensure that the object you operate on exists, and if it doesn't provides a safe warning message. This double-checking is part of what makes maya.cmds slow; conversely, the lack of it is part of why cmdx is so fast.

Common causes of a crash is:

  • Use of a node that has been deleted
  • ... (add your issue here)

This can happen when, for example, you experiment in the Script Editor, and retain access to nodes created from a different scene, or after the node has simply been deleted.

Can I have attribute access via ".", e.g. myNode.translate?

Unfortunately not, it isn't safe.

The problem is how it shadows attribute access for attributes on the object itself with attributes in Maya. In the above example, translate could refer to a method that translates a given node, or it could be Maya's .translate attribute. If there isn't a method in cmdx to translate a node today, then when that feature is introduced, your code would break.

Furthermore it makes the code more difficult to read, as the reader won't know whether an attribute is referring to an Maya attribute or an attribute or method on the object.

With the dictionary access - e.g. myNode["translate"], there's no question about this.

Why is PyMEL slow?

...

Doesn't PyMEL also use the Maya API?

Yes and no. Some functionality, such as listRelatives call on cmds.listRelatives and later convert the output to instances of PyNode. This performs at best as well as cmds, with the added overhead of converting the transient path to a PyNode.

Other functionality, such as pymel.core.datatypes.Matrix wrap the maya.api.OpenMaya.MMatrix class and would have come at virtually no cost, had it not inherited 2 additional layers of superclasses and implemented much of the computationally expensive functionality in pure-Python.


Debugging

Either whilst developing for or with cmdx, debugging can come in handy.

For performance, you might be interested in CMDX_TIMINGS below. For statistics on the various types of reuse, have a look at this.

import cmdx
cmdx.createNode("transform", name="MyTransform")
cmdx.encode("|MyTransform")
print(cmdx.NodeReuseCount)
# 0

cmdx.encode("|MyTransform")
cmdx.encode("|MyTransform")

print(cmdx.NodeReuseCount)
# 2

Available Statistics

Gathering these members are cheap and happens without setting any flags.

  • cmdx.NodeReuseCount
  • cmdx.NodeInitCount
  • cmdx.PlugReuseCount

Flags

For performance and debugging reasons, parts of cmdx can be customised via environment variables.

IMPORTANT - The below affects only the performance and memory characteristics of cmdx, it does not affects its functionality. That is to say, these can be switched on/off without affecting or require changes to your code.

Example

$ set CMDX_ENABLE_NODE_REUSE=1
$ mayapy

NOTE: These can only be changed prior to importing or reloading cmdx, as they modify the physical layout of the code.

CMDX_ENABLE_NODE_REUSE

This opt-in variable enables cmdx to keep track of any nodes it has instantiated in the past and reuse its instantiation in order to save time. This will have a neglible impact on memory use (1 mb/1,000,000 nodes)

node = cmdx.createNode("transform", name="myName")
assert cmdx.encode("|myName") is node

CMDX_ENABLE_PLUG_REUSE

Like node reuse, this will enable each node to only ever look-up a plug once and cache the results for later use. These two combined yields a 30-40% increase in performance.

CMDX_TIMINGS

Print timing information for performance critical sections of the code. For example, with node reuse, this will print the time taken to query whether an instance of a node already exists. It will also print the time taken to create a new instance of said node, such that they may be compared.

WARNING: Use sparingly, or else this can easily flood your console.

CMDX_MEMORY_HOG_MODE

Do not bother cleaning up after yourself. For example, callbacks registered to keep track of when a node is destroyed is typically cleaned up in order to avoid leaking memory. This however comes at a (neglible) cost which this flag prevents.

CMDX_IGNORE_VERSION

cmdx was written with Maya 2015 SP3 and above in mind and will check on import whether this is true to avoid unexpected side-effects. If you are sure an earlier version will work fine, this variable can be set to circumvent this check.

If you find this to be true, feel free to submit a PR lowering this constant!

CMDX_ROGUE_MODE

In order to save on performance, cmdx holds onto MObject and MFn* instances. However this is discouraged in the Maya API documentation and can lead to a number of problems unless handled carefully.

The carefulness of cmdx is how it monitors the destruction of any node via the MNodeMessage.addNodeDestroyedCallback and later uses the result in access to any attribute.

For example, if a node has been created..

node = cmdx.createNode("transform")

And a new scene created..

cmds.file(new=True, force=True)

Then this reference is no longer valid..

node.name()
Traceback (most recent call last):
...
ExistError: "Cannot perform operation on deleted node"

Because of the above callback, this will throw a cmdx.ExistError (inherits RuntimeError).

This callback, and checking of whether the callback has been called, comes at a cost which "Rogue Mode" circumvents. In Rogue Mode, the above would instead cause an immediate and irreversible fatal crash.

CMDX_SAFE_MODE

The existence of this variable disables any of the above optimisations and runs as safely as possible.


Notes

Additional thoughts.

MDagModifier

createNode of OpenMaya.MDagModifier is ~20% faster than cmdx.createNode excluding load. Including load is 5% slower than cmdx.

from maya.api import OpenMaya as om

mod = om.MDagModifier()

def prepare():
    New()
    for i in range(10):
        mobj = mod.createNode(cmdx.Transform)
        mod.renameNode(mobj, "node%d" % i)

def createManyExclusive():
    mod.doIt()


def createManyInclusive():
    mod = om.MDagModifier()

    for i in range(10):
        mobj = mod.createNode(cmdx.Transform)
        mod.renameNode(mobj, "node%d" % i)

    mod.doIt()

def createMany(number=10):
    for i in range(number):
        cmdx.createNode(cmdx.Transform, name="node%d" % i)

Test("API 2.0", "createNodeBulkInclusive", createManyInclusive, number=1, repeat=100, setup=New)
Test("API 2.0", "createNodeBulkExclusive", createManyExclusive, number=1, repeat=100, setup=prepare)
Test("cmdx", "createNodeBulk", createMany, number=1, repeat=100, setup=New)

# createNodeBulkInclusive API 2.0: 145.2 ms (627.39 ยตs/call)
# createNodeBulkExclusive API 2.0: 132.8 ms (509.58 ยตs/call)
# createNodeBulk cmdx: 150.5 ms (620.12 ยตs/call)

Examples

One-off examples using cmdx.

Transferring of attributes

Zeroing out rotate by moving them to jointOrient.

from maya import cmds
import cmdx

for joint in cmdx.ls(selection=True, type="joint"):
    joint["jointOrient", cmdx.Degrees] = joint["rotate"]
    joint["rotate"] = 0

Transferring the orientation of a series of joints to the jointOrient

cmdx's People

Contributors

chelloiaco avatar monkeez avatar mottosso avatar muream avatar vonbehr avatar wougzy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cmdx's Issues

Modifiers and data types

This bit me recently.

node = cmdx.createNode("translate")
node["translateX"] = 5
node["translateY"] = 5
node["translateZ"] = 5

with cmdx.DagModifier() as mod:
  mod.setAttr(node["translate"], 0)

What value do you expect translate to have?

  1. [0, 0, 0]?
  2. [5, 5, 5]?
  3. [5, 0, 0]?
  4. [0, 5, 5]?
  5. [15161353, 135135136425, 63513]?

That's right! The numbers will be garbage!

What happened?

Here's what the underlying code looks like.

if isinstance(value, int):
    mod.newPlugValueInt(mplug, value)

Where value is what you gave the modifier, in this case 0. 0 is an integer, which would prompt a call to MDGModifier.newPlugValue(). Notice how it has Int int its name? Whatever value you give it, it will assume that it is an int. An int has a pre-defined number of bytes, so what Maya will do here is reach into your int for that number of bytes, even though the actual attribute needs many more bytes than that, as it is a compound attribute. translateX, Y and Z.

So, translateX migth be given the correct value, but the remaining values would get garbage. You didn't pass enough bytes, it would likely try and reach into memory it doesn't own and assign that. Resulting in gibberish values, possibly random, possibly random unless that space in memory is occupied by something else, like a YouTube video or whatever.

How do I solve it?

Make sure you pass enough data. Maya will not tell you whether or not you did, you'll have to know what you are doing.

# Alternative 1
with cmdx.DagModifier() as mod:
  mod.setAttr(node["translateX"], 0)
  mod.setAttr(node["translateY"], 0)
  mod.setAttr(node["translateZ"], 0)

# Alternative 2

with cmdx.DagModifier() as mod:
  mod.setAttr(node["translateX"], [0, 0, 0])

Future Work

Normal attribute setters account for this.

node = cmdx.createNode("transform")
node["translate"] = 5

This figures out that you meant (5, 5, 5) because translate is a compound of 3 children.

We could address this for modifiers in the same way. If it's a Vector3, and you've passed a single int, convert that int to 3 floats. It would come at a performance cost (likely from querying of the attribute type), so we could put it behind a safe=True that defaults to True and enables the user to optimise by disabling it.

Matrix access API

Maya 2020 and beyond has a greatly improved UI for dealing with matrices. Let's facilitate that.

Sometimes, you just want to view or modify members of a matrix.

>>> import cmdx
>>> mat = cmdx.Matrix4()
>>> mat[1, 3]
5.5
>>> mat[0, 3] = 12  # I.e. translate X
>>> mat[0]  # Print row
(1, 0, 0, 12)
>>> mat[, 2]  # Print column
(1, 0, 0, 0)
>>> mat[0] = (1, 0, 0, 11)  # Write row
>>> someNode["myMatrix"] = cmdx.MatrixAttribute(default=mat)

Replicate NumPy's array interface for familiarity.

Multiple versions simultaneously

You can embed cmdx in a library and distribute it alongside your tool. This has the benefit of not depending on anything external and guarantee the functionality and behavior of your tool. As well as enabling you to make changes to the embedded version of cmdx specific to your tool.

But, the user currently can't have two or more versions of cmdx loaded at the same time if their versions differ. E.g. one version embedded in toolA and another version embedded in toolB and maybe a third version installed globally. That's because the undo/redo mechanism is built as a Maya plug-in, and there can only ever be one plug-in by the name of cmdx. That's a bummer! And a bug.

DagNode.addChild crashes in certain circumstances

i identified a crash with addChild easy to reproduce

try this :

import cmdx as mx
j0 = mx.create_node(mx.tJoint)
j1 = mx.create_node(mx.tJoint, parent=j0)
j2 = mx.create_node(mx.tJoint, parent=j1)
j0.add_child(j2)

it crashes probably because j2 is a descendent of j0. you can fix it by removing the hierarchy link before like this

    def addChild(self, child, index=Last):
        mobject = child._mobject
        parent = child.parent()
        if parent is not None:
            parent._fn.removeChild(mobject)
        self._fn.addChild(mobject, index)

i think this is the simplest form possible since checking ancestors would require to find parents anyway. but i'm not sure about the performance impact of removing children if not necessary
what do you think?

Deploy on CI

@monkeez I've moved CI from Azure to GitHub Actions, since Azure wasn't playing ball anymore. It wasn't providing a good UI on GitHub either, making it hard to spot when and where things went wrong.

The documentation deployment got lost in the transition, and there are a few too many things I don't recognise in the setup. Would you happen to have a moment to see if you can port it over?

  # -----------------------------------------------------------------------
  #
  # Deploy docs
  #
  # -----------------------------------------------------------------------

  - job: Deploy_Docs
    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
    pool:
      vmImage: "ubuntu-latest"
    steps:
      - task: DownloadPipelineArtifact@2
        inputs:
          artifact: docs
          path: $(Build.SourcesDirectory)/build/html

      - script: |
          git init
          git config --local user.name "Azure Pipelines"
          git config --local user.email "[email protected]"
          git add .
          touch .nojekyll
          git add .nojekyll
          git commit -m "Commit generated documentation"
        displayName: "Commit pages"
        workingDirectory: build/html

      - task: InstallSSHKey@0
        inputs:
          knownHostsEntry: $(KNOWN_HOST)
          sshKeySecureFile: deploy_key

      - script: |
          git remote add origin [email protected]:mottosso/cmdx.git
          git push -f origin HEAD:gh-pages
        displayName: "Publish GitHub Pages"
        workingDirectory: build/html

    dependsOn: Maya

I think it's safe to write as a separate "workflow" that only triggers on tags, similar to the PyPI deployment workflow currently in place.

Auto offset group

Zero out translate/rotate channels by adding an intermediate parent group to hold those values. This can be preferable to offsetParentMatrix since that's only supported by Maya 2020+.

Usage

Select one or more nodes with non-zero translate/rotate channels, and run this.

import cmdx

with cmdx.DagModifier() as mod:
    for src in cmdx.selection():
        group = mod.create_node("transform",
                                 name=src.name() + "_parent",
                                 parent=src.parent())
        mod.set_attr(group["translate"], src["translate"])
        mod.set_attr(group["rotate"], src["rotate"])
        mod.set_attr(src["translate"], (0, 0, 0))
        mod.set_attr(src["rotate"], (0, 0, 0))

        mod.parent(src, group)
offsetgroup.mp4

Compound of Compounds

For some reason, compound children of a compound locks up Maya, presumably there's an infinite loop somewhere.

import cmdx

node = cmdx.createNode("transform")

# This works
node.addAttr(
    cmdx.Compound("parent", children=[
        cmdx.Double("age"),
        cmdx.Double("height")
    ])
)

# This does *not*
node.addAttr(
    cmdx.Compound("parent", children=[
        cmdx.Compound("child", children=[
            cmdx.Double("age"),
            cmdx.Double("height")
        ])
    ])
)

DGModifier.disconnect and Undo

For whatever reason, you can't disconnect and connect in the same "do_it" batch without breaking undo.

tx = cmdx.createNode("animCurveTL")
tx.keys(times=[1, 2, 3], values=[0.0, 1.0, 0.0])

tm = cmdx.createNode("transform")
tm["tx"] << tx["output"]

with cmdx.DagModifier() as mod:
  mod.disconnect(tm["tx"])
  mod.connect(tm["sx"], tm["tx"])

cmds.undo()
# Error: (kFailure): Unexpected Internal Failure
# Traceback (most recent call last):
#   File "C:/cmdx.py", line 5548, in undoIt
#     shared.undos[self.undo]()
# RuntimeError: (kFailure): Unexpected Internal Failure # 

Plugs not taking and returning UI units

Hey, whilst implementing the AnimCurve updates (almost done), I noticed this bug with the way plugs are read and written.

Under the Units section of the README it mentions that cmdx takes and returns values in units used by the UI, but this doesn't seem to be the case. It looks like it's currently using Maya's internal units by default. You can confirm this by running this (with Maya UI set to degrees):

node = cmdx.createNode(cmdx.tTransform)
node["rx"] = 5
node["rx", cmdx.Degrees]
# Result: 286.478897565 #

I believe I know how to fix this and am happy to do it, but it will be a bit of work so I'm just checking that using the UI units is the intended functionality before I start?

It seems like using the UI units would make the most sense.

Attribute reuse and undo

Whenever we access a plug, we initiate a call to MFnDependencyNode.findPlug which searches for a plug by name. We speed that up by caching any previous search and store the found plug like this.

if "plugName" in cache:
  return cache["plugName"]

Which means we no longer perform the search and instead return it by hashing this native Python dictionary. Fast. And that works great, with one exception. Undo!

When an attribute is created and found, it is stored in the cache. If the user then undos the creation, the found attribute remains behind. So any subsequent accesses to this plug would leave Maya scratching it's head.

image

Any attempt at removing the "ghost connection" results in..

// Error: line 1: There is no connection from 'l_hair_4_2.blendSimulation' to 'pairBlend2.weight' to disconnect //

You are however able to re-connect to the attribute. But that's not good enough.

Add type annotations

Ive been using cmdx on and off for a few months now and I've been very happy with it!
My only (mild) pain point with it is the lack of type annotations

Would you be open for me to add that in?

I'm asking here before making a PR because I feel like this is something that should be supported by every contributor going forward so I don't want to start working on it if that's not something you want ๐Ÿ™‚

Mypy can be used to ensure the annotations are good as part of the CI/CD so it can be enforced and made consistent.

Type annotations can either be using the python 3 syntax:

def createNode(type: Union[str, MTypeId], name: Optional[str] = None, parent: Optional[DagNode] = None) -> Node:
    ...

or the python 2 compatible comment based syntax:

def createNode(type, name=None, parent=None):
    # type: (Union[str, MTypeId], Optional[str], Optional[DagNode]) -> Node
    ...

The python 3 syntax is more comfortable to use though considering cmdx supports older versions of Maya, the comment based syntax is probably the only viable option. We use this at work and works very well with both Mypy and Pylance (Apparently PyCharm supports it as well)

Let me know your thoughts on this ๐Ÿ‘

Direct passing of cmdx.Node to vanilla cmds

Using cmds.listRelatives as example, are there plans to allow direct passing of cmdx.Node objects, without explicitly converting them?
At the moment this is possible using cmds.listRelatives( str( myNode ) ) but will error out if the object is passed directly.
I tried this myself a while ago, but providing str() on the object is not enough to fool cmds, you actually need to subclass str itself. My solution never worked too well, I wondered if you had any other ideas on it, or plans for this in general.

Animation Import/Export

Compatible with Maya 2015 SP4 --> Maya 2021+

Out of the many ways to export/import animation in Maya, from the built-in .atom format from 2013 to the .anim file format form 1993, here's a different take.

  1. ---> Export related animCurve nodes as a mayaAscii scene
  2. <--- Import animCurve nodes from the mayaAscii scene, elsewhere
  3. -><- Connect each node to their corresponding target

It is..

  • Simple ~100 lines of Python, no "clipboard", no "views", no "templates", no MEL, no smarts
  • Robust In that the original animation is preserved exactly
  • Lightweight In that only animated channels are exported, no pre-processing required
  • Fast As fast as Maya is able to export and import scene files
  • Native As a regular Maya Ascii file, import it yourself, anywhere and without the need for Python

With a few caveats..

  • No static channels
  • No animation layers
  • No partial-range export or import
  • Names must match exactly between export/import targets

Each of which could potentially be solved with some further tinkering.

animio1


Usage

  1. Download cmdx to your ~/maya/scripts folder
  2. Copy/paste the implementation below, and use it like this..

Based on your current selection

# Custom suffix
fname = cmds.file("anim1.manim", expandName=True, query=True)

# From scene A
export_animation(fname)

# From scene B
import_animation(fname)

Implementation

import cmdx
from maya import cmds  # for select()

def export_animation(fname):
    """Export animation for selected nodes to `fname`

    Animation is exported as native Maya nodes, e.g. animCurveTU
    and later imported and re-connected to their original nodes
    
    Limitations:
        - No animation layers
        - Names much match exactly between exported and imported nodes

    """

    animation = []
    for node in cmdx.selection(type="transform"):  # Optionally limited to transform nodes

        # Find any curve connecting to this node
        for curve in node.connections():
            if not isinstance(curve, cmdx.AnimCurve):
                continue

            # Encode target connection as string attribute
            # for retrieval during `import_animation`
            if not curve.has_attr("target"):
                curve["target"] = cmdx.String()

            # Find the attribute to which this curve connects
            plug = curve.connection(plug=True)
            curve["target"] = plug.path()
    
            animation.append(curve)

    if not animation:
        cmds.warning(
            "Select objects in the scene to "
            "export any connected animation"
        )

        return cmds.warning("No animation found, see Script Editor for details")

    previous_selection = cmds.ls(selection=True)
    cmds.select(map(str, animation))

    try:
        cmds.file(
            fname,

            # Overwrite existing
            force=True,

            # Use our own suffix
            defaultExtensions=False,

            # Internal format, despite our format
            type="mayaAscii",

            exportSelected=True,

            # We don't want anything but the selected animation curves
            constructionHistory=False
        )

    except Exception:
        import traceback
        traceback.print_exc()
        cmds.warning("Something unexpected happened when trying to export, see Script Editor for details.")

    finally:
        cmds.select(previous_selection)

    print("Successfully exported '%s'" % fname)


def import_animation(fname):
    previous_selection = cmds.ls(selection=True)

    try:
        animation = cmds.file(fname, i=True, returnNewNodes=True)

    except Exception:
        import traceback
        traceback.print_exc()
        return cmds.warning(
            "Something unexpected happened when trying "
            "to import '%s', see Script Editor for details"
            % fname
        )

    finally:
        cmds.select(previous_selection)

    for curve in animation:
        curve = cmdx.encode(curve)

        if not curve.has_attr("target"):
            cmds.warning("Skipped: '%s' did not have the `target` "
                                  "attribute used to reconnect the animation"
                                  % curve)
            continue

        # Stored as "<node>.<attr>" e.g. "pCube1.tx"
        node, attr = curve["target"].read().rsplit(".", 1)

        try:
            target = cmdx.encode(node)
        except cmdx.ExistError:
            cmds.warning("Skipped: '%s' did not exist" % node)
            continue

        # Make the connection, replacing any existing
        curve["output"] >> target[attr]

    print("Successfully imported '%s'!" % fname)

Next Steps

  1. Avoid nameclashes When importing animation the second time, the curve nodes share a name which throws a warning. By importing into a namespace, any node matching the name outside of this namespace can be removed prior to import
  2. Search-and-replace To support alternative names, namespaces and multiple instances of the same character or scene
  3. Animation Layers It'd be a matter of exporting not just the curve, but the network leading into the node being animated, which would include the animation layer setup.

An example of how names clash.

animio2


When to Use

That is, why not use .anim or .atom? If you can, you probably should. The are already installed (or are they?), they've got documentation (or do they?), they've got more features (of value?) and others may be able to help out when things go south (or can they?).

I would use it when what I want is to export/import animation from one scene to another without fuss; when I don't remember my login details to HighEnd3D and aren't interested in experimenting with the various MEL alternatives on there, and when I haven't got the budget nor time to understand or implement support for ATOM.

YMMV :) Let me know what you think!

MObjectHandle hashCode...

Hi:),
first of all, thank you: cmdx is beautifully written and indeed a nice performance boost compared to PyMel and maya.cmds.

I've just a question: wrappers are stored in the Singleton._instances dictionary by using node MObjectHandle::hashCode(); however the doc explicitly says that these are just hashes and clashes are possible for nodes in the same scene... Apparently it's just a hash and not an UUID.

In case of a clash, the original wrapper will be forgotten and I didn't check yet if that's an issue (probably not:))

What do you think?

Gui

Two users, one machine

To support undo, cmdx installs itself as a plug-in using a unique filename. However! The file is written using the currently logged on users permission bits and places it in the system-wide temp directory. Therefore, if another user logs in and attempts to use cmdx, bam!

This is a bug.

design proposition for components

So i tried to implement some design for components (i use them a lot in my rigging scripts)

At the beggining i wanted to make a standalone class that would use cmdx. But the more I went further, the more I needed to modify the existing classes. that's why i also tried to implement them directly in cmdx

My biggest concern was the need to update how encode and ObjectSet should work with components. actually it's only designed to work with nodes. and i found it very tricky to work with deformers for example

Have a look at my last commit here wougzy@1d4aa2d

Check first how encode was updated. I made it more flexible but at the price of a very small check, so i'm not entirely sure about it. I also added a encodeList function that is faster than map(encode) (only if the list exceed 5 elements) and is more likely to work with components

I noticed how you had to deal with the legacy of MFnSet too. I modified that a bit to be able to use the api2 Fn. but i'm not sure about it. I should make some profiling to see if we have some performance benefits of using api2 when possible. in any case i made the ObjectSet compliant with components now (and even plugs)

see for example how easy it is to add vertex to some deformerSet :

dfm = cmdx.encode('cluster1')
points = cmdx.encode('pCube1.vtx[3]')
dfm.deformerSet.add(points)

print dfm.deformerSet.member().elements

i also added some deformer and shape to be able to cast components more easily. it's still very basic and there's probably a lot to do to implement the most useful functions of them. for now i kept it to the minimum before bringing more.

i liked the way you can smuggle data cache into Node. i took advantage of it by caching dagpath or iterators already built in Node instances.

Support for proxy attributes

Namely..

cmds.addAttr("nodeA", ln="proxyTranslateX", proxy="nodeB.translateX")

The only question is, what should it look like?

nodeA["proxyTranslate", cmdx.Proxy] = nodeB["translateX"]

Also taking into account that we also need undo, and preferably handle it via the DG/DagModifier.

Apply Shader

In cmdx, object sets work like Python's native set(), and Maya's shaders are nothing but object sets. Add mesh to a shader like this.

import cmdx
tm = cmdx.createNode("transform")
shape = cmdx.createNode("mesh", parent=tm)
cube = cmdx.createNode("polyCube")
cube["width"] = 2.0
cube["output"] >> shape["inMesh"]

# Add to default Lambert
default_shader = cmdx.encode("initialShadingGroup")
default_shader.add(shape)

AnimCurve class functionality is limited

I noticed that cmdx.AnimCurve is limited in what it can do. I was going to add some functionality but wanted to check with you first if you had any thoughts?

How to debug Maya crashes?

Apologies for the perhaps noobish question, but I keep getting hard crashes (Maya shows a "fatal error" dialog, then the crash reporter, then exits), which I'm 90% sure come from cmdx (more specifically, modifying existing compound attributes on a DagNode).

I don't have debug symbols (tips on where to find them? I installed the 2020 devkit but I don't see any pdbs), so the dump doesn't tell me much, except for a partial callstack (somewhat better in the crash log). The crash comes from python > OpenMaya > DependEngine.dll plug::name.

Here's the crash dump in case someone's feeling generous :) ! But I'd appreciate any tips on how to debug myself.
dumpdata.zip

attributes' default value not saved in scene

If I create an attribute with a default value, but don't change it, the value is lost after saving & reloading the scene.
I don't know if it's by design or not, but I do see the default value I set appear correctly in the attribute editor, so it's extremely confusing to have the attribute be wiped after reloading the scene.

Here's an all-in-one example:

  • create a new scene, add a sphere
  • add 3 attributes to the sphere, with default values
  • change the value of the first one
  • assign the second one's to the same value
  • don't touch the third one
  • save & reload the scene: you'll see the first attribute's value is "1a", but the second & third are empty!
# create new scene, no prompt
cmds.file(new=True, force=True) # new file, no prompt

sphere = cmdx.encode(cmds.sphere()[0])
sphere.add_attr(cmdx.String("string_1", default="1"))
sphere.add_attr(cmdx.String("string_2", default="2"))
sphere.add_attr(cmdx.String("string_3", default="3"))
sphere["string_1"] = "1a"  # will be saved
sphere["string_2"] = "2"   # will be lost!
                           # string_3 will also be lost!

# save & reload scene, no prompt
scene_path = "test.ma"
cmds.file(rename=scene_path)
cmds.file(save=True, type="mayaAscii")
cmds.file(scene_path, open=True, force=True)

cmdx.DgNode: sample usage?

I'm trying to use your declarative plugin syntax. I pasted your sample class, but then I'm not sure how to instantiate it?

I tried:
node = cmdx.create_node(MyNode)
but Maya complains:
TypeError: '<class 'MyNode'>' is not a valid node type

I then tried:
node = cmdx.create_node("MyNode")
but now:
TypeError: 'MyNode' is not a valid node type

I can simply do:
node = MyNode()
... but then I don't know what to do with the node (how to register it in the DAG).

Apologies for being quite the Maya noob, so what I'm trying to do might be misguided. What I really want to do is create my own node type, so I can add it to the scene, and serialize it/store information in it. I'm not sure it's easily done, I could I guess use custom attributes for this, but I was looking for a more "strongly typed" approach.

Oh, and I believe there's a typo in your example, there's no MTypeId in cmdx, IMO it should be simply typeid = cmdx.TypeId(0x85006) (no M).

Move docs into gh-pages branch

Command reference is currently generated with Sphinx locally, and pushed to the repository alongside code. This is problematic because..

  1. The user need all dependencies (Maya, Sphinx, nose, plug-ins) in order to update the docs
  2. Repository bloated with generated content, hard to know what to edit when making changes.

Instead, each push should trigger a re-build of the documentation and upload into the gh-pages branch of this repository.

unsupported type on cloned attribute

Am I doing it wrong? In this case I cannot copy the attribute.

        import cmdx
        cmds.file(new=True, force=True)
        src_node, src_node_shape = cmds.polyCube()
        dst_node, dst_node_shape = cmds.polyCube()
        attr_name = "test"
        cmds.addAttr(src_node, ln=attr_name, dt="string")

        s = cmdx.encode(src_node)

        print(s[attr_name].type())  # -> kTypedAttribute
        attr2 = s[attr_name].clone(attr_name)
        print(attr2.type())  # -> 4 ?

        d = cmdx.encode(dst_node)
        d.add_attr(attr2)

for some reason, the cloned attribute type is not supported when I add it to the destination node:
TypeError: Unsupported Python type '<class 'OpenMaya.MObject'>'

Support keyframing of pairBlended attributes

Whenever you keyframe a constrained channel, it'll create a pairBlend. If you then subsequently attempt to..

node["translateX"] = {1: 0.0, 5: 10.0, 7: 11.0}

Then that new animation will break thee pairBlend, rather than get redirected to wherever it's blending.

Set a nodes transform

Hey!

I am trying to align the transform of one node to another, but have got to a point where I can't see a way forward.
Here is my function:

def align_transforms_x(node1, node2):
    """
    Align the transform of node2 to node1

        Args:
            - node1: The target node.
            - node2: The node to align.

        Returns:
            None
    """
    node1 = cmdx.encode(node1)
    node2 = cmdx.encode(node2)
    transform = node1.transform()
    # this method does not exist:
    node2.set_transform(transform)

Is there something I am missing?

cmdx.listRelatives always returns None

Steps to reproduce:

  1. Create cube primitive.
  2. run
    `import cmdx
    import maya.cmds as cmds

print cmdx.listRelatives("pCube1")
print cmds.listRelatives("pCube1")`

cmds.listRelatives returns [u'pCubeShape1'] while
cmdx.listRelatives("pCube1") returns None

Example from the command reference also returns false and not true.

cmdx 0.4.6
Maya 2020.2
Windows 10

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.