diff --git a/AL/__init__.py b/AL/__init__.py index 56af7f4..d5ce2b6 100644 --- a/AL/__init__.py +++ b/AL/__init__.py @@ -1,3 +1,18 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + """ This file will not be used. It is a proxy for the real AL.__init__.py declared in PythonLibs source code """ diff --git a/AL/omx/__init__.py b/AL/omx/__init__.py index a6fc1e0..e35c5c1 100644 --- a/AL/omx/__init__.py +++ b/AL/omx/__init__.py @@ -1,16 +1,17 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -""" -OMX is a thin wrapper around Maya OM2. - -OMX's goal is to make OM2 more user-friendly but still retain the API's performance. - -Main entry points are: -:py:func:`omx.createDagNode` and :py:func:`omx.createDGNode` which return instances of :py:class:`omx.XNode` - -See :doc:`/topics/omx` for more information. - -""" from ._xplug import XPlug # NOQA: F401 from ._xnode import XNode # NOQA: F401 from ._xmodifier import ( @@ -25,6 +26,9 @@ XModifier, queryTrackedNodes, TrackCreatedNodes, + setJournalToggle, + isJournalOn, + JournalContext, ) # NOQA: F401 try: @@ -48,4 +52,7 @@ "XModifier", "queryTrackedNodes", "TrackCreatedNodes", + "setJournalToggle", + "isJournalOn", + "JournalContext", ] diff --git a/AL/omx/_xcommand.py b/AL/omx/_xcommand.py index 2b69ee3..e386bb5 100644 --- a/AL/omx/_xcommand.py +++ b/AL/omx/_xcommand.py @@ -1,16 +1,34 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os import logging -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2.omx import _xmodifier + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx import _xmodifier logger = logging.getLogger(__name__) class XCommand(om2.MPxCommand): + """An internal dynamic command plugin class called by createAL_Command, it is an undoable + MPxCommand omx uses to support undo/redo in Maya. - """Dynamic command plugin called by createAL_Command + Notes: + You don't need to ever touch this command or manually call it. It is completely for + internal use only. """ PLUGIN_CMD_NAME = "AL_OMXCommand" @@ -50,5 +68,14 @@ def undoIt(self): def ensureLoaded(cls): if cls._CMD_PLUGIN_LOADED: return + + # to ensure plugin is loadable outside AL: + pluginDirEnvName = "MAYA_PLUG_IN_PATH" + pluginDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugin") + plugInDirs = os.environ.get(pluginDirEnvName, "").split(";") + if pluginDir not in plugInDirs: + plugInDirs.append(pluginDir) + os.environ[pluginDirEnvName] = ";".join(plugInDirs) + cmds.loadPlugin(cls.PLUGIN_CMD_NAME, quiet=True) cls._CMD_PLUGIN_LOADED = True diff --git a/AL/omx/_xmodifier.py b/AL/omx/_xmodifier.py index eecb4d4..673b6f8 100644 --- a/AL/omx/_xmodifier.py +++ b/AL/omx/_xmodifier.py @@ -1,4 +1,16 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import inspect import contextlib @@ -7,25 +19,93 @@ import logging from functools import wraps -from maya import cmds -from maya.api import OpenMaya as om2 -from maya.api import OpenMayaAnim as om2anim -from AL.maya2.omx import _xnode -from AL.maya2.omx.utils import _nodes -from AL.maya2.omx.utils import _modifiers +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx.utils._stubs import om2anim + +from AL.omx import _xnode +from AL.omx.utils import _nodes +from AL.omx.utils import _modifiers logger = logging.getLogger(__name__) _CURRENT_MODIFIER_LIST = [] +_JOURNAL_TOGGLE = None + + +def setJournalToggle(state): + """By default we don't keep journal for all the creation or edit by omx. Use this function + to turn it on. + + Notes: + Keep in mind this is a global state, turning on journal will slow down the overall + performance! + + Another way to toggle the journal on is to set it to None (default value) and set the + logging level to ``logging.DEBUG`` for ``AL.omx._xmodifier``. + + Also turning the journal on only makes omx start to record journal, the creation or + edits that are already done still won't be in the journal. + + Args: + state (bool | None): the state of toggle. + True = force on, off = force off, None = depends on it is DEBUG logging level + """ + global _JOURNAL_TOGGLE + _JOURNAL_TOGGLE = state + if state is None: + logger.info( + "The omx journal state now depends on logging level for AL.omx._xmodifier" + ) + else: + logger.info("The omx journal state is turned %s", state) + + +def isJournalOn(): + """Query if we are actually recording journal for each creation or edit by omx. + """ + if _JOURNAL_TOGGLE is None: + return logger.isEnabledFor(logging.DEBUG) + + return _JOURNAL_TOGGLE + + +class JournalContext: + """A python context where you set the journal state by force. + + Notes: + Turning on by calling :func:`setJournalToggle(True)` will slowdown omx performance + globally. This is the suggested way to have the journal temporarily set to on/off. + """ + + def __init__(self, state=True): + self._state = state + self._oldState = _JOURNAL_TOGGLE + + def __enter__(self): + setJournalToggle(self._state) + return self + + def __exit__(self, *_, **__): + setJournalToggle(self._oldState) class NodeCreationLog: - """Helper class to enable/disable and manage tracked nodes. + """Helper class to enable/disable and manage tracked nodes, you are suppose to only have + one instance of this class in memory, within this python module. - Log entries are done in a First In Last Out approach to allow for nested tracking - when a parent that wants to track nodes calls a child that also wants to track nodes. + Notes: + Log entries are done in a First In Last Out approach to allow for nested tracking + when a parent that wants to track nodes calls a child that also wants to track nodes. + + The data is held in this form: + [ + [:class:`om2.MObjectHandle`,...], + [:class:`om2.MObjectHandle`,...], + ... + ] """ def __init__(self): @@ -42,18 +122,20 @@ def clearLogEntry(self, clearAll=False): """Remove all or the last list of tracked nodes in the log. Args: - clearAll (bool): If true, remove the entire log. + clearAll (bool, optional): If true, remove the entire log. """ if clearAll: self._log = [] - self._log.pop() + else: + self._log.pop() self._isActive = len(self._log) >= 1 def trackedNodes(self, queryAll=False): """The nodes that have been tracked in the creation log. Args: - queryAll (bool): If true, get the entire log, otherwise just the last key in the log. + queryAll (bool, optional): If true, get the entire log, otherwise retrieve the + last key in the log. Returns: list[:class:`om2.MObjectHandle`]: The list of created nodes. @@ -70,7 +152,7 @@ def trackNode(self, node): """Add a node to the last active key in the tracking log. Args: - node (:class:`omx.XNode`): The node to track. + node (:class:`XNode`): The node to track. """ if not self.isActive(): return @@ -99,7 +181,8 @@ def endTrackingNodes(endAll=False): """Stop and clear the last (or all) active log(s) of tracked nodes. Args: - endAll (bool): If true, ends all active tracking. + endAll (bool, optional): If true, ends all active tracking. + Returns: list[:class:`om2.MObjectHandle`]: The list of created nodes that had been tracked. """ @@ -116,8 +199,8 @@ def queryTrackedNodes(queryAll=False): """The mobject handles to the nodes that have been created since tracking has been started. Args: - queryAll (bool): If true, return the entire list of handles, otherwise just the handles - since startTrackingNodes has last been called. + queryAll (bool, optional): If true, return the entire list of handles, otherwise just the handles + since startTrackingNodes has last been called. Returns: list[:class:`om2.MObjectHandle`]: The list of created nodes. @@ -130,22 +213,23 @@ def queryTrackedNodes(queryAll=False): class TrackCreatedNodes: - """ - A Python Context Decorator to temporarily track nodes that have been created with omx + """A Python Context Decorator to temporarily track nodes that have been created with omx - Example usage: + Examples: - @TrackCreatedNodes() - def methodToCreateNodes(): - # Create nodes - nodesCreated = omx.queryTrackedNodes() + .. code:: python - OR + # The class can be used as decorator, or python context: - def methodToCreateNodes(): - with TrackCreatedNodes() as tracker: + @TrackCreatedNodes() + def methodToCreateNodes(): # Create nodes - nodesCreated = tracker.trackedNodes() + nodesCreated = omx.queryTrackedNodes() + + def methodToCreateNodes(): + with TrackCreatedNodes() as tracker: + # Create nodes + nodesCreated = tracker.trackedNodes() """ def __call__(self, func): @@ -167,11 +251,11 @@ def trackedNodes(self, queryAll=False): """Get the om2.MObjectHandle(s) created that are tracked. Args: - queryAll (bool): Whether return all batches of om2.MObjectHandles or just the last - batch. + queryAll (bool, optional): Whether return all batches of om2.MObjectHandles or just the last + batch. Returns: - list[:class:`om2.MObjectHandle`]: Created nodes. + [:class:`om2.MObjectHandle`]: Created nodes. """ return queryTrackedNodes(queryAll) @@ -180,6 +264,12 @@ class XModifierLog: __slots__ = ["method", "values"] def __init__(self, method, values): + """Internal wrapper object to hold the method name and argument values. + + Args: + method (str): the method name. + values (list): the list of arguments for method call, excluding self. + """ self.method = method self.values = values @@ -188,17 +278,35 @@ def __str__(self): def _modifierMethod(method): + """A function decorator for :class:`XModifier` methods. + + Notes: + This decorator; + - Converts :class:`XNode` instances to om2.MObjects . + - Records a method call log in the journal. + - Calls the method. + - Calls doIt() to apply the potential edits in immediate mode. + + Args: + method (callable): the callable method object. + + Returns: + callable: the wrapped method + """ + @wraps(method) def wrapper(*args, **kwargs): self = args[0] - # Add journal entry, convert all MObjects to MObjectHandles - values = inspect.getcallargs(method, *args, **kwargs) - del values["self"] - for k, v in values.items(): - if isinstance(v, om2.MObject): - values[k] = om2.MObjectHandle(v) - self._journal.append(XModifierLog(method.__name__, values)) # NOQA + # Add journal entry if needed, convert all MObjects to MObjectHandles + if isJournalOn(): + values = inspect.getcallargs(method, *args, **kwargs) + del values["self"] + for k, v in values.items(): + if isinstance(v, om2.MObject): + values[k] = om2.MObjectHandle(v) + self._journal.append(XModifierLog(method.__name__, values)) # NOQA + self._clean = False # NOQA # Process args to convert any XNode to MObject @@ -207,6 +315,7 @@ def wrapper(*args, **kwargs): if isinstance(arg, _xnode.XNode): arg = arg.object() newArgs.append(arg) + for k, v in kwargs.items(): if isinstance(v, _xnode.XNode): kwargs[k] = v.object() @@ -223,11 +332,12 @@ def wrapper(*args, **kwargs): class XModifier: - """ A wrapper around _modifiers.MModifier that supports :class:`_xnode.XNode` instances directly + """ A wrapper around :class:`MModifier` that supports :class:`XNode` instances directly - When created in immediate mode, every time any modifier method is run on this object the doIt method is also run from within a - dynamic AL_OMXCommand instance to allow undoing. - Immediate mode will always be much slower than non-immediate mode, and is only there to allow simple experimentation from the maya script editor. + Notes: + When created in immediate mode, every time any modifier method is run on this object the doIt method is also run from within a + dynamic :class:`AL_OMXCommand` instance to allow undoing. Immediate mode will always be much slower than non-immediate mode, + and is only there to allow simple experiments from the Maya script editor. """ def __init__(self, immediate=False): @@ -245,7 +355,7 @@ def _reset(self): self._clean = True def journal(self): - """Returns the current list of operations to run + """Returns the current list of operations to run. Returns: list(str): A list of strings describing the operations to run. @@ -282,28 +392,34 @@ def _reallyDoIt(self, keepJournal=False): DoItModifierWrapper(self, self._modifier).doIt() finally: if logger.isEnabledFor(logging.DEBUG): - logger.debug("Just called doIt on:\n%s", "\n".join(self.journal())) + if isJournalOn(): + logger.debug("Just called doIt on:\n%s", "\n".join(self.journal())) + else: + logger.debug("Just called doIt (No journal available)") + if not keepJournal: self._journal = [] def isClean(self): """Returns True if the modifier has nothing to do. - isClean() will also return True if the modifier has already been used by a Command. + Notes: + It will also return True if the modifier has already been used by a Command. Returns: - bool: anything to do with this modifier? + bool: the clean state. """ return self._clean def doIt(self, keepJournal=False): - """Executes the modifier in Maya. In immediate mode this will actually execute doIt from within a dynamic maya command to allow undo to function. + """Executes the operations held by this modifier in Maya. + + Notes: + In immediate mode this will actually execute doIt from within a dynamic Maya command to allow undo to function. - Executes the modifier's operations. If doIt() is called multiple times - in a row, without any intervening calls to undoIt(), then only the - operations which were added since the previous doIt() call will be - executed. If undoIt() has been called then the next call to doIt() will - do all operations. + If doIt() is called multiple times in a row, without any intervening calls to undoIt(), then only the + operations which were added since the previous doIt() call will be executed. If undoIt() has been + called then the next call to doIt() will do all operations. Args: keepJournal (bool, optional): Retains the journal for further inspection. Defaults to False. @@ -318,10 +434,12 @@ def doIt(self, keepJournal=False): self._reset() def undoIt(self, keepJournal=False): - """Undo the modifier operation in Maya. In immediate mode this function does nothing, as you should already be able to undo it in Maya. + """Undo the modifier operation in Maya. In immediate mode this function does nothing, as you should already + be able to undo it in Maya. Notes: It is only used in the scenario that a user creates a modifier manually by calling omx.newModifier() + Args: keepJournal (bool, optional): Retains the journal for further inspection. Defaults to False. """ @@ -337,7 +455,13 @@ def undoIt(self, keepJournal=False): DoItModifierWrapper(self, self._modifier).undoIt() finally: if logger.isEnabledFor(logging.DEBUG): - logger.debug("Just called undoIt on:\n%s", "\n".join(self.journal())) + if isJournalOn(): + logger.debug( + "Just called undoIt on:\n%s", "\n".join(self.journal()) + ) + else: + logger.debug("Just called undoIt (No journal available)") + if not keepJournal: self._journal = [] @@ -350,7 +474,8 @@ def addAttribute(self, node, attribute): be added as well, so only the parent needs to be added using this method. Args: - node (:class:`_xnode.XNode` | :class:`om2.MObject`): the node to add an attribute to + node (:class:`XNode` | :class:`om2.MObject`): the node to add an attribute to + attribute (:class:`om2.MObject`): the attribute MObject Returns: @@ -365,12 +490,14 @@ def addAttribute(self, node, attribute): def addExtensionAttribute(self, nodeClass, attribute): """Adds an extension attribute to a node class - Adds an operation to the modifier to add a new extension attribute to - the given node class. If the attribute is a compound its children will be - added as well, so only the parent needs to be added using this method. + Notes: + Adds an operation to the modifier to add a new extension attribute to + the given node class. If the attribute is a compound its children will be + added as well, so only the parent needs to be added using this method. Args: nodeClass (:class:`om2.MNodeClass`): The node class + attribute (:class:`om2.MObject`): The attribute MObject to add Returns: @@ -383,13 +510,14 @@ def addExtensionAttribute(self, nodeClass, attribute): def commandToExecute(self, command): """Adds an operation to the modifier to execute a MEL command. - The command should be fully undoable otherwise unexpected results may occur. If - the command contains no undoable portions whatsoever, the call to - doIt() may fail, but only after executing the command. It is best to - use multiple commandToExecute() calls rather than batching multiple - commands into a single call to commandToExecute(). They will still be - undone together, as a single undo action by the user, but Maya will - better be able to recover if one of the commands fails. + Notes: + The command should be fully undoable otherwise unexpected results may occur. If + the command contains no undoable portions whatsoever, the call to + doIt() may fail, but only after executing the command. It is best to + use multiple commandToExecute() calls rather than batching multiple + commands into a single call to commandToExecute(). They will still be + undone together, as a single undo action by the user, but Maya will + better be able to recover if one of the commands fails. Args: command (str): The command string @@ -434,14 +562,15 @@ def connect(self, *args, **kwargs): @_modifierMethod def createDGNode(self, typeName, nodeName=""): - """Creates a DG node + """Creates a DG node. Args: - typeName (str): the type of the object to create, e.g. "transform" + typeName (str): the type of the object to create, e.g. "transform". + nodeName (str, optional): the node name, if non empty will be used in a modifier.renameObject call. Defaults to "". Returns: - :class:`_xnode.XNode`: An _xnode.XNode instance around the created MObject. + :class:`XNode`: A XNode instance around the created MObject. """ mob = self._modifier.createDGNode(typeName) if nodeName: @@ -463,7 +592,7 @@ def createDagNode( manageTransformIfNeeded=True, returnAllCreated=False, ): - """Creates a DAG node + """Creates a DAG node. Adds an operation to the modifier to create a DAG node of the specified type. If a parent DAG node is provided the new node will be parented under it. If no parent is provided and the new DAG node is a transform type then it will be parented under the world. In both of these cases the method returns @@ -475,20 +604,29 @@ def createDagNode( None of the newly created nodes will be added to the DAG until the modifier's doIt() method is called. Notes: - If you try to use createDagNode() to create an empty NURBSCurve or Mesh, calling bestFn() on the returned - `XNode` will give you MFnNurbsCurve or MFnMesh but these are invalid to work with. You will end up getting a - misleading "Object does not exist." error as Maya doesn't like an empty NURBSCurve or Mesh. + If you try to use :func:`createDagNode()` to create an empty NurbsCurve or Mesh, calling bestFn() on the returned + :class:`XNode` will give you `MFnNurbsCurve` or `MFnMesh` but these are invalid to work with. You will end up getting a + misleading "Object does not exist." error as Maya doesn't like an empty NurbsCurve or Mesh. Raises: :class:`TypeError` if the node type does not exist or if the parent is not a transform type. Args: - typeName (str): the type of the object to create, e.g. "transform" - parent (:class:`om2.MObject` | :class:`_xnode.XNode`, optional): An optional parent for the DAG node to create + typeName (str): the type of the object to create, e.g. "transform". + + parent (:class:`om2.MObject` | :class:`XNode`, optional): An optional parent for the DAG node to create. + nodeName (str, optional): the node name, if non empty will be used in a modifier.renameObject call. Defaults to "". - returnAllCreated (bool, optional): If True, it will return all newly created nodes, potentially including any new parent transforms and the shape of the type. + + manageTransformIfNeeded (bool, optional): when you create a shape without a parent, Maya will create both transform and shape, and + return parent om2.MObject instead. if manageTransformIfNeeded is True, than we will also rename the transform, + and return shape MObject instead. Most of time we keep it default True value. + + returnAllCreated (bool, optional): If True, it will return all newly created nodes, potentially including any new parent + transforms and the shape of the type. + Returns: - :class:`_xnode.XNode` | list: An _xnode.XNode instance around the created MObject, or the list of all created nodes, if returnAllCreated is True. + :class:`XNode` | list: An _xnode.XNode instance around the created MObject, or the list of all created nodes, if returnAllCreated is True. """ if parent is None: parent = om2.MObject.kNullObj @@ -499,6 +637,7 @@ def createDagNode( xparent = parent else: xparent = _xnode.XNode(parent) + if not xparent.object().hasFn(om2.MFn.kTransform): parent = xparent.bestFn().parent(0) @@ -540,7 +679,7 @@ def createNode(self, typeName, *args, **kwargs): typeName (str): the type of the object to create, e.g. "transform" Returns: - :py:class:`om2.MObject`: The created MObject + :class:`om2.MObject`: The created MObject """ # if any parent keyword is specified, we want to create a dag node for sure. Otherwise, we check the node type. if kwargs.get("parent") or "dagNode" in cmds.nodeType( @@ -613,7 +752,7 @@ def linkExtensionAttributeToPlugin(self, plugin, attribute): Args: plugin (:class:`om2.MObject`): The plugin - attribute (:class:`om2.MObject`): The attribute + attribute (:class:`om2.MObject`): The attribute MObject Returns: :class:`XModifier`: A reference to self @@ -795,7 +934,7 @@ def pythonCommandToExecute(self, callable_): recover if one of the commands fails. Args: - callable_ (callable | str): The command to execute + callable (callable | str): The command to execute Returns: :class:`XModifier`: A reference to self @@ -815,7 +954,8 @@ def removeAttribute(self, node, attribute): call as their behaviour may become unpredictable. Args: - node (:class:`_xnode.XNode` | :class:`om2.MObject`): the node to remove the attribute from + node (:class:`XNode` | :class:`om2.MObject`): the node to remove the attribute from + attribute (:class:`om2.MObject`): the attribute MObject Returns: @@ -837,6 +977,7 @@ def removeExtensionAttribute(self, nodeClass, attribute): Args: nodeClass (:class:`om2.MNodeClass`): The node class + attribute (:class:`om2.MObject`): The attribute MObject to add Returns: @@ -859,6 +1000,7 @@ def removeExtensionAttributeIfUnset(self, nodeClass, attribute): Args: nodeClass (:class:`om2.MNodeClass`): The node class + attribute (:class:`om2.MObject`): The attribute MObject to add Returns: @@ -873,6 +1015,7 @@ def removeMultiInstance(self, plug, breakConnections): Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug + breakConnections (bool): breaks the connections Returns: @@ -886,9 +1029,12 @@ def renameAttribute(self, node, attribute, newShortName, newLongName): """Adds an operation to the modifer that renames a dynamic attribute on the given dependency node. Args: - node (:class:`_xnode.XNode` | :class:`om2.MObject`): the node to rename the attribute on + node (:class:`XNode` | :class:`om2.MObject`): the node to rename the attribute on + attribute (:class:`om2.MObject`): the attribute MObject + newShortName (str): The new short name + newLongName (str): The new long name Returns: @@ -902,7 +1048,8 @@ def renameNode(self, node, newName): """Adds an operation to the modifer to rename a node. Args: - node (:class:`_xnode.XNode` | :class:`om2.MObject`): the node to rename + node (:class:`XNode` | :class:`om2.MObject`): the node to rename + newName (str): the new name Returns: @@ -916,7 +1063,8 @@ def setNodeLockState(self, node, newState): """Adds an operation to the modifier to set the lockState of a node. Args: - node (:class:`_xnode.XNode` | :class:`om2.MObject`): the node to lock + node (:class:`XNode` | :class:`om2.MObject`): the node to lock + newState (bool): the lock state Returns: @@ -941,6 +1089,7 @@ def unlinkExtensionAttributeFromPlugin(self, plugin, attribute): Args: plugin (:class:`om2.MObject`): The plugin + attribute (:class:`om2.MObject`): The attribute MObject to add Returns: @@ -959,18 +1108,20 @@ def reparentNode(self, node, newParent=None, absolute=False): If it is not a transform type then the doIt() will raise a RuntimeError. Args: - node (:class:`om2.MObject` | :class:`_xnode.XNode`): The Dag node to reparent - newParent (:class:`om2.MObject` | :class:`_xnode.XNode`, optional): The new parent. Defaults to None. - absolute (bool): Whether or not we try to maintain the world transform of the node. - If the node has some transform channels locked, it will try to fill the unlocked channels with debug - message. + node (:class:`om2.MObject` | :class:`XNode`): The DAG node to reparent + + newParent (:class:`om2.MObject` | :class:`XNode`, optional): The new parent. Defaults to None. + + absolute (bool, optional): Whether or not we try to maintain the world transform of the node. + If the node has some transform channels locked, it will try to fill the unlocked channels with debug + message. Returns: :class:`XModifier`: A reference to self """ if not node.hasFn(om2.MFn.kDagNode): raise TypeError( - "The XModifier.reparentNode() received non-Dag node to reparent." + "The XModifier.reparentNode() received non-DAG node to reparent." ) nodeX = _xnode.XNode(node) @@ -992,7 +1143,7 @@ def reparentNode(self, node, newParent=None, absolute=False): if not parentNodeX.hasFn(om2.MFn.kDagNode): raise TypeError( - "The XModifier.reparentNode() received non-Dag node to reparent to." + "The XModifier.reparentNode() received non-DAG node to reparent to." ) # Avoid reparenting if it is already under the parent: @@ -1037,36 +1188,49 @@ def doIt(self): self._mmod.doIt() except Exception as e: _, exc_value, _ = sys.exc_info() - j = self._xmod.journal() - if not j: - logger.error("Failed to call doIt: %s", exc_value) - if len(j) == 1: - logger.error("Failed to call doIt on %s: %s", j[0], exc_value) - else: - logger.error( - "Failed to run doIt on operations: %s\n%s", exc_value, "\n".join(j) - ) - journal = ", ".join(j) - raise Exception(f"{exc_value} when calling {journal}") from e + if isJournalOn(): + j = self._xmod.journal() + if not j: + logger.error("Failed to call doIt: %s", exc_value) + if len(j) == 1: + logger.error("Failed to call doIt on %s: %s", j[0], exc_value) + else: + logger.error( + "Failed to run doIt on operations: %s\n%s", + exc_value, + "\n".join(j), + ) + + journal = ", ".join(j) + raise Exception(f"{exc_value} when calling {journal}") from e + + raise Exception( + f"{exc_value} when calling doIt (journal unavailable)" + ) from e def undoIt(self): try: self._mmod.undoIt() except Exception as e: _, exc_value, _ = sys.exc_info() - j = self._xmod.journal() - if not j: - logger.error("Failed to call undoIt: %s", exc_value) - elif len(j) == 1: - logger.error("Failed to call undoIt on %s: %s", j[0], exc_value) - else: - logger.error( - "Failed to run undoIt on operations: %s\n%s", - exc_value, - "\n".join(j), - ) - journal = ", ".join(j) - raise Exception(f"{exc_value} when calling {journal}") from e + if isJournalOn(): + j = self._xmod.journal() + if not j: + logger.error("Failed to call undoIt: %s", exc_value) + elif len(j) == 1: + logger.error("Failed to call undoIt on %s: %s", j[0], exc_value) + else: + logger.error( + "Failed to run undoIt on operations: %s\n%s", + exc_value, + "\n".join(j), + ) + journal = ", ".join(j) + raise Exception(f"{exc_value} when calling {journal}") from e + + raise Exception( + f"{exc_value} when calling undoIt (journal unavailable)" + ) from e def redoIt(self): self.doIt() @@ -1092,7 +1256,7 @@ def currentModifier(): """Returns the last XModifier from the current modifier list. If the current list is empty it creates and returns a new immediate XModifier. Returns: - :class:`XModifier`: A XModifier instance ready to use. + :class:`XModifier`: A :class:`XModifier` instance ready to use. """ if not _CURRENT_MODIFIER_LIST: mod = XModifier(immediate=True) @@ -1116,7 +1280,7 @@ def newModifier(): def newAnimCurveModifier(): - """Creates a new MAnimCurveChange object, adds it to the current list of modifiers and returns it. + """Creates a new `om2anim.MAnimCurveChange` object, adds it to the current list of modifiers and returns it. Returns: :class:`om2anim.MAnimCurveChange`: The newly created MAnimCurveChange @@ -1140,7 +1304,7 @@ def executeModifiersWithUndo(): """Execute modifier actions with undo support. Notes: - This will push a AL_OMXCommand mpx undoable command in the Maya undo queue. + This will push a ``AL_OMXCommand`` mpx undoable command in the Maya undo queue. """ if _CURRENT_MODIFIER_LIST: cmds.AL_OMXCommand() @@ -1148,10 +1312,10 @@ def executeModifiersWithUndo(): @contextlib.contextmanager def newModifierContext(): - """Create a new xModifier for the context, and call xModifier.doIt() on context exit. + """Create a new :class:`XModifier` for the context, and call :func:`XModifier.doIt()` on context exit. Notes: - Any edits done within the python context, they are using the new xModifier. + Any edits done within the python context, they are using the new :class:`XModifier`. """ if _CURRENT_MODIFIER_LIST: # execute any previous doIt upon entering new context @@ -1174,7 +1338,7 @@ def commandModifierContext(command): This is a util only for AL internal use. Args: - command (:py:class:`AL.libs.command.command.Command`): The command instance + command (:class:`Command`): The command instance """ command._managedByXModifer = True # NOQA @@ -1213,22 +1377,23 @@ def commandModifierContext(command): def createDagNode( typeName, parent=om2.MObject.kNullObj, nodeName="", returnAllCreated=False ): - """Creates a DAG Node within the current active XModifier + """Creates a DAG Node within the current active :class:`XModifier` Note: - We automatically work around a limitation of the Maya MDagModifier here, where Maya would return the shape's parent - transform MObject. Instead we return an `XNode` for the newly created Shape node if the type is of Shape. + We automatically work around a limitation of the om2.MDagModifier here, where Maya would return the shape's parent + transform MObject. Instead we return an :class:`XNode` for the newly created Shape node if the type is of Shape. Args: - typeName (str): The type of the DAG node to create - parent (:class:`XNode` | :class:`om2.MObject` | :class:`om2.MFnDagNode` | str, optional): The parent of the DAG node to create. - Defaults to om2.MObject.kNullObj. + typeName (str): The type of the DAG node to create. + + parent (:class:`XNode` | :class:`om2.MObject` | :class:`om2.MFnDagNode` | str, optional): The parent of the DAG node to create. Defaults to `om2.MObject.kNullObj`. + nodeName (str, optional): The name of the node to create (used to call mod.renameNode after creation). Defaults to "". - returnAllCreated (bool, optional): If True, it will return any newly created nodes, including potential new parent transform - and the shape of the type. + + returnAllCreated (bool, optional): If True, it will return any newly created nodes, including potential new parent transform and the shape of the type. Returns: - :class:`XNode`: The created XNode + :class:`XNode` | [:class:`XNode`]: The created XNode or a list of XNodes, based on returnAllCreated argument. """ return currentModifier().createDagNode( typeName, parent=parent, nodeName=nodeName, returnAllCreated=returnAllCreated @@ -1236,20 +1401,21 @@ def createDagNode( def createDGNode(typeName, nodeName=""): - """Creates a DG Node within the current active XModifier + """Creates a DG Node within the current active :class:`XModifier` Args: - typeName (str): The node type name + typeName (str): The node type name. + nodeName (str, optional): The node name (to be used in mod.renameNode after creation). Defaults to "". Returns: - :class:`XNode`: The created XNode + :class:`XNode` | [:class:`XNode`]: The created XNode. """ return currentModifier().createDGNode(typeName, nodeName=nodeName) def doIt(): - """Runs doIt on all current modifiers + """Runs doIt on all current modifiers, similar to om2.MDGModifier.doIt(). """ for mod in _CURRENT_MODIFIER_LIST[:]: mod.doIt() diff --git a/AL/omx/_xnode.py b/AL/omx/_xnode.py index a1baefb..db13bd7 100644 --- a/AL/omx/_xnode.py +++ b/AL/omx/_xnode.py @@ -1,59 +1,61 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import logging -from maya.api import OpenMaya as om2 -from AL.maya2.omx import _xplug -from AL.maya2.omx.utils import _nodes -from AL.maya2.omx.utils import _plugs +from AL.omx.utils._stubs import om2 + +from AL.omx import _xplug +from AL.omx.utils import _nodes +from AL.omx.utils import _plugs logger = logging.getLogger(__name__) class XNode: - """ Easy wrapper around om2 objects mainly to access plugs" - - Example: - This shows how to use omx to connect 2 nodes:: - - from AL.maya2 import omx - - trn = omx.createDagNode("transform", nodeName="myTrn") - mtx = trn.worldMatrix[0].value() - - reparent = omx.createDGNode("AL_rig_reparentSingle") - reparent.worldMatrix.connect(trn.worldMatrix[0]) + """Easy wrapper around om2 objects mainly to access plugs. """ _NODE_CLASS_CACHE = {} _ATTRIBUTE_CACHE = {} - def __init__(self, thingToEasify): + def __init__(self, obj): """ Creates a new XNode Args: - thingToEasify (:py:class:`om2.MObject` | :py:class:`XNode` | :py:class:`om2.MFnBase` | string): A maya object to wrap + obj (:class:`om2.MObject` | :class:`XNode` | :class:`om2.MFnBase` | string): A object to wrap Returns: - :py:class:`omx.XNode`: An instance of a XNode object + :class:`XNode`: An instance of a XNode object """ - if isinstance(thingToEasify, om2.MObject): - mob = thingToEasify - elif isinstance(thingToEasify, om2.MObjectHandle): - mob = thingToEasify.object() - elif isinstance(thingToEasify, XNode): - mob = thingToEasify.object() - elif isinstance(thingToEasify, om2.MFnBase): - mob = thingToEasify.object() - elif isinstance(thingToEasify, om2.MDagPath): - mob = thingToEasify.node() - elif isinstance(thingToEasify, str): - mob = _nodes.findNode(thingToEasify) + if isinstance(obj, om2.MObject): + mob = obj + elif isinstance(obj, om2.MObjectHandle): + mob = obj.object() + elif isinstance(obj, XNode): + mob = obj.object() + elif isinstance(obj, om2.MFnBase): + mob = obj.object() + elif isinstance(obj, om2.MDagPath): + mob = obj.node() + elif isinstance(obj, str): + mob = _nodes.findNode(obj) if mob is None: - raise RuntimeError(f"Object {thingToEasify} is not valid!") + raise RuntimeError(f"Object {obj} is not valid!") else: - raise RuntimeError(f"Cannot use {thingToEasify} with XNode!") + raise RuntimeError(f"Cannot use {obj} with XNode!") if mob != om2.MObject.kNullObj and not mob.hasFn(om2.MFn.kDependencyNode): raise RuntimeError( @@ -61,9 +63,9 @@ def __init__(self, thingToEasify): ) self._mobHandle = om2.MObjectHandle(mob) - depNode = om2.MFnDependencyNode(mob) - self._lastKnownName = depNode.absoluteName() - self._mayaType = mayaType = depNode.typeName + nodeFn = om2.MFnDependencyNode(mob) + self._lastKnownName = nodeFn.absoluteName() + self._mayaType = mayaType = nodeFn.typeName nodeCls = XNode._NODE_CLASS_CACHE.get(mayaType, None) if nodeCls is None: nodeCls = om2.MNodeClass(mayaType) @@ -89,6 +91,7 @@ def __getattribute__(self, name): nodeClass = XNode._NODE_CLASS_CACHE[mayaType] attrs = XNode._ATTRIBUTE_CACHE[mayaType] attr = attrs.get(name, None) + # if it is a dynamic attribute: if attr is None: if not nodeClass.hasAttribute(name): plug = _plugs.findPlug(name, mob) @@ -104,14 +107,16 @@ def __getattribute__(self, name): def object(self): """Returns the associated MObject + Raises: + RuntimeError: When the MObject is invalid. + Returns: - :py:class:`om2.MObject`: the associated MObject + :class:`om2.MObject`: the associated MObject """ mobHandle = object.__getattribute__(self, "_mobHandle") - if not mobHandle.isAlive(): - lastName = object.__getattribute__(self, "_lastKnownName") - raise RuntimeError(f"XNode({lastName}) is not alive!") + # no need to test isAlive() as being valid is guaranteed to be alive. + # and usually we only care if it is valid. if not mobHandle.isValid(): lastName = object.__getattribute__(self, "_lastKnownName") raise RuntimeError(f"XNode({lastName}) is not valid!") @@ -181,10 +186,11 @@ def createDagNode(self, typeName, nodeName=""): Args: typeName (string): the type of the object to create, e.g. "transform" + nodeName (str, optional): the node name, if non empty will be used in a modifier.renameObject call. Defaults to "". Returns: - :class:`xnode.XNode`: An xnode.XNode instance around the created MObject. + :class:`XNode`: An XNode instance around the created MObject. """ mob = self.object() if not mob.hasFn(om2.MFn.kDagNode): @@ -192,7 +198,7 @@ def createDagNode(self, typeName, nodeName=""): f"Cannot create a DAG node {nodeName}[{typeName}] under this non-DAG node {self}" ) - from AL.maya2.omx import _xmodifier + from AL.omx import _xmodifier return _xmodifier.createDagNode(typeName, parent=mob, nodeName=nodeName) @@ -217,7 +223,7 @@ def basicFn(self): """Returns the basic MFnDAGNode or MFnDependencyNode for the associated MObject Notes: - Usually you would use xnode.bestFn() to get the most useful function set. But for an empty NURBS curve + Usually you would use xnode.bestFn() to get the most useful function set. But for an empty nurbsCurve or an empty mesh node, only xnode.basicFn() will work as expected. Returns: @@ -230,6 +236,15 @@ def basicFn(self): return om2.MFnDependencyNode(mob) def __str__(self): + """Returns an easy-readable str representation of this XNode. + + Construct a minimum unique path to support duplicate MObjects in scene. + For invalid MObject we return the last known name with a suffix (dead) + or (invalid) respectively. + + Returns: + str: the string representation. + """ mobHandle = object.__getattribute__(self, "_mobHandle") if not mobHandle.isAlive(): lastName = object.__getattribute__(self, "_lastKnownName") @@ -249,6 +264,11 @@ def __str__(self): return "None" def __repr__(self): + """Get the more unambiguous str representation. This is mainly for debugging purposes. + + Returns: + str + """ return f'XNode("{self}")' def __eq__(self, other): @@ -263,6 +283,8 @@ def __eq__(self, other): return False def __ne__(self, other): + """Add support for XNode comparison. + """ if isinstance(other, XNode): return self.object().__ne__(other.object()) @@ -272,4 +294,6 @@ def __ne__(self, other): return True def __hash__(self): + """Add support for using XNode for containers that require uniqueness, e.g. dict key. + """ return object.__getattribute__(self, "_mobHandle").hashCode() diff --git a/AL/omx/_xplug.py b/AL/omx/_xplug.py index 2d60701..bbb1143 100644 --- a/AL/omx/_xplug.py +++ b/AL/omx/_xplug.py @@ -1,16 +1,30 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import logging -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2.omx.utils import _plugs -from AL.maya2.omx.utils import _exceptions + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx.utils import _plugs +from AL.omx.utils import _exceptions logger = logging.getLogger(__name__) def _currentModifier(): - from AL.maya2.omx import _xmodifier + from AL.omx import _xmodifier return _xmodifier.currentModifier() @@ -21,16 +35,24 @@ class XPlug(om2.MPlug): You can use omx.XPlug over an om2.MPlug to take advantage of the extra convenience features. Examples: - xplug.get() --> Get you the value of the plug - xplug.set(value) --> Set the value of the plug - xplug[0] --> The element xplug by the logicial index 0 if xplug is an array plug. - xplug['childAttr'] --> The child xplug named 'childAttr' if xplug is a compound plug. - xplug.xnode() --> Get you the parent xnode. - - Invalid Examples: - xplug['childAttr.attr1'] !!! This won't work, use xplug['childAttr']['attr1'] instead. - xplug['childAttr[0]'] !!! This won't work, use xplug['childAttr'][0] instead. + + .. code:: python + + xplug = xnode.attr + xplug.get() # Get you the value of the plug + xplug.set(value) # Set the value of the plug + xplug[0] # The element xplug by the logicial index 0 if xplug is an array plug. + xplug['childAttr'] # The child xplug named 'childAttr' if xplug is a compound plug. + xplug.xnode() # Get you the parent xnode. + + Invalid Examples: + + .. code:: python + + xplug['childAttr.attr1'] # This won't work, use xplug['childAttr']['attr1'] instead. + xplug['childAttr[0]'] # This won't work, use xplug['childAttr'][0] instead. ... + """ def __init__(self, *args, **kwargs): @@ -40,8 +62,7 @@ def get(self, asDegrees=False): """Get the value of the plug. Args: - asDegrees (bool): For an angle unit attribute we return the value in degrees - or in radians. + asDegrees (bool, optional): For an angle unit attribute we return the value in degrees or in radians. """ return _plugs.valueFromPlug(self, flattenComplexData=False, asDegrees=asDegrees) @@ -50,13 +71,18 @@ def set(self, value, asDegrees=False): Notes: For enum attribute, you can set value by both short int or a valid enum name string. - To retrieve the valid enum names, use XPlug.enumNames() + To retrieve the valid enum names, use :func:`XPlug.enumNames()` + + This method does many checks to make sure it works for different types of attributes. + If you know the type of attribute, the preference would be to use the set*() methods + instead, they are more lightweight and have better performance. Args: value (any): The plug value to set. - asDegrees (bool): When it is an angle unit attribute, if this is True than we take the - value as degrees, otherwise as radians.This flag has no effect - when it is not an angle unit attribute. + + asDegrees (bool, optional): When it is an angle unit attribute, if this is True than + we take the value as degrees, otherwise as radians.This flag has no effect when it is + not an angle unit attribute. Returns: The previous value if it is simple plug and the set was successful. None or empty list @@ -66,6 +92,102 @@ def set(self, value, asDegrees=False): self, value, modifier=_currentModifier(), doIt=False, asDegrees=asDegrees ) + def setBool(self, value): + """Adds an operation to the modifier to set a value onto a bool plug. + + Args: + value (bool): the value. + """ + _currentModifier().newPlugValueBool(self, value) + + def setChar(self, value): + """Adds an operation to the modifier to set a value onto a char (single + byte signed integer) plug. + + Args: + value (int): the value. + """ + _currentModifier().newPlugValueChar(self, value) + + def setDouble(self, value): + """Adds an operation to the modifier to set a value onto a double-precision + float plug. + + Args: + value (float): the value. + """ + _currentModifier().newPlugValueDouble(self, value) + + def setFloat(self, value): + """Adds an operation to the modifier to set a value onto a single-precision + float plug. + + Args: + value (float): the value. + """ + _currentModifier().newPlugValueFloat(self, value) + + def setInt(self, value): + """Adds an operation to the modifier to set a value onto an int plug. + + Args: + value (int): the value. + """ + _currentModifier().newPlugValueInt(self, value) + + def setAngle(self, value): + """Adds an operation to the modifier to set a value onto an angle plug. + + Args: + value (``om2.MAngle``): the value. + """ + _currentModifier().newPlugValueMAngle(self, value) + + def setDistance(self, value): + """Adds an operation to the modifier to set a value onto a distance plug. + + Args: + value (``om2.MDistance``): the value. + """ + _currentModifier().newPlugValueMDistance(self, value) + + def setTime(self, value): + """Adds an operation to the modifier to set a value onto a time plug. + + Args: + value (``om2.MTime``): the value. + """ + _currentModifier().newPlugValueMTime(self, value) + + def setShort(self, value): + """Adds an operation to the modifier to set a value onto a short + integer plug. + + Args: + value (int): the value. + """ + _currentModifier().newPlugValueShort(self, value) + + def setString(self, value): + """Adds an operation to the modifier to set a value onto a string plug. + + Args: + value (str): the value. + """ + _currentModifier().newPlugValueString(self, value) + + def setCompoundDouble(self, value): + """Adds an operation to the modifier to compound attribute's double + plugs children. + + Args: + value ([double]): the list of double value whose amount should be + no larger to the amount of children. + """ + for i, v in enumerate(value): + child = self.child(i) + _currentModifier().newPlugValueDouble(child, v) + def enumNames(self): """Get the enum name tuple if it is an enum attribute, otherwise None. @@ -78,30 +200,31 @@ def disconnectFromSource(self): """ Disconnect this plug from a source plug, if it's connected """ if self.isDestination: - self._modifyInUnlocked(_currentModifier().disconnect, self.source(), self) + with self.UnlockedModification(self): + _currentModifier().disconnect(self.source(), self) def connectTo(self, destination, force=False): """ Connect this plug to a destination plug Args: destination (:class:`om2.MPlug` | :class:`XPlug`): destination plug - force (bool): override existing connection + + force (bool, optional): override existing connection """ - if destination.isDestination: + destXPlug = XPlug(destination) + if destXPlug.isDestination: if not force: logger.warning( "%s connected to %s, use force=True to override this connection", - destination, - destination.source(), + destXPlug, + destXPlug.source(), ) return - XPlug(destination).disconnectFromSource() + destXPlug.disconnectFromSource() - connector = _currentModifier().connect - XPlug(destination)._modifyInUnlocked( - connector, self, destination - ) # pylint: disable=protected-access + with self.UnlockedModification(destXPlug): + _currentModifier().connect(self, destXPlug) def connectFrom(self, source, force=False): """ Connect this plug to a source plug @@ -110,7 +233,8 @@ def connectFrom(self, source, force=False): Args: source (:class:`om2.MPlug` | :class:`XPlug`): source plug - force (bool): override existing connection + + force (bool, optional): override existing connection """ if self.isDestination: if not force: @@ -121,7 +245,9 @@ def connectFrom(self, source, force=False): ) return self.disconnectFromSource() - self._modifyInUnlocked(_currentModifier().connect, source, self) + + with self.UnlockedModification(self): + _currentModifier().connect(source, self) def setLocked(self, locked): """Set the plug's lock state. @@ -174,7 +300,7 @@ def xnode(self): Returns: omx.XNode """ - from AL.maya2.omx import _xnode + from AL.omx import _xnode return _xnode.XNode(om2.MPlug.node(self)) @@ -204,13 +330,21 @@ def _setState(self, flag, currentState, targetState): logger.debug(cmd) _currentModifier().commandToExecute(cmd) - def _modifyInUnlocked(self, action, *args, **kwargs): - locked = self.isLocked - if locked: - self.setLocked(False) - action(*args, **kwargs) - if locked: - self.setLocked(True) + class UnlockedModification: + def __init__(self, xplug): + self._xplug = xplug + + def __enter__(self): + self._oldLocked = self._xplug.isLocked + if self._oldLocked: + # here we cannot use `plug.isLocked = False` because when doIt() + # is called, it is already reverted to locked. + self._xplug.setLocked(False) + return self + + def __exit__(self, *_, **__): + if self._oldLocked: + self._xplug.setLocked(True) @staticmethod def _iterPossibleNamesOfPlug(plug): @@ -241,10 +375,10 @@ def _childPlugByName(cls, xplug, key): def __iter__(self): """Adds support for the `for p in xPlug` syntax so you can iter through an - array's elements, or a compound's children, as XPlugs. + array's elements, or a compound's children, as XPlugs. Yields: - omx.XPlug + :class:`XPlug` """ if self.isArray: for logicalIndex in self.getExistingArrayAttributeIndices(): @@ -254,11 +388,14 @@ def __iter__(self): yield XPlug(self.child(i)) def __getitem__(self, key): - """Adds support for the `xplug[key]` syntax where you can get one of an - array's elements, or a compound's child, as XPlug. + """Adds support for the `xplug[key]` syntax where you can get one of an array's elements, + or a compound's child, as XPlug. + + Args: + key (int | str): The array element plug logical index or the child plug name. Returns: - omx.XPlug + :class:`XPlug` Raises: AttributeError if compound plug doesn't have the child with name, TypeError @@ -289,8 +426,12 @@ def __getitem__(self, key): raise TypeError(f"The valid types for XPlug['{key}'] are: int | string.") def __contains__(self, key): - """Add support for the `key in xPlug` syntax. Checks if the index is an existing index - of an array, or the str name is a valid child of a compound. + """Add support for the `key in xPlug` syntax. + + Checks if the index is an existing index of an array, or the str name is a valid child of a compound. + + Args: + key (int | str): The array element plug logical index or the child plug name. Notes: We can extend to accept om2.MPlug or omx.XPlug as the input to check, but here we just diff --git a/AL/omx/plugin/AL_OMXCommand.py b/AL/omx/plugin/AL_OMXCommand.py index 57afc3c..641d879 100644 --- a/AL/omx/plugin/AL_OMXCommand.py +++ b/AL/omx/plugin/AL_OMXCommand.py @@ -1,15 +1,25 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # This module should be discoverable by Maya plugin loader. -# To-do: come up with a easier way for generic user to install the plugin file for opensourcing. -from AL.maya2 import om2 -from AL.maya2.omx import _xcommand, _xmodifier +from AL.omx.utils._stubs import om2 +from AL.omx import _xcommand, _xmodifier def maya_useNewAPI(): - """ - The presence of this function tells Maya that the plugin produces, and + """The presence of this function tells Maya that the plugin produces, and expects to be passed, objects created using the Maya Python API 2.0. """ return True @@ -19,6 +29,9 @@ def maya_useNewAPI(): def installCallbacks(): + """Install callbacks for events like after new Maya scene, before Maya scene open + and Maya quit. This will be called when the omx plug-in is loaded. + """ __CALLBACK_ID_LIST.append( om2.MSceneMessage.addCallback( om2.MSceneMessage.kAfterNew, _xmodifier.ensureModifierStackIsClear, None @@ -37,6 +50,8 @@ def installCallbacks(): def uninstallCallbacks(): + """Uninstall previously registered callbacks. This will be called when the omx plug-in is unloaded. + """ global __CALLBACK_ID_LIST for i in __CALLBACK_ID_LIST: om2.MMessage.removeCallback(i) diff --git a/AL/omx/plugin/__init__.py b/AL/omx/plugin/__init__.py index e69de29..3fe5cc7 100644 --- a/AL/omx/plugin/__init__.py +++ b/AL/omx/plugin/__init__.py @@ -0,0 +1,13 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/AL/omx/tests/__init__.py b/AL/omx/tests/__init__.py index e69de29..3fe5cc7 100644 --- a/AL/omx/tests/__init__.py +++ b/AL/omx/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/AL/omx/tests/runall.py b/AL/omx/tests/runall.py new file mode 100644 index 0000000..5b9a119 --- /dev/null +++ b/AL/omx/tests/runall.py @@ -0,0 +1,65 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is the module to run all the tests, you need to execute it within mayapy: + + mayapy -m AL.omx.tests.runall + +""" + +import os +from unittest import loader, suite +from unittest import runner + +from AL.omx import tests as omxtests +from AL.omx.utils._stubs import isInsideMaya + + +class _MayaPyContext: + def __enter__(self): + self.standaloneMod = None + try: + from maya import standalone + + standalone.initialize("Python") + standaloneMod = standalone + except: + print("Please Run this module with mayapy or Maya Interactive.") + + def __exit__(self, *_, **__): + if not isInsideMaya(): + return + + if self.standaloneMod: + self.standaloneMod.uninitialize() + + +def runAllOMXTests(): + with _MayaPyContext(): + if not isInsideMaya(): + print( + "The AL_omx tests cannot be run outside Maya environment, you need to run this within Maya Interactive or with mayapy." + ) + return 1 + + topDir = os.path.dirname(omxtests.__file__) + testItems = loader.defaultTestLoader.discover( + "AL.omx.tests", top_level_dir=topDir + ) + testRunner = runner.TextTestRunner() + testRunner.run(testItems) + + +if __name__ == "__main__": + runAllOMXTests() diff --git a/AL/omx/tests/test_omxundo.py b/AL/omx/tests/test_omxundo.py index 8640002..c5f7ce2 100644 --- a/AL/omx/tests/test_omxundo.py +++ b/AL/omx/tests/test_omxundo.py @@ -1,12 +1,25 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import logging import unittest +from AL.omx.utils._stubs import cmds from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2 import omx -from AL.maya2.omx.utils import _contexts as omu_contexts +from AL import omx +from AL.omx.utils import _contexts as omu_contexts logger = logging.getLogger(__name__) diff --git a/AL/omx/tests/test_xmodifier.py b/AL/omx/tests/test_xmodifier.py index 75a8d8a..32f7ae0 100644 --- a/AL/omx/tests/test_xmodifier.py +++ b/AL/omx/tests/test_xmodifier.py @@ -1,10 +1,23 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import unittest import logging -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2 import omx + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL import omx logger = logging.getLogger(__name__) diff --git a/AL/omx/tests/test_xnode.py b/AL/omx/tests/test_xnode.py index d68e3ed..1a35ffb 100644 --- a/AL/omx/tests/test_xnode.py +++ b/AL/omx/tests/test_xnode.py @@ -1,10 +1,24 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import unittest import logging -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2 import omx + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL import omx +from AL.omx import _xmodifier logger = logging.getLogger(__name__) @@ -115,37 +129,63 @@ def testConnectPlugs(self): def testCustomNode(self): transform = omx.createDagNode("transform", nodeName="myTransform") - rep = omx.createDGNode("AL_rig_reparentSingle", "myReparent") - transform.translate.connectFrom(rep.translate) + mtx = omx.createDGNode("decomposeMatrix", "myMtx") + transform.translate.connectFrom(mtx.outputTranslate) self.assertEqual( cmds.listConnections("myTransform.translate", plugs=True), - ["myReparent.translate"], + ["myMtx.outputTranslate"], ) def testPlugMethods(self): transform = omx.createDagNode("transform", nodeName="myTransform") - rep = omx.createDGNode("AL_rig_reparentSingle", "myReparent") - transform.translate.connectFrom(rep.translate) + upstream = omx.createDGNode("decomposeMatrix", "upstreamDG") + transform.translate.connectFrom(upstream.outputTranslate) - self.assertIsInstance(rep.outSRT.child(0), omx.XPlug) + self.assertIsInstance(upstream.outputTranslate.child(0), omx.XPlug) self.assertIsInstance(transform.translate.source(), omx.XPlug) - self.assertIsInstance(rep.translate.destinations()[0], omx.XPlug) + self.assertIsInstance(upstream.outputTranslate.destinations()[0], omx.XPlug) + + def _checkJournal(self, modifier, expectedJournal): + transform = omx.createDagNode("transform", nodeName="myTransform") + upstream = omx.createDGNode("decomposeMatrix", "upstreamMtx") + transform.translate.connectFrom(upstream.outputTranslate) + modifier.doIt(keepJournal=True) + self.assertEqual(modifier.journal(), expectedJournal) def testMofifierJournal(self): + fullJournal = [ + "mod.createDagNode({'manageTransformIfNeeded': 'True', 'nodeName': 'myTransform', 'parent': None, 'returnAllCreated': 'False', 'typeName': 'transform'})", + "mod.createDGNode({'nodeName': 'upstreamMtx', 'typeName': 'decomposeMatrix'})", + "mod.connect({'args': '(XPlug(\"upstreamMtx.outputTranslate\"), XPlug(\"myTransform.translate\"))', 'kwargs': '{}'})", + ] + # check when journal is forced on: with omx.newModifierContext() as mod: - transform = omx.createDagNode("transform", nodeName="myTransform") - rep = omx.createDGNode("AL_rig_reparentSingle", "myReparent") - transform.translate.connectFrom(rep.translate) - mod.doIt(keepJournal=True) - self.assertEqual( - mod.journal(), - [ - "mod.createDagNode({'manageTransformIfNeeded': 'True', 'nodeName': 'myTransform', 'parent': None, 'returnAllCreated': 'False', 'typeName': 'transform'})", - "mod.createDGNode({'nodeName': 'myReparent', 'typeName': 'AL_rig_reparentSingle'})", - "mod.connect({'args': '(XPlug(\"myReparent.translate\"), XPlug(\"myTransform.translate\"))', 'kwargs': '{}'})", - ], - ) + with omx.JournalContext(): + self.assertTrue(omx.isJournalOn()) + self._checkJournal(mod, fullJournal) + + # check when journal is forced off: + with omx.newModifierContext() as mod: + with omx.JournalContext(): + self.assertTrue(omx.isJournalOn()) + with omx.JournalContext(state=False): + self.assertFalse(omx.isJournalOn()) + self._checkJournal(mod, []) + + # check when journal is decided by logging level: + oldLevel = _xmodifier.logger.level + cmds.file(new=True, f=True) # create new file so node name don't change. + for level in (logging.DEBUG, logging.INFO): + _xmodifier.logger.setLevel(level) + isInDebug = _xmodifier.logger.isEnabledFor(logging.DEBUG) + expectedJournal = fullJournal if isInDebug else [] + with omx.newModifierContext() as mod: + with omx.JournalContext(state=None): + self.assertEqual(omx.isJournalOn(), isInDebug) + self._checkJournal(mod, expectedJournal) + + _xmodifier.logger.setLevel(oldLevel) def testDynamicAttrs(self): transform1 = omx.createDagNode("transform", nodeName="myTransform1") @@ -205,22 +245,22 @@ def testStrings(self): ) child1 = omx.createDagNode("transform", nodeName="child", parent=transform1) child2 = omx.createDagNode("transform", nodeName="child", parent=transform2) - rep = omx.createDGNode("AL_rig_reparentSingle", "myReparent") + timeNode = omx.createDGNode("time", "myDG") self.assertEqual(str(transform1), "myTransform1") self.assertEqual(repr(transform1), 'XNode("myTransform1")') self.assertEqual(str(child1), "myTransform1|child") self.assertEqual(str(child2), "myTransform2|child") - self.assertEqual(repr(rep), 'XNode("myReparent")') - self.assertEqual(str(rep.translate), "myReparent.translate") - self.assertEqual(repr(rep.translate), 'XPlug("myReparent.translate")') + self.assertEqual(repr(timeNode), 'XNode("myDG")') + self.assertEqual(str(timeNode.outTime), "myDG.outTime") + self.assertEqual(repr(timeNode.frozen), 'XPlug("myDG.frozen")') def testBestFn(self): transform = omx.createDagNode("transform", nodeName="myTransform1") fn = transform.bestFn() self.assertIsInstance(fn, om2.MFnTransform) - rep = omx.createDGNode("AL_rig_reparentSingle", "myReparent") - fn = rep.bestFn() + timeNode = omx.createDGNode("time", "myDG") + fn = timeNode.bestFn() self.assertIsInstance(fn, om2.MFnDependencyNode) def testUndoContext(self): @@ -354,13 +394,13 @@ def testCreateNode(self): mod.createNode("locator", nodeName="loc1Shape", parent=loc1) mod.createNode("transform", parent=loc1) mod.createNode("transform", parent=loc1) - mod.createNode("AL_rig_reparentSingle", nodeName="rep1") + mod.createNode("time", nodeName="myTime") self.assertTrue(cmds.objExists("loc1")) self.assertTrue(cmds.objExists("loc1|loc1Shape")) self.assertTrue(cmds.objExists("loc1|transform1")) self.assertTrue(cmds.objExists("loc1|transform2")) - self.assertTrue(cmds.objExists("rep1")) + self.assertTrue(cmds.objExists("myTime")) def testCoumpoundPlug(self): cond = omx.createDGNode("condition", nodeName="cond") diff --git a/AL/omx/tests/test_xplug.py b/AL/omx/tests/test_xplug.py index 0e228d1..1bae240 100644 --- a/AL/omx/tests/test_xplug.py +++ b/AL/omx/tests/test_xplug.py @@ -1,12 +1,24 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import unittest import logging -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2 import omx -from AL.maya2.omx.utils import _contexts as omu_contexts -from AL.maya2.omx.tests.utils import common + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL import omx +from AL.omx.tests.utils import common logger = logging.getLogger(__name__) diff --git a/AL/omx/tests/utils/__init__.py b/AL/omx/tests/utils/__init__.py index e69de29..3fe5cc7 100644 --- a/AL/omx/tests/utils/__init__.py +++ b/AL/omx/tests/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/AL/omx/tests/utils/common.py b/AL/omx/tests/utils/common.py index ae7ceec..c8cd4ae 100644 --- a/AL/omx/tests/utils/common.py +++ b/AL/omx/tests/utils/common.py @@ -1,7 +1,20 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -from maya import cmds -from AL.maya2.omx.utils import _contexts + +from AL.omx.utils._stubs import cmds +from AL.omx.utils import _contexts def runTestsWithUndoEnabled(testcase): diff --git a/AL/omx/tests/utils/test_mmodifier.py b/AL/omx/tests/utils/test_mmodifier.py index e935e8e..e4fdfe7 100644 --- a/AL/omx/tests/utils/test_mmodifier.py +++ b/AL/omx/tests/utils/test_mmodifier.py @@ -1,9 +1,22 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import unittest -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2.omx.utils import _modifiers + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx.utils import _modifiers class MModifierTestCase(unittest.TestCase): @@ -94,7 +107,7 @@ def test_DGModeCreateNode(self): self.assertTrue(cmds.objExists(blinn2NodeName)) self.assertTrue(cmds.objExists(transform1Name)) - # Test the Dag mode: + # Test the DAG mode: with _modifiers.ToDagModifier(modifier) as dagmodifier: transformNew = dagmodifier.createNode("transform", parentNode) dagmodifier.doIt() diff --git a/AL/omx/tests/utils/test_nodes.py b/AL/omx/tests/utils/test_nodes.py index c537f32..e6d4e1a 100644 --- a/AL/omx/tests/utils/test_nodes.py +++ b/AL/omx/tests/utils/test_nodes.py @@ -1,10 +1,23 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import unittest -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2.omx.utils import _nodes -from AL.maya2.omx.tests.utils import common + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx.utils import _nodes +from AL.omx.tests.utils import common class NodesNameCase(unittest.TestCase): diff --git a/AL/omx/tests/utils/test_plugs.py b/AL/omx/tests/utils/test_plugs.py index 9855087..f4677d0 100644 --- a/AL/omx/tests/utils/test_plugs.py +++ b/AL/omx/tests/utils/test_plugs.py @@ -1,11 +1,24 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import unittest -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2.omx.utils import _nodes -from AL.maya2.omx.utils import _plugs -from AL.maya2.omx.tests.utils import common + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx.utils import _nodes +from AL.omx.utils import _plugs +from AL.omx.tests.utils import common @_plugs._plugLockElision # pylint: disable=protected-access @@ -231,10 +244,19 @@ def test_setNonNumericCompoundPlugValueWithModifier(self): initValues = _plugs.valueFromPlug(plug) self.assertNotEqual(newValues, initValues) _plugs.setValueOnPlug(plug, newValues, modifier=modifier, doIt=True) - self.assertEqual(_plugs.valueFromPlug(plug), newValues) + actualValues = _plugs.valueFromPlug(plug) + self._assertNonNumericCompoundPlugValues(actualValues, newValues) + modifier.undoIt() self.assertEqual(_plugs.valueFromPlug(plug), initValues) + def _assertNonNumericCompoundPlugValues(self, actualValue, expectedValues): + # in different maya version, the actual values might contains extra keys, so here + # we just assert all the expected keys and values are matched + for k, v in expectedValues.items(): + self.assertTrue(k in actualValue) + self.assertEqual(v, actualValue[k]) + def test_getSimpleFloatArrayPlugValue(self): cube = cmds.polyCube()[0] attr = "testFloatArrayAttr" diff --git a/AL/omx/utils/__init__.py b/AL/omx/utils/__init__.py index e69de29..3fe5cc7 100644 --- a/AL/omx/utils/__init__.py +++ b/AL/omx/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/AL/omx/utils/_contexts.py b/AL/omx/utils/_contexts.py index 51dec76..3faab2a 100644 --- a/AL/omx/utils/_contexts.py +++ b/AL/omx/utils/_contexts.py @@ -1,7 +1,20 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import functools -from maya import cmds + +from AL.omx.utils._stubs import cmds class UndoStateSwitcher: diff --git a/AL/omx/utils/_exceptions.py b/AL/omx/utils/_exceptions.py index d0c9fb8..6e131d9 100644 --- a/AL/omx/utils/_exceptions.py +++ b/AL/omx/utils/_exceptions.py @@ -1,7 +1,22 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. class NullXPlugError(RuntimeError): + """Used when an unexpected null XPlug is encountered. + """ + pass @@ -19,11 +34,14 @@ def __init__(self, message): class PlugMayaInvalidException(Exception): - """This is very specific. - Sometimes Maya generates plugs in an invalid state that are perfectly legal, but won't - behave as expected. These plugs can hard crash Maya when manipulated or queried. - The most common example are arrays of compounds without any elements. - This is to be used for those cases, and for those cases only. + """This is very specific Exception for invalid maya plug. + + Notes: + Sometimes Maya generates plugs in an invalid state that are perfectly legal, but won't + behave as expected. These plugs can hard crash Maya when manipulated or queried. + The most common example are arrays of compounds without any elements. + + This is to be used for those cases, and for those cases only. """ __DEFAULT_MESSAGE = ( @@ -37,14 +55,14 @@ def __init__(self, plug, message=""): plug (om2.MPlug): The plug to raise exception for. message (str, optional): The message override, a default message will be used if not provided. """ - self.plugname = None + self.plugName = None localMessage = message or self.__DEFAULT_MESSAGE try: - self.plugname = plug.name() + self.plugName = plug.name() except Exception: # yeah, it's an except all, but we don't want errorception here. - self.plugname = self.__PLUGNOTFOUND_MESSAGE + self.plugName = self.__PLUGNOTFOUND_MESSAGE - self.message = f"error on plug: {self.plugname} - {localMessage}" + self.message = f"Error on plug: {self.plugName} - {localMessage}" super().__init__(self.message) @@ -63,14 +81,14 @@ def __init__(self, plug, typeID, subTypeID, message=""): subTypeID (int): The numeric type, `om2.MFnNumericData.k*`. message (str, optional): The message override, a default message will be used if not provided. """ - self.plugname = None + self.plugName = None localMessage = message or self.__DEFAULT_MESSAGE try: - self.plugname = plug.name() + self.plugName = plug.name() except Exception: - self.plugname = self.__PLUGNOTFOUND_MESSAGE + self.plugName = self.__PLUGNOTFOUND_MESSAGE - self.message = f"error on plug: {self.plugname} of type and subtype: {typeID},{subTypeID} - {localMessage}" + self.message = f"Error on plug: {self.plugName} of type and subtype: {typeID},{subTypeID} - {localMessage}" super().__init__(self.message) @@ -85,11 +103,32 @@ def __init__(self, plug): Args: plug (om2.MPlug): The plug to raise exception for. """ - self.plugname = None + self.plugName = None + try: + self.plugName = plug.name() + except Exception: + self.plugName = self.__PLUGNOTFOUND_MESSAGE + + self.message = f"Error on plug: {self.plugName} attribute functors failed" + super().__init__(self.message) + + +class PlugLockedForEditError(Exception): + """An exception raised when calling predicate function results in an error. + """ + + __PLUGNOTFOUND_MESSAGE = "[INVALID PLUG]." + + def __init__(self, plug): + """ + Args: + plug (om2.MPlug): The plug to raise exception for. + """ + self.plugName = None try: - self.plugname = plug.name() + self.plugName = plug.name() except Exception: - self.plugname = self.__PLUGNOTFOUND_MESSAGE + self.plugName = self.__PLUGNOTFOUND_MESSAGE - self.message = f"error on plug: {self.plugname} attribute functors failed" + self.message = f"The plug: {self.plugName} is locked from edit." super().__init__(self.message) diff --git a/AL/omx/utils/_modifiers.py b/AL/omx/utils/_modifiers.py index 1134848..e1a2af3 100644 --- a/AL/omx/utils/_modifiers.py +++ b/AL/omx/utils/_modifiers.py @@ -1,18 +1,27 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import logging import warnings -from maya import cmds -from maya.api import OpenMaya as om2 -from maya.api import OpenMayaAnim as om2anim +from AL.omx.utils._stubs import om2 logger = logging.getLogger(__name__) class MModifier(om2.MDagModifier): - """A `MDagModifier` that implements the ability to create a DG node. - - This way we don't need both an MDGModifier and a MDAGModifier which messes up the Undo/Redo. + """MModifier is a `om2.MDagModifier` that implements the ability to create both DG and DAG node. """ def __init__(self): @@ -48,7 +57,7 @@ def createDagNode(self, typeName, parent=om2.MObject.kNullObj): Args: typeName (str): The Maya node type name. - parent (om2.MObject): The parent MObject. + parent (om2.MObject, optional): The parent MObject. Returns: om2.MObject: The DAG node created. @@ -67,7 +76,7 @@ def redoIt(self): om2.MDagModifier.doIt(self) def createNode(self, *args, **kwargs): - """Create Dag or DG node based on the internal mode state. + """Create DAG or DG node based on the internal mode state. Returns: om2.MObject: The DAG/DG node created. @@ -82,7 +91,7 @@ def createNode(self, *args, **kwargs): class ToDGModifier: - """A python context to use an MModifer in DGmode, which means MModifier.createNode() will create a DG node. + """A python context to use an :class:`MModifier` in DG mode, which means :func:`MModifier.createNode()` will create a DG node. """ def __init__(self, mmodifer): @@ -98,7 +107,7 @@ def __exit__(self, *_, **__): class ToDagModifier: - """A python context to use an MModifer in DGmode, which means MModifier.createNode() will create a DAG node. + """A python context to use an :class:`MModifier` in DAG mode, which means :func:`MModifier.createNode()` will create a DAG node. """ def __init__(self, mmodifer): diff --git a/AL/omx/utils/_nodes.py b/AL/omx/utils/_nodes.py index 1bb4e3c..b6418d5 100644 --- a/AL/omx/utils/_nodes.py +++ b/AL/omx/utils/_nodes.py @@ -1,8 +1,21 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import logging -from maya import cmds -from maya.api import OpenMaya as om2 + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 logger = logging.getLogger(__name__) @@ -50,7 +63,7 @@ def closestAvailableNodeName(nodeName, _maximumAttempts=10000): Args: nodeName (str): the input name. - _maximumAttempts (int): The maximum attempts before get a node name that does not exists. + _maximumAttempts (int, optional): The maximum attempts before get a node name that does not exists. Returns: str | None: Return the closest name that is available if nodeName is a valid Maya @@ -97,7 +110,7 @@ def findNode(nodeName): nodeName (str): full or short name of the node Returns: - (MObject) if found (None) otherwise + om2.MObject if found or None otherwise """ # to avoid expensive check for existence, wrap with try except... # A cmds.objExists() test will return True for duplicate-named nodes, but it will diff --git a/AL/omx/utils/_plugs.py b/AL/omx/utils/_plugs.py index d24418a..9141677 100644 --- a/AL/omx/utils/_plugs.py +++ b/AL/omx/utils/_plugs.py @@ -1,10 +1,24 @@ -# Copyright (C) Animal Logic Pty Ltd. All rights reserved. +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import logging import functools -from maya import cmds -from maya.api import OpenMaya as om2 -from AL.maya2.omx.utils import _exceptions + +from AL.omx.utils._stubs import cmds +from AL.omx.utils._stubs import om2 +from AL.omx.utils import _exceptions logger = logging.getLogger(__name__) @@ -15,7 +29,7 @@ def createAttributeDummy(): Notes: Usually this temp node will need to be removed soon after the attribute is used. - More info in the notes for `getOrExtendMPlugArray()`. + More info in the notes for :func:`getOrExtendMPlugArray()`. Returns: om2.MObject: The MObject created. @@ -39,8 +53,10 @@ def getOrExtendMPlugArray(arrayPlug, logicalIndex, dummy=None): Args: arrayPlug (om2.MPlug): A valid array plug. + logicalIndex (int): The logical index. - dummy (None | om2.MObject): The dummy MObject that contains a 'kMessage', + + dummy (None | om2.MObject, optional): The dummy MObject that contains a 'kMessage', createAttributeDummy() will be called to create one if it is None. Notes: @@ -51,8 +67,7 @@ def getOrExtendMPlugArray(arrayPlug, logicalIndex, dummy=None): this runs with plenty elbow room around it or only once. Returns: - om2.MPlug: The plug at the logical index, or None if it is not a valid - array plug. + om2.MPlug: The plug at the logical index, or None if it is not a valid array plug. """ cleanUpDummy = False if dummy is None: @@ -94,15 +109,15 @@ def _findLeafPlug(plug, iterCount=None, maxRecursion=8, relaxed=False): Args: plug (om2.MPlug): the root plug to recursively walk - iterCount (int): this is just to ensure an iteration lock to prevent odd + iterCount (int, optional): this is just to ensure an iteration lock to prevent odd infinite loops, something for the function, when it runs recursively, to pass back to itself and increment - maxRecursion (int): the maximum number of recursions allowed - relaxed (bool): Whether we keep silent on the potential error + maxRecursion (int, optional): the maximum number of recursions allowed + relaxed (bool, optional): Whether we keep silent on the potential error Raises: StopIteration: If iteration exceeds the maxRecursion. - _exceptions.PlugArrayOutOfBounds: If the plug is an array plug with no elements. + :class:`PlugArrayOutOfBounds`: If the plug is an array plug with no elements. Returns: None : If there is an error (shouldn't return). @@ -164,19 +179,20 @@ def findSubplugByName(plug, token): return findPlug(plugName, plug.node()) -def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False, dummy=None): +def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False): """ Args: mob (om2.MObject): Maya MObject for a node we want to find the plug on plugName (str): string for the name of the plug we want to look for. Paths supported - networked (bool): pass-through or emulation for Maya's argument that will + networked (bool, optional): pass-through or emulation for Maya's argument that will ensure the only plugs returned are networked ones - relaxed (bool): clients of this function potentially deal with large sets of data + relaxed (bool, optional): clients of this function potentially deal with large sets of data that might contain None, False, or something else flagging an uninteresting index. Relaxed will ensure invalid mobs will be silently skipped if set to True + Returns: - MPlug: the plug found unless it outright fails. + om2.MPlug: the plug found unless it outright fails. Caller needs to ensure it's not null to know if it's valid or not Notes: @@ -211,6 +227,7 @@ def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False, dummy # Chances are the plug is a subplug of an array, a compound, # or a nesting of a combination of both cleanUpDummy = False + dummy = None compoundSplit = plugName.split(".") # any compounding will be dot delimited currPlug = None firstRun = False @@ -231,7 +248,7 @@ def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False, dummy if not relaxed: logger.error("Cannot find plug: %s", token) logger.error(err) - return None # early escape + return om2.MPlug() # early escape firstRun = True if aIndex is not None: # if it's an array plug... @@ -243,7 +260,7 @@ def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False, dummy token, dep.name(), ) - return None + return om2.MPlug() raise RuntimeError( f"findplug failed in finding array entry for plugname {plugName}" @@ -290,18 +307,45 @@ def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False, dummy def findPlug(plugName, node=None): - """ Find the MPlug by name (and node) + """ Find the om2.MPlug by name (and node) - It allows to pass either attribute name plus node MObject + It allows to pass either attribute name plus node om2.MObject or the full plug path without a node. Args: - plugName (str): plugname or fullname with path - node Optional(MObject): node of the plug + plugName (str): plug name or node.attr + node(om2.MObject, optional): node of the plug, it is optional. Returns: - (MPlug) if found (None) otherwise + om2.MPlug if found None otherwise """ + # most of time, MFnDependencyNode.findPlug() will work and be faster, but just + # be aware of the behavior differences between the two: + # 1. We need to check and return None for a plug whose node is invalid (e.g. deleted), + # as MFnDependencyNode.findPlug() will still return a valid plug even when the + # node has been removed, MSelectionList.getPlug() does the check for you. + # 2. MSelectionList supports nodeName.plugName notion, even with [] or . for array element + # plug or child plug full path, MFnDependencyNode.findPlug() does not. + # 3. MSelectionList always returns non-networked plug, while MFnDependencyNode.findPlug() + # depends on the argument you passed in. + # 4. MSelectionList.getPlug() will find a plug on a child shape node if such plug does not + # exist on a transform, when you passed in "transformNodeName.shapeAttrName", which + # doesn't sound right but client code might expect that behavior. + + if node and not node.isNull() and om2.MObjectHandle(node).isValid(): + fnDep = om2.MFnDependencyNode(node) + # Avoid processing child plug full path, array element, or plug on child shape. + if fnDep.hasAttribute(plugName): + try: + # We need to use non-network plug as you can never know if the plug + # will be disconnected later. + plug = fnDep.findPlug(plugName, False) + return None if plug.isNull else plug + + except Exception: + return _findPlugOnNodeInternal(node, plugName, relaxed=True) + + # Then we deal with complex plug path, e.g. compound.child, array[index], etc. if node: if node.hasFn(om2.MFn.kDagNode): nodeName = om2.MFnDagNode(node).fullPathName() @@ -367,9 +411,8 @@ def plugIsValid(plug): Courtesy of Maya having issues when a plug; - can be obtained that is fully formed - responds to methods such as isCompound, isElement etc. - - also respond negatively to isNull BUT actually has an uninitialized array in its stream and will - therefore hard crash if queried for anything relating to its array properties such as numElements - etc. + - also respond negatively to isNull BUT actually has an uninitialized array in its stream and will + therefore hard crash if queried for anything relating to its array properties such as numElements etc. Args: plug (om2.MPlug): The plug to do validity check. @@ -446,6 +489,9 @@ def _plugLockElision(f): """ Internal use only. It is a decorator to enable functions operating on plugs as first argument or plug keyed argument to elide eventual locks present on the plug + Args: + f (function): the python function to wrap. + Notes: This requires the functor getting decorated has a argument call "plug" and "_wasLocked". @@ -500,6 +546,9 @@ def iterAttributeFnTypesAndClasses(): def attributeTypeAndFnFromPlug(plug): """Get the attribute type and MFn*Attribute class for the plug. + Args: + plug (om2.MPlug): The plug to query type and attribute functor for. + Returns: om2.MFn.*, om2.MFn*Attribute. """ @@ -509,11 +558,11 @@ def attributeTypeAndFnFromPlug(plug): retType = om2.MFn.kAttribute for attrType, fn in iterAttributeFnTypesAndClasses(): if attr.hasFn(attrType): - retFn = fn(plug.attribute()) + retFn = fn(attr) retType = attrType break if retFn is None: - retFn = om2.MFnAttribute(plug.attribute()) + retFn = om2.MFnAttribute(attr) return retType, retFn @@ -530,12 +579,18 @@ def valueFromPlug( Args: plug (om2.MPlug): The plug to get the value from. - context (om2.MDGContext): The DG context to get the value for. - faultTolerant (bool): Whether to raise on errors or proceed silently - flattenComplexData (bool): Whether to convert MMatrix to list of doubles - returnNoneOnMsgPlug (bool): Whether we return None on message plug or the plug itself. - asDegrees (bool): For an angle unit attribute we return the value in degrees + + context (om2.MDGContext, optional): The DG context to get the value for. + + faultTolerant (bool, optional): Whether to raise on errors or proceed silently + + flattenComplexData (bool, optional): Whether to convert MMatrix to list of doubles + + returnNoneOnMsgPlug (bool, optional): Whether we return None on message plug or the plug itself. + + asDegrees (bool, optional): For an angle unit attribute we return the value in degrees or in radians. + Returns: om2.MFn.*, om2.MFn*Attribute. """ @@ -607,15 +662,13 @@ def valueFromPlug( def __num_as_float(t): - """ - internal use only, solely meant for code compression + """Internal use only, solely meant for code compression """ return t in (om2.MFnNumericData.kFloat, om2.MFnNumericData.kDouble) def __num_as_int(t): - """ - internal use only, solely meant for code compression + """Internal use only, solely meant for code compression """ # in order of likelyhood/speed centric return ( @@ -662,27 +715,29 @@ def valueAndTypesFromPlug( flattenComplexData=True, asDegrees=False, ): - """Retrieves the value, attribute functor type, and attribute type per functor for any given plug + """Retrieves the value, attribute functor type, and attribute type per functor for any given plug. Args: plug (om2.MPlug): A maya type plug of any description - context (om2.MDGContext): The maya context to retrieve the plug at. This isn't always applicable, - and w indicate so in the switches when it's not, but it's always accepted - When a context isn't passed the default is kNormal, which is current - context at current time - exclusionPredicate (function): Predicated on the attribute functor internal to the function - this enables us to filter by an internal which - preceeds the plug check stages, enabling things - such as early filtering of hidden attributes - or factory ones etc. - inclusionPredicate (function): Predicated on the attribute functor internal to the function - this enables us to filter by an internal which - preceeds the plug check stages, enabling things - such as early filtering by specific attribute - functor calls before choosing to opt in. - faultTolerant (bool): Whether to raise on errors or proceed silently - flattenComplexData (bool): Whether to convert MMatrix to list of doubles - asDegrees (bool): For an angle unit attribute we return the value in degrees + + context (om2.MDGContext, optional): The maya context to retrieve the plug at. This isn't always applicable, + and w indicate so in the switches when it's not, but it's always accepted + When a context isn't passed the default is kNormal, which is current + context at current time. + + exclusionPredicate (function, optional): Predicated on the attribute functor internal to the function + this enables us to filter by an internal which preceeds the plug check stages, enabling things + such as early filtering of hidden attributes or factory ones etc. + + inclusionPredicate (function, optional): Predicated on the attribute functor internal to the function + this enables us to filter by an internal which preceeds the plug check stages, enabling things + such as early filtering by specific attribute functor calls before choosing to opt in. + + faultTolerant (bool, optional): Whether to raise on errors or proceed silently. + + flattenComplexData (bool, optional): Whether to convert MMatrix to list of doubles. + + asDegrees (bool, optional): For an angle unit attribute we return the value in degrees or in radians. Returns: @@ -1010,20 +1065,30 @@ def setValueOnPlug( """Set plug value using a modifier. Args: - plug (om2.MPlug): The plug to set to the specified value contained in the following argument - value: the value to set the plug to - exclusionPredicate (function): See valueAndTypesFromPlug - inclusionPredicate (function): See valueAndTypesFromPlug - faultTolerant (bool): Whether to raise on errors or proceed silently - _wasLocked (bool): internal use only, this is required for plugLock eliding - functions to play nice with elision conditions, since the decorator - will always run first and unlock the plug, and therefore has to be - able to signal the wrapped function of the previous state of the plug - for both the check AND restoration of the lock before an eventual exception - modifier (om2.MDGModifier): to support modifier for undo/redo purpose. - doIt (bool): True means modifier.doIt() will be called immediately to apply the plug value change. - False enable you to defer and call modifier.doIt() later in one go. - asDegrees (bool): When it is an angle unit attribute, if this is True than we take the + plug (om2.MPlug): The plug to set to the specified value contained in the following argument. + + value (any): the value to set the plug to + + elideLock (bool, optional): Whether we unlock the plug (only) during value setting. + + exclusionPredicate (function, optional): See valueAndTypesFromPlug + + inclusionPredicate (function, optional): See valueAndTypesFromPlug + + faultTolerant (bool, optional): Whether to raise on errors or proceed silently + + _wasLocked (bool, optional): internal use only, this is required for plugLock eliding + functions to play nice with elision conditions, since the decorator + will always run first and unlock the plug, and therefore has to be + able to signal the wrapped function of the previous state of the plug + for both the check AND restoration of the lock before an eventual exception + + modifier (om2.MDGModifier, optional): to support modifier for undo/redo purpose. + + doIt (bool, optional): True means modifier.doIt() will be called immediately to apply the plug value change. + False enable you to defer and call modifier.doIt() later in one go. + + asDegrees (bool, optional): When it is an angle unit attribute, if this is True than we take the value as degrees, otherwise as radians. This flag has no effect when it is not an angle unit attribute. @@ -1049,22 +1114,11 @@ def setValueOnPlug( "invalid plug found before trying to set value on it from setValueOnPlug", ) - if plug.isNull: + if not elideLock and _wasLocked: if faultTolerant: return None - plugname = ( - "failed to retrieve name from plug, argument might not even be a plug" - ) - try: - plugname = plug.partialName(useLongNames=True) - except Exception: # yeah, it's an except all, but we don't want errorception here. - pass - - raise RuntimeError(f"Plug {plugname}") - if not elideLock and _wasLocked: - # @todo: add a debug or something? - return None + raise _exceptions.PlugLockedForEditError(plug) attrType, fn = attributeTypeAndFnFromPlug(plug) if exclusionPredicate(fn) or not inclusionPredicate(fn): @@ -1085,7 +1139,7 @@ def setValueOnPlug( if faultTolerant: return None raise RuntimeError( - "value is a seuqence of length different than the length of the array / compound children length of the plug it's trying to be set on" + "value is a sequence of length different than the length of the array / compound children length of the plug it's trying to be set on" ) for i in range(count): diff --git a/AL/omx/utils/_stubs.py b/AL/omx/utils/_stubs.py new file mode 100644 index 0000000..e2ac8e0 --- /dev/null +++ b/AL/omx/utils/_stubs.py @@ -0,0 +1,106 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a middle layer to import modules from Maya. If we fail we will +fallback to stub mode. +""" + +from unittest.mock import MagicMock + + +class _OpenMaya(MagicMock): + """A dummy stub object for maya.api.OpenMaya to silence the module import error when + importing AL.omx outside the Autodesk Maya environment; mainly for API document + generation purpose. + """ + + class MPxCommand: + """A dummy for MPxCommand inheritance to avoid python import error. + """ + + pass + + class MPlug: + """A dummy for MPlug inheritance to avoid python import error. + """ + + pass + + class MDagModifier: + """A dummy for MDagModifier inheritance to avoid python import error. + """ + + pass + + +cmds = None +om2 = None +om2anim = None + + +def isInsideMaya(): + """Return if we are currently running inside the Autodesk Maya environment. + + Returns: + bool: True if inside, False otherwise. + """ + global cmds + return hasattr(cmds, "loadPlugin") and not isinstance(cmds, _MayaStub) + + +def _importStandardMayaModules(): + global cmds + global om2 + global om2anim + + try: + import maya.cmds as m_cmds + import maya.api.OpenMaya as m_om2 + import maya.api.OpenMayaAnim as m_om2anim + + cmds = m_cmds + om2 = m_om2 + om2anim = m_om2anim + + return isInsideMaya() + + except: + return False + + +def _importStandaloneMayaModules(): + try: + import maya.standalone + + maya.standalone.initialize("Python") + return _importStandardMayaModules() + + except: + return False + + +def _useDummyMayaModules(): + global cmds + global om2 + global om2anim + cmds = MagicMock() + om2 = _OpenMaya() + om2anim = MagicMock() + print( + "Run AL_omx in dummy mode, all the Maya related features including tests won't work." + ) + + +# initialize maya modules: +_importStandardMayaModules() or _importStandaloneMayaModules() or _useDummyMayaModules() diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f0532ca --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,11 @@ +Copyright © 2023 Animal Logic. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this +file except in compliance with the License. You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + ANY KIND, either express or implied. See the License for the specific language governing + permissions and limitations under the License.' + \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..30ec1e9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include docs * +include reformat.* +global-exclude *.pyc build/ +exclude AL/omx/package.py AL/omx/CMakeLists.txt \ No newline at end of file diff --git a/README.md b/README.md index 5ee06f1..33e7ff2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ -#AL_omx +# AL_OMX -OMX is a opensource library, that provide a thin wrapper around Maya OM2 which makes OM2 more user-friendly but still retain the API's performance. +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[OMX Documentation](comming soon) + +AL_OMX is a opensource library, that provides a thin wrapper around Maya OM2 which makes OM2 more user-friendly but still retain the API's performance. + +[OMX Documentation](coming soon) +More contents here are coming soon once the public documentation url is available. diff --git a/__site__/__AL__/CMakeLists.txt b/__site__/__AL__/CMakeLists.txt deleted file mode 100755 index bf132b5..0000000 --- a/__site__/__AL__/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 2.8) - -include(RezBuild) - -rez_find_packages(PREFIX pkgs AUTO) - -add_pythonlibs_build(PACKAGE AL.omx MAKE_WHEEL NO_TESTS) diff --git a/__site__/__AL__/package.py b/__site__/__AL__/package.py deleted file mode 100644 index 4158812..0000000 --- a/__site__/__AL__/package.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -config_version = 0 - -name = "AL_omx" - -uuid = "5e1153b0-6735-11ee-b706-78ac445c0b95" - -version = "1.0.0" - -description = """ - A thin wrapper around Maya OM2 which makes OM2 more user-friendly but still retains the API's performance. -""" - -authors = [ - "Aloys Baillet", - "Miguel Gao", - "Valerie Bernard", - "James Dunlop", - "Daniel Springall", -] - -private_build_requires = [ - "cmake-3", - "AL_CMakeLibALPythonLibs-3.7+<4", -] - -requires = ["demandimport-0", "enum34", "maya-2020+", "python-3.7+<4"] - - -def commands(): - prependenv( - "PYTHONPATH", "{root}/AL_maya2-${REZ_AL_MAYA2_VERSION}-none-any.whl", - ) - prependenv("MAYA_PLUG_IN_PATH", "{root}/maya") - - -tests = { - "maya": { - "command": "run_maya_nose2 AL.omx.tests", - "requires": [ - "nose2", - "python-3", - "AL_CMakeLibPython-6.9+<7", - "maya-2022", - "mayaLoadVersionedTool", - "AL_maya_rig_nodes_reparent", - "AL_maya_math_nodes", - ], - }, - "black": { - "command": "black . --check", - "run_in_root": True, - "requires": ["black-19"], - }, -} diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index ea1472e..0000000 --- a/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -output/ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..69fe55e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/api/AL.maya2.constants.beast.rst b/docs/api/AL.maya2.constants.beast.rst deleted file mode 100644 index c0d930b..0000000 --- a/docs/api/AL.maya2.constants.beast.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.constants.beast -=============================== - -.. automodule:: AL.maya2.constants.beast - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.constants.io.rst b/docs/api/AL.maya2.constants.io.rst deleted file mode 100644 index 09cfb3b..0000000 --- a/docs/api/AL.maya2.constants.io.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.constants.io -============================ - -.. automodule:: AL.maya2.constants.io - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.constants.nodes.rst b/docs/api/AL.maya2.constants.nodes.rst deleted file mode 100644 index 3a129fa..0000000 --- a/docs/api/AL.maya2.constants.nodes.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.constants.nodes -=============================== - -.. automodule:: AL.maya2.constants.nodes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.constants.plugs.rst b/docs/api/AL.maya2.constants.plugs.rst deleted file mode 100644 index f2acacd..0000000 --- a/docs/api/AL.maya2.constants.plugs.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.constants.plugs -=============================== - -.. automodule:: AL.maya2.constants.plugs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.exceptions.nodes.rst b/docs/api/AL.maya2.exceptions.nodes.rst deleted file mode 100644 index edcabea..0000000 --- a/docs/api/AL.maya2.exceptions.nodes.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.exceptions.nodes -================================ - -.. automodule:: AL.maya2.exceptions.nodes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.exceptions.plugs.rst b/docs/api/AL.maya2.exceptions.plugs.rst deleted file mode 100644 index 1bca12e..0000000 --- a/docs/api/AL.maya2.exceptions.plugs.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.exceptions.plugs -================================ - -.. automodule:: AL.maya2.exceptions.plugs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.exceptions.session.rst b/docs/api/AL.maya2.exceptions.session.rst deleted file mode 100644 index 623c8bd..0000000 --- a/docs/api/AL.maya2.exceptions.session.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.exceptions.session -================================== - -.. automodule:: AL.maya2.exceptions.session - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.iterators.nodes.rst b/docs/api/AL.maya2.iterators.nodes.rst deleted file mode 100644 index dacb659..0000000 --- a/docs/api/AL.maya2.iterators.nodes.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.iterators.nodes -=============================== - -.. automodule:: AL.maya2.iterators.nodes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.iterators.plugs.rst b/docs/api/AL.maya2.iterators.plugs.rst deleted file mode 100644 index d180615..0000000 --- a/docs/api/AL.maya2.iterators.plugs.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.iterators.plugs -=============================== - -.. automodule:: AL.maya2.iterators.plugs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.iterators.scene.rst b/docs/api/AL.maya2.iterators.scene.rst deleted file mode 100644 index 829b4d1..0000000 --- a/docs/api/AL.maya2.iterators.scene.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.iterators.scene -=============================== - -.. automodule:: AL.maya2.iterators.scene - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.om2.rst b/docs/api/AL.maya2.om2.rst deleted file mode 100644 index 63989da..0000000 --- a/docs/api/AL.maya2.om2.rst +++ /dev/null @@ -1,8 +0,0 @@ -AL.maya2.om2 -============ - -.. automodule:: AL.maya2.om2 - :members: - :undoc-members: - :show-inheritance: - :imported-members: diff --git a/docs/api/AL.maya2.om2anim.rst b/docs/api/AL.maya2.om2anim.rst deleted file mode 100644 index 42ec9b8..0000000 --- a/docs/api/AL.maya2.om2anim.rst +++ /dev/null @@ -1,8 +0,0 @@ -AL.maya2.om2anim -=================== - -.. automodule:: AL.maya2.om2anim - :members: - :undoc-members: - :show-inheritance: - :imported-members: diff --git a/docs/api/AL.maya2.omx.rst b/docs/api/AL.maya2.omx.rst deleted file mode 100644 index ed58610..0000000 --- a/docs/api/AL.maya2.omx.rst +++ /dev/null @@ -1,8 +0,0 @@ -AL.maya2.omx -============ - -.. automodule:: AL.maya2.omx - :members: - :undoc-members: - :show-inheritance: - :imported-members: diff --git a/docs/api/AL.maya2.predicates.beast.rst b/docs/api/AL.maya2.predicates.beast.rst deleted file mode 100644 index c80b1fa..0000000 --- a/docs/api/AL.maya2.predicates.beast.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.predicates.beast -================================ - -.. automodule:: AL.maya2.predicates.beast - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.predicates.containers.rst b/docs/api/AL.maya2.predicates.containers.rst deleted file mode 100644 index 235cef6..0000000 --- a/docs/api/AL.maya2.predicates.containers.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.predicates.containers -===================================== - -.. automodule:: AL.maya2.predicates.containers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.predicates.nodes.rst b/docs/api/AL.maya2.predicates.nodes.rst deleted file mode 100644 index 3b9424c..0000000 --- a/docs/api/AL.maya2.predicates.nodes.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.predicates.nodes -================================ - -.. automodule:: AL.maya2.predicates.nodes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.predicates.plugs.rst b/docs/api/AL.maya2.predicates.plugs.rst deleted file mode 100644 index e62e995..0000000 --- a/docs/api/AL.maya2.predicates.plugs.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.predicates.plugs -================================ - -.. automodule:: AL.maya2.predicates.plugs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.containers.rst b/docs/api/AL.maya2.session.containers.rst deleted file mode 100644 index d8979c6..0000000 --- a/docs/api/AL.maya2.session.containers.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.containers -================================== - -.. automodule:: AL.maya2.session.containers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.contexts.rst b/docs/api/AL.maya2.session.contexts.rst deleted file mode 100644 index e4a09e8..0000000 --- a/docs/api/AL.maya2.session.contexts.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.contexts -================================ - -.. automodule:: AL.maya2.session.contexts - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.entity.rst b/docs/api/AL.maya2.session.entity.rst deleted file mode 100644 index 50f2d9e..0000000 --- a/docs/api/AL.maya2.session.entity.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.entity -============================== - -.. automodule:: AL.maya2.session.entity - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.io.rst b/docs/api/AL.maya2.session.io.rst deleted file mode 100644 index 2908a24..0000000 --- a/docs/api/AL.maya2.session.io.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.io -========================== - -.. automodule:: AL.maya2.session.io - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.keyframes.rst b/docs/api/AL.maya2.session.keyframes.rst deleted file mode 100644 index 0d7acc4..0000000 --- a/docs/api/AL.maya2.session.keyframes.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.keyframes -================================= - -.. automodule:: AL.maya2.session.keyframes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.modifiers.rst b/docs/api/AL.maya2.session.modifiers.rst deleted file mode 100644 index 9f10fa6..0000000 --- a/docs/api/AL.maya2.session.modifiers.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.modifiers -================================= - -.. automodule:: AL.maya2.session.modifiers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.nodes.rst b/docs/api/AL.maya2.session.nodes.rst deleted file mode 100644 index 8fec968..0000000 --- a/docs/api/AL.maya2.session.nodes.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.nodes -============================= - -.. automodule:: AL.maya2.session.nodes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.plugs.rst b/docs/api/AL.maya2.session.plugs.rst deleted file mode 100644 index e4cbfe9..0000000 --- a/docs/api/AL.maya2.session.plugs.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.plugs -============================= - -.. automodule:: AL.maya2.session.plugs - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.project.rst b/docs/api/AL.maya2.session.project.rst deleted file mode 100644 index 28d3a2d..0000000 --- a/docs/api/AL.maya2.session.project.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.project -=============================== - -.. automodule:: AL.maya2.session.project - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.reference.rst b/docs/api/AL.maya2.session.reference.rst deleted file mode 100644 index b30d493..0000000 --- a/docs/api/AL.maya2.session.reference.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.reference -================================= - -.. automodule:: AL.maya2.session.reference - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/AL.maya2.session.utils.rst b/docs/api/AL.maya2.session.utils.rst deleted file mode 100644 index 10a281b..0000000 --- a/docs/api/AL.maya2.session.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -AL.maya2.session.utils -============================= - -.. automodule:: AL.maya2.session.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/index.rst b/docs/api/index.rst deleted file mode 100644 index bb023f2..0000000 --- a/docs/api/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -API Reference -============= - -Information on specific functions, classes, and methods. - -.. toctree:: - :maxdepth: 2 - :glob: - - ./* - diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 1b1dcdf..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Documentation build configuration file -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix of source filenames. -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = u"AL_maya2" -project_display_name = u"AL_maya2" -copyright = u"Animal Logic Pty Ltd. All rights reserved." - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -vprefix = "REZ_" + project.upper() -version = ( - os.environ.get(vprefix + "_MAJOR_VERSION") - + "." - + os.environ.get(vprefix + "_MINOR_VERSION") -) -# The full version, including alpha/beta/rc tags. -release = os.environ.get(vprefix + "_VERSION") - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ["_build"] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "default" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = project + "doc" - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - #'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ( - "index", - "%s.tex" % project, - "%s Documentation" % project_display_name, - u"R&D", - "manual", - ), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ("index", project.lower(), "%s Documentation" % project_display_name, [u"R&D"], 1) -] - - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - "index", - project, - "%s Documentation" % project_display_name, - u"R&D", - project, - "One line description of project.", - "Miscellaneous", - ), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False - -html_theme = "sphinx_rtd_theme" - -path = os.getenv("AL_USER_GUIDE_SPHINX_RTD_THEME_PATH", "") -html_theme_path = [ - path, -] - - -# -- Animal Logic options ------------------------------------------------- -al_rst_source_dir = "./" -al_html_output_dir = "./output/" - -al_build_api_reference = False # API is fairly stable and we have modifier a few files under the api folder, use with caution! - -al_source_code_dirs = ["../src"] -al_source_code_exclude_patterns = [".*tests", "package.*"] # regex - -al_gh_pages_repo = "http://github.al.com.au/documentation/AL_maya2.git" diff --git a/docs/env b/docs/env deleted file mode 100755 index 39a2523..0000000 --- a/docs/env +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -rez-env Sphinx AL_documentation_utils AL_maya2 maya-2018 "$@" diff --git a/docs/examples/omx_command.py b/docs/examples/omx_command.py deleted file mode 100644 index 0f63905..0000000 --- a/docs/examples/omx_command.py +++ /dev/null @@ -1,37 +0,0 @@ -from AL.maya2 import om2, omx -from AL.libs.commandmaya import undoablecommands - -import logging - -logger = logging.getLogger(__name__) - - -class SimpleConstraint(undoablecommands.UndoablePureMayaOm2Command): - def resolve(self): - if not self.argumentValue("driver") and not self.argumentValue("driven"): - sel = om2.MGlobal.getActiveSelectionList(orderedSelectionIfAvailable=True) - if sel.length() == 2: - self.setArgumentValue("driver", sel.getDagPath(0)) - self.setArgumentValue("driven", sel.getDagPath(1)) - else: - logger.error("ParentConstraint needs 2 objects selected!") - self.cancelExecution("ParentConstraint needs 2 objects selected!") - - def doIt(self, driver=None, driven=None): - with omx.commandModifierContext(self) as modifier: - # This converts possible string, MObject inputs to XNode: - driver = omx.XNode(driver) - driven = omx.XNode(driven) - - # You can get and set plug values easily, all the modification will be done - # using the modifier we created in the context above`: - driver.translateX.set(5) - print(driver.translateX.get()) - print(driver.worldMatrix[0].get()) - - reparent = modifier.createDGNode("AL_rig_reparentSingle") - reparent.worldMatrix.connectFrom(driver.worldMatrix[0]) - driven.translate.connectFrom(reparent.translate) - driven.rotate.connectFrom(reparent.rotate) - driven.scale.connectFrom(reparent.scale) - driven.rotateOrder.connectFrom(reparent.rotateOrder_out) diff --git a/docs/examples/omx_simple.py b/docs/examples/omx_simple.py deleted file mode 100644 index fd8bd4a..0000000 --- a/docs/examples/omx_simple.py +++ /dev/null @@ -1,21 +0,0 @@ -from AL.maya2 import omx - -driver = omx.createDagNode("locator", nodeName="driver") -driven = omx.createDagNode("locator", nodeName="driven") - -# If you are going to retrieve plug values you need to run doIt before hand. -# N.B this is optional when executing code from the maya script editor as doIt is run automatically every time it is needed! -# But if you use this code inside an ALCommand, doIt becomes necessary. -omx.doIt() - -# You can get and set plug values easily: -driver.translateX.set(5) -print(driver.translateX.get()) -print(driver.worldMatrix[0].get()) - -reparent = omx.createDGNode("AL_rig_reparentSingle") -reparent.worldMatrix.connectFrom(driver.worldMatrix[0]) -driven.translate.connectFrom(reparent.translate) -driven.rotate.connectFrom(reparent.rotate) -driven.scale.connectFrom(reparent.scale) -driven.rotateOrder.connectFrom(reparent.rotateOrder_out) diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 1340b8d..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -AL_maya2 -===================== - -AL_maya2 is a set of utilities and entry points to work with -modern maya and `OM2 `_ . - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - topics/index.rst - api/index.rst diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..543c6b1 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/advanced/modifiers.rst b/docs/source/advanced/modifiers.rst new file mode 100644 index 0000000..c0d656f --- /dev/null +++ b/docs/source/advanced/modifiers.rst @@ -0,0 +1,65 @@ +Modifiers +======================== + +|project| at it's core, relies on Maya om2anim.MAnimCurveChange, ``om2.MDGModifier`` and ``om2.MDagModifier`` to modify the Maya scene data. +Just like Maya's modifiers, after all the edit operations have been called, you need to call +modifier.doIt() at the correct time to execute these operations. + +.. image:: ../images/XModifierDiagram.png + +1. First, there are ``om2.MDGModifier`` and ``om2.MDagModifier`` in Maya native API. +The ``om2.MDagModifier`` inherits from ``om2.MDGModifier`` and comes with extra DAG creation and edit support, +so we can do pretty much everything with one single ``om2.MDagModifier`` instance, +except when it comes to the method ``createNode()``. +``om2.MDagModifier`` overrides ``createNode()`` to only accept DAG node types, passing +it a DG node type will raise a :class:`TypeError`. + +To overcome this, we introduce :class:`AL.omx.utils._modifiers.MModifier`, which derives from +``om2.MDagModifier``, but with explicit methods ``createDGNode()`` and ``createDagNode()``. For +``createDGNode()``, we explicitly call ``om2.MDGModifier.create(self, typeName)`` instead. + +2. Then we introduce :class:`AL.omx.XModifier`, which owns a :class:`AL.omx.utils._modifiers.MModifier`, as +shown in the graph above. |project| maintains a global list called ``_CURRENT_MODIFIER_LIST``, which contains +a bunch of :class:`AL.omx.XModifier`. + +3. By default, :class:`AL.omx.XModifier` has the immediate state turned off. +When you call :func:`AL.omx.newModifier()`, it creates and pushes a non-immediate :class:`AL.omx.XModifier` instance into ``_CURRENT_MODIFIER_LIST``. +On the other hand, if you call :func:`AL.omx.currentModifier()`, it gives you the last pushed :class:`AL.omx.XModifier` from +``_CURRENT_MODIFIER_LIST``, if no modifier has ever created and pushed before, it will create and push an immediate :class:`AL.omx.XModifier` +instance into ``_CURRENT_MODIFIER_LIST``, then return it to you. + +4. When a new Maya scene is created, or before a Maya scene is opened, or Maya quits, +the ``_CURRENT_MODIFIER_LIST`` will be cleared and emptied. This is ensured during the omx mpx command plug-in load/unload. + +.. tip:: + Use :func:`AL.omx.currentModifier()` to retrieve an `XModifier` most of the time, as it has the advantage over the ``om2`` modifier where you won't need + to pass the modifier around as arguments. + Only call :func:`AL.omx.newModifier()` when you need to organize your scene edits using different modifiers or manage the undo yourself. + +.. _immediate_mode: + +XModifier Immediate Mode +------------------------------- + +If :class:`AL.omx.XModifier`._immediate is True, whenever you call its method to edit Maya's scene data, it will call +`doIt()` to apply the edit to the scene immediately. On the other hand, if :class:`AL.omx.XModifier`._immediate is False, then +you'll have to manually call :func:`AL.omx.doIt()` or `XModifier.doIt()` to apply the edit. + +This is the case except when it comes to node creation. You can not have a valid `om2.MObject` immediately when creating +a node using a modifier. So regardless of the immediate state, it will always call `doIt()` automatically after node creation. + +If you do not call :func:`AL.omx.newModifier()` or manually create a :func:`AL.omx.XModifier()`, and use creation or edit method from +:class:`AL.omx.XNode` or :class:`AL.omx.XPlug`, it will push an immediate ``omx.XModifier`` into the global list. +That is why in the script editor when you run omx scripts, it immediately reflects the edit in the Maya scene. + +.. note:: + Same as Maya's modifiers, stacking operation and call `doIt()` at the end is of best performance. + The immediate :class:`AL.omx.XModifier` does cost you performance. + +.. note:: + Calling :func:`AL.omx.doIt()` will call all :func:`AL.omx.XModifier.doIt()` with the global list, and clear the list. + If you only want to be more specific about applying edits from a certain modifier, use :func:`AL.omx.XModifier.doIt()`. + +.. seealso:: + :doc:`undoability` + Check out the undoability document to see how :class:`AL.omx.XModifier` fits into Maya's undo & redo system. \ No newline at end of file diff --git a/docs/source/advanced/performance.rst b/docs/source/advanced/performance.rst new file mode 100644 index 0000000..b9cd099 --- /dev/null +++ b/docs/source/advanced/performance.rst @@ -0,0 +1,50 @@ +Performance Tips +======================== + +Use Non-Immediate Mode +------------------------- +As shown in :doc:`../introduction/perf_compare`, `immediate mode` does bring quite a performance penalty. +Only use `immediate mode` for interactive scripting or quick code testing, use `non-immediate` mode for production code. + +Check here for more information on :ref:`immediate_mode`. + + +Journal +------------------------- +|project| supports a journal for all creation and edit operations. +Enabling the journal comes with quite a performance penalty, not as much as `immediate mode` but still a significant enough impact to warrant mentioning. +In the 10000+ nodes performance test, it was roughly 6~8 seconds difference. + +There is API :func:`AL.omx.setJournalToggle` to turn on the journal globally, but it is strongly not suggested. +You can use :func:`AL.omx.isJournalOn` to check if the journal is actually turned on. +If you want to turn on the journal temporarily, the suggested way is to wrap your code within :class:`JournalContext`: + +.. code:: python + + with omx.JournalContext(): + ... + +To query journal: + +.. code:: python + + # note that if the xmodifier is in immediate mode, the journal will be cleaned after each execution. + # so you won't see any records. + print(omx.currentModifier().journal()) + + +Plug Setting +--------------------------- +:class:`AL.omx.XPlug` comes with many set methods for plug value setting. +The :func:`AL.omx.XPlug.set` is the simplest way to do it, but not the fastest in performance. Internally it does many checks +and sets the value using different ways based on attribute type. +If you know what type of plug you are setting, then use the type-specific set method instead. + +There is one thing also worth noticing is setting plug states, like `isLocked`, `isKeyable`, `isChannelbox`. +Take `isLocked` for example, performance-wise, using ``omx.XPlug.isLocked = bool`` is preferable than +``omx.XPlug.setLocked(bool)``, because the ``setLocked()`` come with extra costs. +For undoability, ``omx.XPlug.setLocked(True)`` is fully undoable; However, ``omx.XPlug.isLocked = bool`` is not, so you are +only able to use it when you don't care about undoability, or the node or plug was created within that same modifier, where +the undoability of plug state change does not matter. + +The same rule applies to all the other plug states. \ No newline at end of file diff --git a/docs/source/advanced/undoability.rst b/docs/source/advanced/undoability.rst new file mode 100644 index 0000000..731411c --- /dev/null +++ b/docs/source/advanced/undoability.rst @@ -0,0 +1,78 @@ +Undoability +======================== + +.. _undoability: + +Automatic Undo & Manual Undo +-------------------------------------------- +There are two types of undoability in omx: +1. When using omx for scripting, e.g. creating nodes using ``omx.create*Node``, calling methods from :class:`AL.omx.XNode` +, :class:`AL.omx.XPlug` or call :class:`AL.omx.XModifier` method from :func:`AL.omx.currentModifier()` instead of :func:`AL.omx.newModifier()` +for editing, one execution will be one undo item in Maya. + +After execution, you simply use `ctrl+Z` and `shift+Z` to undo and redo. + +2. When using omx within your tools or framework, where you want to manage undo and redo yourself, you will need +to ensure you are using non-immediate :class:`AL.omx.XModifier`, and call :func:`AL.omx.XModifier.doIt()`, :func:`AL.omx.XModifier.undoIt()` +manually in the proper places. + + +.. _mix_cmds_omx: + +Mixing maya.cmds & AL.omx calls +-------------------------------------------- +Mixing ``om2`` (maya.api.OpenMaya) and ``cmds`` in your code is a wrong choice generally. +The rule of thumb is to try to use ``om2`` as much as you can as it brings you the best performance in Python, +and use ``om1`` (maya.OpenMaya) if the feature is not available in ``om2``, and use ``cmds`` +only for queries. When you have to use ``cmds`` with ``om2`` for editing, wrap it with ``om2.MDGModifier.pythonCommandToExecute()``. + +The reason for these head-ups is the undo problems caused by mixed-use. When you undo in Maya, edit by ``cmds`` +will be undone but edits by ``om2`` will not, thus the broken states in the scene. + +When it comes to mixing ``maya.cmds`` and ``omx`` calls, that depends. +If you ensure ``omx`` calls all using immediate mode XModifier ( check :ref:`immediate_mode` ), it is fine to mix it with maya.cmds: + +.. code:: python + + from AL import omx + from maya.api import OpenMaya as om2 + + # reminder: create a new scene to ensure we are using the immediate mode of xmodifier. + + # cmds calls: + transformName1, shapeName1 = cmds.polyCube() + + # omx calls: + transform1 = omx.XNode(transformName1) + transform1.t.set((2,3,4)) + + # cmds calls: + transformName2, = cmds.spaceLocator() + # omx calls: + transform2 = omx.XNode(transformName2) + transform2.t.connectFrom(transform1.t) + + # cmds calls: + cmds.polySphere() + + # the undo/redo will work. + +If you use omx in some production code where you manage undo & redo yourself, mixing ``cmds`` with ``omx`` calls is the same +as mixing ``cmds`` with om2. You will have undo issues. + + +Mixing om2 Modifier and omx.XModifier +-------------------------------------------- +This is a bad idea. +If you use omx.XModifier, it is better and safer to only use omx.XModifier, to ensure the undo is done in a sequential way, +use :func:`AL.omx.newModifier` to create separate XModifier if you need it. +Alternatively, use omx.XModifiers and om2.MDagModifier, but stick to native API modifier's for the edits, do not use omx API for editing. + +Undo with XPlug States Change +-------------------------------------------- +When you need to set XPlug state, ``isLocked``, ``isKeyable``, ``isChannelBox``, etc, you have two options, take ``isLocked`` for example: +``omx.XPlug.setLocked(bool)`` and ``omx.XPlug.isLocked = bool``. +The difference between the two is ``setLocked()`` is undoable with :class:`omx.XModifier`, but you pay more performance cost while the +``isLocked`` approach is not undoable and will be likely to ruin the surrounding undo states, but it is faster than ``setLocked()``. +As a rule of thumb, use ``omx.XPlug.isLocked = bool`` when you don't need to undo the state change, use ``setLocked()`` if the undoability +matters. The same rule applies to other state edits like ``isKeyable`` and ``isChannelBox``. \ No newline at end of file diff --git a/docs/source/api/private.rst b/docs/source/api/private.rst new file mode 100644 index 0000000..f06730c --- /dev/null +++ b/docs/source/api/private.rst @@ -0,0 +1,27 @@ +Private APIs +===================================== + +.. warning:: + + The classes or functions documented here are all for internal use only! Do not use them in your code! + This document is only here to help you understand them better. + +AL.omx._xcommand +------------------------------- + +.. automodule:: AL.omx._xcommand + :members: + :undoc-members: + :show-inheritance: + + +AL.omx.plugin.AL_OMXCommand +------------------------------- +This module contains the method to load/unload the omx mpx plug-in. +When the plug-in is loaded, several callbacks are registered and they will be unloaded when the plug-in is unloaded. +The callbacks are used to clean up omx modifier stacks on events like scene new, scene open and Maya quit. + +.. automodule:: AL.omx.plugin.AL_OMXCommand + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/public.rst b/docs/source/api/public.rst new file mode 100644 index 0000000..c4bac60 --- /dev/null +++ b/docs/source/api/public.rst @@ -0,0 +1,10 @@ +AL.omx +---------- +AL.omx contains all the public-facing APIs. They will be carefully maintained. + +.. automodule:: AL.omx + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__, __str__, __repr__, __hash__, __contains__, __getitem__, __iter__, __eq__, __ne__ + \ No newline at end of file diff --git a/docs/source/api/utils.rst b/docs/source/api/utils.rst new file mode 100644 index 0000000..2166f2d --- /dev/null +++ b/docs/source/api/utils.rst @@ -0,0 +1,51 @@ +AL.omx.utils +===================================== + +.. warning:: + + The utils within this package are mainly for internal use only, they are more likely to be changed, so use them at your own risk. + + +AL.omx.utils._nodes +------------------------------- + +.. automodule:: AL.omx.utils._nodes + :members: + :undoc-members: + :show-inheritance: + + +AL.omx.utils._plugs +------------------------------- + +.. automodule:: AL.omx.utils._plugs + :members: + :undoc-members: + :show-inheritance: + + +AL.omx.utils._modifiers +------------------------------- + +.. automodule:: AL.omx.utils._modifiers + :members: + :undoc-members: + :show-inheritance: + + +AL.omx.utils._contexts +------------------------------- + +.. automodule:: AL.omx.utils._contexts + :members: + :undoc-members: + :show-inheritance: + + +AL.omx.utils._exceptions +------------------------------- + +.. automodule:: AL.omx.utils._exceptions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/changes.rst b/docs/source/changes.rst new file mode 100644 index 0000000..d458b7f --- /dev/null +++ b/docs/source/changes.rst @@ -0,0 +1,9 @@ +Change Logs +================ + +1.0.1 +-------------- +Features + - Build up opensource facility. + - Write user documentation. + - PR `#1 `_, `#2 `_, Ticket `PERF-3742 `_ diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..fdb0b70 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + + +# -- Project information ----------------------------------------------------- + +project = "AL_OMX" +copyright = "Animal Logic Pty Ltd." +author = "Animal Logic" +license = "Apache License Version 2.0" + +# The short X.Y version +version = "1.0.1" +# The full version, including alpha/beta/rc tags +release = "1.0.1" + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx.ext.autosummary", + "sphinx_copybutton", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["tests", "utils", "plugin"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# since we are not using it, and empty folder get ignored by git, here we +# comment it out until we have custom static files. +# html_static_path = ["_static"] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "AL_omxdoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, "AL_omx.tex", "AL\\_omx Documentation", "Animal Logic", "manual"), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, "al_omx", "AL_omx Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "AL_omx", + "AL_omx Documentation", + author, + "AL_omx", + "One line description of project.", + "Miscellaneous", + ), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ["search.html"] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- to support the use of some commonly used variables --------------------- +variables_to_export = [ + "project", + "copyright", + "version", + "license", +] +_epilog_map = [ + f".. |{key}| replace:: {value}" + for key, value in locals().items() + if key in variables_to_export +] +rst_epilog = "\n".join(_epilog_map) diff --git a/docs/source/feedback/contributing.rst b/docs/source/feedback/contributing.rst new file mode 100644 index 0000000..150838a --- /dev/null +++ b/docs/source/feedback/contributing.rst @@ -0,0 +1,14 @@ +How To Contribute +================= + +.. todo:: + The documentation on how to contribute to |project| is coming soon. + + +Code of Conduct +----------------- + +- Use camel case in your code. +- Use expressive variable names and function names. +- Document your function/method/class. +- Run ./reformat.sh or reformat.bat to make sure the coding format is correct before code submit. diff --git a/docs/source/feedback/feedback.rst b/docs/source/feedback/feedback.rst new file mode 100644 index 0000000..64f0df0 --- /dev/null +++ b/docs/source/feedback/feedback.rst @@ -0,0 +1,4 @@ +Feedback +================= +.. todo:: + The feedback channel is yet to be set up. \ No newline at end of file diff --git a/docs/source/getting_started/cookbook.rst b/docs/source/getting_started/cookbook.rst new file mode 100644 index 0000000..7333e9d --- /dev/null +++ b/docs/source/getting_started/cookbook.rst @@ -0,0 +1,171 @@ +Cookbook +======================== + +.. note:: + The main purpose of the cookbook is to convey the idea of how to use omx, you may need to change + some node names or import relevant modules to make it work in your environment. + + The import statements are omitted from all the code snippets below. + These are two typical module import statements you need: + + .. code:: python + + from AL import omx + from maya.api import OpenMaya as om2 + + In this cookbook, we always refer ``maya.api.OpenMaya`` as ``om2``, and ``AL.omx`` as ``omx``. + + .. seealso:: + :ref:`immediate_mode` + The execute mode of omx dictates whether you need to call :func:`AL.omx.doIt()` manually. + + +Create DagNode +--------------------------- +Use :func:`AL.omx.createDagNode` and expect an ``XNode`` to be returned. + +.. code:: python + + # create transform node and its shape: + locatorTransform1 = omx.createDagNode("transform", nodeName="myLoc1") + locatorShape1 = omx.createDagNode("locator", parent=locatorTransform1, nodeName="myLoc1Shape") + + # create shape and its parent transform in one go, by default parent transform will have a default name, + # and here the function returns the shape only: + locatorShape2 = omx.createDagNode("locator", nodeName="myLoc2Shape") + + # create shape and its parent transform in on go, but return [transform, shape]: + locatorTransform3, locatorShape3 = omx.createDagNode("locator", nodeName="myLoc3Shape", returnAllCreated=True) + + omx.doIt() # optional in script editor + + +Create DGNode +--------------------------- +Use :func:`AL.omx.createDGNode` and expect an ``XNode`` to be returned. + +.. code:: python + + timeNode = omx.createDGNode("time", nodeName="myTime") + omx.doIt() # optional in script editor + + +XNode from an existing node +--------------------------- +You can use; node name/dag path, om2.MObject, om2.MFnBase or another XNode to construct an ``XNode``. +Refer to :class:`AL.omx.XNode` for more details. + +.. code:: python + + # from string: + persp = omx.XNode("persp") + + # construct XNode from MObject: + persp = omx.XNode(SOME_MOBJECT) + + # from MFn: + fn = om2.MFnDependencyNode(SOME_MOBJECT) + xnode = omx.XNode(fn) + + # from another XNode: + xnode = omx.XNode(persp) + + +Query XNode States +--------------------------- +An ``XNode`` is not an ``om2.MObject``, instead it is a thin wrapper around it. However, all the methods available +in ``om2.MObject`` are also available in ``XNode``, plus more. Refer to :class:`AL.omx.XNode` for more details. + +.. code:: python + + # from string: + perspShape = omx.XNode("perspShape") + print("XNode is an om2.MObject:", isinstance(perspShape, om2.MObject)) + print("Camera api type: ", perspShape.apiType()) + print("Has camera functor: ", perspShape.hasFn(om2.MFn.kCamera)) + print("Camera is null: ", perspShape.isNull()) + print("Camera is valid: ", perspShape.isValid()) + print("Camera MObject: ", perspShape.object()) + + +Access to Plug +--------------------------- +You first need to get an ``XNode``, then you can get access to the ``XPlug`` from it. + +.. code:: python + + persp = omx.XNode("persp") + visXPlug = persp.visibility # normal plug + wmXPlug = persp.wm[0] # array element by logical index + bgColorRXPlug = perspShape.backgroundColorR # compound child + bgColorGXPlug = perspShape.backgroundColor.child(1) # compound child + bgColorGXPlug = perspShape.backgroundColor['backgroundColorB'] # compound child + + + +Get & Set Plug Value +--------------------------- +An ``XPlug`` is actually an instance of ``om2.MPlug``, this means you have access to all of the ``om2.MPlug`` methods, +and you can use ``XPlug`` whenever an ``om2.MPlug`` is needed. Refer to :class:`AL.omx.XPlug` for more details. + +.. code:: python + + persp = omx.XNode("persp") + worldMatrix = persp.wm[0].get() + vis = persp.visibility.get() + + visPlug.set(not vis) + + +Connection +--------------------------- +.. code:: python + + persp = omx.XNode("persp") + side = omx.XNode("side") + + # connection + persp.t.connectTo(side.t) + side.r.connectFrom(persp.r) + + # disconnection + side.r.disconnectFromSource() + + +Undo & Redo +--------------------------- +Read the :doc:`../advanced/undoability` document to know how undo & redo actually works. + +.. code:: python + + transform = omx.createDagNode("transform", nodeName="myLoc1") + shape = omx.createDagNode("locator", parent=transform, nodeName="myLoc1Shape") + omx.doIt() # optional in script editor + + omx.currentModifier().undoIt() + omx.currentModifier().doIt() # here calling omx.doIt() is the same. + + +Getting om2.MFn Functors +--------------------------- +.. code:: python + + # retrieve basic functor, om2.MFnDependencyNode for DG node and om2.MFnDagNode for DAG node: + print("basic functor for dag:", omx.XNode("persp").basicFn()) + print("basic functor for dg:", omx.XNode("time1").basicFn()) + + # retrieve the most type-specific functor: + print("basic functor for transform:", omx.XNode("persp").bestFn()) + print("basic functor for camera:", omx.XNode("perspShape").bestFn()) + + +Stringfication +------------------- +:class:`AL.omx.XNode` and :class:`AL.omx.XPlug` both support stringfication, when used in ``print()`` or logger, it will be converted to a nice-formed string. + +.. code:: python + + node = omx.XNode("persp") + visPlug = node.visibility + print("node:", node) # the minimum dag path or dg name will be used. + print("plug", visPlug) # minimumDagPath.plugLongName will be used. diff --git a/docs/source/getting_started/gen_docs.rst b/docs/source/getting_started/gen_docs.rst new file mode 100644 index 0000000..4034c42 --- /dev/null +++ b/docs/source/getting_started/gen_docs.rst @@ -0,0 +1,60 @@ +Generate |project| Documentation Locally +============================================ + +The online version of |project| documentation is always available on here (ToBeAdded). +The documentation source files are also included with each distribution, which means you can generate HTML documentation from them yourself locally. + +**To generate the document:** + +1. Install Sphinx, check out here for `more information `_. +Install using pip is suggested, but keep in mind that you can not use mayapy for the pip install, you will need the native pip to do that, or pip in a virtual env. + +2. After installation, use this to verify it is installed correctly: + +.. code:: shell + + sphinx-build --version + +3. Install the required Sphinx theme and extension: + +.. code:: shell + + pip install sphinx_rtd_theme + pip install sphinx-copybutton + +4. Add the root directory of |project| in your `PYTHONPATH` so that the document generator can import `AL.omx` for API documentation auto-generation. +You can do so by simply installing |project| using pip install: + +.. code:: shell + + pip install AL_omx + +Alternatively, you can simply set the environment variable instead: + +.. code:: shell + + # for Linux and MacOSX: + export PYTHONPATH="$PYTHONPATH:path/to/omxRootDir" + + # for Windows: + set PYTHONPATH=%PYTHONPATH%;path/to/omxRootDir + +5. Run make in the doc folder: + +.. code:: shell + + cd path/to/omxRootDir/docs + + # for Linux and MacOSX: + make html + + # for Windows: + make.bat html + +6. The generated documentation will be in `path/to/omxRootDir/docs/build` folder. Load the `index.html` in your web browser +to use the documentation. Uninstall |project| if you installed it with `pip install` as it will do nothing in vanilla Python +beyond the Maya environment. Skip this step if you did it by setting the `PYTHONPATH` environment variable. + +.. code:: shell + + pip uninstall AL_omx diff --git a/docs/source/getting_started/installation.rst b/docs/source/getting_started/installation.rst new file mode 100644 index 0000000..7828595 --- /dev/null +++ b/docs/source/getting_started/installation.rst @@ -0,0 +1,81 @@ +Install |project| In Maya +================================ + +Install With Pip and mayapy +--------------------------------- +|project| is available in `PyPi `__, that means you can easily install in Maya using ``pip install``. +`mayapy` is an executable to bootstrap the process to start Maya's bundled Python interpreter, it is available on each distribution of Maya. + +To install |project| with pip, you first need to ensure pip is already installed for your ``mayapy``, try running the script below in the console: + +.. code:: shell + + /path/to/mayapy -m pip --version + +If this prints out the version correctly, means pip has already been installed and is available. +Otherwise, refer to `this `_ for how to install pip within ``mayapy``, keep in mind that +instead of calling ``python``, you need to call ``/actual/path/to/mayapy`` instead. + +Now to install |project| with pip in Windows, you just do: + +.. code:: shell + + mayapy -m pip install AL_OMX + +In Linux and macOS: + +.. code:: shell + + sudo ./mayapy -m pip install AL_OMX + +To uninstall, do this in Windows: + +.. code:: console + + mayapy -m pip uninstall AL_OMX + +In Linux and macOS: + +.. code:: shell + + sudo ./mayapy -m pip uninstall AL_OMX + + +.. note:: + + You will need to have administrator privilege to install/uninstall pip and |project| in this way. + + +Install Manually by PYTHONPATH +------------------------------------------------ +Alternatively, you can download |project| from `PyPi `__ or from `Github `_, extract files to wherever +you want, and add the root directory that contains ``AL`` to your ``PYTHONPATH``. +This can be done by adding the directory to ``PYTHONPATH`` global environment variable setting in your OS, or by python script. + +At the moment, |project| does not contain external pip dependency, so directly using it by adding it to ``PYTHONPATH`` is feasible. +The upside is you don't need administrator privileges to install this way. + + +Verify the Installation +------------------------------------------------ +After the installation, you can lunch the relevant version of Maya and start using |project|. +Try this in Maya's script editor to see if things are installed correctly: + +.. code:: python + + from AL import omx + +For the first run, Maya will pop up dialog, confirming that if you want to load the AL_OMXCommand.py plugin. +You need to allow it for the undo/redo works for edits made by |project|. Then if you exit Maya normally, the next time you +do `from AL import omx` it won't ask you again. + + +Supported Environment +------------------------------------ ++------------+------------+ +| Python | Maya | ++============+============+ +| 3.7+ | 2022+ | ++------------+------------+ + +|project| might still work in earlier Maya version, but Maya 2022 and later versions are the ones that we heavily tested upon. \ No newline at end of file diff --git a/docs/source/getting_started/quick_samples.rst b/docs/source/getting_started/quick_samples.rst new file mode 100644 index 0000000..2fb82c5 --- /dev/null +++ b/docs/source/getting_started/quick_samples.rst @@ -0,0 +1,13 @@ +Quick Example Codes +======================== + +.. note:: + Creating a new Maya scene before executing this code is suggested. + Execute all the Python code snippets within Maya's script editor for immediate scene feedback. + +.. seealso:: + :ref:`immediate_mode` + The execute mode of omx dictates whether you need to call :func:`AL.omx.doIt()` or :func:`AL.XModifier.doIt()` manually. + + +.. literalinclude:: ../python/quick_samples.py diff --git a/docs/source/getting_started/run_tests.rst b/docs/source/getting_started/run_tests.rst new file mode 100644 index 0000000..c7bb979 --- /dev/null +++ b/docs/source/getting_started/run_tests.rst @@ -0,0 +1,26 @@ +Running All |project| Tests +============================================ + +Each |project| distribution comes with full test suites. + +**Run Tests in Maya Standalone** + +1. Make sure you install the |project| for that Maya version, check out :doc:`installation`. + +2. Run tests using mayapy: + +.. code:: shell + + path/to/mayapy -m AL.omx.tests.runall + + +**Run Tests in Maya Interactive Mode** + +1. Make sure you install the |project| for that Maya version, check out :doc:`installation`. + +2. Run Python command in Maya's script editor: + +.. code:: python + + from AL.omx.tests import runall + runall.runAllOMXTests() diff --git a/docs/source/images/XModifierDiagram.png b/docs/source/images/XModifierDiagram.png new file mode 100644 index 0000000..ce5fe42 Binary files /dev/null and b/docs/source/images/XModifierDiagram.png differ diff --git a/docs/source/images/omxCodeShot.png b/docs/source/images/omxCodeShot.png new file mode 100644 index 0000000..b08c76b Binary files /dev/null and b/docs/source/images/omxCodeShot.png differ diff --git a/docs/source/images/perfLineGraph.png b/docs/source/images/perfLineGraph.png new file mode 100644 index 0000000..744e32f Binary files /dev/null and b/docs/source/images/perfLineGraph.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..2da7d1e --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,60 @@ + +Welcome to |project|! +==================================================================== +|project| is a lightweight Python library for use within the 3D application Maya from Autodesk. It is a thin wrapper around the `maya.api.OpenMaya `_ (referred as ``om2``) API. +The current version for |project| is |version|, under the |license|. + +.. image:: images/omxCodeShot.png + :alt: |project| + + +.. toctree:: + :caption: Introduction + :maxdepth: 1 + + introduction/why_omx + introduction/perf_compare + + +.. toctree:: + :caption: Getting Started + :maxdepth: 2 + + getting_started/installation + getting_started/quick_samples + getting_started/cookbook + getting_started/run_tests + getting_started/gen_docs + + +.. toctree:: + :caption: Reference + :maxdepth: 1 + + api/public + api/utils + api/private + +.. toctree:: + :caption: Technical Details + :maxdepth: 1 + + advanced/modifiers + advanced/undoability + advanced/performance + changes + +.. toctree:: + :caption: Feedback & Contributing + :maxdepth: 1 + + feedback/contributing + feedback/feedback + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/introduction/perf_compare.rst b/docs/source/introduction/perf_compare.rst new file mode 100644 index 0000000..1afe268 --- /dev/null +++ b/docs/source/introduction/perf_compare.rst @@ -0,0 +1,85 @@ + +Performance Comparison +========================= + +The table below compares the performance of ``maya.cmds``, ``pymel``, ``AL.omx``, ``maya.api.OpenMaya (om2)``, +when they are dealing with 100+, 1000+, 10000+ nodes. It also shows the performance difference between +``AL.omx`` in and out of immediate mode. + ++-----------------------+--------+--------+-----------------+--------+--------+ +| 100+ nodes (seconds) | cmds | pymel | omx(immediate) | omx | om2 | ++=======================+========+========+=================+========+========+ +| creation | 0.1837 | 0.2977 | 0.0825 | 0.061 | 0.0746 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| edit | 0.0677 | 0.3975 | 0.1692 | 0.0536 | 0.0270 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| rename | 0.0115 | 0.0310 | 0.016 | 0.0055 | 0.0053 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| query | 0.0082 | 0.1422 | 0.0146 | 0.0101 | 0.0057 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| delete | 0.1156 | 0.1205 | 0.0133 | 0.0069 | 0.0059 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| total | 0.3868 | 0.9890 | 0.2956 | 0.1371 | 0.1185 | ++-----------------------+--------+--------+-----------------+--------+--------+ + ++-----------------------+--------+--------+-----------------+--------+--------+ +| 1000+ nodes (seconds) | cmds | pymel | omx(immediate) | omx | om2 | ++=======================+========+========+=================+========+========+ +| creation | 1.6022 | 2.8140 | 0.9777 | 0.6206 | 0.2677 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| edit | 0.7045 | 4.1965 | 1.5021 | 0.4553 | 0.1740 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| rename | 0.1246 | 0.3209 | 0.1138 | 0.0606 | 0.0434 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| query | 0.0819 | 1.4160 | 0.1128 | 0.1231 | 0.0432 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| delete | 0.8158 | 0.6460 | 0.1044 | 0.0777 | 0.0459 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| total | 3.3291 | 9.3934 | 2.8108 | 1.3372 | 0.5742 | ++-----------------------+--------+--------+-----------------+--------+--------+ + + ++-----------------------+--------+--------+-----------------+--------+--------+ +| 10000+ nodes(seconds) | cmds | pymel | omx(immediate) | omx | om2 | ++=======================+========+========+=================+========+========+ +| creation |15.7968 |27.2677 | 9.2528 | 7.2431 | 2.4226 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| edit | 7.0859 |41.1648 | 13.5523 | 4.8801 | 1.7376 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| rename | 1.4020 | 3.4139 | 1.1828 | 0.6959 | 0.5311 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| query | 0.8260 |14.0232 | 1.2039 | 1.275 | 0.4394 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| delete | 6.2583 | 5.6137 | 1.6232 | 0.9046 | 0.5580 | ++-----------------------+--------+--------+-----------------+--------+--------+ +| total | 31.369 |91.4833 | 26.815 |14.9987 | 5.6887 | ++-----------------------+--------+--------+-----------------+--------+--------+ + + +The graph below shows the time taken to run each code snippet as the node count scales. + +.. image:: ../images/perfLineGraph.png + :alt: The time taken as the node count scales for each library. + + +Performance Measuring Code +--------------------------- +Common utils used by each script, it needs to be executed first: + +.. literalinclude:: ../python/perf_tests/common.py + +**maya.cmds:** + +.. literalinclude:: ../python/perf_tests/perf_cmds.py + +**PyMel:** + +.. literalinclude:: ../python/perf_tests/perf_pymel.py + +**AL.OMX:** + +.. literalinclude:: ../python/perf_tests/perf_omx.py + +**maya.api.OpenMaya (OM2):** + +.. literalinclude:: ../python/perf_tests/perf_om2.py \ No newline at end of file diff --git a/docs/source/introduction/why_omx.rst b/docs/source/introduction/why_omx.rst new file mode 100644 index 0000000..0341bdd --- /dev/null +++ b/docs/source/introduction/why_omx.rst @@ -0,0 +1,7 @@ + +Why |project|? +================================== +- More user-friendly than Maya's native `om2 `_ (aka maya.api.OpenMaya) API. +- Simpler explicit code. Check the :doc:`../getting_started/quick_samples` and :doc:`../getting_started/cookbook`. +- Closer to Maya's `om2 `_ API's performance over other libraries, check the :doc:`perf_compare`. +- Built-in :ref:`undoability`. \ No newline at end of file diff --git a/docs/.nojekyll b/docs/source/python/perf_tests/__init__.py similarity index 100% rename from docs/.nojekyll rename to docs/source/python/perf_tests/__init__.py diff --git a/docs/source/python/perf_tests/common.py b/docs/source/python/perf_tests/common.py new file mode 100644 index 0000000..937af78 --- /dev/null +++ b/docs/source/python/perf_tests/common.py @@ -0,0 +1,68 @@ +# execute these codes once before actual performance testing codes. + +import random +import time + + +def getRandomPosition(furthestDist): + return [(random.random() - 0.5) * furthestDist for _ in range(3)] + + +def getRandomScale(): + return [random.random() * 2.0 for _ in range(3)] + + +def getRandomColor(): + return random.randint(0, 31) + + +def getRandomIndex(maxValue): + return random.randint(0, maxValue) + + +def getMaxValue(locCount): + return int(locCount / 10) + + +class PerfMeasurement: + def __init__(self, label): + self._label = label + self._gap = 0.0 + + def __enter__(self,): + self._start = time.time() + self._gap = 0.0 + + def __exit__(self, *_, **__): + self._gap = time.time() - self._start + print(f"{self._label} took {round(self._gap, 4)} seconds.") + + def timeConsumed(self): + return self._gap + + +class TotalPerfMeasurement: + def __init__(self, label): + self._measurers = [] + self._label = label + + def add(self, label): + measurement = PerfMeasurement(label) + self._measurers.append(measurement) + return measurement + + def __enter__(self,): + print("-" * 20) + return self + + def __exit__(self, *_, **__): + total = 0.0 + for m in self._measurers: + total = total + m.timeConsumed() + + print(f"{self._label} took {round(total, 4)} seconds.") + print("-" * 20) + + +NUM_NODES_LIST = (100, 1000, 10000) +REFINED_NUM_NODES_LIST = (10, 50, 100, 500, 1000, 5000, 10000, 50000) diff --git a/docs/source/python/perf_tests/perf_cmds.py b/docs/source/python/perf_tests/perf_cmds.py new file mode 100644 index 0000000..8049eb9 --- /dev/null +++ b/docs/source/python/perf_tests/perf_cmds.py @@ -0,0 +1,119 @@ +from maya import cmds + +# execute the code from common in maya script editor first. + + +def createInCmds(locCount): + maxValue = getMaxValue(locCount) + + controller = cmds.joint() + cmds.addAttr(controller, ln="flash", at="long", min=0, max=maxValue) + + stars = [None] * locCount + toDelete = [None] * locCount + parent = cmds.createNode("transform", n="stars") + + for i in range(locCount): + condition = cmds.createNode("condition") + + (loc,) = cmds.spaceLocator() + cmds.parent(loc, parent) + cmds.addAttr(loc, ln="flashIndex", at="long", min=0, max=maxValue) + + (testDel,) = cmds.spaceLocator() + cmds.parent(testDel, loc) + + cmds.objExists(testDel) + stars[i] = (loc, condition) + toDelete[i] = testDel + + return controller, stars, toDelete + + +def editInCmds(controller, stars): + maxValue = getMaxValue(len(stars)) + + cmds.setAttr(f"{controller}.radius", 10) + cmds.setAttr(f"{controller}.flash", keyable=True) + cmds.setKeyframe(f"{controller}.flash", time=(1,), value=0) + cmds.setKeyframe(f"{controller}.flash", time=(120,), value=maxValue) + + for loc, condition in stars: + cmds.setAttr(f"{condition}.colorIfTrue", 1.0, 1.0, 1.0) + cmds.setAttr(f"{condition}.colorIfFalse", 0.0, 0.0, 0.0) + + cmds.setAttr(f"{loc}.overrideEnabled", True) + cmds.setAttr(f"{loc}.overrideColor", getRandomColor()) + + pos = getRandomPosition(maxValue) + cmds.move(pos[0], pos[1], pos[2], loc) + cmds.setAttr(f"{loc}.s", *getRandomScale()) + cmds.setAttr(f"{loc}.displayHandle", lock=True) + cmds.setAttr(f"{loc}.overrideDisplayType", lock=True) + cmds.setAttr(f"{loc}.overrideDisplayType", lock=False) + + cmds.setAttr(f"{loc}.flashIndex", getRandomIndex(maxValue)) + + cmds.connectAttr(f"{controller}.r", f"{loc}.r") + cmds.connectAttr(f"{controller}.overrideShading", f"{loc}.overrideShading") + cmds.disconnectAttr(f"{controller}.overrideShading", f"{loc}.overrideShading") + + cmds.connectAttr(f"{controller}.flash", f"{condition}.firstTerm") + cmds.connectAttr(f"{loc}.flashIndex", f"{condition}.secondTerm") + cmds.connectAttr(f"{condition}.outColorR", f"{loc}.visibility") + + +def renameInCmds(nodesToRename): + for node in nodesToRename: + cmds.rename(node, f"{node}New") + cmds.rename(f"{node}New", node) + + +def queryInCmds(controller, stars): + cmds.listConnections(f"{controller}.flash") + for loc, _ in stars: + cmds.objExists(loc) + cmds.getAttr(f"{loc}.t") + cmds.getAttr(f"{loc}.wm[0]") + cmds.getAttr(f"{loc}.overrideDisplayType", lock=True) + + +def deleteInCmds(nodesToDelete): + for toDel in nodesToDelete: + cmds.delete(toDel) + + +def categorizedPerformanceTestInCmds(): + for num in NUM_NODES_LIST: + cmds.file(new=True, force=True) + with TotalPerfMeasurement(f"Deal with {num} nodes in maya.cmds") as measure: + with measure.add(f"Create {num}+ nodes in maya.cmds"): + controller, stars, nodes = createInCmds(num) + + with measure.add(f"Edit {num}+ nodes in maya.cmds"): + editInCmds(controller, stars) + + with measure.add(f"Rename {num} nodes in maya.cmds"): + renameInCmds(nodes) + + with measure.add(f"Query {num}+ nodes in maya.cmds"): + queryInCmds(controller, stars) + + with measure.add(f"Remove {num} nodes in maya.cmds"): + deleteInCmds(nodes) + + +def totalPerformanceTestInCmds(): + for num in REFINED_NUM_NODES_LIST: + cmds.file(new=True, force=True) + with PerfMeasurement(f"Deal with {num} nodes in maya.cmds"): + controller, stars, nodes = createInCmds(num) + editInCmds(controller, stars) + renameInCmds(nodes) + queryInCmds(controller, stars) + deleteInCmds(nodes) + + +if __name__ == "__main__": + categorizedPerformanceTestInCmds() + # totalPerformanceTestInCmds() diff --git a/docs/source/python/perf_tests/perf_om2.py b/docs/source/python/perf_tests/perf_om2.py new file mode 100644 index 0000000..1596050 --- /dev/null +++ b/docs/source/python/perf_tests/perf_om2.py @@ -0,0 +1,188 @@ +from maya.api import OpenMaya as om2 +from maya import cmds + +# execute the code from common in maya script editor first. + + +def createInOM2(modifier, locCount): + maxValue = getMaxValue(locCount) + + controller = modifier.createNode("joint") + modifier.renameNode(controller, "controller") + + fnAttr = om2.MFnNumericAttribute() + attrObj = fnAttr.create("flash", "flash", om2.MFnNumericData.kInt) + fnAttr.setMin(0) + fnAttr.setMax(maxValue) + modifier.addAttribute(controller, attrObj) + + parent = modifier.createNode("transform") + modifier.renameNode(parent, "stars") + + stars = [None] * locCount + toDelete = [None] * locCount + for i in range(locCount): + condition = om2.MDGModifier.createNode(modifier, "condition") + + loc = modifier.createNode("transform", parent=parent) + modifier.createNode("locator", parent=loc) + attrObj = fnAttr.create("flashIndex", "flashIndex", om2.MFnNumericData.kInt) + fnAttr.setMin(0) + fnAttr.setMax(maxValue) + modifier.addAttribute(loc, attrObj) + + testDel = modifier.createNode("transform", parent=parent) + modifier.createNode("locator", parent=testDel) + + om2.MObjectHandle(testDel).isValid() + stars[i] = (loc, condition) + toDelete[i] = testDel + + modifier.doIt() + return controller, stars, toDelete + + +def editInOM2(modifier, controller, stars): + maxValue = getMaxValue(len(stars)) + controllerFn = om2.MFnDependencyNode(controller) + plug = controllerFn.findPlug("radius", True) + modifier.newPlugValueInt(plug, 10) + + plug = controllerFn.findPlug("flash", True) + plug.isKeyable = True + modifier.commandToExecute( + f"setKeyframe -attribute flash -t 1 -v 0 {controllerFn.name()}" + ) + modifier.commandToExecute( + f"setKeyframe -attribute flash -t 120 -v {maxValue} {controllerFn.name()}" + ) + + colorIfTrueNames = ("colorIfTrueR", "colorIfTrueG", "colorIfTrueB") + colorIfFalseNames = ("colorIfFalseR", "colorIfFalseG", "colorIfFalseB") + + translation = ("tx", "ty", "tz") + scale = ("sx", "sy", "sz") + for loc, condition in stars: + locFn = om2.MFnDependencyNode(loc) + conditionFn = om2.MFnDependencyNode(condition) + for trueAttr in colorIfTrueNames: + plug = conditionFn.findPlug(trueAttr, True) + modifier.newPlugValueDouble(plug, 1.0) + + for falseAttr in colorIfFalseNames: + plug = conditionFn.findPlug(falseAttr, True) + modifier.newPlugValueDouble(plug, 0.0) + + plug = locFn.findPlug("overrideEnabled", True) + modifier.newPlugValueBool(plug, True) + + plug = locFn.findPlug("overrideColor", True) + modifier.newPlugValueInt(plug, getRandomColor()) + + for name, value in zip(translation, getRandomPosition(maxValue)): + plug = locFn.findPlug(name, True) + modifier.newPlugValueDouble(plug, value) + + for name, value in zip(scale, getRandomScale()): + plug = locFn.findPlug(name, True) + modifier.newPlugValueDouble(plug, value) + + plug = locFn.findPlug("displayHandle", True) + plug.isLocked = True + + plug = locFn.findPlug("overrideDisplayType", True) + plug.isLocked = True + plug.isLocked = False + + plug = locFn.findPlug("flashIndex", True) + modifier.newPlugValueInt(plug, getRandomIndex(maxValue)) + + src = controllerFn.findPlug("r", True) + dst = locFn.findPlug("r", True) + modifier.connect(src, dst) + + src = controllerFn.findPlug("overrideShading", True) + dst = locFn.findPlug("overrideShading", True) + modifier.connect(src, dst) + modifier.disconnect(src, dst) + + src = controllerFn.findPlug("flash", True) + dst = conditionFn.findPlug("firstTerm", True) + modifier.connect(src, dst) + + src = locFn.findPlug("flashIndex", True) + dst = conditionFn.findPlug("secondTerm", True) + modifier.connect(src, dst) + + src = conditionFn.findPlug("outColorR", True) + dst = locFn.findPlug("visibility", True) + modifier.connect(src, dst) + + modifier.doIt() + + +def renameInOM2(modifier, nodesToRename): + for node in nodesToRename: + transformName = str(node) + modifier.renameNode(node, f"{transformName}New") + modifier.renameNode(node, f"{transformName}") + + modifier.doIt() + + +def queryInOM2(controller, stars): + translation = ("tx", "ty", "tz") + controllerFn = om2.MFnDependencyNode(controller) + controllerFn.findPlug("flash", True).destinations() + for loc, _ in stars: + om2.MObjectHandle(loc).isValid() + locFn = om2.MFnDependencyNode(loc) + [locFn.findPlug(n, True).asDouble() for n in translation] + wm0Plug = locFn.findPlug("wm", True).elementByLogicalIndex(0) + attrData = om2.MFnMatrixData(wm0Plug.asMObject()).matrix() + [attrData[i] for i in range(len(attrData))] + locFn.findPlug("overrideDisplayType", True).isLocked + + +def deleteInOM2(modifier, nodesToDelete): + for toDel in nodesToDelete: + modifier.deleteNode(toDel) + modifier.doIt() + + +def categorizedPerformanceTestInOM2(): + for num in NUM_NODES_LIST: + cmds.file(new=True, force=True) + with TotalPerfMeasurement(f"Deal with {num} nodes in om2") as measure: + modifier = om2.MDagModifier() + with measure.add(f"Create {num}+ nodes in om2"): + controller, stars, nodes = createInOM2(modifier, num) + + with measure.add(f"Edit {num}+ nodes in om2"): + editInOM2(modifier, controller, stars) + + with measure.add(f"Rename {num} nodes in om2"): + renameInOM2(modifier, nodes) + + with measure.add(f"Query {num}+ nodes in om2"): + queryInOM2(controller, stars) + + with measure.add(f"Remove {num} nodes in om2"): + deleteInOM2(modifier, nodes) + + +def totalPerformanceTestInOM2(): + for num in REFINED_NUM_NODES_LIST: + cmds.file(new=True, force=True) + with PerfMeasurement(f"Deal with {num} nodes in om2"): + modifier = om2.MDagModifier() + controller, stars, nodes = createInOM2(modifier, num) + editInOM2(modifier, controller, stars) + renameInOM2(modifier, nodes) + queryInOM2(controller, stars) + deleteInOM2(modifier, nodes) + + +if __name__ == "__main__": + categorizedPerformanceTestInOM2() + # totalPerformanceTestInOM2() diff --git a/docs/source/python/perf_tests/perf_omx.py b/docs/source/python/perf_tests/perf_omx.py new file mode 100644 index 0000000..b87176e --- /dev/null +++ b/docs/source/python/perf_tests/perf_omx.py @@ -0,0 +1,164 @@ +from AL import omx +from maya import cmds +from maya.api import OpenMaya as om2 + +# execute the code from common in maya script editor first. + + +def createInOMX(locCount, immediate): + maxValue = getMaxValue(locCount) + modifier = omx.newModifier() + if immediate: + # usually you use omx.currentModifier() but it is not guaranteed + # to be a immediate one, like in our case here. + modifier._immediate = immediate + + controller = omx.createDagNode("joint", nodeName="controller") + + fnAttr = om2.MFnNumericAttribute() + attrObj = fnAttr.create("flash", "flash", om2.MFnNumericData.kInt) + fnAttr.setMin(0) + fnAttr.setMax(maxValue) + modifier.addAttribute(controller.object(), attrObj) + + parent = omx.createDagNode("transform", nodeName="stars") + + stars = [None] * locCount + toDelete = [None] * locCount + + for i in range(locCount): + condition = omx.createDGNode("condition") + + loc = omx.createDagNode("transform", parent=parent) + omx.createDagNode("locator", parent=loc) + attrObj = fnAttr.create("flashIndex", "flashIndex", om2.MFnNumericData.kInt) + fnAttr.setMin(0) + fnAttr.setMax(maxValue) + modifier.addAttribute(loc.object(), attrObj) + + testDel = omx.createDagNode("transform", parent=parent) + omx.createDagNode("locator", parent=testDel) + + testDel.isValid() + stars[i] = (loc, condition) + toDelete[i] = testDel + + modifier.doIt() + return controller, stars, toDelete + + +def editInOMX(controller, stars): + maxValue = getMaxValue(len(stars)) + modifier = omx.currentModifier() + + controller.radius.setInt(10) + controller.flash.isKeyable = True + modifier.commandToExecute(f"setKeyframe -attribute flash -t 1 -v 0 {controller}") + modifier.commandToExecute( + f"setKeyframe -attribute flash -t 120 -v {maxValue} {controller}" + ) + + for loc, condition in stars: + condition.colorIfTrue.setCompoundDouble((1.0, 1.0, 1.0)) + condition.colorIfFalse.setCompoundDouble((0.0, 0.0, 0.0)) + + loc.overrideEnabled.setBool(True) + loc.overrideColor.setInt(getRandomColor()) + + loc.t.setCompoundDouble(getRandomPosition(maxValue)) + loc.s.setCompoundDouble(getRandomScale()) + + # here we don't care about plug state change undoability as the whole + # node creation is done in the same XModifier. + # otherwise we would use loc.displayHandle.setLocked(True) + loc.displayHandle.isLocked = True + loc.overrideDisplayType.isLocked = True + loc.overrideDisplayType.isLocked = False + + loc.flashIndex.setInt(getRandomIndex(maxValue)) + + controller.r.connectTo(loc.r) + controller.overrideShading.connectTo(loc.overrideShading) + loc.overrideShading.disconnectFromSource() + + controller.flash.connectTo(condition.firstTerm) + loc.flashIndex.connectTo(condition.secondTerm) + condition.outColorR.connectTo(loc.visibility) + + modifier.doIt() + + +def renameInOMX(nodesToRename): + modifier = omx.currentModifier() + for node in nodesToRename: + transformName = str(node) + modifier.renameNode(node.object(), f"{transformName}New") + modifier.renameNode(node.object(), f"{transformName}") + + modifier.doIt() + + +def queryInOMX(controller, stars): + controller.flash.destinations() + for loc, _ in stars: + loc.isValid() + loc.t.get() + loc.wm[0].get() + loc.overrideDisplayType.isLocked + + +def deleteInOMX(nodesToDelete): + modifier = omx.currentModifier() + for toDel in nodesToDelete: + modifier.deleteNode(toDel.object()) + modifier.doIt() + + +def categorizedPerformanceTestInOMX(): + for num in NUM_NODES_LIST: + for immediate in (True, False): + cmds.file(new=True, force=True) + with TotalPerfMeasurement( + f"Deal with {num} nodes in AL.omx, immediate={immediate}" + ) as measure: + with measure.add( + f"Create {num}+ nodes in AL.omx, immediate={immediate}" + ): + controller, stars, nodes = createInOMX(num, immediate=immediate) + + with measure.add(f"Edit {num}+ nodes in AL.omx, immediate={immediate}"): + editInOMX(controller, stars) + + with measure.add( + f"Rename {num} nodes in AL.omx, immediate={immediate}" + ): + renameInOMX(nodes) + + with measure.add( + f"Query {num}+ nodes in AL.omx, immediate={immediate}" + ): + queryInOMX(controller, stars) + + with measure.add( + f"Remove {num} nodes in AL.omx, immediate={immediate}" + ): + deleteInOMX(nodes) + + +def totalPerformanceTestInOMX(): + for immediate in (True, False): + for num in REFINED_NUM_NODES_LIST: + cmds.file(new=True, force=True) + with PerfMeasurement( + f"Deal with {num} nodes in AL.omx, immediate={immediate}" + ): + controller, stars, nodes = createInOMX(num, immediate=immediate) + editInOMX(controller, stars) + renameInOMX(nodes) + queryInOMX(controller, stars) + deleteInOMX(nodes) + + +if __name__ == "__main__": + categorizedPerformanceTestInOMX() + # totalPerformanceTestInOMX() diff --git a/docs/source/python/perf_tests/perf_pymel.py b/docs/source/python/perf_tests/perf_pymel.py new file mode 100644 index 0000000..035d7eb --- /dev/null +++ b/docs/source/python/perf_tests/perf_pymel.py @@ -0,0 +1,118 @@ +import pymel.core as pmc + +# execute the code from common in maya script editor first. + + +def createInPyMel(locCount): + maxValue = getMaxValue(locCount) + + controller = pmc.joint() + controller.addAttr("flash", attributeType="long", min=0, max=maxValue) + parent = pmc.createNode("transform", n="stars") + + stars = [None] * locCount + toDelete = [None] * locCount + + for i in range(locCount): + condition = pmc.createNode("condition") + + loc = pmc.spaceLocator() + pmc.parent(loc, parent) + loc.addAttr("flashIndex", at="long", min=0, max=maxValue) + + testDel = pmc.spaceLocator() + pmc.parent(testDel, loc) + + pmc.objExists(testDel) + stars[i] = (loc, condition) + toDelete[i] = testDel + + return controller, stars, toDelete + + +def editInPyMel(controller, stars): + maxValue = getMaxValue(len(stars)) + + controller.radius.set(10) + controller.flash.setKeyable(True) + pmc.setKeyframe(controller.flash, time=(1,), value=0) + pmc.setKeyframe(controller.flash, time=(120,), value=maxValue) + + for loc, condition in stars: + condition.colorIfTrue.set([1.0, 1.0, 1.0]) + condition.colorIfFalse.set([0.0, 0.0, 0.0]) + + loc.overrideEnabled.set(True) + loc.overrideColor.set(getRandomColor()) + + loc.t.set(getRandomPosition(maxValue)) + loc.s.set(getRandomScale()) + loc.displayHandle.lock() + loc.overrideDisplayType.lock() + loc.overrideDisplayType.unlock() + + loc.flashIndex.set(getRandomIndex(maxValue)) + + controller.r.connect(loc.r) + controller.overrideShading.connect(loc.overrideShading) + controller.overrideShading.disconnect(loc.overrideShading) + + controller.flash.connect(condition.firstTerm) + loc.flashIndex.connect(condition.secondTerm) + condition.outColorR.connect(loc.visibility) + + +def renameInPyMel(nodesToRename): + for node in nodesToRename: + node.rename(f"{node}New") + node.rename(str(node)) + + +def queryInPyMel(controller, stars): + controller.flash.outputs() + for loc, _ in stars: + pmc.objExists(loc) + loc.t.get() + loc.wm[0].get() + loc.overrideDisplayType.isLocked() + + +def deleteInPyMel(nodesToDelete): + for toDel in nodesToDelete: + pmc.delete(toDel) + + +def categorizedPerformanceTestInPyMel(): + for num in NUM_NODES_LIST: + pmc.system.newFile(force=True) + with TotalPerfMeasurement(f"Deal with {num} nodes in PyMel") as measure: + with measure.add(f"Create {num}+ nodes in PyMel"): + controller, stars, nodes = createInPyMel(num) + + with measure.add(f"Edit {num}+ nodes in PyMel"): + editInPyMel(controller, stars) + + with measure.add(f"Rename {num} nodes in PyMel"): + renameInPyMel(nodes) + + with measure.add(f"Query {num}+ nodes in PyMel"): + queryInPyMel(controller, stars) + + with measure.add(f"Remove {num} nodes in PyMel"): + deleteInPyMel(nodes) + + +def totalPerformanceTestInPyMel(): + for num in REFINED_NUM_NODES_LIST: + pmc.system.newFile(force=True) + with PerfMeasurement(f"Deal with {num} nodes in PyMel"): + controller, stars, nodes = createInPyMel(num) + editInPyMel(controller, stars) + renameInPyMel(nodes) + queryInPyMel(controller, stars) + deleteInPyMel(nodes) + + +if __name__ == "__main__": + categorizedPerformanceTestInPyMel() + # totalPerformanceTestInPyMel() diff --git a/docs/source/python/quick_samples.py b/docs/source/python/quick_samples.py new file mode 100644 index 0000000..2d53bb8 --- /dev/null +++ b/docs/source/python/quick_samples.py @@ -0,0 +1,51 @@ +from AL import omx +from maya.api import OpenMaya as om2 + +# you might need to create new Maya scene to avoid potential node name conflicts. + +# creating nodes: +transform, locator = omx.createDagNode( + "locator", nodeName="myLocShape", returnAllCreated=True +) +omx.createDGNode("time", nodeName="myTime") + +# constructing XNode from existing nodes: +persp = omx.XNode("persp") +perspShape = omx.XNode("perspShape") +print("a XNode is a om2.MObject:", isinstance(persp, om2.MObject)) +print("camera api type: ", perspShape.apiType()) +print("has camera functor: ", perspShape.hasFn(om2.MFn.kCamera)) +print("camera is null: ", perspShape.isNull()) +print("camera is valid: ", perspShape.isValid()) +print("camera MObject: ", perspShape.object()) + +# getting om2.MFn functors. +print("basic functor for DAG:", persp.basicFn()) +print("basic functor for DG:", omx.XNode("time1").basicFn()) +print("basic functor for transform:", persp.bestFn()) +print("basic functor for camera:", perspShape.bestFn()) + +# XPlug set and get: +print("a XPlug is an MPlug:", isinstance(persp.visibility, om2.MPlug)) +persp.visibility.setLocked(True) +print(f"XNode from xplug {persp.v} :", persp.v.xnode()) +print(f"om2.MObject from xplug {persp.v} :", persp.v.node()) +transform.t.set([2.0, 2.0, 2.0]) +print("locator translation:", transform.t.get()) +print(f"locator worldMatrix: {transform.wm[0].get()}") +locator.overrideEnabled.set(True) +locator.overrideColor.set(14) + +# connection +persp.r.connectTo(transform.r) +print(f"Source of {transform.r}: {transform.r.source()}") +transform.sx.connectFrom(omx.XNode("time1").outTime) +transform.sy.connectFrom(omx.XNode("time1").outTime) + +# disconnect +transform.r.disconnectFromSource() +print(f"After disconnection, source of {transform.r}: {transform.r.source()}") + +# undo & redo +omx.currentModifier().undoIt() +omx.currentModifier().doIt() diff --git a/docs/topics/index.rst b/docs/topics/index.rst deleted file mode 100644 index adac2aa..0000000 --- a/docs/topics/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Topic Guides -============ - -Introductions to the key parts of AL_maya2 you'll need to know: - -.. toctree:: - :maxdepth: 2 - :glob: - - ./* diff --git a/docs/topics/omx.rst b/docs/topics/omx.rst deleted file mode 100644 index bbf5175..0000000 --- a/docs/topics/omx.rst +++ /dev/null @@ -1,23 +0,0 @@ -OMX -======= - -Friendly and Fast Maya Python API. - -OMX tries to make OM2 easier to use but still fast. - -This presentation highlights some of the reasoning and how OMX works: -`OMX Presentation `_ - -See :py:mod:`AL.maya2.omx` for reference docs. - -Examples --------- - -Example script-style code: - -.. literalinclude:: ../examples/omx_simple.py - -Example code for an undoable ALCommand: - -.. literalinclude:: ../examples/omx_command.py - diff --git a/reformat.bat b/reformat.bat new file mode 100644 index 0000000..c1214c3 --- /dev/null +++ b/reformat.bat @@ -0,0 +1,7 @@ +@echo off + +REM Ensure the copyright header comment for each source code file and black format for source code. + +python -m reformat + +black AL \ No newline at end of file diff --git a/reformat.py b/reformat.py new file mode 100644 index 0000000..1570f9f --- /dev/null +++ b/reformat.py @@ -0,0 +1,69 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import os + + +def _copyrightHeaderComments(): + copyRightLines = [] + with open(__file__, "r") as f: + for l in f: + if l.startswith("#"): + copyRightLines.append(str(l)) + else: + break + + return copyRightLines + + +def _iterAllPythonSourceFiles(rootdir): + # for py-2 compatibility we cannot use glob.iglob or os.scandir. + for root, directories, files in os.walk(rootdir): + for f in files: + if f.endswith(".py") and f != "package.py": + yield os.path.join(root, f) + + +def prepandCopyrightComment(): + copyRightLines = _copyrightHeaderComments() + copyRightStr = "".join(copyRightLines) + rootdir = os.path.dirname(__file__) + + docFolder = os.path.join(rootdir, "docs") + buildFolder = os.path.join("", "build", "") + + for filename in _iterAllPythonSourceFiles(rootdir): + if filename == __file__: + continue + + if filename.startswith(docFolder): + continue + + if buildFolder in filename: + continue + + with open(filename, "r") as f: + data = f.read() + + if data.startswith(copyRightLines[0]): + continue + + with open(filename, "w") as f: + f.write(copyRightStr + os.linesep + os.linesep + data) + print("Prepend copyright comment: {}".format(filename)) + + +if __name__ == "__main__": + prepandCopyrightComment() diff --git a/reformat.sh b/reformat.sh new file mode 100755 index 0000000..6ce51c2 --- /dev/null +++ b/reformat.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# Ensure the copyright header comment for each source code file and black format for source code. + +set -e + +python -m reformat + +black AL \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..721dad2 --- /dev/null +++ b/setup.py @@ -0,0 +1,70 @@ +# Copyright © 2023 Animal Logic. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License.# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import re +from setuptools import setup + + +def readme(): + with open("README.md") as f: + return f.read() + + +_VERSION_RETRIEVE_PATTERN = None + + +def version(): + global _VERSION_RETRIEVE_PATTERN + if _VERSION_RETRIEVE_PATTERN is None: + _VERSION_RETRIEVE_PATTERN = re.compile("[0-9\.]+") + + with open("docs/source/changes.rst") as f: + for line in f: + content = line.strip() + if _VERSION_RETRIEVE_PATTERN.fullmatch(content): + return content + + +setup( + name="AL_OMX", + version=version(), + description="A fast and user-friendly library built on top of Autodesk Maya's OpenMaya library.", + long_description=readme(), + long_description_content_type="text/markdown", + url="https://github.com/animallogic/AL_omx", + author="Animal Logic", + author_email="", + license="Apache License, Version 2.0", + classifiers=[ + "License :: OSI Approved :: Apache License, Version 2.0", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development", + "Topic :: Software Development :: DCC", + "Intended Audience :: Developers", + ], + packages=[ + "AL", + "AL.omx", + "AL.omx.plugin", + "AL.omx.tests", + "AL.omx.tests.utils", + "AL.omx.utils", + ], + include_package_data=True, + install_requires=[], + extras_require={}, +)