mottosso / cmdx Goto Github PK
View Code? Open in Web Editor NEWFast and persistent subset of maya.cmds
Home Page: https://mottosso.com/cmdx
License: BSD 2-Clause "Simplified" License
Fast and persistent subset of maya.cmds
Home Page: https://mottosso.com/cmdx
License: BSD 2-Clause "Simplified" License
Command reference is currently generated with Sphinx locally, and pushed to the repository alongside code. This is problematic because..
Instead, each push should trigger a re-build of the documentation and upload into the gh-pages branch of this repository.
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.
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.
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")
])
])
)
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
I've found the code formatter Black to be useful on my own projects. Might be worthwhile on a large file like cmdx.py
to keep the code style consistent?
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?
[0, 0, 0]
?[5, 5, 5]
?[5, 0, 0]
?[0, 5, 5]
?[15161353, 135135136425, 63513]
?That's right! The numbers will be garbage!
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.
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])
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.
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.
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.
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.
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.
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
import cmdx
node = cmdx.createNode("transform")
# Make some animation
node["tx"] = {1: 0.0, 50: 10.0, 100: 0.0}
# Query it
node["tx"].read(time=50)
# Result: 0.0 #
cmdx vesion:0.6.4
Maya vesion: 2018 & 2020
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).
Steps to reproduce:
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
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)
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
.
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 #
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 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)
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?
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'>'
maya 2024 on windows 10.
run mayapy
import cmdx
this breaks however:
from cmdx import ContainerNode
with this error:
ImportError: cannot import name 'ContainerNode' from 'cmdx' (cmdx.py)
since there is no maya cmds yet the version check fails:
try:
__maya_version__ = int(cmds.about(version=True))
except (AttributeError, ValueError):
__maya_version__ = 2015 # E.g. Preview Release 95
and it is set to maya 2015. this in turn disables ContainerNode:
if __maya_version__ >= 2017:
class ContainerNode(Node):
which makes the code fail in commandline tools that initialise maya after cmdx was imported.
2024-07-25 18:24:17: 0: STDOUT: File "cmdx.py", line 7357, in listRelatives
2024-07-25 18:24:17: 0: STDOUT: _parent = node.parent(type=type)
2024-07-25 18:24:17: 0: STDOUT: File "cmdx.py", line 1853, in parent
2024-07-25 18:24:17: 0: STDOUT: return cls(mobject)
2024-07-25 18:24:17: 0: STDOUT: File "cmdx.py", line 480, in __call__
2024-07-25 18:24:17: 0: STDOUT: sup = ContainerNode
2024-07-25 18:24:17: 0: STDOUT: NameError: name 'ContainerNode' is not defined
it is not always easy to control the order of imports in more complicated pipelines.
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)
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.
--->
Export related animCurve
nodes as a mayaAscii
scene<---
Import animCurve
nodes from the mayaAscii
scene, elsewhere-><-
Connect each node to their corresponding targetIt is..
With a few caveats..
Each of which could potentially be solved with some further tinkering.
~/maya/scripts
folderBased 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)
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)
An example of how names clash.
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!
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.
@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.
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?
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.
As it happens, 2017 is slower than the rest at creating nodes by ~20%. The performance test currently doesn't take this into account.
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 ๐
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?
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.
I noticed these don't take the time argument, I wasn't sure if there was a particular reason for this?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.