├── docs └── source │ ├── _static │ └── css │ │ ├── basic.css │ │ ├── small.css │ │ ├── avalon.css │ │ ├── cmdx.css │ │ ├── pygments.css │ │ └── pocoo.css │ ├── index.rst │ └── conf.py ├── run_docs.py ├── .gitignore ├── setup.cfg ├── run_tests.sh ├── examples ├── interop.py ├── basic.py ├── dgnode.py └── locatornode.py ├── Dockerfile ├── LICENSE ├── setup.py ├── run_tests.py ├── generate_cmdt.py ├── .github └── workflows │ └── main.yml ├── test_performance.py ├── plot.py └── plots ├── dump.svg ├── node.addAttr.svg ├── json.svg ├── node.attr=5.svg ├── node.attr.svg ├── uuid.svg ├── createNode.svg ├── long.svg ├── listRelatives.svg ├── ls.svg └── addAttr.svg /docs/source/_static/css/basic.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /run_docs.py: -------------------------------------------------------------------------------- 1 | """Run each example in the README as a doctest""" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | cover/** 3 | .coverage 4 | dist 5 | *.egg-info 6 | build 7 | 8 | # Automatically generated 9 | test_docs.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE.txt 3 | 4 | [bdist_wheel] 5 | # Support for both Python 2 and 3 6 | universal=1 7 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | docker run \ 2 | --rm \ 3 | -v $(pwd):/workspace \ 4 | -e PYTHONPATH=/workspace:/usr/local/lib/python2.6/site-packages \ 5 | --workdir /workspace \ 6 | --entrypoint mayapy \ 7 | cmdx \ 8 | -u ./run_tests.py -------------------------------------------------------------------------------- /examples/interop.py: -------------------------------------------------------------------------------- 1 | # cmdx and cmdx interop 2 | from maya import cmds 3 | import cmdx 4 | 5 | node = cmds.createNode("transform") 6 | node = cmds.rename(node, "MyNode") 7 | 8 | node = cmdx.encode(node) 9 | node["rotate"] = (0, 45, 0) 10 | 11 | cmds.select(str(node)) 12 | -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | # Basic scenegraph manipulation 2 | import cmdx 3 | 4 | node1 = cmdx.createNode("transform") 5 | node2 = cmdx.createNode(cmdx.Transform) 6 | 7 | node1["tx"] = 2.5 8 | print(node1) 9 | print(node1["tx"]) 10 | # 2.5 11 | 12 | node1["tx"] >> node2["tx"] 13 | print(node2["tx"]) 14 | # 2.5 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mottosso/maya:2022 2 | 3 | RUN wget https://bootstrap.pypa.io/pip/get-pip.py && \ 4 | mayapy get-pip.py --user && \ 5 | mayapy -m pip install --user \ 6 | nose \ 7 | nose-exclude \ 8 | coverage \ 9 | flaky \ 10 | sphinx \ 11 | sphinxcontrib-napoleon 12 | 13 | # Avoid creation of auxilliary files 14 | ENV PYTHONDONTWRITEBYTECODE=1 15 | 16 | WORKDIR /workspace 17 | 18 | ENTRYPOINT mayapy 19 | -------------------------------------------------------------------------------- /examples/dgnode.py: -------------------------------------------------------------------------------- 1 | import cmdx 2 | 3 | class CustomDGNode(cmdx.DgNode): 4 | name = "customDGNode" 5 | typeid = cmdx.TypeId(0x85006) 6 | 7 | attributes = [ 8 | cmdx.String("myString"), 9 | cmdx.Message("myMessage"), 10 | cmdx.Matrix("myMatrix"), 11 | cmdx.Time("myTime", default=0.0), 12 | ] 13 | 14 | affects = [ 15 | ("myString", "myMatrix"), 16 | ("myMessage", "myMatrix"), 17 | ("myTime", "myMatrix"), 18 | ] 19 | 20 | 21 | # Tell Maya to use Maya API 2.0 22 | initializePlugin2 = cmdx.initialize2(CustomNode) 23 | uninitializePlugin2 = cmdx.uninitialize2(CustomNode) 24 | 25 | """ 26 | Now from Maya, load the plug-in like normal 27 | 28 | from maya import cmds 29 | cmds.loadPlugin("/path/to/dgnode.py") 30 | cmds.createNode("customDGNode") 31 | 32 | You should now see your custom DG node in the Node Editor 33 | """ -------------------------------------------------------------------------------- /examples/locatornode.py: -------------------------------------------------------------------------------- 1 | import cmdx 2 | 3 | class CustomLocatorNode(cmdx.LocatorNode): 4 | name = "customLocatorNode" 5 | typeid = cmdx.TypeId(0x85006) 6 | 7 | attributes = [ 8 | cmdx.String("myString"), 9 | cmdx.Message("myMessage"), 10 | cmdx.Matrix("myMatrix"), 11 | cmdx.Time("myTime", default=0.0), 12 | ] 13 | 14 | affects = [ 15 | ("myString", "myMatrix"), 16 | ("myMessage", "myMatrix"), 17 | ("myTime", "myMatrix"), 18 | ] 19 | 20 | 21 | # Tell Maya to use Maya API 2.0 22 | initializePlugin2 = cmdx.initialize2(CustomNode) 23 | uninitializePlugin2 = cmdx.uninitialize2(CustomNode) 24 | 25 | """ 26 | Now from Maya, load the plug-in like normal 27 | 28 | from maya import cmds 29 | cmds.loadPlugin("/path/to/locatornode.py") 30 | cmds.createNode("customLocatorNode") 31 | 32 | You should now see your custom locator node in your viewport! 33 | """ -------------------------------------------------------------------------------- /docs/source/_static/css/small.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 20px 30px; 4 | } 5 | 6 | div.documentwrapper { 7 | float: none; 8 | background: white; 9 | } 10 | 11 | div.sphinxsidebar { 12 | display: block; 13 | float: none; 14 | width: 102.5%; 15 | margin: 50px -30px -20px -30px; 16 | padding: 10px 20px; 17 | background: #333; 18 | color: white; 19 | } 20 | 21 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 22 | div.sphinxsidebar h3 a { 23 | color: white; 24 | } 25 | 26 | div.sphinxsidebar a { 27 | color: #aaa; 28 | } 29 | 30 | div.sphinxsidebar p.logo { 31 | display: none; 32 | } 33 | 34 | div.sphinxsidebar ul.versions a.current { 35 | font-style: italic; 36 | border-bottom: 1px solid white; 37 | color: white; 38 | } 39 | 40 | div.document { 41 | width: 100%; 42 | margin: 0; 43 | } 44 | 45 | div.related { 46 | display: block; 47 | margin: 0; 48 | padding: 10px 0 20px 0; 49 | } 50 | 51 | div.related ul, 52 | div.related ul li { 53 | margin: 0; 54 | padding: 0; 55 | } 56 | 57 | div.footer { 58 | display: none; 59 | } 60 | 61 | div.bodywrapper { 62 | margin: 0; 63 | } 64 | 65 | div.body { 66 | min-height: 0; 67 | padding: 0; 68 | } 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, Marcus Ottosson 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A fast subset of maya.cmds""" 2 | 3 | import os 4 | from setuptools import setup 5 | 6 | __version__ = None 7 | with open(os.path.join(os.path.dirname(__file__), "cmdx.py")) as f: 8 | for line in f: 9 | if line.startswith("__version__"): 10 | exec(line) 11 | break 12 | 13 | assert __version__, "Could not determine version" 14 | 15 | 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "License :: OSI Approved :: BSD License", 19 | "Intended Audience :: Developers", 20 | "Operating System :: OS Independent", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 2", 23 | "Programming Language :: Python :: 2.7", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.6", 26 | "Programming Language :: Python :: 3.7", 27 | "Topic :: Utilities", 28 | "Topic :: Software Development", 29 | "Topic :: Software Development :: Libraries :: Python Modules", 30 | ] 31 | 32 | 33 | setup( 34 | name="cmdx", 35 | version=__version__, 36 | description=__doc__, 37 | keywords="Fast subset of maya.cmds", 38 | long_description=__doc__, 39 | url="https://github.com/mottosso/cmdx", 40 | author="Marcus Ottosson", 41 | author_email="me@mottosso.com", 42 | license="BSD", 43 | zip_safe=False, 44 | py_modules=["cmdx"], 45 | classifiers=classifiers, 46 | ) 47 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | """Test Pure Python functionality""" 2 | 3 | import os 4 | import sys 5 | import nose 6 | import flaky.flaky_nose_plugin as flaky 7 | 8 | # For nose 9 | if sys.version_info[0] == 3: 10 | import collections 11 | collections.Callable = collections.abc.Callable 12 | 13 | 14 | if __name__ == "__main__": 15 | print("Initialising Maya..") 16 | from maya import standalone, cmds 17 | standalone.initialize() 18 | cmds.loadPlugin("matrixNodes", quiet=True) 19 | 20 | argv = sys.argv[:] 21 | argv.extend([ 22 | "--verbose", 23 | "--with-doctest", 24 | 25 | "--with-flaky", 26 | "--with-coverage", 27 | "--cover-html", 28 | "--cover-package", "cmdx", 29 | "--cover-erase", 30 | "--cover-tests", 31 | 32 | "tests.py", 33 | "test_performance.py", 34 | "cmdx.py", 35 | ]) 36 | 37 | result = nose.main( 38 | argv=argv, 39 | addplugins=[flaky.FlakyPlugin()], 40 | 41 | # We'll exit in our own way, 42 | # since Maya typically enjoys throwing 43 | # segfaults during cleanup of normal exits 44 | exit=False 45 | ) 46 | 47 | if os.getenv("TRAVIS_JOB_ID"): 48 | import coveralls 49 | coveralls.wear() 50 | else: 51 | sys.stdout.write("Skipping coveralls\n") 52 | 53 | if os.name == "nt": 54 | # Graceful exit, only Windows seems to like this consistently 55 | standalone.uninitialize() 56 | 57 | # Trust but verify 58 | os._exit(0 if result.success else 1) 59 | -------------------------------------------------------------------------------- /generate_cmdt.py: -------------------------------------------------------------------------------- 1 | """Generate cmdt.py, with all available Type IDs""" 2 | import os 3 | import sys 4 | 5 | from maya.api import OpenMaya as om 6 | from maya import standalone, cmds 7 | 8 | 9 | def main(fname=None): 10 | standalone.initialize() 11 | 12 | blacklist = ( 13 | # Causes crash (Maya 2018) 14 | "caddyManipBase", 15 | 16 | # Causes TypeError 17 | "applyAbsOverride", 18 | "applyOverride", 19 | "applyRelOverride", 20 | "childNode", 21 | "lightItemBase", 22 | "listItem", 23 | "override", 24 | "selector", 25 | "valueOverride", 26 | ) 27 | 28 | cmdt = [ 29 | "from maya.api import OpenMaya as om", 30 | "" 31 | ] 32 | 33 | dg = om.MFnDependencyNode() 34 | for name in cmds.allNodeTypes(): 35 | if name in blacklist: 36 | continue 37 | 38 | try: 39 | mobj = dg.create(name) 40 | print(mobj.apiType()) 41 | fn = om.MFnDependencyNode 42 | try: 43 | # If `name` is a shape, then the transform 44 | # is returned. We need the shape. 45 | mobj = om.MFnDagNode(mobj).child(0) 46 | except RuntimeError: 47 | pass 48 | 49 | except TypeError: 50 | # This shouldn't happen, but might depending 51 | # on the Maya version, and if there are any 52 | # custom plug-ins registereing new (bad) nodes. 53 | # 54 | # If so: 55 | # Add to `blacklist` 56 | # 57 | sys.stderr.write("%s threw a TypeError\n" % name) 58 | continue 59 | 60 | typeId = fn(mobj).typeId 61 | cmdt += ["{type} = om.MTypeId({id})".format( 62 | type=name[0].upper() + name[1:], 63 | id=str(typeId)) 64 | ] 65 | 66 | text = os.linesep.join(cmdt) 67 | 68 | dirname = os.path.dirname(__file__) 69 | fname = fname or os.path.join(dirname, "cmdt.py") 70 | with open(fname, "w") as f: 71 | f.write(text) 72 | 73 | 74 | if __name__ == '__main__': 75 | import argparse 76 | parser = argparse.ArgumentParser() 77 | parser.add_argument("--fname", default="") 78 | opt = parser.parse_args() 79 | main(opt.fname) 80 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. `cmdx` documentation master file, created by 2 | sphinx-quickstart on Tue Jun 20 20:12:06 2017. 3 | You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: Contents: 8 | 9 | .. image:: https://user-images.githubusercontent.com/2152766/34321609-f134e0cc-e80a-11e7-8dad-d124fea80e77.png 10 | :alt: `cmdx` 11 | :width: 200 12 | :align: center 13 | :target: /cmdx/ 14 | 15 | .. centered:: *The fast subset of `maya.cmds`* 16 | 17 | | 18 | 19 | `cmdx` 20 | ====== 21 | 22 | `cmdx` is a Python module for fast scenegraph manipulation in Autodesk Maya. 23 | 24 | - `Documentation `_ 25 | - Command reference below 26 | 27 | | 28 | 29 | .. currentmodule:: cmdx 30 | 31 | API 32 | --- 33 | 34 | The basics from `maya.cmds` are included. 35 | 36 | .. autofunction:: createNode 37 | .. autofunction:: getAttr 38 | .. autofunction:: setAttr 39 | .. autofunction:: addAttr 40 | .. autofunction:: connectAttr 41 | .. autofunction:: listRelatives 42 | .. autofunction:: listConnections 43 | 44 | | 45 | | 46 | 47 | Object Oriented Interface 48 | ------------------------- 49 | 50 | In addition to the familiar interface inherited from `cmds`, `cmdx` offers a faster, object oriented alternative to all of that functionality, and more. 51 | 52 | .. autoclass:: Node 53 | :members: 54 | :undoc-members: 55 | 56 | | 57 | 58 | .. autoclass:: DagNode 59 | :members: 60 | :undoc-members: 61 | 62 | | 63 | 64 | .. autoclass:: Plug 65 | :members: 66 | :undoc-members: 67 | 68 | | 69 | | 70 | 71 | Attributes 72 | ---------- 73 | 74 | .. autoclass:: Enum 75 | .. autoclass:: Divider 76 | .. autoclass:: String 77 | .. autoclass:: Message 78 | .. autoclass:: Matrix 79 | .. autoclass:: Long 80 | .. autoclass:: Double 81 | .. autoclass:: Double3 82 | .. autoclass:: Boolean 83 | .. autoclass:: AbstractUnit 84 | .. autoclass:: Angle 85 | .. autoclass:: Time 86 | .. autoclass:: Distance 87 | 88 | | 89 | | 90 | 91 | Units 92 | ----- 93 | 94 | .. autodata:: Degrees 95 | .. autodata:: Radians 96 | .. autodata:: AngularMinutes 97 | .. autodata:: AngularSeconds 98 | .. autodata:: Millimeters 99 | .. autodata:: Centimeters 100 | .. autodata:: Meters 101 | .. autodata:: Kilometers 102 | .. autodata:: Inches 103 | .. autodata:: Feet 104 | .. autodata:: Miles 105 | .. autodata:: Yards 106 | 107 | | 108 | | 109 | 110 | Interoperability 111 | ---------------- 112 | 113 | .. autofunction:: encode 114 | .. autofunction:: decode 115 | -------------------------------------------------------------------------------- /docs/source/_static/css/avalon.css: -------------------------------------------------------------------------------- 1 | @import url(pocoo.css); 2 | @import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,400italic,700,700italic); 3 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,500); 4 | @import url(https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css); 5 | 6 | /* fonts */ 7 | body { font-family: 'Ubuntu Mono', 'Consolas', 'Menlo', 8 | 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono'; 9 | font-size: 15px; } 10 | div.admonition p.admonition-title, div.sphinxsidebar h3, div.sphinxsidebar h4, 11 | div.sphinxsidebar input, div.body h1, div.body h2, div.body h3, div.body h4, 12 | div.body h5, div.body h6 { font-family: 'Open Sans', 'Helvetica', 'Arial', 13 | sans-serif; font-weight: 400; } 14 | div.body h1, div.body h2, div.body h3, div.body h4, 15 | div.body h5, div.body h6 { color: black; } 16 | pre, code { font-family: 'Ubuntu Mono', 'Consolas', 'Menlo', 17 | 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono'; 18 | font-size: 15px; background: transparent; } 19 | pre, * pre { padding: 7px 0 7px 30px!important; 20 | margin: 15px 0!important; 21 | line-height: 1.3; } 22 | 23 | /* colors */ 24 | div.body { color: #3E4349; } 25 | a { color: #5D2CD1; } 26 | a:hover { color: #7546E3; } 27 | p.version-warning { background-color: #7546E3; } 28 | a.reference { border-bottom: 1px dotted #5D2CD1; } 29 | a.reference:hover { border-bottom: 1px solid #7546E3; } 30 | a.footnote-reference { border-bottom: 1px dotted #5D2CD1; } 31 | a.footnote-reference:hover { border-bottom: 1px solid #7546E3; } 32 | a:hover code { background-color: #eeeeee; } 33 | code.xref, a code { background-color: #E8EFF0; 34 | border-bottom: 1px solid white; } 35 | 36 | /* special elements */ 37 | div.indexwrapper h1 { 38 | text-indent: -999999px; 39 | background: url(click.png) no-repeat center center; 40 | height: 200px; 41 | } 42 | 43 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { 44 | div.indexwrapper h1 { 45 | background: url(click@2x.png) no-repeat center center; 46 | background-size: 420px 175px; 47 | } 48 | } 49 | 50 | h1 { 51 | display: none; 52 | } 53 | 54 | div.body h2 { 55 | /* Align section with table of contents */ 56 | padding-top: 50px; 57 | } 58 | 59 | div.sphinxsidebarwrapper h3 { 60 | display: none; 61 | } 62 | 63 | div.sphinxsidebarwrapper h3 { 64 | display: none; 65 | } 66 | 67 | #searchbox { 68 | display: none !important; 69 | } 70 | 71 | .sphinxsidebarwrapper { 72 | position: fixed; 73 | } 74 | 75 | @media only screen and (max-width: 60em) { 76 | .sphinxsidebarwrapper { 77 | display: none; 78 | } 79 | 80 | div.document { 81 | width: 100%; 82 | margin: 0; 83 | } 84 | 85 | div.body { 86 | padding: 0 60px 0 60px; 87 | } 88 | 89 | div.bodywrapper { 90 | margin: 0; 91 | width: 100%; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/source/_static/css/cmdx.css: -------------------------------------------------------------------------------- 1 | @import url(pocoo.css); 2 | @import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,400italic,700,700italic); 3 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,500); 4 | @import url(https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css); 5 | 6 | /* fonts */ 7 | body { font-family: 'Ubuntu Mono', 'Consolas', 'Menlo', 8 | 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono'; 9 | font-size: 15px; } 10 | div.admonition p.admonition-title, div.sphinxsidebar h3, div.sphinxsidebar h4, 11 | div.sphinxsidebar input, div.body h1, div.body h2, div.body h3, div.body h4, 12 | div.body h5, div.body h6 { font-family: 'Open Sans', 'Helvetica', 'Arial', 13 | sans-serif; font-weight: 700; } 14 | div.body h1, div.body h2, div.body h3, div.body h4, 15 | div.body h5, div.body h6 { color: black; } 16 | pre, code { font-family: 'Ubuntu Mono', 'Consolas', 'Menlo', 17 | 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono'; 18 | font-size: 15px; background: transparent; } 19 | pre, * pre { padding: 7px 0 7px 30px!important; 20 | margin: 15px 0!important; 21 | line-height: 1.3; } 22 | 23 | /* colors */ 24 | div.body { color: #3E4349; } 25 | a { color: #5D2CD1; } 26 | a:hover { color: #7546E3; } 27 | p.version-warning { background-color: #7546E3; } 28 | a.reference { border-bottom: 1px dotted #5D2CD1; } 29 | a.reference:hover { border-bottom: 1px solid #7546E3; } 30 | a.footnote-reference { border-bottom: 1px dotted #5D2CD1; } 31 | a.footnote-reference:hover { border-bottom: 1px solid #7546E3; } 32 | a:hover code { background-color: #eeeeee; } 33 | code.xref, a code { background-color: #E8EFF0; 34 | border-bottom: 1px solid white; } 35 | 36 | /* special elements */ 37 | div.indexwrapper h1 { 38 | text-indent: -999999px; 39 | background: url(click.png) no-repeat center center; 40 | height: 200px; 41 | } 42 | 43 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { 44 | div.indexwrapper h1 { 45 | background: url(click@2x.png) no-repeat center center; 46 | background-size: 420px 175px; 47 | } 48 | } 49 | 50 | h1 { 51 | display: none; 52 | } 53 | 54 | div.body h2 { 55 | /* Align section with table of contents */ 56 | padding-top: 50px; 57 | } 58 | 59 | div.sphinxsidebarwrapper h3 { 60 | display: none; 61 | } 62 | 63 | div.sphinxsidebarwrapper h3 { 64 | display: none; 65 | } 66 | 67 | #searchbox { 68 | display: none !important; 69 | } 70 | 71 | .sphinxsidebarwrapper { 72 | position: fixed; 73 | } 74 | 75 | @media only screen and (max-width: 60em) { 76 | .sphinxsidebarwrapper { 77 | display: none; 78 | } 79 | 80 | div.document { 81 | width: 100%; 82 | margin: 0; 83 | } 84 | 85 | div.body { 86 | padding: 0 60px 0 60px; 87 | } 88 | 89 | div.bodywrapper { 90 | margin: 0; 91 | width: 100%; 92 | } 93 | } 94 | 95 | div.note { 96 | background: "#f3f3f3" 97 | } 98 | 99 | dd div.admonition { 100 | margin-left: 0; 101 | padding-left: 15px; 102 | border: none; 103 | } 104 | 105 | div.admonition p.admonition-title { 106 | font-weight: 800; 107 | font-size: 1em; 108 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: cmdx-workflow 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | maya: 13 | # The type of runner that the job will run on 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | 18 | # Without this, all containers stop if any fail 19 | # That's bad, we want to know whether it's only one 20 | # or if it happens to multiples or all. 21 | fail-fast: false 22 | 23 | matrix: 24 | include: 25 | - maya: "2018" 26 | pip: "2.7/get-pip.py" 27 | - maya: "2019" 28 | pip: "2.7/get-pip.py" 29 | - maya: "2020" 30 | pip: "2.7/get-pip.py" 31 | - maya: "2022" 32 | pip: "3.7/get-pip.py" 33 | - maya: "2023" 34 | pip: "get-pip.py" 35 | - maya: "2024" 36 | pip: "get-pip.py" 37 | 38 | container: mottosso/maya:${{ matrix.maya }} 39 | 40 | # Steps represent a sequence of tasks that will be executed as part of the job 41 | steps: 42 | - name: Checkout code 43 | uses: actions/checkout@v1 44 | 45 | # We'll lock each version to one that works with both Python 2.7 and 3.7 46 | - name: pip install 47 | run: | 48 | wget https://bootstrap.pypa.io/pip/${{ matrix.pip }} 49 | mayapy get-pip.py --user 50 | mayapy -m pip install --user \ 51 | nose==1.3.7 \ 52 | nose-exclude==0.5.0 \ 53 | coverage==5.5 \ 54 | flaky==3.7.0 \ 55 | docutils==0.17.1 \ 56 | sphinx==1.8.5 \ 57 | sphinxcontrib-napoleon==0.7 58 | 59 | # Since 2019, this sucker throws an unnecessary warning if not declared 60 | - name: Environment 61 | run: | 62 | export XDG_RUNTIME_DIR=/var/tmp/runtime-root 63 | export MAYA_DISABLE_ADP=1 64 | 65 | - name: Unittests 66 | run: | 67 | pwd 68 | ls 69 | mayapy --version 70 | mayapy run_tests.py 71 | 72 | - name: Test docs 73 | run: | 74 | mayapy build_livedocs.py && mayapy test_docs.py 75 | 76 | - name: Build docs 77 | run: | 78 | mayapy build_docs.py 79 | 80 | # We only upload the docs if the Maya version is 2022 as we only need one copy 81 | - name: Upload docs as artifact 82 | if: ${{ matrix.maya == '2022' }} 83 | uses: actions/upload-artifact@v2 84 | with: 85 | name: docs 86 | path: ./build/html 87 | 88 | deploy: 89 | name: Build and Release 90 | runs-on: ubuntu-latest 91 | if: contains(github.ref, 'refs/tags/') 92 | 93 | environment: 94 | name: pypi 95 | url: https://pypi.org/p/cmdx 96 | permissions: 97 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 98 | 99 | steps: 100 | - name: Checkout repository 101 | uses: actions/checkout@v4 102 | - name: Set up Python 103 | uses: actions/setup-python@v4 104 | with: 105 | python-version: "3.7.x" 106 | - name: Install build dependency 107 | run: python3 -m pip install --upgrade build 108 | - name: Build a binary wheel and a source tarball 109 | run: python3 -m build 110 | - name: Release to PyPi 111 | uses: pypa/gh-action-pypi-publish@release/v1 112 | 113 | docs: 114 | runs-on: ubuntu-latest 115 | if: contains(github.ref, 'refs/tags/') 116 | needs: maya 117 | steps: 118 | - uses: actions/checkout@v2 119 | - name: Download built docs 120 | uses: actions/download-artifact@v2 121 | with: 122 | name: docs 123 | path: ./public 124 | - name: Deploy 125 | uses: peaceiris/actions-gh-pages@v3 126 | with: 127 | github_token: ${{ secrets.GITHUB_TOKEN }} 128 | -------------------------------------------------------------------------------- /test_performance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import timeit 5 | import contextlib 6 | 7 | from maya import cmds, mel, OpenMaya as om1 8 | from maya.api import OpenMaya as om2 9 | import cmdx 10 | 11 | from flaky import flaky 12 | from nose.tools import ( 13 | assert_greater, 14 | ) 15 | 16 | timings = {} 17 | 18 | 19 | def Compare(method, 20 | task, 21 | func, 22 | setup=None, 23 | number=200, 24 | repeat=4, 25 | precision=1, 26 | quiet=True): 27 | 28 | setup = setup or (lambda: None) 29 | 30 | text = "%s %s: %.1f ms (%.2f {precision}/call)".format( 31 | precision="μs" if precision else "ms" 32 | ) 33 | 34 | results = timeit.Timer(func, setup=setup).repeat( 35 | repeat=repeat, number=number) 36 | 37 | if not quiet: 38 | print(text % ( 39 | task, 40 | method, 41 | 10 ** 3 * sum(results), 42 | 10 ** (6 if precision else 3) * min(results) / number 43 | )) 44 | 45 | if task not in timings: 46 | timings[task] = {} 47 | 48 | # Store for comparison 49 | timings[task][method] = { 50 | "func": func, 51 | "number": number, 52 | "results": results, 53 | "min": min(results), 54 | "percall": min(results) / number 55 | } 56 | 57 | 58 | @contextlib.contextmanager 59 | def environment(key, value=None): 60 | env = os.environ.copy() 61 | os.environ[key] = value or "1" 62 | try: 63 | sys.modules.pop("cmdx") 64 | __import__("cmdx") 65 | yield 66 | finally: 67 | os.environ.update(env) 68 | 69 | 70 | @contextlib.contextmanager 71 | def pop_environment(key): 72 | env = os.environ.copy() 73 | os.environ.pop(key, None) 74 | try: 75 | sys.modules.pop("cmdx") 76 | __import__("cmdx") 77 | yield 78 | finally: 79 | os.environ.update(env) 80 | 81 | 82 | def New(setup=None): 83 | cmds.file(new=True, force=True) 84 | (setup or (lambda: None))() 85 | 86 | 87 | @flaky(max_runs=20, min_passes=3) 88 | def test_createNode_performance(): 89 | """createNode cmdx vs cmds > 2x""" 90 | 91 | versions = ( 92 | ("mel", lambda: mel.eval("createNode \"transform\"")), 93 | ("cmds", lambda: cmds.createNode("transform")), 94 | ("cmdx", lambda: cmdx.createNode(cmdx.tTransform)), 95 | # ("PyMEL", lambda: pm.createNode("transform")), 96 | ("API 1.0", lambda: om1.MFnDagNode().create("transform")), 97 | ("API 2.0", lambda: om2.MFnDagNode().create("transform")), 98 | ) 99 | 100 | for contender, test in versions: 101 | Compare(contender, "createNode", test, setup=New) 102 | 103 | cmdx_vs_cmds = ( 104 | timings["createNode"]["cmds"]["percall"] / 105 | timings["createNode"]["cmdx"]["percall"] 106 | ) 107 | 108 | cmdx_vs_api = ( 109 | timings["createNode"]["API 2.0"]["percall"] / 110 | timings["createNode"]["cmdx"]["percall"] 111 | ) 112 | 113 | if cmdx.__maya_version__ == 2017: 114 | assert_greater(cmdx_vs_cmds, 0.4545) # at most 2.2x slower than cmds 115 | assert_greater(cmdx_vs_api, 0.1818) # at most 5.5x slower than API 2.0 116 | else: 117 | assert_greater(cmdx_vs_cmds, 0.5) # at most 2x slower than cmds 118 | assert_greater(cmdx_vs_api, 0.20) # at most 5x slower than API 2.0 119 | 120 | 121 | @flaky(max_runs=20, min_passes=3) 122 | def test_rouge_mode(): 123 | """CMDX_ROGUE_MODE is faster""" 124 | 125 | node = cmdx.createNode("transform") 126 | Compare("norogue", "createNode", node.name) 127 | 128 | with environment("CMDX_ROGUE_MODE"): 129 | node = cmdx.createNode("transform") 130 | Compare("rogue", "createNode", node.name) 131 | 132 | rogue_vs_norogue = ( 133 | timings["createNode"]["norogue"]["percall"] / 134 | timings["createNode"]["rogue"]["percall"] 135 | ) 136 | 137 | assert_greater(rogue_vs_norogue, 0.9) 138 | -------------------------------------------------------------------------------- /docs/source/_static/css/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #f8f8f8; } 3 | .highlight .c { color: #8f5902; font-style: italic } /* Comment */ 4 | .highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ 5 | .highlight .g { color: #000000 } /* Generic */ 6 | .highlight .k { color: #204a87; font-weight: bold } /* Keyword */ 7 | .highlight .l { color: #000000 } /* Literal */ 8 | .highlight .n { color: #000000 } /* Name */ 9 | .highlight .o { color: #ce5c00; font-weight: bold } /* Operator */ 10 | .highlight .x { color: #000000 } /* Other */ 11 | .highlight .p { color: #000000; font-weight: bold } /* Punctuation */ 12 | .highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */ 15 | .highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 18 | .highlight .gd { color: #a40000 } /* Generic.Deleted */ 19 | .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 20 | .highlight .gr { color: #ef2929 } /* Generic.Error */ 21 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 23 | .highlight .go { color: #000000; font-style: italic } /* Generic.Output */ 24 | .highlight .gp { color: #8f5902 } /* Generic.Prompt */ 25 | .highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 26 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 28 | .highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */ 29 | .highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */ 30 | .highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */ 31 | .highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */ 32 | .highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */ 33 | .highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */ 34 | .highlight .ld { color: #000000 } /* Literal.Date */ 35 | .highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */ 36 | .highlight .s { color: #4e9a06 } /* Literal.String */ 37 | .highlight .na { color: #c4a000 } /* Name.Attribute */ 38 | .highlight .nb { color: #204a87 } /* Name.Builtin */ 39 | .highlight .nc { color: #000000 } /* Name.Class */ 40 | .highlight .no { color: #000000 } /* Name.Constant */ 41 | .highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */ 42 | .highlight .ni { color: #ce5c00 } /* Name.Entity */ 43 | .highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 44 | .highlight .nf { color: #000000 } /* Name.Function */ 45 | .highlight .nl { color: #f57900 } /* Name.Label */ 46 | .highlight .nn { color: #000000 } /* Name.Namespace */ 47 | .highlight .nx { color: #000000 } /* Name.Other */ 48 | .highlight .py { color: #000000 } /* Name.Property */ 49 | .highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */ 50 | .highlight .nv { color: #000000 } /* Name.Variable */ 51 | .highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */ 52 | .highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 53 | .highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */ 54 | .highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */ 55 | .highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */ 56 | .highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */ 57 | .highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */ 58 | .highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ 59 | .highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ 60 | .highlight .sc { color: #4e9a06 } /* Literal.String.Char */ 61 | .highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ 62 | .highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 63 | .highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ 64 | .highlight .se { color: #4e9a06 } /* Literal.String.Escape */ 65 | .highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 66 | .highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ 67 | .highlight .sx { color: #4e9a06 } /* Literal.String.Other */ 68 | .highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ 69 | .highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ 70 | .highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ 71 | .highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 72 | .highlight .fm { color: #000000 } /* Name.Function.Magic */ 73 | .highlight .vc { color: #000000 } /* Name.Variable.Class */ 74 | .highlight .vg { color: #000000 } /* Name.Variable.Global */ 75 | .highlight .vi { color: #000000 } /* Name.Variable.Instance */ 76 | .highlight .vm { color: #000000 } /* Name.Variable.Magic */ 77 | .highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # cmdx documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jun 20 20:12:06 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | import cmdx 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.doctest', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.autosummary', 40 | # 'sphinx.ext.viewcode', 41 | 'sphinxcontrib.napoleon' 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'cmdx' 58 | copyright = '2018, Marcus Ottosson' 59 | author = 'Marcus Ottosson' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = cmdx.__version__ 67 | # The full version, including alpha/beta/rc tags. 68 | release = version 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | # This patterns also effect to html_static_path and html_extra_path 80 | exclude_patterns = [] 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | pygments_style = 'sphinx' 84 | 85 | # If true, `todo` and `todoList` produce output, else they produce nothing. 86 | todo_include_todos = False 87 | 88 | 89 | # -- Options for HTML output ---------------------------------------------- 90 | 91 | # The theme to use for HTML and HTML Help pages. See the documentation for 92 | # a list of builtin themes. 93 | # 94 | html_theme = 'alabaster' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | # 100 | # html_theme_options = {} 101 | 102 | # Add any paths that contain custom static files (such as style sheets) here, 103 | # relative to this directory. They are copied after the builtin static files, 104 | # so a file named "default.css" will overwrite the builtin "default.css". 105 | html_static_path = ['_static'] 106 | 107 | 108 | # -- Options for HTMLHelp output ------------------------------------------ 109 | 110 | # Output file base name for HTML help builder. 111 | htmlhelp_basename = 'Avalondoc' 112 | 113 | 114 | # -- Options for LaTeX output --------------------------------------------- 115 | 116 | latex_elements = { 117 | # The paper size ('letterpaper' or 'a4paper'). 118 | # 119 | # 'papersize': 'letterpaper', 120 | 121 | # The font size ('10pt', '11pt' or '12pt'). 122 | # 123 | # 'pointsize': '10pt', 124 | 125 | # Additional stuff for the LaTeX preamble. 126 | # 127 | # 'preamble': '', 128 | 129 | # Latex figure (float) alignment 130 | # 131 | # 'figure_align': 'htbp', 132 | } 133 | 134 | # Grouping the document tree into LaTeX files. List of tuples 135 | # (source start file, target name, title, 136 | # author, documentclass [howto, manual, or own class]). 137 | latex_documents = [ 138 | (master_doc, 'cmdx.tex', 'cmdx Documentation', 139 | 'Marcus Ottosson', 'manual'), 140 | ] 141 | 142 | 143 | # -- Options for manual page output --------------------------------------- 144 | 145 | # One entry per manual page. List of tuples 146 | # (source start file, name, description, authors, manual section). 147 | man_pages = [ 148 | (master_doc, 'cmdx', 'cmdx Documentation', 149 | [author], 1) 150 | ] 151 | 152 | 153 | # -- Options for Texinfo output ------------------------------------------- 154 | 155 | # Grouping the document tree into Texinfo files. List of tuples 156 | # (source start file, target name, title, author, 157 | # dir menu entry, description, category) 158 | texinfo_documents = [ 159 | (master_doc, 'cmdx', 'cmdx Documentation', 160 | author, 'cmdx', 'Fast subset of maya.cmds', 161 | 'Miscellaneous'), 162 | ] 163 | 164 | 165 | def setup(app): 166 | app.add_stylesheet('css/cmdx.css') 167 | app.add_stylesheet('css/pygments.css') 168 | 169 | 170 | # add_module_names = False 171 | html_show_sourcelink = False 172 | -------------------------------------------------------------------------------- /docs/source/_static/css/pocoo.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @import url("basic.css"); 5 | 6 | /* -- page layout ----------------------------------------------------------- */ 7 | 8 | body { 9 | font-family: sans-serif; 10 | font-size: 17px; 11 | background-color: white; 12 | color: #000; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | div.document { 18 | width: 940px; 19 | margin: 30px auto 0 auto; 20 | } 21 | 22 | div.documentwrapper { 23 | float: left; 24 | width: 100%; 25 | } 26 | 27 | div.bodywrapper { 28 | margin: 0 0 0 220px; 29 | } 30 | 31 | div.sphinxsidebar { 32 | width: 220px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | padding: 0 30px 0 30px; 42 | } 43 | 44 | div.footer { 45 | width: 940px; 46 | margin: 20px auto 30px auto; 47 | font-size: 14px; 48 | color: #888; 49 | text-align: right; 50 | } 51 | 52 | div.footer a { 53 | color: #888; 54 | } 55 | 56 | div.related { 57 | display: none; 58 | } 59 | 60 | div.sphinxsidebar a { 61 | color: #444; 62 | text-decoration: none; 63 | border-bottom: 1px dotted #999; 64 | } 65 | 66 | div.sphinxsidebar a:hover { 67 | border-bottom: 1px solid #999; 68 | } 69 | 70 | div.sphinxsidebar { 71 | font-size: 14px; 72 | line-height: 1.5; 73 | } 74 | 75 | div.sphinxsidebarwrapper { 76 | padding: 18px 10px; 77 | } 78 | 79 | div.sphinxsidebarwrapper p.logo { 80 | padding: 0 0 20px 0; 81 | margin: 0; 82 | text-align: center; 83 | } 84 | 85 | div.sphinxsidebar h3, 86 | div.sphinxsidebar h4 { 87 | color: #444; 88 | font-size: 24px; 89 | font-weight: normal; 90 | margin: 0 0 5px 0; 91 | padding: 0; 92 | } 93 | 94 | div.sphinxsidebar h4 { 95 | font-size: 20px; 96 | } 97 | 98 | div.sphinxsidebar h3 a { 99 | color: #444; 100 | } 101 | 102 | div.sphinxsidebar p.logo a, 103 | div.sphinxsidebar h3 a, 104 | div.sphinxsidebar p.logo a:hover, 105 | div.sphinxsidebar h3 a:hover { 106 | border: none; 107 | } 108 | 109 | div.sphinxsidebar p { 110 | color: #555; 111 | margin: 10px 0; 112 | } 113 | 114 | div.sphinxsidebar ul { 115 | margin: 10px 0; 116 | padding: 0; 117 | color: #000; 118 | } 119 | 120 | div.sphinxsidebar input { 121 | border: 1px solid #ccc; 122 | font-size: 1em; 123 | } 124 | 125 | div.sphinxsidebar #searchbox input[type="text"] { 126 | width: 142px; 127 | } 128 | 129 | div.sphinxsidebar ul.versions a.current { 130 | font-style: italic; 131 | border-bottom: 1px solid black; 132 | color: black; 133 | } 134 | 135 | div.sphinxsidebar ul.versions span.note { 136 | color: #888; 137 | } 138 | 139 | /* -- warning --------------------------------------------------------------- */ 140 | 141 | p.version-warning { 142 | padding: 0 10px; 143 | height: 28px!important; 144 | line-height: 28px!important; 145 | font-size: 17px; 146 | margin: 0 0 10px 0; 147 | border-radius: 2px; 148 | 149 | letter-spacing: 1px; 150 | font-size: 13px; 151 | color: white; 152 | text-align: center; 153 | 154 | background: #d40; 155 | background-size: 28px 28px; 156 | background-image: linear-gradient(-45deg, 157 | rgba(255, 255, 255, 0.2) 0%, 158 | rgba(255, 255, 255, 0.2) 25%, 159 | transparent 25%, 160 | transparent 50%, 161 | rgba(255, 255, 255, 0.2) 50%, 162 | rgba(255, 255, 255, 0.2) 75%, 163 | transparent 75%, 164 | transparent 165 | ); 166 | } 167 | 168 | /* -- body styles ----------------------------------------------------------- */ 169 | 170 | a { text-decoration: underline; } 171 | 172 | div.body h1, 173 | div.body h2, 174 | div.body h3, 175 | div.body h4, 176 | div.body h5, 177 | div.body h6 { 178 | font-weight: normal; 179 | margin: 30px 0px 10px 0px; 180 | padding: 0; 181 | } 182 | 183 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 184 | div.body h2 { font-size: 180%; } 185 | div.body h3 { font-size: 150%; } 186 | div.body h4 { font-size: 130%; } 187 | div.body h5 { font-size: 100%; } 188 | div.body h6 { font-size: 100%; } 189 | 190 | a.headerlink { 191 | color: #ddd; 192 | padding: 0 4px; 193 | text-decoration: none; 194 | } 195 | 196 | a.headerlink:hover { 197 | color: #444; 198 | background: #eaeaea; 199 | } 200 | 201 | div.body p, div.body dd, div.body li { 202 | line-height: 1.4em; 203 | } 204 | 205 | div.admonition { 206 | background: #fafafa; 207 | margin: 20px -30px; 208 | padding: 10px 30px; 209 | border-top: 1px solid #ccc; 210 | border-bottom: 1px solid #ccc; 211 | } 212 | 213 | div.admonition code.xref, div.admonition a code { 214 | border-bottom: 1px solid #fafafa; 215 | } 216 | 217 | dd div.admonition { 218 | margin-left: -60px; 219 | padding-left: 60px; 220 | } 221 | 222 | div.admonition p.admonition-title { 223 | font-weight: normal; 224 | font-size: 24px; 225 | margin: 0 0 10px 0; 226 | padding: 0; 227 | line-height: 1; 228 | } 229 | 230 | div.admonition p.last { 231 | margin-bottom: 0; 232 | } 233 | 234 | div.highlight { 235 | background-color: white; 236 | } 237 | 238 | dt:target, .highlight { 239 | background: #FAF3E8; 240 | } 241 | 242 | div.note { 243 | background-color: #eee; 244 | border: 1px solid #ccc; 245 | } 246 | 247 | div.seealso { 248 | background-color: #ffc; 249 | border: 1px solid #ff6; 250 | } 251 | 252 | div.topic { 253 | background-color: #eee; 254 | } 255 | 256 | p.admonition-title { 257 | display: inline; 258 | } 259 | 260 | p.admonition-title:after { 261 | content: ":"; 262 | } 263 | 264 | pre, code { 265 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 266 | font-size: 0.9em; 267 | } 268 | 269 | img.screenshot { 270 | } 271 | 272 | code.descname, code.descclassname { 273 | font-size: 0.95em; 274 | } 275 | 276 | code.descname { 277 | padding-right: 0.08em; 278 | } 279 | 280 | img.screenshot { 281 | -moz-box-shadow: 2px 2px 4px #eee; 282 | -webkit-box-shadow: 2px 2px 4px #eee; 283 | box-shadow: 2px 2px 4px #eee; 284 | } 285 | 286 | table.docutils { 287 | border: 1px solid #888; 288 | -moz-box-shadow: 2px 2px 4px #eee; 289 | -webkit-box-shadow: 2px 2px 4px #eee; 290 | box-shadow: 2px 2px 4px #eee; 291 | } 292 | 293 | table.docutils td, table.docutils th { 294 | border: 1px solid #888; 295 | padding: 0.25em 0.7em; 296 | } 297 | 298 | table.field-list, table.footnote { 299 | border: none; 300 | -moz-box-shadow: none; 301 | -webkit-box-shadow: none; 302 | box-shadow: none; 303 | } 304 | 305 | table.footnote { 306 | margin: 15px 0; 307 | width: 100%; 308 | border: 1px solid #eee; 309 | background: #fdfdfd; 310 | font-size: 0.9em; 311 | } 312 | 313 | table.footnote + table.footnote { 314 | margin-top: -15px; 315 | border-top: none; 316 | } 317 | 318 | table.field-list th { 319 | padding: 0 0.8em 0 0; 320 | } 321 | 322 | table.field-list td { 323 | padding: 0; 324 | } 325 | 326 | table.footnote td.label { 327 | width: 0px; 328 | padding: 0.3em 0 0.3em 0.5em; 329 | } 330 | 331 | table.footnote td { 332 | padding: 0.3em 0.5em; 333 | } 334 | 335 | dl { 336 | margin: 0; 337 | padding: 0; 338 | } 339 | 340 | dl dd { 341 | margin-left: 30px; 342 | } 343 | 344 | blockquote { 345 | margin: 0 0 0 30px; 346 | padding: 0; 347 | } 348 | 349 | ul, ol { 350 | margin: 10px 0 10px 30px; 351 | padding: 0; 352 | } 353 | 354 | pre { 355 | background: #eee; 356 | padding: 7px 30px; 357 | margin: 15px -30px; 358 | line-height: 1.3em; 359 | } 360 | 361 | dl pre, blockquote pre, li pre { 362 | margin-left: -60px; 363 | padding-left: 60px; 364 | } 365 | 366 | dl dl pre { 367 | margin-left: -90px; 368 | padding-left: 90px; 369 | } 370 | 371 | code { 372 | background-color: #ecf0f3; 373 | color: #222; 374 | /* padding: 1px 2px; */ 375 | } 376 | 377 | code.xref, a code { 378 | background-color: #FBFBFB; 379 | border-bottom: 1px solid white; 380 | } 381 | 382 | a.reference { 383 | text-decoration: none; 384 | } 385 | 386 | a.footnote-reference { 387 | text-decoration: none; 388 | font-size: 0.7em; 389 | vertical-align: top; 390 | } 391 | 392 | a:hover code { 393 | background: #EEE; 394 | } 395 | 396 | /* carbon ads */ 397 | div.carbon_ads { 398 | text-align: center; 399 | overflow: hidden; 400 | margin: 15px 0; 401 | border: 1px solid #ccc; 402 | background: #eee; 403 | padding: 10px 7px; 404 | } 405 | 406 | div.carbon_ads a.carbon-img { 407 | display: block; 408 | margin-bottom: 7px; 409 | border: none; 410 | } 411 | 412 | div.carbon_ads a.carbon-poweredby { 413 | margin-top: 7px; 414 | margin-left: 15px; 415 | float: right; 416 | font-size: 11px; 417 | } -------------------------------------------------------------------------------- /plot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Plot performance comparisons between mel, cmds, cmdx and PyMEL""" 3 | 4 | import os 5 | import sys 6 | import timeit 7 | import importlib 8 | from copy import deepcopy 9 | 10 | from maya import cmds, mel 11 | from maya import OpenMaya as om1 12 | from maya.api import OpenMaya as om2 13 | from pymel import core as pm 14 | 15 | import cmdx 16 | 17 | try: 18 | # Python 3 support 19 | reload = importlib.reload 20 | except AttributeError: 21 | pass 22 | 23 | try: 24 | # Mock irrelevant pygal dependency 25 | sys.modules["pkg_resources"] = type("Mock", (object,), { 26 | "iter_entry_points": lambda *args, **kwargs: [] 27 | })() 28 | 29 | import pygal 30 | except ImportError: 31 | raise ImportError("plot.py requires pygal") 32 | 33 | # Results from tests end up here 34 | data = dict() 35 | 36 | # Precisions 37 | Milliseconds = 0 38 | Nanoseconds = 1 39 | 40 | 41 | def Test(method, 42 | task, 43 | func, 44 | setup=None, 45 | teardown=None, 46 | number=1000, 47 | repeat=5, 48 | precision=1): 49 | 50 | results = list() 51 | 52 | setup = setup or (lambda: None) 53 | teardown = teardown or (lambda: None) 54 | 55 | text = "%s %s: %.1f ms (%.2f {precision}/call)".format( 56 | precision="μs" if precision else "ms" 57 | ) 58 | 59 | for iteration in range(repeat): 60 | setup() 61 | results += [timeit.Timer(func).timeit(number)] 62 | teardown() 63 | 64 | print(text % ( 65 | task, 66 | method, 67 | 10 ** 3 * sum(results), 68 | 10 ** (6 if precision else 3) * min(results) / number 69 | )) 70 | 71 | if task not in data: 72 | data[task] = {} 73 | 74 | # Store for plot 75 | data[task][method] = { 76 | "func": func, 77 | "number": number, 78 | "results": results, 79 | "min": sum(results), 80 | "percall": min(results) / number 81 | } 82 | 83 | 84 | def New(setup=None): 85 | cmds.file(new=True, force=True) 86 | (setup or (lambda: None))() 87 | 88 | 89 | def reload_pymel(): 90 | """PyMEL consists of many submodules 91 | 92 | PyMEL does initialisation on import. 93 | The duration of the initialisation increases 94 | linearly with the number of plug-ins available 95 | on import. 96 | 97 | """ 98 | 99 | for mod in sys.modules.copy(): 100 | if mod.startswith("pymel"): 101 | sys.modules.pop(mod) 102 | 103 | import pymel.core 104 | pymel.core # avoid linter warning 105 | 106 | 107 | def om1GetAttr(): 108 | """Fastest way of getting an attribute with API 2.0""" 109 | plug = api2mfn.findPlug("translateX", False) 110 | return plug.asDouble() 111 | 112 | 113 | def om2GetAttr(): 114 | """Fastest way of getting an attribute with API 2.0""" 115 | plug = api2mfn.findPlug("translateX", False) 116 | return plug.asDouble() 117 | 118 | 119 | def om1SetAttr(value): 120 | """Fastest way of getting an attribute with API 2.0""" 121 | plug = api2mfn.findPlug("translateX", False) 122 | return plug.setDouble(value) 123 | 124 | 125 | def om2SetAttr(value): 126 | """Fastest way of getting an attribute with API 2.0""" 127 | plug = api2mfn.findPlug("translateX", False) 128 | return plug.setDouble(value) 129 | 130 | 131 | New() 132 | 133 | Test("cmdx", "import", lambda: reload(cmdx), number=100) 134 | Test("cmds", "import", lambda: reload(cmds), number=100) 135 | Test("PyMEL", "import", reload_pymel, number=1) 136 | 137 | New() 138 | 139 | node = cmdx.createNode("transform", name="Node") 140 | path = node.path() 141 | pynode = pm.PyNode(path) 142 | api1node = om1.MFnDagNode().create("transform") 143 | api2node = om2.MFnDagNode().create("transform") 144 | api1mfn = om1.MFnDagNode(api1node) 145 | api2mfn = om2.MFnDagNode(api2node) 146 | 147 | node = cmdx.createNode("transform", name="Node") 148 | path = node.path() 149 | pynode = pm.PyNode(path) 150 | api1node = om1.MFnDagNode().create("transform") 151 | api2node = om2.MFnDagNode().create("transform") 152 | api1mfn = om1.MFnDagNode(api1node) 153 | api2mfn = om2.MFnDagNode(api2node) 154 | 155 | Test("cmds", "long", lambda: cmds.ls(path, long=True)) 156 | Test("cmdx", "long", lambda: node.path()) 157 | Test("PyMEL", "long", lambda: pm.ls(path, long=True)) 158 | Test("API 1.0", "long", lambda: api2mfn.fullPathName()) 159 | Test("API 2.0", "long", lambda: api2mfn.fullPathName()) 160 | 161 | Test("mel", "getAttr", lambda: mel.eval("getAttr %s" % (path + ".tx")), number=10000) 162 | Test("cmds", "getAttr", lambda: cmds.getAttr(path + ".tx"), number=10000) 163 | Test("cmdx", "getAttr", lambda: cmdx.getAttr(node + ".tx", type=cmdx.Double), number=10000) 164 | Test("PyMEL", "getAttr", lambda: pynode.tx.get(), number=10000) 165 | Test("API 1.0", "getAttr", lambda: om1GetAttr(), number=10000) 166 | Test("API 2.0", "getAttr", lambda: om2GetAttr(), number=10000) 167 | 168 | Test("mel", "setAttr", lambda: mel.eval("setAttr %s %s" % (path + ".tx", 5))) 169 | Test("cmds", "setAttr", lambda: cmds.setAttr(path + ".tx", 5)) 170 | Test("cmdx", "setAttr", lambda: cmdx.setAttr(node + ".tx", 5, type=cmdx.Double)) 171 | Test("PyMEL", "setAttr", lambda: pm.setAttr(pynode + ".tx", 5)) 172 | Test("API 1.0", "setAttr", lambda: om1SetAttr(5)) 173 | Test("API 2.0", "setAttr", lambda: om2SetAttr(5)) 174 | 175 | Test("cmdx", "node.attr", lambda: node["tx"].read(), number=10000) 176 | Test("PyMEL", "node.attr", lambda: pynode.tx.get(), number=10000) 177 | 178 | Test("cmdx", "node.attr=5", lambda: node["tx"].write(5), number=10000) 179 | Test("PyMEL", "node.attr=5", lambda: pynode.tx.set(5), number=10000) 180 | 181 | Test("mel", "createNode", lambda: mel.eval("createNode \"transform\""), New) 182 | Test("cmds", "createNode", lambda: cmds.createNode("transform"), New) 183 | Test("cmdx", "createNode", lambda: cmdx.createNode(cmdx.Transform), New) 184 | Test("PyMEL", "createNode", lambda: pm.createNode("transform"), New) 185 | Test("API 1.0", "createNode", lambda: om1.MFnDagNode().create("transform"), New) 186 | Test("API 2.0", "createNode", lambda: om2.MFnDagNode().create("transform"), New) 187 | 188 | New() 189 | 190 | root1 = cmdx.createNode("transform") 191 | root2 = cmdx.createNode("transform") 192 | child = cmdx.createNode("transform", parent=root1) 193 | 194 | def teardown(): 195 | cmds.parent("transform3", "transform1", r=1) 196 | 197 | melparent = 'parent -r "transform3" "transform2";' 198 | Test("mel", "parent", lambda: mel.eval(melparent), number=1, repeat=1000, teardown=teardown) 199 | Test("cmds", "parent", lambda: cmds.parent("transform3", "transform2", r=1), number=1, repeat=1000, teardown=teardown) 200 | Test("cmdx", "parent", lambda: cmdx.parent(child, root2), number=1, repeat=1000, teardown=teardown) 201 | root1, root2, child = pm.ls(map(str, (root1, root2, child))) 202 | Test("PyMEL", "parent", lambda: pm.parent(child, root2, r=1), number=1, repeat=1000, teardown=teardown) 203 | 204 | New() 205 | 206 | parent = cmdx.createNode("transform") 207 | path = parent.path() 208 | pynode = pm.PyNode(path) 209 | 210 | for x in range(100): 211 | cmdx.createNode("transform", parent=parent) 212 | 213 | Test("mel", "listRelatives", lambda: mel.eval('listRelatives -children "transform1"')) 214 | Test("cmds", "listRelatives", lambda: cmds.listRelatives(path, children=True)) 215 | Test("cmdx", "listRelatives", lambda: cmdx.listRelatives(parent, children=True)) 216 | Test("PyMEL", "listRelatives", lambda: pm.listRelatives(pynode, children=True)) 217 | 218 | New() 219 | 220 | root = cmdx.createNode("transform") 221 | parent = root 222 | path = root.path() 223 | pynode = pm.PyNode(path) 224 | 225 | for x in range(100): 226 | parent = cmdx.createNode("transform", parent=parent) 227 | 228 | Test("mel", "allDescendents", lambda: mel.eval('listRelatives -allDescendents "transform1"')) 229 | Test("cmds", "allDescendents", lambda: cmds.listRelatives(path, allDescendents=True)) 230 | Test("cmdx", "allDescendents", lambda: cmdx.listRelatives(root, allDescendents=True)) 231 | Test("PyMEL", "allDescendents", lambda: pm.listRelatives(pynode, allDescendents=True)) 232 | 233 | New() 234 | 235 | node1 = cmdx.createNode("transform") 236 | node2 = cmdx.createNode("transform") 237 | 238 | 239 | def teardown(): 240 | cmds.disconnectAttr("transform1.tx", "transform2.tx") 241 | 242 | 243 | melconnect = 'connectAttr "transform1.tx" "transform2.tx"' 244 | Test("mel", "connectAttr", lambda: mel.eval(melconnect), teardown=teardown, number=1, repeat=1000) 245 | Test("cmds", "connectAttr", lambda: cmds.connectAttr("transform1.tx", "transform2.tx"), teardown=teardown, number=1, repeat=5000) 246 | Test("cmdx", "connectAttr", lambda: cmdx.connectAttr(node1["tx"], node2["tx"]), teardown=teardown, number=1, repeat=5000) 247 | Test("PyMEL", "connectAttr", lambda: pm.connectAttr("transform1.tx", "transform2.tx"), teardown=teardown, number=1, repeat=5000) 248 | 249 | New() 250 | 251 | def teardown(): 252 | cmds.deleteAttr("transform1.myAttr") 253 | 254 | 255 | node = cmdx.createNode("transform") 256 | path = node.path() 257 | 258 | meladdattr = 'addAttr -ln "myAttr" -at double -dv 0 transform1;' 259 | Test("mel", "addAttr", lambda: mel.eval(meladdattr), number=1, repeat=1000, teardown=teardown) 260 | Test("cmds", "addAttr", lambda: cmds.addAttr(path, longName="myAttr", attributeType="double", defaultValue=0), number=1, repeat=1000, teardown=teardown) 261 | Test("cmdx", "addAttr", lambda: cmdx.addAttr(node, longName="myAttr", attributeType=cmdx.Double, defaultValue=0), number=1, repeat=1000, teardown=teardown) 262 | Test("PyMEL", "addAttr", lambda: pm.addAttr(path, longName="myAttr", attributeType="double", defaultValue=0), number=1, repeat=1000, teardown=teardown) 263 | 264 | Test("cmdx", "node.addAttr", lambda: node.addAttr(cmdx.Double("myAttr")), number=1, repeat=1000, teardown=teardown) 265 | 266 | # 267 | # Render performance characteristics as bar charts 268 | # 269 | # |___ 270 | # |___|_______ 271 | # |___________| 272 | # |______|___ 273 | # |__________|____ 274 | # |_______________|________ 275 | 276 | # Mock irrelevant pygal dependency 277 | sys.modules["pkg_resources"] = type("Mock", (object,), { 278 | "iter_entry_points": lambda *args, **kwargs: [] 279 | })() 280 | 281 | 282 | def stacked(data, dirname): 283 | data = deepcopy(data) 284 | tasks = sorted(data.keys()) 285 | 286 | # Use a fixed order of methods in the plot 287 | methods = ("mel", "cmds", "PyMEL", "cmdx") 288 | 289 | # [group1 result, group2 result, ... of MEL] 290 | # [group1 result, group2 result, ... of cmds] 291 | # ... 292 | 293 | cols = list() 294 | for method in methods: 295 | col = list() 296 | for task in tasks: 297 | col += [data[task].get(method, {}).get("min", 0)] 298 | cols.append(col) 299 | 300 | # Normalise along Y-axis 301 | rows = zip(*cols) 302 | 303 | for index, row in enumerate(rows[:]): 304 | rows[index] = [100.0 * col / sum(row) for col in row] 305 | 306 | cols = zip(*rows) 307 | 308 | line_chart = pygal.StackedBar() 309 | line_chart.title = "cmdx performance plot (in %)" 310 | line_chart.x_labels = tasks 311 | 312 | for method, col in enumerate(cols): 313 | line_chart.add(methods[method], col) 314 | 315 | fname = os.path.join(dirname, "stacked.svg") 316 | line_chart.render_to_file(fname) 317 | 318 | 319 | def horizontal(data, dirname): 320 | data = deepcopy(data) 321 | order = ("PyMEL", "mel", "cmds", "cmdx") 322 | 323 | for task, methods in data.items(): 324 | chart = pygal.HorizontalBar() 325 | chart.title = task + u" (μs)" 326 | for method in order: 327 | values = methods.get(method, {}) 328 | if not values: 329 | continue 330 | chart.add(method, 10 ** 6 * values.get("percall", 0)) 331 | 332 | fname = os.path.join( 333 | dirname, r"%s.svg" % task 334 | ) 335 | 336 | chart.render_to_file(fname) 337 | 338 | 339 | def average(x, y, data): 340 | data = deepcopy(data) 341 | 342 | times_faster = list() 343 | print("| | Times | Task") 344 | print("|:--------|:------------|:------------") 345 | for task, methods in data.items(): 346 | try: 347 | a = methods[x]["percall"] 348 | b = methods[y]["percall"] 349 | except KeyError: 350 | continue 351 | 352 | faster = a / float(b) 353 | print("| cmdx is | %.1fx faster | %s" % (faster, task)) 354 | times_faster.append(faster) 355 | 356 | average = sum(times_faster) / len(times_faster) 357 | return round(average, 2) 358 | 359 | 360 | # Draw plots 361 | dirname = os.path.join(os.path.dirname(cmdx.__file__), "plots") 362 | stacked(data, dirname) 363 | horizontal(data, dirname) 364 | avg = average("PyMEL", "cmdx", data) 365 | print("- cmdx is on average %.2fx faster than PyMEL" % avg) 366 | avg = average("cmds", "cmdx", data) 367 | print("- cmdx is on average %.2fx faster than cmds" % avg) -------------------------------------------------------------------------------- /plots/dump.svg: -------------------------------------------------------------------------------- 1 | 2 | dump (ms)01020304044.10961853352.2257.0dump (ms)cmdx -------------------------------------------------------------------------------- /plots/node.addAttr.svg: -------------------------------------------------------------------------------- 1 | 2 | node.addAttr (µs)0481216202428323639.27268915352.2257.0node.addAttr (µs)cmdx -------------------------------------------------------------------------------- /plots/json.svg: -------------------------------------------------------------------------------- 1 | 2 | json (ms)00.020.040.060.080.10.120.140.160.180.20.220.240.260.2661820763352.2257.0json (ms)cmdx -------------------------------------------------------------------------------- /plots/node.attr=5.svg: -------------------------------------------------------------------------------- 1 | 2 | node.attr=5 (µs)02040608010012014016018016.9710865743.9536062398365.730769231185.7695168348.0148.269230769node.attr=5 (µs)PyMELcmdx -------------------------------------------------------------------------------- /plots/node.attr.svg: -------------------------------------------------------------------------------- 1 | 2 | node.attr (µs)010203040506070809010011012013014011.0790195638.8490651545365.730769231145.5837619348.0148.269230769node.attr (µs)PyMELcmdx -------------------------------------------------------------------------------- /plots/uuid.svg: -------------------------------------------------------------------------------- 1 | 2 | uuid (ms)024681012141618200.352970039219.0739565367401.97435897416.16583386266.716479821257.021.62426339352.2112.025641026uuid (ms)melcmdscmdx -------------------------------------------------------------------------------- /plots/createNode.svg: -------------------------------------------------------------------------------- 1 | 2 | createNode (µs)010020030040075.8467143371.2841398428420.096153846278.1951242225.751961004311.365384615299.9502546242.359294847202.634615385438.3365446348.093.9038461538createNode (µs)PyMELmelcmdscmdx -------------------------------------------------------------------------------- /plots/long.svg: -------------------------------------------------------------------------------- 1 | 2 | long (µs)01020304050607080901001106.45575131631.9797900347401.97435897423.3502803980.6428663156257.0116.1695843348.0112.025641026long (µs)PyMELcmdscmdx -------------------------------------------------------------------------------- /plots/listRelatives.svg: -------------------------------------------------------------------------------- 1 | 2 | listRelatives (µs)0100020003000400050006000902.31912559.1753825149420.09615384686.5264615517.7756483812311.365384615166.421656821.8301587858202.6346153856593.684273348.093.9038461538listRelatives (µs)PyMELmelcmdscmdx -------------------------------------------------------------------------------- /plots/ls.svg: -------------------------------------------------------------------------------- 1 | 2 | ls (µs)01000200030004000500060007000318.314490528.1955158628420.096153846169.987558821.2939908319311.365384615254.48897425.2257692752202.6346153857191.52261348.093.9038461538ls (µs)PyMELmelcmdscmdx -------------------------------------------------------------------------------- /plots/addAttr.svg: -------------------------------------------------------------------------------- 1 | 2 | addAttr (µs)01020304050607039.27268915181.731485725420.09615384647.99995344219.141901492311.36538461567.39387402302.276158507202.63461538578.06053037348.093.9038461538addAttr (µs)PyMELmelcmdscmdx --------------------------------------------------------------------------------