diff --git a/.circleci/config.yml b/.circleci/config.yml
index c837ba8..e01a6fa 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -23,7 +23,7 @@ default-run: &default-run
command: make install-deps
- save-cache: &d2-save-cache
paths:
- - ~/venv/
+ - ~/venv/
key: a1-dependencies-{{ checksum "requirements.txt" }}
- run: &run-tests
name: run tests
diff --git a/docs/changes.rst b/docs/changes.rst
index aa38999..48606fc 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -2,17 +2,56 @@ Changes
==============================================================================
+Release 2.1.2
+********************
+
+Features added
+--------------------
+* Added the following operators: > node_calculator/issues/80
+
+ * sum
+ * quatAdd
+ * quatConjugate
+ * quatInvert
+ * quatNegate
+ * quatNormalize
+ * quatProd
+ * quatSub
+ * quatToEuler
+ * eulerToQuat
+ * holdMatrix
+ * reverse
+ * passMatrix
+ * remapColor
+ * remapHsv
+ * rgbToHsv
+ * wtAddMatrix
+ * closestPointOnMesh
+ * closestPointOnSurface
+ * pointOnSurfaceInfo
+ * pointOnCurveInfo
+ * nearestPointOnCurve
+ * fourByFourMatrix
+
+* Operator unittests are more generic now: A dictionary contains which inputs/outputs to use for each Operators test.
+* Added more unittests for some issues that came up: non-unique node names, aliased attributes, accessing shape-attributes through the transform (see Features added in Release 2.1.1). > node_calculator/issues/76
+
+Bugs fixed
+--------------------
+* sum(), average() and mult_matrix() operators now work correctly when given lists/tuples/NcLists as args.
+
+
Release 2.1.1
-*************
+********************
Bugs fixed
-----------
+--------------------
* Now supports non-unique names > node_calculator/issues/74
* Catch error when user sets a non-existent attribute on an NcList item (now only throws a warning) > node_calculator/issues/73
Release 2.1.0
-*************
+********************
Incompatible changes
--------------------
@@ -20,7 +59,7 @@ Incompatible changes
* The decompose_matrix and pair_blend Operators now have a "return_all_outputs"-flag. By default they return an NcNode now, not all outputs in an NcList! > node_calculator/issues/67
Features added
---------------
+--------------------
* Tests are now standalone (not dependent on CMT anymore) and can be run from a console! Major kudos to Andres Weber!
* CircleCi integration to auto-run checks whenever repo is updated. Again: Major kudos to Andres Weber!
* The default Operators are now factored out into their own files: base_functions.py & base_operators.py > node_calculator/issues/59
@@ -28,24 +67,24 @@ Features added
* The noca.cleanup(keep_selected=False) function allows to delete all nodes created by the NodeCalculator to unclutter heavy prototyping scenes. > node_calculator/issues/63
Bugs fixed
-----------
+--------------------
* The dot-Operator now correctly returns a 1D result (returned a 3D result before) > node_calculator/issues/68
Release 2.0.1
-*************
+********************
Bugs fixed
-----------
+--------------------
* Aliased attributes can now be accessed (om_util.get_mplug_of_mobj couldn't find them before)
* Operation values of zero are now set correctly (they were ignored)
Release 2.0.0
-*************
+********************
Dependencies
-------------
+--------------------
Incompatible changes
--------------------
@@ -54,11 +93,11 @@ Incompatible changes
* multi_input & multi_output doesn't have to be declared anymore! The tag "{array}" will cause an input/output to be interpreted as multi.
Deprecated
-----------
+--------------------
* Container support. It wasn't properly implemented and Maya containers are not useful (imo).
Features added
---------------
+--------------------
* Easy to add custom/proprietary nodes via extension
* Convenience functions for transforms, locators & create_node.
* auto_consolidate & auto_unravel can be turned off (globally & individually)
@@ -74,7 +113,7 @@ Features added
* Tests added, using `Chad Vernon's test suite `_
Bugs fixed
-----------
+--------------------
* Uses MObjects and MPlugs to reference to Maya nodes and attributes; Renaming of objects, attributes with index, etc. are no longer an issue.
* Cleaner code; Clear separation of classes and their functionality (NcList, NcNode, NcAttrs, NcValue)
* Any child attribute will be consolidated (array, normal, ..)
@@ -82,13 +121,13 @@ Bugs fixed
* Conforms pretty well to PEP8 (apart from tests)
Testing
---------
+--------------------
Features removed
-----------------
+--------------------
Release 1.0.0
-*************
+********************
* First working version: Create, connect and set Maya nodes with Python commands.
diff --git a/node_calculator/base_functions.py b/node_calculator/base_functions.py
index f6c741b..a4249d7 100644
--- a/node_calculator/base_functions.py
+++ b/node_calculator/base_functions.py
@@ -27,9 +27,10 @@ def soft_approach(in_value, fade_in_range=0.5, target_value=1):
fade_in_range (NcNode or NcAttrs or str or int or float): Value or
attr. This defines a range over which the target_value will be
approached. Before the in_value is within this range the output
- of this and the in_value will be equal.
+ of this and the in_value will be equal. Defaults to 0.5.
target_value (NcNode or NcAttrs or str or int or float): Value or
attr. This is the value that will be approached slowly.
+ Defaults to 1.
Returns:
NcNode: Instance with node and output-attr.
diff --git a/node_calculator/base_operators.py b/node_calculator/base_operators.py
index ca51945..3208e39 100644
--- a/node_calculator/base_operators.py
+++ b/node_calculator/base_operators.py
@@ -19,7 +19,7 @@
from node_calculator.core import LOG
# Any Maya plugin that should be loaded for the NodeCalculator
-REQUIRED_EXTENSION_PLUGINS = ["matrixNodes"]
+REQUIRED_EXTENSION_PLUGINS = ["matrixNodes", "quatNodes"]
# Dict of all available operations: used node-type, inputs, outputs, etc.
@@ -85,6 +85,35 @@
],
},
+ "closest_point_on_mesh": {
+ "node": "closestPointOnMesh",
+ "inputs": [
+ ["inMesh"],
+ ["inPositionX", "inPositionY", "inPositionZ"],
+ ],
+ "outputs": [
+ ["positionX", "positionY", "positionZ"],
+ ["parameterU", "parameterV"],
+ ["normalX", "normalY", "normalZ"],
+ ["closestVertexIndex"],
+ ["closestFaceIndex"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "closest_point_on_surface": {
+ "node": "closestPointOnSurface",
+ "inputs": [
+ ["inputSurface"],
+ ["inPositionX", "inPositionY", "inPositionZ"],
+ ],
+ "outputs": [
+ ["positionX", "positionY", "positionZ"],
+ ["parameterU", "parameterV"],
+ ],
+ "output_is_predetermined": True,
+ },
+
"compose_matrix": {
"node": "composeMatrix",
"inputs": [
@@ -100,6 +129,19 @@
],
},
+ "cross": {
+ "node": "vectorProduct",
+ "inputs": [
+ ["input1X", "input1Y", "input1Z"],
+ ["input2X", "input2Y", "input2Z"],
+ ["normalizeOutput"],
+ ],
+ "outputs": [
+ ["outputX", "outputY", "outputZ"],
+ ],
+ "operation": 2,
+ },
+
"decompose_matrix": {
"node": "decomposeMatrix",
"inputs": [
@@ -114,6 +156,56 @@
"output_is_predetermined": True,
},
+ "dot": {
+ "node": "vectorProduct",
+ "inputs": [
+ ["input1X", "input1Y", "input1Z"],
+ ["input2X", "input2Y", "input2Z"],
+ ["normalizeOutput"],
+ ],
+ "outputs": [
+ ["outputX"],
+ ],
+ "operation": 1,
+ },
+
+ "euler_to_quat": {
+ "node": "eulerToQuat",
+ "inputs": [
+ ["inputRotateX", "inputRotateY", "inputRotateZ"],
+ ["inputRotateOrder"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "four_by_four_matrix": {
+ "node": "fourByFourMatrix",
+ "inputs": [
+ [
+ "in00", "in01", "in02", "in03",
+ "in10", "in11", "in12", "in13",
+ "in20", "in21", "in22", "in23",
+ "in30", "in31", "in32", "in33",
+ ],
+ ],
+ "outputs": [
+ ["output"],
+ ],
+ },
+
+ "hold_matrix": {
+ "node": "holdMatrix",
+ "inputs": [
+ ["inMatrix"],
+ ],
+ "outputs": [
+ ["outMatrix"],
+ ],
+ },
+
"inverse_matrix": {
"node": "inverseMatrix",
"inputs": [
@@ -158,6 +250,19 @@
],
},
+ "nearest_point_on_curve": {
+ "node": "nearestPointOnCurve",
+ "inputs": [
+ ["inputCurve"],
+ ["inPositionX", "inPositionY", "inPositionZ"],
+ ],
+ "outputs": [
+ ["positionX", "positionY", "positionZ"],
+ ["parameter"],
+ ],
+ "output_is_predetermined": True,
+ },
+
"normalize_vector": {
"node": "vectorProduct",
"inputs": [
@@ -187,6 +292,17 @@
"output_is_predetermined": True,
},
+ "pass_matrix": {
+ "node": "passMatrix",
+ "inputs": [
+ ["inMatrix"],
+ ["inScale"],
+ ],
+ "outputs": [
+ ["outMatrix"],
+ ],
+ },
+
"point_matrix_mult": {
"node": "pointMatrixMult",
"inputs": [
@@ -199,6 +315,172 @@
],
},
+ "point_on_curve_info": {
+ "node": "pointOnCurveInfo",
+ "inputs": [
+ ["inputCurve"],
+ ["parameter"],
+ ["turnOnPercentage"],
+ ],
+ "outputs": [
+ ["positionX", "positionY", "positionZ"],
+ ["normalX", "normalY", "normalZ"],
+ ["normalizedNormalX", "normalizedNormalY", "normalizedNormalZ"],
+ ["tangentX", "tangentY", "tangentZ"],
+ ["normalizedTangentX", "normalizedTangentY", "normalizedTangentZ"],
+ ["curvatureCenterX", "curvatureCenterY", "curvatureCenterZ"],
+ ["curvatureRadius"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "point_on_surface_info": {
+ "node": "pointOnSurfaceInfo",
+ "inputs": [
+ ["inputSurface"],
+ ["parameterU", "parameterV"],
+ ["turnOnPercentage"],
+ ],
+ "outputs": [
+ ["positionX", "positionY", "positionZ"],
+ ["normalX", "normalY", "normalZ"],
+ ["normalizedNormalX", "normalizedNormalY", "normalizedNormalZ"],
+ ["tangentUx", "tangentUy", "tangentUz"],
+ [
+ "normalizedTangentUX",
+ "normalizedTangentUY",
+ "normalizedTangentUZ",
+ ],
+ ["tangentVx", "tangentVy", "tangentVz"],
+ [
+ "normalizedTangentVX",
+ "normalizedTangentVY",
+ "normalizedTangentVZ",
+ ],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_add": {
+ "node": "quatAdd",
+ "inputs": [
+ ["input1QuatX", "input1QuatY", "input1QuatZ", "input1QuatW"],
+ ["input2QuatX", "input2QuatY", "input2QuatZ", "input2QuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_conjugate": {
+ "node": "quatConjugate",
+ "inputs": [
+ ["inputQuatX", "inputQuatY", "inputQuatZ", "inputQuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_invert": {
+ "node": "quatInvert",
+ "inputs": [
+ ["inputQuatX", "inputQuatY", "inputQuatZ", "inputQuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_negate": {
+ "node": "quatNegate",
+ "inputs": [
+ ["inputQuatX", "inputQuatY", "inputQuatZ", "inputQuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_normalize": {
+ "node": "quatNormalize",
+ "inputs": [
+ ["inputQuatX", "inputQuatY", "inputQuatZ", "inputQuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_mul": {
+ "node": "quatProd",
+ "inputs": [
+ ["input1QuatX", "input1QuatY", "input1QuatZ", "input1QuatW"],
+ ["input2QuatX", "input2QuatY", "input2QuatZ", "input2QuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_sub": {
+ "node": "quatSub",
+ "inputs": [
+ ["input1QuatX", "input1QuatY", "input1QuatZ", "input1QuatW"],
+ ["input2QuatX", "input2QuatY", "input2QuatZ", "input2QuatW"],
+ ],
+ "outputs": [
+ ["outputQuatX", "outputQuatY", "outputQuatZ", "outputQuatW"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "quat_to_euler": {
+ "node": "quatToEuler",
+ "inputs": [
+ ["inputQuatX", "inputQuatY", "inputQuatZ", "inputQuatW"],
+ ["inputRotateOrder"],
+ ],
+ "outputs": [
+ ["outputRotateX", "outputRotateY", "outputRotateZ"],
+ ],
+ "output_is_predetermined": True,
+ },
+
+ "remap_color": {
+ "node": "remapColor",
+ "inputs": [
+ ["colorR", "colorG", "colorB"],
+ ["outputMin"],
+ ["outputMax"],
+ ["inputMin"],
+ ["inputMax"],
+ ],
+ "outputs": [
+ ["outColorR", "outColorG", "outColorB"],
+ ],
+ },
+
+ "remap_hsv": {
+ "node": "remapHsv",
+ "inputs": [
+ ["colorR", "colorG", "colorB"],
+ ["outputMin"],
+ ["outputMax"],
+ ["inputMin"],
+ ["inputMax"],
+ ],
+ "outputs": [
+ ["outColorR", "outColorG", "outColorB"],
+ ],
+ },
+
"remap_value": {
"node": "remapValue",
"inputs": [
@@ -213,6 +495,27 @@
],
},
+ "reverse": {
+ "node": "reverse",
+ "inputs": [
+ ["inputX", "inputY", "inputZ"],
+ ],
+ "outputs": [
+ ["outputX", "outputY", "outputZ"],
+ ]
+ },
+
+ "rgb_to_hsv": {
+ "node": "rgbToHsv",
+ "inputs": [
+ ["inRgbR", "inRgbG", "inRgbB"],
+ ],
+ "outputs": [
+ ["outHsvH", "outHsvS", "outHsvV"],
+ ],
+ "output_is_predetermined": True,
+ },
+
"set_range": {
"node": "setRange",
"inputs": [
@@ -227,40 +530,39 @@
],
},
- "transpose_matrix": {
- "node": "transposeMatrix",
+ "sum": {
+ "node": "plusMinusAverage",
"inputs": [
- ["inputMatrix"],
+ [
+ "input3D[{array}].input3Dx",
+ "input3D[{array}].input3Dy",
+ "input3D[{array}].input3Dz"
+ ],
],
"outputs": [
- ["outputMatrix"],
+ ["output3Dx", "output3Dy", "output3Dz"],
],
+ "operation": 1,
},
- "dot": {
- "node": "vectorProduct",
+ "transpose_matrix": {
+ "node": "transposeMatrix",
"inputs": [
- ["input1X", "input1Y", "input1Z"],
- ["input2X", "input2Y", "input2Z"],
- ["normalizeOutput"],
+ ["inputMatrix"],
],
"outputs": [
- ["outputX"],
+ ["outputMatrix"],
],
- "operation": 1,
},
- "cross": {
- "node": "vectorProduct",
+ "weighted_add_matrix": {
+ "node": "wtAddMatrix",
"inputs": [
- ["input1X", "input1Y", "input1Z"],
- ["input2X", "input2Y", "input2Z"],
- ["normalizeOutput"],
+ ["wtMatrix[{array}].matrixIn", "wtMatrix[{array}].weightIn"],
],
"outputs": [
- ["outputX", "outputY", "outputZ"],
+ ["matrixSum"],
],
- "operation": 2,
},
}
@@ -345,9 +647,9 @@ def angle_between(vector_a, vector_b=(1, 0, 0)):
Args:
vector_a (NcNode or NcAttrs or int or float or list): Vector to
- consider for angle between
- vector_b (NcNode or NcAttrs or int or float or list): Vector to
- consider for angle between
+ consider for angle between.
+ vector_b (NcNode or NcAttrs or int or float or list or tuple): Vector
+ to consider for angle between. Defaults to (1, 0, 0).
Returns:
NcNode: Instance with angleBetween-node and output-attribute(s)
@@ -369,7 +671,8 @@ def average(*attrs):
"""Create plusMinusAverage-node for averaging input attrs.
Args:
- attrs (NcNode or NcAttrs or string or list): Inputs to be averaged
+ attrs (NcNode or NcAttrs or NcList or string or list or tuple):
+ Inputs to be averaged.
Returns:
NcNode: Instance with plusMinusAverage-node and output-attribute(s)
@@ -379,6 +682,9 @@ def average(*attrs):
Op.average(Node("pCube.t"), [1, 2, 3])
"""
+ if len(attrs) == 1:
+ attrs = attrs[0]
+
return _create_operation_node("average", attrs)
@@ -392,7 +698,7 @@ def blend(attr_a, attr_b, blend_value=0.5):
attr_b (NcNode or NcAttrs or str or int or float): Plug or value to
blend to
blend_value (NcNode or str or int or float): Plug or value defining
- blend-amount
+ blend-amount. Defaults to 0.5.
Returns:
NcNode: Instance with blend-node and output-attributes
@@ -414,9 +720,9 @@ def choice(inputs, selector=0):
So we package a copy of the same selector for each input.
Args:
- inputs (list): Any number of input values or plugs
+ inputs (NcList, NcAttrs, list): Any number of input values or plugs.
selector (NcNode or NcAttrs or int): Selector-attr on choice node
- to select one of the inputs based on their index.
+ to select one of the inputs based on their index. Defaults to 0.
Returns:
NcNode: Instance with choice-node and output-attribute(s)
@@ -430,6 +736,9 @@ def choice(inputs, selector=0):
choice_node = Op.choice([option_a, option_b], selector=switch)
Node("pTorus1").tx = choice_node
"""
+ if not isinstance(inputs, (list, tuple)):
+ inputs = [inputs]
+
choice_node_obj = _create_operation_node("choice", inputs, selector)
return choice_node_obj
@@ -442,9 +751,9 @@ def clamp(attr_a, min_value=0, max_value=1):
Args:
attr_a (NcNode or NcAttrs or str or int or float): Input value
min_value (NcNode or NcAttrs or int or float or list): min-value
- for clamp-operation
+ for clamp-operation. Defaults to 0.
max_value (NcNode or NcAttrs or int or float or list): max-value
- for clamp-operation
+ for clamp-operation. Defaults to 1.
Returns:
NcNode: Instance with clamp-node and output-attribute(s)
@@ -458,61 +767,150 @@ def clamp(attr_a, min_value=0, max_value=1):
@noca_op
-def compose_matrix(**kwargs):
- """Create composeMatrix-node to assemble matrix from transforms.
+def closest_point_on_mesh(mesh, position=(0, 0, 0), return_all_outputs=False):
+ """Get the closest point on a mesh, from the given position.
Args:
- kwargs (dict): Possible kwargs below. longName flags take
- precedence over the short names in [brackets]!
- translate (NcNode or NcAttrs or str or int or float): [t] translate
- rotate (NcNode or NcAttrs or str or int or float): [r] rotate
- scale (NcNode or NcAttrs or str or int or float): [s] scale
- shear (NcNode or NcAttrs or str or int or float): [sh] shear
- rotate_order (NcNode or NcAttrs or str or int): [ro] rot-order
- euler_rotation (NcNode or NcAttrs or bool): Euler rot or quaternion
+ mesh (NcNode or NcAttrs or str): Mesh node.
+ position (NcNode or NcAttrs or int or float or list): Find closest
+ point on mesh to this position. Defaults to (0, 0, 0).
+ return_all_outputs (boolean): Return all outputs as an NcList.
+ Defaults to False.
Returns:
- NcNode: Instance with composeMatrix-node and output-attribute(s)
+ NcNode or NcList: If return_all_outputs is set to True, an NcList is
+ returned with all outputs. Otherwise only the first output
+ (position) is returned as an NcNode instance.
Example:
::
- in_a = Node("pCube1")
- in_b = Node("pCube2")
- decomp_a = Op.decompose_matrix(in_a.worldMatrix)
- decomp_b = Op.decompose_matrix(in_b.worldMatrix)
- Op.compose_matrix(r=decomp_a.outputRotate, s=decomp_b.outputScale)
+ cube = Node("pCube1")
+ Op.closest_point_on_mesh(cube.outMesh, [1, 0, 0])
"""
- # Using kwargs not to have a lot of flags in the function call
- translate = kwargs.get("translate", kwargs.get("t", 0))
- rotate = kwargs.get("rotate", kwargs.get("r", 0))
- scale = kwargs.get("scale", kwargs.get("s", 1))
- shear = kwargs.get("shear", kwargs.get("sh", 0))
- rotate_order = kwargs.get("rotate_order", kwargs.get("ro", 0))
- euler_rotation = kwargs.get("euler_rotation", True)
-
- compose_matrix_node = _create_operation_node(
- "compose_matrix",
- translate,
- rotate,
- scale,
- shear,
- rotate_order,
- euler_rotation
+ return_value = _create_operation_node(
+ "closest_point_on_mesh",
+ mesh,
+ position,
)
- return compose_matrix_node
+ if return_all_outputs:
+ return return_value
+ return return_value[0]
-@noca_op
-def condition(condition_node, if_part=False, else_part=True):
- """Set up condition-node.
- Note:
- condition_node can be a NcNode-instance of a Maya condition node.
- An appropriate NcNode-object gets automatically created when
- NodeCalculator objects are used in comparisons (==, >, >=, <, <=).
- Simply use comparison operators in the first argument. See example.
+@noca_op
+def closest_point_on_surface(
+ surface,
+ position=(0, 0, 0),
+ return_all_outputs=False):
+ """Get the closest point on a surface, from the given position.
+
+ Args:
+ surface (NcNode or NcAttrs or str): NURBS surface node.
+ position (NcNode or NcAttrs or int or float or list): Find closest
+ point on surface to this position. Defaults to (0, 0, 0).
+ return_all_outputs (boolean): Return all outputs as an NcList.
+ Defaults to False.
+
+ Returns:
+ NcNode or NcList: If return_all_outputs is set to True, an NcList is
+ returned with all outputs. Otherwise only the first output
+ (position) is returned as an NcNode instance.
+
+ Example:
+ ::
+
+ sphere = Node("nurbsSphere1")
+ Op.closest_point_on_surface(sphere.local, [1, 0, 0])
+ """
+ return_value = _create_operation_node(
+ "closest_point_on_surface",
+ surface,
+ position,
+ )
+
+ if return_all_outputs:
+ return return_value
+
+ return return_value[0]
+
+
+@noca_op
+def compose_matrix(
+ translate=None,
+ rotate=None,
+ scale=None,
+ shear=None,
+ rotate_order=None,
+ euler_rotation=None,
+ **kwargs):
+ """Create composeMatrix-node to assemble matrix from transforms.
+
+ Args:
+ translate (NcNode or NcAttrs or str or int or float): translate [t]
+ Defaults to None, which corresponds to value 0.
+ rotate (NcNode or NcAttrs or str or int or float): rotate [r]
+ Defaults to None, which corresponds to value 0.
+ scale (NcNode or NcAttrs or str or int or float): scale [s]
+ Defaults to None, which corresponds to value 1.
+ shear (NcNode or NcAttrs or str or int or float): shear [sh]
+ Defaults to None, which corresponds to value 0.
+ rotate_order (NcNode or NcAttrs or str or int): rot-order [ro]
+ Defaults to None, which corresponds to value 0.
+ euler_rotation (NcNode or NcAttrs or bool): Euler or quaternion [uer]
+ Defaults to None, which corresponds to True.
+ kwargs (dict): Short flags, see in [brackets] for each arg above.
+ Long names take precedence!
+
+ Returns:
+ NcNode: Instance with composeMatrix-node and output-attribute(s)
+
+ Example:
+ ::
+
+ in_a = Node("pCube1")
+ in_b = Node("pCube2")
+ decomp_a = Op.decompose_matrix(in_a.worldMatrix)
+ decomp_b = Op.decompose_matrix(in_b.worldMatrix)
+ Op.compose_matrix(r=decomp_a.outputRotate, s=decomp_b.outputScale)
+ """
+ if translate is None:
+ translate = kwargs.get("t", 0)
+ if rotate is None:
+ rotate = kwargs.get("r", 0)
+ if scale is None:
+ scale = kwargs.get("s", 1)
+ if shear is None:
+ shear = kwargs.get("sh", 0)
+ if rotate_order is None:
+ rotate_order = kwargs.get("ro", 0)
+ if euler_rotation is None:
+ euler_rotation = kwargs.get("uer", True)
+
+ compose_matrix_node = _create_operation_node(
+ "compose_matrix",
+ translate,
+ rotate,
+ scale,
+ shear,
+ rotate_order,
+ euler_rotation
+ )
+
+ return compose_matrix_node
+
+
+@noca_op
+def condition(condition_node, if_part=False, else_part=True):
+ """Set up condition-node.
+
+ Note:
+ condition_node can be a NcNode-instance of a Maya condition node.
+ An appropriate NcNode-object gets automatically created when
+ NodeCalculator objects are used in comparisons (==, >, >=, <, <=).
+ Simply use comparison operators in the first argument. See example.
Args:
condition_node (NcNode or bool or int or float): Condition-statement.
@@ -579,10 +977,11 @@ def cross(attr_a, attr_b=0, normalize=False):
"""Create vectorProduct-node for vector cross-multiplication.
Args:
- attr_a (NcNode or NcAttrs or str or int or float or list): Vector A
- attr_b (NcNode or NcAttrs or str or int or float or list): Vector B
+ attr_a (NcNode or NcAttrs or str or int or float or list): Vector A.
+ attr_b (NcNode or NcAttrs or str or int or float or list): Vector B.
+ Defaults to 0.
normalize (NcNode or NcAttrs or boolean): Whether resulting vector
- should be normalized
+ should be normalized. Defaults to False.
Returns:
NcNode: Instance with vectorProduct-node and output-attribute(s)
@@ -632,10 +1031,11 @@ def dot(attr_a, attr_b=0, normalize=False):
"""Create vectorProduct-node for vector dot-multiplication.
Args:
- attr_a (NcNode or NcAttrs or str or int or float or list): Vector A
- attr_b (NcNode or NcAttrs or str or int or float or list): Vector B
+ attr_a (NcNode or NcAttrs or str or int or float or list): Vector A.
+ attr_b (NcNode or NcAttrs or str or int or float or list): Vector B.
+ Defaults to 0.
normalize (NcNode or NcAttrs or boolean): Whether resulting vector
- should be normalized
+ should be normalized. Defaults to False.
Returns:
NcNode: Instance with vectorProduct-node and output-attribute(s)
@@ -648,6 +1048,29 @@ def dot(attr_a, attr_b=0, normalize=False):
return _create_operation_node("dot", attr_a, attr_b, normalize)
+@noca_op
+def euler_to_quat(angle, rotate_order=0):
+ """Create eulerToQuat-node to add two quaternions together.
+
+ Args:
+ angle (NcNode or NcAttrs or str or list or tuple): Euler angles to
+ convert into a quaternion.
+ rotate_order (NcNode or NcAttrs or or int): Order of rotation.
+ Defaults to 0, which represents rotate order "xyz".
+
+ Returns:
+ NcNode: Instance with eulerToQuat-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.euler_to_quat(Node("pCube").rotate, 2)
+ """
+ created_node = _create_operation_node("euler_to_quat", angle, rotate_order)
+
+ return created_node
+
+
@noca_op
def exp(attr_a):
"""Raise attr_a to the base of natural logarithms.
@@ -666,6 +1089,132 @@ def exp(attr_a):
return math.e ** attr_a
+@noca_op
+def four_by_four_matrix(
+ vector_a=None,
+ vector_b=None,
+ vector_c=None,
+ translate=None):
+ """Create a four by four matrix out of its components.
+
+ Args:
+ vector_a (NcNode or NcAttrs or str or list or tuple or int or float):
+ First vector of the matrix; the "x-axis". Or can contain all 16
+ elements that make up the 4x4 matrix. Defaults to None, which
+ means the identity matrix will be used.
+ vector_b (NcNode or NcAttrs or str or list or tuple or int or float):
+ Second vector of the matrix; the "y-axis". Defaults to None, which
+ means the vector (0, 1, 0) will be used, if matrix is not defined
+ solely by vector_a.
+ vector_c (NcNode or NcAttrs or str or list or tuple or int or float):
+ Third vector of the matrix; the "z-axis". Defaults to None, which
+ means the vector (0, 0, 1) will be used, if matrix is not defined
+ solely by vector_a.
+ translate (NcNode or NcAttrs or str or list or tuple or int or float):
+ Translate-elements of the matrix. Defaults to None, which means
+ the vector (0, 0, 0) will be used, if matrix is not defined
+ solely by vector_a.
+
+ Returns:
+ NcNode: Instance with fourByFourMatrix-node and output-attr(s)
+
+ Example:
+ ::
+
+ cube = Node("pCube1")
+ vec_a = Op.point_matrix_mult(
+ [1, 0, 0],
+ cube.worldMatrix,
+ vector_multiply=True
+ )
+ vec_b = Op.point_matrix_mult(
+ [0, 1, 0],
+ cube.worldMatrix,
+ vector_multiply=True
+ )
+ vec_c = Op.point_matrix_mult(
+ [0, 0, 1],
+ cube.worldMatrix,
+ vector_multiply=True
+ )
+ out = Op.four_by_four_matrix(
+ vector_a=vec_a,
+ vector_b=vec_b,
+ vector_c=vec_c,
+ translate=[cube.tx, cube.ty, cube.tz]
+ )
+ """
+ # If any vector is not None: The operator won't return the identity matrix.
+ vectors = [vector_a, vector_b, vector_c, translate]
+ if any([vector is not None for vector in vectors]):
+
+ # If a vector other than vector_a is not None: Assume the matrix
+ # should be created from multiple vectors.
+ if any([vector is not None for vector in vectors[1:]]):
+
+ # Start with the identity matrix and set/connect any given vector.
+ created_node = _create_operation_node(
+ "four_by_four_matrix",
+ [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ )
+
+ if vector_a:
+ _unravel_and_set_or_connect_a_to_b(
+ [created_node.in00, created_node.in01, created_node.in02],
+ vector_a,
+ )
+ if vector_b:
+ _unravel_and_set_or_connect_a_to_b(
+ [created_node.in10, created_node.in11, created_node.in12],
+ vector_b,
+ )
+ if vector_c:
+ _unravel_and_set_or_connect_a_to_b(
+ [created_node.in20, created_node.in21, created_node.in22],
+ vector_c,
+ )
+ if translate:
+ _unravel_and_set_or_connect_a_to_b(
+ [created_node.in30, created_node.in31, created_node.in32],
+ translate,
+ )
+
+ # If only vector_a was given: Assume it contains all elements that
+ # should make up the matrix.
+ else:
+ created_node = _create_operation_node(
+ "four_by_four_matrix",
+ vector_a
+ )
+
+ else:
+ # Default to identity matrix
+ created_node = _create_operation_node(
+ "four_by_four_matrix",
+ [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
+ )
+
+ return created_node
+
+
+@noca_op
+def hold_matrix(matrix):
+ """Create holdMatrix-node for storing a matrix.
+
+ Args:
+ matrix (NcNode or NcAttrs or string or list): Matrix to store.
+
+ Returns:
+ NcNode: Instance with holdMatrix-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.hold_matrix(Node("pCube1.worldMatrix"))
+ """
+ return _create_operation_node("hold_matrix", matrix)
+
+
@noca_op
def inverse_matrix(in_matrix):
"""Create inverseMatrix-node to invert the given matrix.
@@ -689,8 +1238,9 @@ def length(attr_a, attr_b=0):
"""Create distanceBetween-node to measure length between given points.
Args:
- attr_a (NcNode or NcAttrs or str or int or float): Start point
- attr_b (NcNode or NcAttrs or str or int or float): End point
+ attr_a (NcNode or NcAttrs or str or int or float): Start point.
+ attr_b (NcNode or NcAttrs or str or int or float): End point.
+ Defaults to 0.
Returns:
NcNode: Instance with distanceBetween-node and distance-attribute
@@ -710,6 +1260,8 @@ def matrix_distance(matrix_a, matrix_b=None):
Args:
matrix_a (NcNode or NcAttrs or str): Matrix defining start point.
matrix_b (NcNode or NcAttrs or str): Matrix defining end point.
+ Defaults to None, which gives the length between the origin and
+ the point described by matrix_a.
Returns:
NcNode: Instance with distanceBetween-node and distance-attribute
@@ -729,7 +1281,8 @@ def mult_matrix(*attrs):
"""Create multMatrix-node for multiplying matrices.
Args:
- attrs (NcNode or NcAttrs or string or list): Matrices to multiply
+ attrs (NcNode or NcAttrs or NcList or string or list or tuple):
+ Matrices to multiply together.
Returns:
NcNode: Instance with multMatrix-node and output-attribute(s)
@@ -746,16 +1299,57 @@ def mult_matrix(*attrs):
out.rotate = decomp.outputRotate
out.scale = decomp.outputScale
"""
+ if len(attrs) == 1:
+ attrs = attrs[0]
+
return _create_operation_node("mult_matrix", attrs)
+@noca_op
+def nearest_point_on_curve(
+ curve,
+ position=(0, 0, 0),
+ return_all_outputs=False):
+ """Get curve data from a particular point on a curve.
+
+ Args:
+ curve (NcNode or NcAttrs or str): Curve node.
+ position (NcNode or NcAttrs or int or float or list): Find closest
+ point on curve to this position. Defaults to (0, 0, 0).
+ return_all_outputs (boolean): Return all outputs as an NcList.
+ Defaults to False.
+
+ Returns:
+ NcNode or NcList: If return_all_outputs is set to True, an NcList is
+ returned with all outputs. Otherwise only the first output
+ (position) is returned as an NcNode instance.
+
+ Example:
+ ::
+
+ curve = Node("curve1")
+ Op.nearest_point_on_curve(curve.local, [1, 0, 0])
+ """
+ return_value = _create_operation_node(
+ "nearest_point_on_curve",
+ curve,
+ position,
+ )
+
+ if return_all_outputs:
+ return return_value
+
+ return return_value[0]
+
+
@noca_op
def normalize_vector(in_vector, normalize=True):
"""Create vectorProduct-node to normalize the given vector.
Args:
in_vector (NcNode or NcAttrs or str or int or float or list): Vect.
- normalize (NcNode or NcAttrs or boolean): Turn normalize on/off
+ normalize (NcNode or NcAttrs or boolean): Turn normalize on/off.
+ Defaults to True.
Returns:
NcNode: Instance with vectorProduct-node and output-attribute(s)
@@ -788,16 +1382,22 @@ def pair_blend(
Args:
translate_a (NcNode or NcAttrs or str or int or float or list):
Translate value of first transform.
+ Defaults to 0.
rotate_a (NcNode or NcAttrs or str or int or float or list):
Rotate value of first transform.
+ Defaults to 0.
translate_b (NcNode or NcAttrs or str or int or float or list):
Translate value of second transform.
+ Defaults to 0.
rotate_b (NcNode or NcAttrs or str or int or float or list):
Rotate value of second transform.
+ Defaults to 0.
weight (NcNode or NcAttrs or str or int or float or list):
Bias towards first or second transform.
+ Defaults to 1.
quat_interpolation (NcNode or NcAttrs or boolean):
Use euler (False) or quaternions (True) to interpolate rotation
+ Defaults to False.
return_all_outputs (boolean): Return all outputs, as an NcList.
Defaults to False.
@@ -830,6 +1430,26 @@ def pair_blend(
return return_value[0]
+@noca_op
+def pass_matrix(matrix, scale=1):
+ """Create passMatrix-node for passing and optionally scaling a matrix.
+
+ Args:
+ matrix (NcNode or NcAttrs or string or list): Matrix to store.
+ scale (NcNode or NcAttrs or int or float): Scale to be applied to
+ matrix. Defaults to 1.
+
+ Returns:
+ NcNode: Instance with passMatrix-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.pass_matrix(Node("pCube1.worldMatrix"))
+ """
+ return _create_operation_node("pass_matrix", matrix, scale)
+
+
@noca_op
def point_matrix_mult(in_vector, in_matrix, vector_multiply=False):
"""Create pointMatrixMult-node to transpose the given matrix.
@@ -838,7 +1458,7 @@ def point_matrix_mult(in_vector, in_matrix, vector_multiply=False):
in_vector (NcNode or NcAttrs or str or int or float or list): Vect.
in_matrix (NcNode or NcAttrs or str): Matrix
vector_multiply (NcNode or NcAttrs or str or int or bool): Whether
- vector multiplication should be performed.
+ vector multiplication should be performed. Defaults to False.
Returns:
NcNode: Instance with pointMatrixMult-node and output-attribute(s)
@@ -863,12 +1483,282 @@ def point_matrix_mult(in_vector, in_matrix, vector_multiply=False):
@noca_op
-def pow(attr_a, attr_b):
+def quat_add(quat_a, quat_b=(0, 0, 0, 1)):
+ """Create quatAdd-node to add two quaternions together.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): First quaternion.
+ quat_b (NcNode or NcAttrs or str or list or tuple): Second quaternion.
+ Defaults to (0, 0, 0, 1).
+
+ Returns:
+ NcNode: Instance with quatAdd-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_add(
+ create_node("decomposeMatrix").outputQuat,
+ create_node("decomposeMatrix").outputQuat,
+ )
+ """
+ created_node = _create_operation_node("quat_add", quat_a, quat_b)
+
+ return created_node
+
+
+@noca_op
+def quat_conjugate(quat_a):
+ """Create quatConjugate-node to conjugate a quaternion.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): Quaternion to
+ conjugate.
+
+ Returns:
+ NcNode: Instance with quatConjugate-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_conjugate(create_node("decomposeMatrix").outputQuat)
+ """
+ created_node = _create_operation_node("quat_conjugate", quat_a)
+
+ return created_node
+
+
+@noca_op
+def quat_invert(quat_a):
+ """Create quatInvert-node to invert a quaternion.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): Quaternion to
+ invert.
+
+ Returns:
+ NcNode: Instance with quatInvert-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_invert(create_node("decomposeMatrix").outputQuat)
+ """
+ created_node = _create_operation_node("quat_invert", quat_a)
+
+ return created_node
+
+
+@noca_op
+def quat_negate(quat_a):
+ """Create quatNegate-node to negate a quaternion.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): Quaternion to
+ negate.
+
+ Returns:
+ NcNode: Instance with quatNegate-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_negate(create_node("decomposeMatrix").outputQuat)
+ """
+ created_node = _create_operation_node("quat_negate", quat_a)
+
+ return created_node
+
+
+@noca_op
+def quat_normalize(quat_a):
+ """Create quatNormalize-node to normalize a quaternion.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): Quaternion to
+ normalize.
+
+ Returns:
+ NcNode: Instance with quatNormalize-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_normalize(create_node("decomposeMatrix").outputQuat)
+ """
+ created_node = _create_operation_node("quat_normalize", quat_a)
+
+ return created_node
+
+
+@noca_op
+def quat_mul(quat_a, quat_b=(0, 0, 0, 1)):
+ """Create quatProd-node to multiply two quaternions together.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): First quaternion.
+ quat_b (NcNode or NcAttrs or str or list or tuple): Second quaternion.
+ Defaults to (0, 0, 0, 1).
+
+ Returns:
+ NcNode: Instance with quatProd-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_mul(
+ create_node("decomposeMatrix").outputQuat,
+ create_node("decomposeMatrix").outputQuat,
+ )
+ """
+ created_node = _create_operation_node("quat_mul", quat_a, quat_b)
+
+ return created_node
+
+
+@noca_op
+def quat_sub(quat_a, quat_b=(0, 0, 0, 1)):
+ """Create quatSub-node to subtract two quaternions from each other.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): First quaternion.
+ quat_b (NcNode or NcAttrs or str or list or tuple): Second quaternion
+ that will be subtracted from the first. Defaults to (0, 0, 0, 1).
+
+ Returns:
+ NcNode: Instance with quatSub-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_sub(
+ create_node("decomposeMatrix").outputQuat,
+ create_node("decomposeMatrix").outputQuat,
+ )
+ """
+ created_node = _create_operation_node("quat_sub", quat_a, quat_b)
+
+ return created_node
+
+
+@noca_op
+def quat_to_euler(quat_a, rotate_order=0):
+ """Create quatToEuler-node to convert a quaternion into an euler angle.
+
+ Args:
+ quat_a (NcNode or NcAttrs or str or list or tuple): Quaternion to
+ convert into Euler angles.
+ rotate_order (NcNode or NcAttrs or or int): Order of rotation.
+ Defaults to 0, which represents rotate order "xyz".
+
+ Returns:
+ NcNode: Instance with quatToEuler-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.quat_to_euler(create_node("decomposeMatrix").outputQuat, 2)
+ """
+ created_node = _create_operation_node(
+ "quat_to_euler",
+ quat_a,
+ rotate_order
+ )
+
+ return created_node
+
+
+@noca_op
+def point_on_curve_info(
+ curve,
+ parameter=0,
+ as_percentage=False,
+ return_all_outputs=False):
+ """Get curve data from a particular point on a curve.
+
+ Args:
+ curve (NcNode or NcAttrs or str): Curve node.
+ parameter (NcNode or NcAttrs or int or float or list): Get curve data
+ at the position on the curve specified by this parameter.
+ Defaults to 0.
+ as_percentage (NcNode or NcAttrs or int or float or boolean): Use
+ 0-1 values for parameter. Defaults to False.
+ return_all_outputs (boolean): Return all outputs as an NcList.
+ Defaults to False.
+
+ Returns:
+ NcNode or NcList: If return_all_outputs is set to True, an NcList is
+ returned with all outputs. Otherwise only the first output
+ (position) is returned as an NcNode instance.
+
+ Example:
+ ::
+
+ curve = Node("curve1")
+ Op.point_on_curve_info(curve.local, 0.5)
+ """
+ return_value = _create_operation_node(
+ "point_on_curve_info",
+ curve,
+ parameter,
+ as_percentage,
+ )
+
+ if return_all_outputs:
+ return return_value
+
+ return return_value[0]
+
+
+@noca_op
+def point_on_surface_info(
+ surface,
+ parameter=(0, 0),
+ as_percentage=False,
+ return_all_outputs=False):
+ """Get surface data from a particular point on a NURBS surface.
+
+ Args:
+ surface (NcNode or NcAttrs or str): NURBS surface node.
+ parameter (NcNode or NcAttrs or int or float or list): UV values that
+ define point on NURBS surface. Defaults to (0, 0).
+ as_percentage (NcNode or NcAttrs or int or float or boolean): Use
+ 0-1 values for parameters. Defaults to False.
+ return_all_outputs (boolean): Return all outputs as an NcList.
+ Defaults to False.
+
+ Returns:
+ NcNode or NcList: If return_all_outputs is set to True, an NcList is
+ returned with all outputs. Otherwise only the first output
+ (position) is returned as an NcNode instance.
+
+ Example:
+ ::
+
+ sphere = Node("nurbsSphere1")
+ Op.point_on_surface_info(sphere.local, [0.5, 0.5])
+ """
+ return_value = _create_operation_node(
+ "point_on_surface_info",
+ surface,
+ parameter,
+ as_percentage,
+ )
+
+ if return_all_outputs:
+ return return_value
+
+ return return_value[0]
+
+
+@noca_op
+def pow(attr_a, attr_b=2):
"""Raise attr_a to the power of attr_b.
Args:
- attr_a (NcNode or NcAttrs or str or int or float): Value or attr
- attr_b (NcNode or NcAttrs or str or int or float): Value or attr
+ attr_a (NcNode or NcAttrs or str or int or float): Value or attr.
+ attr_b (NcNode or NcAttrs or str or int or float): Value or attr.
+ Defaults to 2.
Returns:
NcNode: Instance with multiplyDivide-node and output-attr(s)
@@ -881,6 +1771,188 @@ def pow(attr_a, attr_b):
return attr_a ** attr_b
+@noca_op
+def remap_color(
+ attr_a,
+ output_min=0,
+ output_max=1,
+ input_min=0,
+ input_max=1,
+ values_red=None,
+ values_green=None,
+ values_blue=None):
+ """Create remapColor-node to remap the given input.
+
+ Args:
+ attr_a (NcNode or NcAttrs or str or int or float): Input color.
+ output_min (NcNode or NcAttrs or int or float or list): minValue.
+ Defaults to 0.
+ output_max (NcNode or NcAttrs or int or float or list): maxValue.
+ Defaults to 1.
+ input_min (NcNode or NcAttrs or int or float or list): old minValue.
+ Defaults to 0.
+ input_max (NcNode or NcAttrs or int or float or list): old maxValue.
+ Defaults to 1.
+ values_red (list): List of tuples for red-graph in the form;
+ (value_Position, value_FloatValue, value_Interp)
+ The value interpolation element is optional (default: linear)
+ Defaults to None.
+ values_green (list): List of tuples for green-graph in the form;
+ (value_Position, value_FloatValue, value_Interp)
+ The value interpolation element is optional (default: linear)
+ Defaults to None.
+ values_blue (list): List of tuples for blue-graph in the form;
+ (value_Position, value_FloatValue, value_Interp)
+ The value interpolation element is optional (default: linear)
+ Defaults to None.
+
+ Returns:
+ NcNode: Instance with remapColor-node and output-attribute(s)
+
+ Raises:
+ TypeError: If given values isn't a list of either lists or tuples.
+ RuntimeError: If given values isn't a list of lists/tuples of
+ length 2 or 3.
+
+ Example:
+ ::
+
+ Op.remap_color(
+ Node("blinn1.outColor"),
+ values_red=[(0.1, .2, 0), (0.4, 0.3)]
+ )
+ """
+ created_node = _create_operation_node(
+ "remap_color", attr_a, output_min, output_max, input_min, input_max
+ )
+
+ value_lists = [values_red, values_green, values_blue]
+ for values, color in zip(value_lists, ["red", "green", "blue"]):
+ for index, value_data in enumerate(values or []):
+ # value_Position, value_FloatValue, value_Interp
+ # "x-axis", "y-axis", interpolation
+
+ if not isinstance(value_data, (list, tuple)):
+ msg = (
+ "The values-flag for remap_color requires a list of "
+ "tuples! Got {0} instead.".format(values)
+ )
+ raise TypeError(msg)
+
+ elif len(value_data) == 2:
+ pos, val = value_data
+ interp = 1
+
+ elif len(value_data) == 3:
+ pos, val, interp = value_data
+
+ else:
+ msg = (
+ "The values-flag for remap_color requires a list of "
+ "tuples of length 2 or 3! Got {0} instead.".format(values)
+ )
+ raise RuntimeError(msg)
+
+ # Set these attributes directly to avoid unnecessary unravelling.
+ _traced_set_attr(
+ "{0}.{1}[{2}]".format(created_node.node, color, index),
+ (pos, val, interp)
+ )
+
+ return created_node
+
+
+@noca_op
+def remap_hsv(
+ attr_a,
+ output_min=0,
+ output_max=1,
+ input_min=0,
+ input_max=1,
+ values_hue=None,
+ values_saturation=None,
+ values_value=None):
+ """Create remapHsv-node to remap the given input.
+
+ Args:
+ attr_a (NcNode or NcAttrs or str or int or float): Input color.
+ output_min (NcNode or NcAttrs or int or float or list): minValue.
+ Defaults to 0.
+ output_max (NcNode or NcAttrs or int or float or list): maxValue.
+ Defaults to 1.
+ input_min (NcNode or NcAttrs or int or float or list): old minValue.
+ Defaults to 0.
+ input_max (NcNode or NcAttrs or int or float or list): old maxValue.
+ Defaults to 1.
+ values_hue (list): List of tuples for hue-graph in the form;
+ (value_Position, value_FloatValue, value_Interp)
+ The value interpolation element is optional (default: linear)
+ Defaults to None.
+ values_saturation (list): List of tuples for saturation-graph in form;
+ (value_Position, value_FloatValue, value_Interp)
+ The value interpolation element is optional (default: linear)
+ Defaults to None.
+ values_value (list): List of tuples for value-graph in the form;
+ (value_Position, value_FloatValue, value_Interp)
+ The value interpolation element is optional (default: linear)
+ Defaults to None.
+
+ Returns:
+ NcNode: Instance with remapHsv-node and output-attribute(s)
+
+ Raises:
+ TypeError: If given values isn't a list of either lists or tuples.
+ RuntimeError: If given values isn't a list of lists/tuples of
+ length 2 or 3.
+
+ Example:
+ ::
+
+ Op.remap_hsv(
+ Node("blinn1.outColor"),
+ values_saturation=[(0.1, .2, 0), (0.4, 0.3)]
+ )
+ """
+ created_node = _create_operation_node(
+ "remap_hsv", attr_a, output_min, output_max, input_min, input_max
+ )
+
+ value_lists = [values_hue, values_saturation, values_value]
+ for values, setting in zip(value_lists, ["hue", "saturation", "value"]):
+ for index, value_data in enumerate(values or []):
+ # value_Position, value_FloatValue, value_Interp
+ # "x-axis", "y-axis", interpolation
+
+ if not isinstance(value_data, (list, tuple)):
+ msg = (
+ "The values-flag for remap_hsv requires a list of "
+ "tuples! Got {0} instead.".format(values)
+ )
+ raise TypeError(msg)
+
+ elif len(value_data) == 2:
+ pos, val = value_data
+ interp = 1
+
+ elif len(value_data) == 3:
+ pos, val, interp = value_data
+
+ else:
+ msg = (
+ "The values-flag for remap_hsv requires a list of "
+ "tuples of length 2 or 3! Got {0} instead.".format(values)
+ )
+ raise RuntimeError(msg)
+
+ # Set these attributes directly to avoid unnecessary unravelling.
+ _traced_set_attr(
+ "{0}.{1}[{2}]".format(created_node.node, setting, index),
+ (pos, val, interp)
+ )
+
+ return created_node
+
+
@noca_op
def remap_value(
attr_a,
@@ -893,20 +1965,25 @@ def remap_value(
Args:
attr_a (NcNode or NcAttrs or str or int or float): Input value
- output_min (NcNode or NcAttrs or int or float or list): minValue
- output_max (NcNode or NcAttrs or int or float or list): maxValue
- input_min (NcNode or NcAttrs or int or float or list): old minValue
- input_max (NcNode or NcAttrs or int or float or list): old maxValue
+ output_min (NcNode or NcAttrs or int or float or list): minValue.
+ Defaults to 0.
+ output_max (NcNode or NcAttrs or int or float or list): maxValue.
+ Defaults to 1.
+ input_min (NcNode or NcAttrs or int or float or list): old minValue.
+ Defaults to 0.
+ input_max (NcNode or NcAttrs or int or float or list): old maxValue.
+ Defaults to 1.
values (list): List of tuples in the following form;
(value_Position, value_FloatValue, value_Interp)
The value interpolation element is optional (default: linear)
+ Defaults to None.
Returns:
NcNode: Instance with remapValue-node and output-attribute(s)
Raises:
- TypeError: If given values isn"t a list of either lists or tuples.
- RuntimeError: If given values isn"t a list of lists/tuples of
+ TypeError: If given values isn't a list of either lists or tuples.
+ RuntimeError: If given values isn't a list of lists/tuples of
length 2 or 3.
Example:
@@ -955,6 +2032,42 @@ def remap_value(
return created_node
+@noca_op
+def reverse(attr_a):
+ """Create reverse-node to get 1 minus the input.
+
+ Args:
+ attr_a (NcNode or NcAttrs or str or int or float): Input value
+
+ Returns:
+ NcNode: Instance with reverse-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.reverse(Node("pCube.visibility"))
+ """
+ return _create_operation_node("reverse", attr_a)
+
+
+@noca_op
+def rgb_to_hsv(rgb_color):
+ """Create rgbToHsv-node to get RGB color in HSV representation.
+
+ Args:
+ rgb_color (NcNode or NcAttrs or str or int or float): Input RGB color.
+
+ Returns:
+ NcNode: Instance with rgbToHsv-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.rgb_to_hsv(Node("blinn1.outColor"))
+ """
+ return _create_operation_node("rgb_to_hsv", rgb_color)
+
+
@noca_op
def set_range(
attr_a,
@@ -965,11 +2078,15 @@ def set_range(
"""Create setRange-node to remap the given input attr to a new min/max.
Args:
- attr_a (NcNode or NcAttrs or str or int or float): Input value
- min_value (NcNode or NcAttrs or int or float or list): new min
- max_value (NcNode or NcAttrs or int or float or list): new max
- old_min_value (NcNode or NcAttrs or int or float or list): old min
- old_max_value (NcNode or NcAttrs or int or float or list): old max
+ attr_a (NcNode or NcAttrs or str or int or float): Input value.
+ min_value (NcNode or NcAttrs or int or float or list): New min.
+ Defaults to 0.
+ max_value (NcNode or NcAttrs or int or float or list): New max.
+ Defaults to 1.
+ old_min_value (NcNode or NcAttrs or int or float or list): Old min.
+ Defaults to 0.
+ old_max_value (NcNode or NcAttrs or int or float or list): Old max.
+ Defaults to 1.
Returns:
NcNode: Instance with setRange-node and output-attribute(s)
@@ -990,6 +2107,28 @@ def set_range(
return return_value
+@noca_op
+def sum(*attrs):
+ """Create plusMinusAverage-node for averaging input attrs.
+
+ Args:
+ attrs (NcNode or NcAttrs or NcList or string or list or tuple):
+ Inputs to be added up.
+
+ Returns:
+ NcNode: Instance with plusMinusAverage-node and output-attribute(s)
+
+ Example:
+ ::
+
+ Op.average(Node("pCube.t"), [1, 2, 3])
+ """
+ if len(attrs) == 1:
+ attrs = attrs[0]
+
+ return _create_operation_node("sum", attrs)
+
+
@noca_op
def sqrt(attr_a):
"""Get the square root of attr_a.
@@ -1024,3 +2163,33 @@ def transpose_matrix(in_matrix):
Op.transpose_matrix(Node("pCube.worldMatrix"))
"""
return _create_operation_node("transpose_matrix", in_matrix)
+
+
+@noca_op
+def weighted_add_matrix(*matrices):
+ """Add matrices with a weight-bias.
+
+ Args:
+ matrices (NcNode or NcAttrs or list or tuple): Any number of matrices.
+ Can be a list of tuples; (matrix, weight) or simply a list of
+ matrices. In that case the weight will be evenly distributed
+ between all given matrices.
+
+ Returns:
+ NcNode: Instance with wtAddMatrix-node and output-attribute(s)
+
+ Example:
+ ::
+ cube_a = Node("pCube1.worldMatrix")
+ cube_b = Node("pCube2.worldMatrix")
+ Op.weighted_add_matrix(cube_a, cube_b)
+ """
+ weighted_matrices = []
+ num_matrices = len(matrices)
+ for matrix in matrices:
+ if isinstance(matrix, tuple) and len(matrix) == 2:
+ weighted_matrices.append(matrix)
+ else:
+ weighted_matrices.append((matrix, 1.0/num_matrices))
+
+ return _create_operation_node("weighted_add_matrix", weighted_matrices)
diff --git a/node_calculator/core.py b/node_calculator/core.py
index 5feab05..ea05e46 100644
--- a/node_calculator/core.py
+++ b/node_calculator/core.py
@@ -3,7 +3,7 @@
:author: Mischa Kolbe
:credits: Mischa Kolbe, Steven Bills, Marco D'Ambros, Benoit Gielly,
Adam Vanner, Niels Kleinheinz, Andres Weber
-:version: 2.1.1
+:version: 2.1.2
Note:
@@ -2189,12 +2189,12 @@ def _unravel_and_set_or_connect_a_to_b(obj_a, obj_b, **kwargs):
# Dimensionality above 3 is most likely not going to be handled reliable!
if obj_a_dim > 3:
- LOG.warn(
+ LOG.info(
"obj_a %s is %dD; greater than 3D! Many operations only work "
"stable up to 3D!", obj_a_unravelled_list, obj_a_dim
)
if obj_b_dim > 3:
- LOG.warn(
+ LOG.info(
"obj_b %s is %dD; greater than 3D! Many operations only work "
"stable up to 3D!", obj_b_unravelled_list, obj_b_dim
)
diff --git a/tests/run_tests.bat b/tests/run_tests.bat
index ddb58f8..4a1307f 100644
--- a/tests/run_tests.bat
+++ b/tests/run_tests.bat
@@ -1 +1 @@
-mayapy -m unittest discover -s E:\Dropbox\__SoftwareSpecific__\Maya\scripts\node_calculator\tests -v
+mayapy -m unittest discover -s E:\Dropbox\__SoftwareSpecific__\Maya\scripts\node_calculator\tests -v
\ No newline at end of file
diff --git a/tests/test_nc_attrs.py b/tests/test_nc_attrs.py
index c5cff7f..d2b32cf 100644
--- a/tests/test_nc_attrs.py
+++ b/tests/test_nc_attrs.py
@@ -27,10 +27,10 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# TESTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-class TestAttrsClass(BaseTestCase):
+class TestNcAttrsClass(BaseTestCase):
def setUp(self):
- super(TestAttrsClass, self).setUp()
+ super(TestNcAttrsClass, self).setUp()
self.test_transform = cmds.createNode("transform", name=TEST_TRANSFORM)
self.node_instance = noca.NcNode(TEST_TRANSFORM)
diff --git a/tests/test_nc_node.py b/tests/test_nc_node.py
index db6fde5..5f9b34f 100644
--- a/tests/test_nc_node.py
+++ b/tests/test_nc_node.py
@@ -25,10 +25,10 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# TESTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-class TestNodeClass(BaseTestCase):
+class TestNcNodeClass(BaseTestCase):
def setUp(self):
- super(TestNodeClass, self).setUp()
+ super(TestNcNodeClass, self).setUp()
self.test_transform = cmds.createNode("transform", name=TEST_TRANSFORM)
diff --git a/tests/test_node_calculator.py b/tests/test_node_calculator.py
index 2e0f0df..5a5c5a1 100644
--- a/tests/test_node_calculator.py
+++ b/tests/test_node_calculator.py
@@ -175,3 +175,50 @@ def test_attributes(self):
"{}.{}".format(TEST_NODES[0], TEST_ATTR),
]
self.assertEqual(blendshape_connections, desired_connections)
+
+ def test_non_unique_node_names(self):
+ """This test requires a specific set of nodes, different from the generic setUp."""
+
+ node_x = cmds.createNode("transform", name="X")
+ group = cmds.createNode("transform", name="grp")
+ node_x_grouped = cmds.createNode("transform")
+ cmds.parent(node_x_grouped, group)
+ cmds.rename(node_x_grouped, "X")
+
+ node_x = "|X"
+ node_x_grouped = "grp|X"
+
+ nc_x = noca.Node(node_x)
+ nc_x_grouped = noca.Node(node_x_grouped)
+
+ nc_x.tx = self.node_a.tx
+ nc_x.ty = 1
+
+ nc_x_grouped.tx = self.node_a.ty
+ nc_x_grouped.ty = 2
+
+ node_x_connection = cmds.listConnections(node_x + ".tx", plugs=True)
+ self.assertEqual(node_x_connection, [self.node_a.node + '.translateX'])
+ self.assertEqual(cmds.getAttr(node_x + ".ty"), 1)
+
+ node_x_grouped_connection = cmds.listConnections(node_x_grouped + ".tx", plugs=True)
+ self.assertEqual(node_x_grouped_connection, [self.node_a.node + '.translateY'])
+ self.assertEqual(cmds.getAttr(node_x_grouped + ".ty"), 2)
+
+ def test_shape_attribute_access(self):
+ """Test whether attrs of a transforms shape are directly accessible"""
+
+ mesh_a = cmds.polyCube(name="testMeshA", constructionHistory=False)[0]
+ mesh_b = cmds.polyCube(name="testMeshB", constructionHistory=False)[0]
+
+ nc_mesh_a = noca.Node(mesh_a)
+ nc_mesh_b = noca.Node(mesh_b)
+
+ # Make sure the NodeCalculator nodes directly refer to the transforms, not the shapes!
+ self.assertEqual(cmds.objectType(nc_mesh_a.node), "transform")
+ self.assertEqual(cmds.objectType(nc_mesh_b.node), "transform")
+
+ # Check whether the shapes get connected correctly, without accessing them explicitly.
+ nc_mesh_a.inMesh = nc_mesh_b.outMesh
+ mesh_a_connections = cmds.listConnections(mesh_a + ".inMesh", plugs=True)
+ self.assertEqual(mesh_a_connections, [mesh_b + 'Shape.outMesh'])
diff --git a/tests/test_om_util.py b/tests/test_om_util.py
index 0bed5be..8a84672 100644
--- a/tests/test_om_util.py
+++ b/tests/test_om_util.py
@@ -64,10 +64,10 @@
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# TESTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-class TestTracerClass(BaseTestCase):
+class TestOmUtilClass(BaseTestCase):
def setUp(self):
- super(TestTracerClass, self).setUp()
+ super(TestOmUtilClass, self).setUp()
self.node_name = "testNode"
self.node_alt_name = "testAltName"
diff --git a/tests/test_op.py b/tests/test_op.py
index c44fb68..63813df 100644
--- a/tests/test_op.py
+++ b/tests/test_op.py
@@ -13,44 +13,318 @@
# Local imports
from base import BaseTestCase
import node_calculator.core as noca
+from node_calculator import om_util
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# GLOBALS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-TEST_NODES = [
- "A",
- "B",
- "C",
- "M",
-]
-MATRIX_OPERATORS = [
- "inverse_matrix",
- "mult_matrix",
- "transpose_matrix",
-]
IRREGULAR_OPERATORS = [
- "decompose_matrix",
- "matrix_distance",
- "compose_matrix",
- "point_matrix_mult",
- "pair_blend",
]
+TEST_DATA_ASSOCIATION = {
+ "add": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "angle_between": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translateX'],
+ },
+ "average": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_a.rotate', 'tf_out_b.translate', 'tf_out_b.rotate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "blend": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "choice": {
+ "input_plugs": [['tf_out_a.translateX', 'tf_out_b.translateX'], 'tf_out_b.translateY'],
+ "output_plugs": ['tf_in_a.translateX'],
+ },
+ "clamp": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "closest_point_on_mesh": {
+ "input_plugs": ['mesh_shape_out_a.outMesh', 'tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "closest_point_on_surface": {
+ "input_plugs": ['surface_shape_out_a.local', 'tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "compose_matrix": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_a.rotate', 'tf_out_a.scale', 'tf_out_a.translate'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "cross": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "decompose_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix'],
+ "output_plugs": ['tf_in_a.translate', 'tf_in_a.rotate', 'tf_in_a.scale', 'tf_in_b.translate'],
+ },
+ "div": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "dot": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translateX'],
+ },
+ "euler_to_quat": {
+ "input_plugs": ['tf_out_a.rotate'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "four_by_four_matrix": {
+ "input_plugs": ['tf_out_a.translateX'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "hold_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "inverse_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "length": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translateX'],
+ },
+ "matrix_distance": {
+ "input_plugs": ['mat_out_a.outMatrix', 'mat_out_b.outMatrix'],
+ "output_plugs": ['tf_in_a.translateX'],
+ },
+ "mul": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "mult_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix', 'mat_out_b.outMatrix'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "nearest_point_on_curve": {
+ "input_plugs": ['curve_shape_out_a.local', 'tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "normalize_vector": {
+ "input_plugs": ['tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "pair_blend": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_a.rotate', 'tf_out_b.translate', 'tf_out_b.rotate'],
+ "output_plugs": ['tf_in_a.translate', 'tf_in_a.rotate'],
+ },
+ "pass_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "point_matrix_mult": {
+ "input_plugs": ['tf_out_a.translate', 'mat_out_a.outMatrix'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "point_on_curve_info": {
+ "input_plugs": ['curve_shape_out_a.local', 'tf_out_a.translateX'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "point_on_surface_info": {
+ "input_plugs": ['surface_shape_out_a.local', 'tf_out_a.translateX'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "pow": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "quat_add": {
+ "input_plugs": ['quat_out_a.outputQuat', 'quat_out_b.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_conjugate": {
+ "input_plugs": ['quat_out_a.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_invert": {
+ "input_plugs": ['quat_out_a.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_mul": {
+ "input_plugs": ['quat_out_a.outputQuat', 'quat_out_b.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_negate": {
+ "input_plugs": ['quat_out_a.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_normalize": {
+ "input_plugs": ['quat_out_a.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_sub": {
+ "input_plugs": ['quat_out_a.outputQuat', 'quat_out_b.outputQuat'],
+ "output_plugs": ['quat_in_a.inputQuat'],
+ },
+ "quat_to_euler": {
+ "input_plugs": ['quat_out_a.outputQuat'],
+ "output_plugs": ['tf_in_a.rotate'],
+ },
+ "remap_color": {
+ "input_plugs": ['tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "remap_hsv": {
+ "input_plugs": ['tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "remap_value": {
+ "input_plugs": ['tf_out_a.translateX'],
+ "output_plugs": ['tf_in_a.translateX'],
+ },
+ "reverse": {
+ "input_plugs": ['tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "rgb_to_hsv": {
+ "input_plugs": ['tf_out_a.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "set_range": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "sub": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_b.translate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "sum": {
+ "input_plugs": ['tf_out_a.translate', 'tf_out_a.rotate', 'tf_out_b.translate', 'tf_out_b.rotate'],
+ "output_plugs": ['tf_in_a.translate'],
+ },
+ "transpose_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ },
+ "weighted_add_matrix": {
+ "input_plugs": ['mat_out_a.outMatrix', 'mat_out_b.outMatrix'],
+ "output_plugs": ['mat_in_a.inMatrix'],
+ "seek_input_parent": False,
+ },
+}
+
+
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+# HELPER FUNCTIONS
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+
+def expand_array_attributes(node_inputs, input_plugs):
+ """Expand a nodes inputs for array-inputs.
+
+ Args:
+ node_inputs (list): Node input-plugs specified in the OPERATORS-dict.
+ input_plugs (list): The plugs that will be connected to the node.
+
+ Returns:
+ list: Adjusted node_inputs, where {array} plugs are correctly expanded.
+
+ Example:
+ ::
+
+ node_inputs = [["matrixIn[{array}]"]]
+ input_plugs = ["bogusMatrixA", "bogusMatrixB", "bogusMatrixC"]
+
+ expand_array_attributes(node_inputs, input_plugs)
+ >>> [['matrixIn[0]'], ['matrixIn[1]'], ['matrixIn[2]']]
+ """
+ adjusted_inputs = []
+ for _input in node_inputs:
+ if any(["{array}" in element for element in _input]):
+ for index in range(len(input_plugs)):
+ indexed_input = [element.format(array=index) for element in _input]
+ adjusted_inputs.append(indexed_input)
+ else:
+ adjusted_inputs.append(_input)
+
+ return adjusted_inputs
+
+
+def convert_literal_to_object(class_instance, literal):
+ """Get the actual class objects from the given string-literals.
+
+ Args:
+ class_instance (class): Class in which the objects should be found.
+ literal (str or list or tuple): Name of the class object or a list of
+ such objects.
+
+ Returns:
+ list or object: If literal is a list, then a list of objects will be
+ returned. If literal was a string corresponding to an object, only
+ the object will be returned.
+
+ Example:
+ ::
+
+ objects = convert_literal_to_object(
+ self,
+ [["tf_in_a.tx", "tf_in_b.tx"], "tf_in_b.ty"]
+ )
+ >>> [[self.tf_in_a.tx, self.tf_in_b.tx], self.tf_in_b.ty]
+ """
+ input_plugs = []
+ if isinstance(literal, (list, tuple)):
+ for literal_element in literal:
+ literal_element_as_object = convert_literal_to_object(class_instance, literal_element)
+ input_plugs.append(literal_element_as_object)
+
+ else:
+ input_plugs = class_instance
+ for literal_part in literal.split("."):
+ input_plugs = getattr(input_plugs, literal_part)
+
+ return input_plugs
+
+
+def flatten(in_list):
+ """Flatten a given list recursively.
+
+ Args:
+ in_list (list or tuple): Can contain scalars, lists or lists of lists.
+
+ Returns:
+ list: List of depth 1; no inner lists, only strings, ints, floats, etc.
+
+ Example:
+ ::
+
+ flatten([1, [2, [3], 4, 5], 6])
+ >>> [1, 2, 3, 4, 5, 6]
+ """
+ flattened_list = []
+ for item in in_list:
+ if isinstance(item, (list, tuple)):
+ flattened_list.extend(flatten(item))
+ else:
+ flattened_list.append(item)
+ return flattened_list
+
+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# TESTS
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def _test_condition_op(operator):
- """
- Basic tests for condition operators
+ """Basic tests for condition operators
Args:
operator (string): Boolean operator: >, <, =, >=, <=
Returns:
- test function for the given operator
+ object: Test function for the given operator
"""
def test(self):
@@ -60,9 +334,9 @@ def test(self):
# Run noca operation
bool_operator_func = getattr(noca.NcBaseClass, "__{}__".format(operator))
- condition_node = bool_operator_func(self.a.tx, condition_value)
- result = noca.Op.condition(condition_node, self.b.tx, false_value)
- self.c.t = result
+ condition_node = bool_operator_func(self.tf_out_a.translateX, condition_value)
+ result = noca.Op.condition(condition_node, self.tf_out_b.translateX, false_value)
+ self.tf_in_a.t = result
# Assertions
self.assertEqual(cmds.nodeType(result.node), node_data.get("node", None))
@@ -71,78 +345,52 @@ def test(self):
self.assertEqual(
cmds.listConnections(result.firstTerm.plugs[0], plugs=True),
- ['A.translateX']
+ self.tf_out_a.translateX.plugs
)
self.assertAlmostEqual(result.secondTerm.get(), condition_value, places=7)
self.assertEqual(result.colorIfFalseR.get(), false_value)
self.assertEqual(
cmds.listConnections(result.colorIfTrueR.plugs[0], plugs=True),
- ['B.translateX']
+ self.tf_out_b.translateX.plugs
)
self.assertEqual(
sorted(cmds.listConnections(result.outColorR.plugs[0], plugs=True)),
- ['C.translateX', 'C.translateY', 'C.translateZ']
+ [self.tf_in_a.translateX.plugs[0], self.tf_in_a.translateY.plugs[0], self.tf_in_a.translateZ.plugs[0]]
)
return test
def _test_regular_op(operator):
- """
- Basic tests that a given value is correctly set up; correct value, type, metadata, ...
+ """Basic tests whether an operator performs correctly.
Args:
- value (bool, int, float, list, dict, ...): Value of any data type
+ operator (str): Name of the operator from the noca.Op-class.
Returns:
- test function for the given value & its type
+ object: Test function for the given operator.
"""
- matrix_operator = False
- if operator in MATRIX_OPERATORS:
- matrix_operator = True
-
def test(self):
- node_data = noca.OPERATORS[operator]
+ # Get input plugs for this operator from dictionary
+ literal_input_plugs = TEST_DATA_ASSOCIATION[operator]["input_plugs"]
+ # Convert these input strings into actual objects of "self"
+ input_plugs = convert_literal_to_object(self, literal_input_plugs)
+
+ # Get output plugs for this operator from dictionary
+ literal_output_plugs = TEST_DATA_ASSOCIATION[operator]["output_plugs"]
+ # Convert these output strings into actual objects of "self"
+ output_plugs = convert_literal_to_object(self, literal_output_plugs)
+
+ # Get NodeCalculator data for this operator
+ node_data = noca.OPERATORS[operator]
node_type = node_data.get("node", None)
- node_inputs = node_data.get("inputs", None)
+ node_inputs = expand_array_attributes(node_data.get("inputs", None), input_plugs)
node_outputs = node_data.get("outputs", None)
node_operation = node_data.get("operation", None)
- node_output_is_predetermined = node_data.get("output_is_predetermined", False)
-
- # Take care of multi-input nodes
- if operator in ["add", "sub"]:
- new_node_inputs = []
- for i in range(2):
- input_item = [x.format(array=i) for x in node_inputs[0]]
- new_node_inputs.append(input_item)
-
- node_inputs = new_node_inputs
-
- # If this is a matrix operator: Use matrix plugs.
- if matrix_operator:
- possible_inputs = [
- self.a.worldMatrix,
- self.b.worldMatrix,
- ]
-
- result_plug = self.m.inMatrix
- # If this is NOT a matrix operator: Use transform plugs.
- else:
- possible_inputs = [
- self.a.translateX,
- self.b.translateX,
- self.a.translateY,
- self.b.translateY,
- self.a.translateZ,
- self.b.translateZ,
- ]
-
- result_plug = self.c.t
- actual_inputs = possible_inputs[0:len(node_inputs)]
# This assignment is necessary because closure argument can't be used directly.
true_operator = operator
@@ -151,55 +399,72 @@ def test(self):
except AttributeError:
noca_operator_func = getattr(noca.Op, true_operator)
- result = noca_operator_func(*actual_inputs)
- result_plug.attrs = result
+ # Perform operation
+ try:
+ results = noca_operator_func(*input_plugs, return_all_outputs=True)
+ except TypeError:
+ results = noca_operator_func(*input_plugs)
+
+ if not isinstance(results, (list, tuple)):
+ results = [results]
+ for output_plug, result in zip(output_plugs, results):
+ output_plug.attrs = result
- result_node_name = result.node
+ # Check that result is an NcNode
+ self.assertTrue(isinstance(result, noca.NcNode))
# Test that the created node is of the correct type
- self.assertEqual(cmds.nodeType(result_node_name), node_type)
+ self.assertEqual(cmds.nodeType(result.node), node_type)
- # Test that the inputs are correct
- for node_input, desired_input in zip(node_inputs, actual_inputs):
+ # Some Operators require a list as input-parameter. These
+ flattened_input_plugs = flatten(input_plugs)
+ for node_input, desired_input in zip(node_inputs, flattened_input_plugs):
if isinstance(node_input, (tuple, list)):
node_input = node_input[0]
- input_plug = "{}.{}".format(
- result_node_name, node_input.format(array="0")
- )
+
+ plug = "{}.{}".format(result.node, node_input)
# Check the input plug actually exists
- input_exists = cmds.objExists(input_plug)
- self.assertTrue(input_exists)
+ self.assertTrue(cmds.objExists(plug))
+
+ # Usually the parent plug gets connected and should be compared.
+ # However, some nodes have oddly parented attributes. In that case
+ # don't get the parent attribute!
+ if TEST_DATA_ASSOCIATION[operator].get("seek_input_parent", True):
+ # Get a potential parent plug, which would have been connected instead.
+ mplug = om_util.get_mplug_of_plug(plug)
+ parent_plug = om_util.get_parent_mplug(mplug)
+ if parent_plug:
+ plug = parent_plug
# Check the correct plug is connected into the input-plug
- input_connections = cmds.listConnections(input_plug, plugs=True)
- # Skip over unitConversion nodes
- if cmds.objectType(input_connections) == "unitConversion":
- conversion_node = input_connections[0].split(".")[0]
- input_connections = cmds.listConnections(
- conversion_node,
- source=True,
- destination=False,
- plugs=True,
- )
+ input_connections = cmds.listConnections(plug, plugs=True, skipConversionNodes=True)
self.assertEqual(input_connections, desired_input.plugs)
# Test that the outputs are correct
- if len(node_outputs) == 1:
- for node_output, desired_output in zip(result, node_outputs):
- self.assertEqual(node_output.attrs_list[0], desired_output[0])
+ for node_output, desired_output in zip(node_outputs, output_plugs):
+ output_is_multidimensional = False
+ if len(node_output) > 1:
+ output_is_multidimensional = True
- output_exists = cmds.objExists(node_output.plugs[0])
- self.assertTrue(output_exists)
+ node_output = node_output[0]
+ plug = "{}.{}".format(result.node, node_output)
- else:
- # This case is not yet necessary/implemented. Will be similar to
- # True-block, but it will have to look through the NcList-elements.
- self.assertTrue(False)
+ # Check the output plug actually exists
+ self.assertTrue(cmds.objExists(plug))
+
+ if output_is_multidimensional:
+ mplug = om_util.get_mplug_of_plug(plug)
+ parent_plug = om_util.get_parent_mplug(mplug)
+ if parent_plug:
+ plug = parent_plug
+
+ output_connections = cmds.listConnections(plug, plugs=True, skipConversionNodes=True)
+ self.assertEqual(output_connections, desired_output.plugs)
# Test if the operation of the created node is correctly set
if node_operation:
- operation_attr_value = cmds.getAttr("{}.operation".format(result_node_name))
+ operation_attr_value = cmds.getAttr("{}.operation".format(result.node))
self.assertEqual(operation_attr_value, node_operation)
return test
@@ -208,9 +473,9 @@ def test(self):
class TestOperatorsMeta(type):
def __new__(_mcs, _name, _bases, _dict):
- """
- Overriding the class creation method allows to create unittests for various
- types on the fly; without specifying the same test for each type specifically
+ """Overriding the class creation method allows to create unittests for
+ various types on the fly; without specifying the same test for each
+ type specifically.
"""
# Add tests for each operator
@@ -219,10 +484,13 @@ def __new__(_mcs, _name, _bases, _dict):
# Skip operators that need an individual test
if operator in IRREGULAR_OPERATORS:
continue
+
+ # Skip all condition operators as well; they need a special test
if data["node"] == "condition":
op_test_name = "test_{}".format(operator)
_dict[op_test_name] = _test_condition_op(operator)
+ # Any other operator can be tested with the regular op-test
else:
op_test_name = "test_{}".format(operator)
_dict[op_test_name] = _test_regular_op(operator)
@@ -237,191 +505,30 @@ class TestOperators(BaseTestCase):
def setUp(self):
super(TestOperators, self).setUp()
- self.a = noca.Node(cmds.createNode("transform", name=TEST_NODES[0]))
- self.b = noca.Node(cmds.createNode("transform", name=TEST_NODES[1]))
- self.c = noca.Node(cmds.createNode("transform", name=TEST_NODES[2]))
- self.m = noca.Node(cmds.createNode("holdMatrix", name=TEST_NODES[3]))
-
- def test_matrix_distance(self):
-
- node_data = noca.OPERATORS["matrix_distance"]
- node_type = node_data.get("node", None)
- node_inputs = node_data.get("inputs", None)
-
- input_plugs = [
- self.a.worldMatrix,
- self.b.worldMatrix,
- ]
-
- result = noca.Op.matrix_distance(*input_plugs)
- self.c.tx = result
-
- # Test that the created node is of the correct type
- self.assertEqual(cmds.nodeType(result.node), node_type)
-
- for node_input, desired_input in zip(node_inputs, input_plugs):
- if isinstance(node_input, (tuple, list)):
- node_input = node_input[0]
- # Check the correct plug is connected into the input-plug
- input_connections = cmds.listConnections(
- "{}.{}".format(result.node, node_input),
- plugs=True
- )
- self.assertEqual(input_connections, desired_input.plugs)
-
- # Test that the outputs are correct
- plug_connected_to_output = cmds.listConnections(result.plugs, plugs=True)[0]
- self.assertEqual(plug_connected_to_output, "{}.translateX".format(TEST_NODES[2]))
-
- def test_compose_matrix(self):
- node_data = noca.OPERATORS["compose_matrix"]
- node_type = node_data.get("node", None)
- node_inputs = node_data.get("inputs", None)
-
- input_plugs = [
- self.a.translateX,
- self.b.rotateX,
- self.a.scaleX,
- self.b.translateX,
- ]
-
- result = noca.Op.compose_matrix(
- translate=input_plugs[0],
- rotate=input_plugs[1],
- scale=input_plugs[2],
- shear=input_plugs[3],
- )
- self.m.inMatrix = result
-
- # Test that the created node is of the correct type
- self.assertEqual(cmds.nodeType(result.node), node_type)
-
- for node_input, desired_input in zip(node_inputs, input_plugs):
- if isinstance(node_input, (tuple, list)):
- node_input = node_input[0]
- # Check the correct plug is connected into the input-plug
- input_connections = cmds.listConnections(
- "{}.{}".format(result.node, node_input),
- plugs=True
- )
- self.assertEqual(input_connections, desired_input.plugs)
-
- # Test that the outputs are correct
- plug_connected_to_output = cmds.listConnections(result.plugs, plugs=True)[0]
- self.assertEqual(plug_connected_to_output, "{}.inMatrix".format(TEST_NODES[3]))
-
- def test_decompose_matrix(self):
- node_data = noca.OPERATORS["decompose_matrix"]
- node_type = node_data.get("node", None)
- node_inputs = node_data.get("inputs", None)
-
- input_plugs = [self.a.worldMatrix]
-
- result = noca.Op.decompose_matrix(input_plugs[0])
- self.c.translateX = result[0][0]
-
- # Check that result is an NcNode
- self.assertTrue(isinstance(result, noca.NcNode))
-
- # Test that the created node is of the correct type
- self.assertEqual(cmds.nodeType(result[0].node), node_type)
-
- for node_input, desired_input in zip(node_inputs, input_plugs):
- if isinstance(node_input, (tuple, list)):
- node_input = node_input[0]
- # Check the correct plug is connected into the input-plug
- input_connections = cmds.listConnections(
- "{}.{}".format(result[0].node, node_input),
- plugs=True
- )
- self.assertEqual(input_connections, desired_input.plugs)
-
- # Test that the outputs are correctly connected
- plug_connected_to_output = cmds.listConnections(result[0].plugs[0], plugs=True)[0]
- self.assertEqual(plug_connected_to_output, "{}.translateX".format(TEST_NODES[2]))
-
- def test_point_matrix_mult(self):
- node_data = noca.OPERATORS["point_matrix_mult"]
- node_type = node_data.get("node", None)
- node_inputs = node_data.get("inputs", None)
-
- input_plugs = [
- self.a.translateX,
- self.m.inMatrix,
- ]
-
- result = noca.Op.point_matrix_mult(*input_plugs)
- self.c.tx = result
-
- # Test that the created node is of the correct type
- self.assertEqual(cmds.nodeType(result.node), node_type)
-
- for node_input, desired_input in zip(node_inputs, input_plugs):
- if isinstance(node_input, (tuple, list)):
- node_input = node_input[0]
- # Check the correct plug is connected into the input-plug
- input_connections = cmds.listConnections(
- "{}.{}".format(result.node, node_input),
- plugs=True
- )
- self.assertEqual(input_connections, desired_input.plugs)
-
- # Test that the outputs are correct
- plug_connected_to_output = cmds.listConnections(result.plugs, plugs=True)[0]
- self.assertEqual(plug_connected_to_output, "{}.translateX".format(TEST_NODES[2]))
-
- def test_pair_blend(self):
- node_data = noca.OPERATORS["pair_blend"]
- node_type = node_data.get("node", None)
- node_inputs = node_data.get("inputs", None)
-
- input_plugs = [
- self.a.translate,
- self.a.rotate,
- self.b.translate,
- self.b.rotate,
- ]
-
- result = noca.Op.pair_blend(*input_plugs)
- self.c.t = result
- self.c.r = result.outRotate
-
- # Check that result is an NcNode
- self.assertTrue(isinstance(result, noca.NcNode))
-
- # Test that the created node is of the correct type
- self.assertEqual(cmds.nodeType(result.node), node_type)
-
- # Check the correct plug is connected into the input-plug
- input_translate1 = cmds.listConnections(
- "{}.inTranslate1".format(result.node), plugs=True
- )
- self.assertEqual(input_translate1, ["A.translate"])
-
- input_translate2 = cmds.listConnections(
- "{}.inTranslate2".format(result.node), plugs=True
- )
- self.assertEqual(input_translate2, ["B.translate"])
-
- input_rotate1 = cmds.listConnections(
- "{}.inRotate1".format(result.node), plugs=True
- )
- self.assertEqual(input_rotate1, ["A.rotate"])
-
- input_rotate2 = cmds.listConnections(
- "{}.inRotate2".format(result.node), plugs=True
- )
- self.assertEqual(input_rotate2, ["B.rotate"])
-
- # Test that the outputs are correct
- plug_connected_to_output = cmds.listConnections(
- "{}.outTranslate".format(result.node), plugs=True
- )[0]
- self.assertEqual(plug_connected_to_output, "{}.translate".format(TEST_NODES[2]))
- plug_connected_to_output = cmds.listConnections(
- "{}.outRotate".format(result.node), plugs=True
- )[0]
- self.assertEqual(plug_connected_to_output, "{}.rotate".format(TEST_NODES[2]))
+ # Create standard nodes on which to perform the operations on.
+ # Transform test nodes
+ self.tf_out_a = noca.create_node("transform", name="transform_out_A")
+ self.tf_out_b = noca.create_node("transform", name="transform_out_B")
+ self.tf_in_a = noca.create_node("transform", name="transform_in_A")
+ self.tf_in_b = noca.create_node("transform", name="transform_in_B")
+
+ # Matrix test nodes
+ self.mat_out_a = noca.create_node("holdMatrix", name="matrix_out_A")
+ self.mat_out_b = noca.create_node("holdMatrix", name="matrix_out_B")
+ self.mat_in_a = noca.create_node("holdMatrix", name="matrix_in_A")
+
+ # Quaternion test nodes
+ self.quat_out_a = noca.create_node("eulerToQuat", name="quaternion_out_A")
+ self.quat_out_b = noca.create_node("eulerToQuat", name="quaternion_out_B")
+ self.quat_in_a = noca.create_node("quatNormalize", name="quaternion_in_A")
+
+ # Mesh, curve and nurbsSurface test nodes
+ self.mesh_out_a = noca.create_node("mesh", name="mesh_out_A")
+ self.mesh_shape_out_a = noca.Node("mesh_out_AShape")
+ self.curve_out_a = noca.create_node("nurbsCurve", name="curve_out_A")
+ self.curve_shape_out_a = noca.Node("curve_out_AShape")
+ self.surface_out_a = noca.create_node("nurbsSurface", name="surface_out_A")
+ self.surface_shape_out_a = noca.Node("surface_out_AShape")
def test_for_every_operator(self):
"""