From a457e767992b72fe5f584fe1266a074555e8b94d Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Mon, 24 Dec 2018 00:39:49 -0800 Subject: [PATCH 01/17] core.py: Changed logging level for set/connect of >3D plugs from warn to info. --- node_calculator/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 ) From c5cd9a8425b6cf06a4f3e6e8ffd93f405a98e582 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Mon, 24 Dec 2018 00:42:04 -0800 Subject: [PATCH 02/17] Added various Operators: wtAddMatrix, pointOnCurveInfo, passMatrix, pointOnSurfaceInfo, rgbToHsv, closestPointOnSurface, holdMatrix, quatToEuler, remapHsv, nearestPointOnCurve, quatSub, eulerToQuat, quatProd, fourByFourMatrix1, remapColor, closestPointOnMesh, quatNegate, quatNormalize, quatConjugate, quatAdd, quatInvert --- node_calculator/base_operators.py | 1219 +++++++++++++++++++++++++++-- 1 file changed, 1161 insertions(+), 58 deletions(-) diff --git a/node_calculator/base_operators.py b/node_calculator/base_operators.py index ca51945..06aeeb5 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,164 @@ ], }, + "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": [ + ["input1QuatX", "input1QuatY", "input1QuatZ", "input1QuatW"], + ["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 +487,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 +522,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, }, } @@ -458,50 +752,121 @@ 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 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(**kwargs): + """Create composeMatrix-node to assemble matrix from transforms. + + 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 + + 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) + """ + # 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 compose_matrix_node @noca_op @@ -648,6 +1013,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 +1054,125 @@ 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. @@ -749,6 +1256,43 @@ def mult_matrix(*attrs): 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. @@ -830,6 +1374,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) + + @noca_op def point_matrix_mult(in_vector, in_matrix, vector_multiply=False): """Create pointMatrixMult-node to transpose the given matrix. @@ -862,6 +1426,274 @@ def point_matrix_mult(in_vector, in_matrix, vector_multiply=False): return created_node +@noca_op +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.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. + 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): """Raise attr_a to the power of attr_b. @@ -881,6 +1713,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 + ) + + all_value_lists = [values_red, values_green, values_blue] + for values, color in zip(all_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 + ) + + all_value_lists = [values_hue, values_saturation, values_value] + for values, setting in zip(all_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 +1907,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 +1974,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, @@ -990,6 +2045,24 @@ def set_range( return return_value +@noca_op +def sum(*attrs): + """Create plusMinusAverage-node for averaging input attrs. + + Args: + attrs (NcNode or NcAttrs or string or list): Inputs to be averaged + + Returns: + NcNode: Instance with plusMinusAverage-node and output-attribute(s) + + Example: + :: + + Op.average(Node("pCube.t"), [1, 2, 3]) + """ + return _create_operation_node("sum", attrs) + + @noca_op def sqrt(attr_a): """Get the square root of attr_a. @@ -1024,3 +2097,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) From 0545982b1d0e942373d1c384005a44bb0dc83420 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Mon, 24 Dec 2018 00:42:25 -0800 Subject: [PATCH 03/17] Added temporary note for todo-tests. --- tests/run_tests.bat | 2 +- tests/test_op.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) 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_op.py b/tests/test_op.py index c44fb68..288d7d7 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -28,6 +28,8 @@ "inverse_matrix", "mult_matrix", "transpose_matrix", + "weighted_add_matrix", + "hold_matrix", ] IRREGULAR_OPERATORS = [ @@ -38,6 +40,25 @@ "pair_blend", ] +UNTESTED_OPERATORS = [ + "pass_matrix", + "closest_point_on_mesh", + "closest_point_on_surface", + "euler_to_quat", + "four_by_four_matrix", + "nearest_point_on_curve", + "point_on_curve_info", + "point_on_surface_info", + "quat_add", + "quat_conjugate", + "quat_invert", + "quat_mul", + "quat_negate", + "quat_normalize", + "quat_sub", + "quat_to_euler", +] + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TESTS From 4909d1da085ab77b070a3b9fecba590f3b50ea29 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 16:01:44 -0800 Subject: [PATCH 04/17] Made pair_blend, sum and point_matrix_mult more generic. --- tests/test_op.py | 223 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 181 insertions(+), 42 deletions(-) diff --git a/tests/test_op.py b/tests/test_op.py index 288d7d7..642192f 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -13,6 +13,9 @@ # Local imports from base import BaseTestCase import node_calculator.core as noca +from node_calculator import om_util + +import unittest # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GLOBALS @@ -38,9 +41,8 @@ "compose_matrix", "point_matrix_mult", "pair_blend", -] -UNTESTED_OPERATORS = [ + "sum", "pass_matrix", "closest_point_on_mesh", "closest_point_on_surface", @@ -59,6 +61,22 @@ "quat_to_euler", ] +UNTESTED_OPERATORS = [ +] + + +def expand_array_attributes(node_inputs, input_plugs): + 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 + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TESTS @@ -132,7 +150,6 @@ def test(self): node_inputs = node_data.get("inputs", None) 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"]: @@ -362,17 +379,58 @@ def test_decompose_matrix(self): 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])) + node_data = noca.OPERATORS["point_matrix_mult"] node_type = node_data.get("node", None) node_inputs = node_data.get("inputs", None) + node_outputs = node_data.get("outputs", None) input_plugs = [ - self.a.translateX, + self.a.translate, self.m.inMatrix, ] + output_plugs = [ + self.c.translate, + ] + node_inputs = expand_array_attributes(node_inputs, input_plugs) - result = noca.Op.point_matrix_mult(*input_plugs) - self.c.tx = result + # Perform operation + results = noca.Op.point_matrix_mult(*input_plugs) + if not isinstance(results, (list, tuple)): + results = [results] + for output_plug, result in zip(output_plugs, results): + output_plug.attrs = result + + # 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) @@ -380,21 +438,96 @@ def test_point_matrix_mult(self): for node_input, desired_input in zip(node_inputs, input_plugs): if isinstance(node_input, (tuple, list)): node_input = node_input[0] + + plug = "{}.{}".format(result.node, node_input) + 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( - "{}.{}".format(result.node, node_input), - plugs=True - ) + input_connections = cmds.listConnections(plug, plugs=True, skipConversionNodes=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])) + for node_output, desired_output in zip(node_outputs, output_plugs): + if isinstance(node_output, (tuple, list)): + node_output = node_output[0] + + plug = "{}.{}".format(result.node, node_output) + 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) def test_pair_blend(self): - node_data = noca.OPERATORS["pair_blend"] + operation = "pair_blend" + input_plugs = [ + self.a.translate, + self.a.rotate, + self.b.translate, + self.b.rotate, + ] + output_plugs = [ + self.c.translate, + self.c.rotate, + ] + + node_data = noca.OPERATORS[operation] node_type = node_data.get("node", None) node_inputs = node_data.get("inputs", None) + node_outputs = node_data.get("outputs", None) + node_inputs = expand_array_attributes(node_inputs, input_plugs) + + # Perform operation + results = noca.Op.__getattribute__(operation)(*input_plugs, return_all_outputs=True) + if not isinstance(results, (list, tuple)): + results = [results] + for output_plug, result in zip(output_plugs, results): + output_plug.attrs = result + + # 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) + + for node_input, desired_input in zip(node_inputs, input_plugs): + if isinstance(node_input, (tuple, list)): + node_input = node_input[0] + + plug = "{}.{}".format(result.node, node_input) + 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(plug, plugs=True, skipConversionNodes=True) + self.assertEqual(input_connections, desired_input.plugs) + + # Test that the outputs are correct + for node_output, desired_output in zip(node_outputs, output_plugs): + if isinstance(node_output, (tuple, list)): + node_output = node_output[0] + + plug = "{}.{}".format(result.node, node_output) + 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) + + def test_sum(self): + node_data = noca.OPERATORS["sum"] + node_type = node_data.get("node", None) + node_inputs = node_data.get("inputs", None) + node_outputs = node_data.get("outputs", None) input_plugs = [ self.a.translate, @@ -402,10 +535,18 @@ def test_pair_blend(self): self.b.translate, self.b.rotate, ] + output_plugs = [ + self.c.translate, + self.c.rotate, + ] + node_inputs = expand_array_attributes(node_inputs, input_plugs) - result = noca.Op.pair_blend(*input_plugs) - self.c.t = result - self.c.r = result.outRotate + # Perform operation + results = noca.Op.sum(*input_plugs) + if not isinstance(results, (list, tuple)): + results = [results] + for output_plug, result in zip(output_plugs, results): + output_plug.attrs = result # Check that result is an NcNode self.assertTrue(isinstance(result, noca.NcNode)) @@ -413,37 +554,35 @@ def test_pair_blend(self): # 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"]) + for node_input, desired_input in zip(node_inputs, input_plugs): + if isinstance(node_input, (tuple, list)): + node_input = node_input[0] - input_rotate1 = cmds.listConnections( - "{}.inRotate1".format(result.node), plugs=True - ) - self.assertEqual(input_rotate1, ["A.rotate"]) + plug = "{}.{}".format(result.node, node_input) + mplug = om_util.get_mplug_of_plug(plug) + parent_plug = om_util.get_parent_mplug(mplug) + if parent_plug: + plug = parent_plug - input_rotate2 = cmds.listConnections( - "{}.inRotate2".format(result.node), plugs=True - ) - self.assertEqual(input_rotate2, ["B.rotate"]) + # Check the correct plug is connected into the input-plug + input_connections = cmds.listConnections(plug, plugs=True, skipConversionNodes=True) + self.assertEqual(input_connections, desired_input.plugs) # 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])) + for node_output, desired_output in zip(node_outputs, output_plugs): + if isinstance(node_output, (tuple, list)): + node_output = node_output[0] + + plug = "{}.{}".format(result.node, node_output) + 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) + @unittest.skip("TEMPORARY SKIP!") def test_for_every_operator(self): """ Make sure every operator has its individual test implemented. From d8a17beda635f9b430034e5456e276eaf370bb8f Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 20:55:24 -0800 Subject: [PATCH 05/17] Fixed quat_to_euler() inputs, fixed pass_matrix(), choice() accepts non-list now, changed compose_matrix()-kwargs setup. --- node_calculator/base_operators.py | 58 +++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/node_calculator/base_operators.py b/node_calculator/base_operators.py index 06aeeb5..358e406 100644 --- a/node_calculator/base_operators.py +++ b/node_calculator/base_operators.py @@ -436,7 +436,7 @@ "quat_to_euler": { "node": "quatToEuler", "inputs": [ - ["input1QuatX", "input1QuatY", "input1QuatZ", "input1QuatW"], + ["inputQuatX", "inputQuatY", "inputQuatZ", "inputQuatW"], ["inputRotateOrder"], ], "outputs": [ @@ -724,6 +724,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 @@ -823,18 +826,30 @@ def closest_point_on_surface( @noca_op -def compose_matrix(**kwargs): +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: - 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 + 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) @@ -848,13 +863,18 @@ def compose_matrix(**kwargs): decomp_b = Op.decompose_matrix(in_b.worldMatrix) Op.compose_matrix(r=decomp_a.outputRotate, s=decomp_b.outputScale) """ - # 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) + 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", @@ -1391,7 +1411,7 @@ def pass_matrix(matrix, scale=1): Op.pass_matrix(Node("pCube1.worldMatrix")) """ - return _create_operation_node("pass_matrix", matrix) + return _create_operation_node("pass_matrix", matrix, scale) @noca_op From dab7eb5bea8ae9116b10e681bd95065a7e5d7f9c Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 20:56:50 -0800 Subject: [PATCH 06/17] Added default values to docStrings of various operators. Slight PEP8 fixes. --- node_calculator/base_functions.py | 3 +- node_calculator/base_operators.py | 103 ++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 36 deletions(-) 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 358e406..235bde5 100644 --- a/node_calculator/base_operators.py +++ b/node_calculator/base_operators.py @@ -346,9 +346,17 @@ ["normalX", "normalY", "normalZ"], ["normalizedNormalX", "normalizedNormalY", "normalizedNormalZ"], ["tangentUx", "tangentUy", "tangentUz"], - ["normalizedTangentUX", "normalizedTangentUY", "normalizedTangentUZ"], + [ + "normalizedTangentUX", + "normalizedTangentUY", + "normalizedTangentUZ", + ], ["tangentVx", "tangentVy", "tangentVz"], - ["normalizedTangentVX", "normalizedTangentVY", "normalizedTangentVZ"], + [ + "normalizedTangentVX", + "normalizedTangentVY", + "normalizedTangentVZ", + ], ], "output_is_predetermined": True, }, @@ -639,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) @@ -686,7 +694,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 @@ -708,9 +716,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) @@ -739,9 +747,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) @@ -964,10 +972,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) @@ -1017,10 +1026,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) @@ -1075,7 +1085,11 @@ def exp(attr_a): @noca_op -def four_by_four_matrix(vector_a=None, vector_b=None, vector_c=None, translate=None): +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: @@ -1119,7 +1133,10 @@ def four_by_four_matrix(vector_a=None, vector_b=None, vector_c=None, translate=N ) 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] + 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. @@ -1216,8 +1233,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 @@ -1237,6 +1255,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 @@ -1319,7 +1339,8 @@ def normalize_vector(in_vector, normalize=True): 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) @@ -1352,16 +1373,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. @@ -1422,7 +1449,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) @@ -1635,7 +1662,7 @@ def quat_to_euler(quat_a, rotate_order=0): @noca_op def point_on_curve_info( curve, - parameter=0.0, + parameter=0, as_percentage=False, return_all_outputs=False): """Get curve data from a particular point on a curve. @@ -1644,6 +1671,7 @@ def point_on_curve_info( 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. @@ -1715,12 +1743,13 @@ def point_on_surface_info( @noca_op -def pow(attr_a, attr_b): +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) @@ -1788,8 +1817,8 @@ def remap_color( "remap_color", attr_a, output_min, output_max, input_min, input_max ) - all_value_lists = [values_red, values_green, values_blue] - for values, color in zip(all_value_lists, ["red", "green", "blue"]): + 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 @@ -1879,8 +1908,8 @@ def remap_hsv( "remap_hsv", attr_a, output_min, output_max, input_min, input_max ) - all_value_lists = [values_hue, values_saturation, values_value] - for values, setting in zip(all_value_lists, ["hue", "saturation", "value"]): + 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 @@ -2040,11 +2069,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) From dcbd32605628dc0f3a8970023775044b36f51874 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 20:58:09 -0800 Subject: [PATCH 07/17] Made Operator tests more generic. Inputs/Outputs are defined through dictionary. --- tests/test_op.py | 807 ++++++++++++++++++++++------------------------- 1 file changed, 377 insertions(+), 430 deletions(-) diff --git a/tests/test_op.py b/tests/test_op.py index 642192f..63813df 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -15,57 +15,233 @@ import node_calculator.core as noca from node_calculator import om_util -import unittest # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GLOBALS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -TEST_NODES = [ - "A", - "B", - "C", - "M", -] -MATRIX_OPERATORS = [ - "inverse_matrix", - "mult_matrix", - "transpose_matrix", - "weighted_add_matrix", - "hold_matrix", -] IRREGULAR_OPERATORS = [ - "decompose_matrix", - "matrix_distance", - "compose_matrix", - "point_matrix_mult", - "pair_blend", - - "sum", - "pass_matrix", - "closest_point_on_mesh", - "closest_point_on_surface", - "euler_to_quat", - "four_by_four_matrix", - "nearest_point_on_curve", - "point_on_curve_info", - "point_on_surface_info", - "quat_add", - "quat_conjugate", - "quat_invert", - "quat_mul", - "quat_negate", - "quat_normalize", - "quat_sub", - "quat_to_euler", ] -UNTESTED_OPERATORS = [ -] + +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]): @@ -78,18 +254,77 @@ def expand_array_attributes(node_inputs, input_plugs): 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): @@ -99,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)) @@ -110,78 +345,53 @@ 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) - # 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 try: @@ -189,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 @@ -246,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 @@ -257,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) @@ -275,314 +505,31 @@ 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])) - - node_data = noca.OPERATORS["point_matrix_mult"] - node_type = node_data.get("node", None) - node_inputs = node_data.get("inputs", None) - node_outputs = node_data.get("outputs", None) - - input_plugs = [ - self.a.translate, - self.m.inMatrix, - ] - output_plugs = [ - self.c.translate, - ] - node_inputs = expand_array_attributes(node_inputs, input_plugs) - - # Perform operation - results = noca.Op.point_matrix_mult(*input_plugs) - if not isinstance(results, (list, tuple)): - results = [results] - for output_plug, result in zip(output_plugs, results): - output_plug.attrs = result - - # 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) - - for node_input, desired_input in zip(node_inputs, input_plugs): - if isinstance(node_input, (tuple, list)): - node_input = node_input[0] - - plug = "{}.{}".format(result.node, node_input) - 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(plug, plugs=True, skipConversionNodes=True) - self.assertEqual(input_connections, desired_input.plugs) - - # Test that the outputs are correct - for node_output, desired_output in zip(node_outputs, output_plugs): - if isinstance(node_output, (tuple, list)): - node_output = node_output[0] - - plug = "{}.{}".format(result.node, node_output) - 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) - - def test_pair_blend(self): - operation = "pair_blend" - input_plugs = [ - self.a.translate, - self.a.rotate, - self.b.translate, - self.b.rotate, - ] - output_plugs = [ - self.c.translate, - self.c.rotate, - ] - - node_data = noca.OPERATORS[operation] - node_type = node_data.get("node", None) - node_inputs = node_data.get("inputs", None) - node_outputs = node_data.get("outputs", None) - node_inputs = expand_array_attributes(node_inputs, input_plugs) - - # Perform operation - results = noca.Op.__getattribute__(operation)(*input_plugs, return_all_outputs=True) - if not isinstance(results, (list, tuple)): - results = [results] - for output_plug, result in zip(output_plugs, results): - output_plug.attrs = result - - # 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) - - for node_input, desired_input in zip(node_inputs, input_plugs): - if isinstance(node_input, (tuple, list)): - node_input = node_input[0] - - plug = "{}.{}".format(result.node, node_input) - 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(plug, plugs=True, skipConversionNodes=True) - self.assertEqual(input_connections, desired_input.plugs) - - # Test that the outputs are correct - for node_output, desired_output in zip(node_outputs, output_plugs): - if isinstance(node_output, (tuple, list)): - node_output = node_output[0] - - plug = "{}.{}".format(result.node, node_output) - 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) - - def test_sum(self): - node_data = noca.OPERATORS["sum"] - node_type = node_data.get("node", None) - node_inputs = node_data.get("inputs", None) - node_outputs = node_data.get("outputs", None) - - input_plugs = [ - self.a.translate, - self.a.rotate, - self.b.translate, - self.b.rotate, - ] - output_plugs = [ - self.c.translate, - self.c.rotate, - ] - node_inputs = expand_array_attributes(node_inputs, input_plugs) - - # Perform operation - results = noca.Op.sum(*input_plugs) - if not isinstance(results, (list, tuple)): - results = [results] - for output_plug, result in zip(output_plugs, results): - output_plug.attrs = result - - # 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) - - for node_input, desired_input in zip(node_inputs, input_plugs): - if isinstance(node_input, (tuple, list)): - node_input = node_input[0] - - plug = "{}.{}".format(result.node, node_input) - 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(plug, plugs=True, skipConversionNodes=True) - self.assertEqual(input_connections, desired_input.plugs) - - # Test that the outputs are correct - for node_output, desired_output in zip(node_outputs, output_plugs): - if isinstance(node_output, (tuple, list)): - node_output = node_output[0] - - plug = "{}.{}".format(result.node, node_output) - 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) + # 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") - @unittest.skip("TEMPORARY SKIP!") def test_for_every_operator(self): """ Make sure every operator has its individual test implemented. From a000bc87aba6834f89a806a0d3720a7b9c123fc4 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 21:04:15 -0800 Subject: [PATCH 08/17] Adjusted docStrings for Sphinx. --- node_calculator/base_operators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node_calculator/base_operators.py b/node_calculator/base_operators.py index 235bde5..d549d78 100644 --- a/node_calculator/base_operators.py +++ b/node_calculator/base_operators.py @@ -843,6 +843,7 @@ def compose_matrix( 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. @@ -1115,6 +1116,7 @@ def four_by_four_matrix( Example: :: + cube = Node("pCube1") vec_a = Op.point_matrix_mult( [1, 0, 0], @@ -1131,7 +1133,6 @@ def four_by_four_matrix( cube.worldMatrix, vector_multiply=True ) - out = Op.four_by_four_matrix( vector_a=vec_a, vector_b=vec_b, From 9badd3ce5c8deffb491a6daf1c02d87202ae5d4b Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 21:50:27 -0800 Subject: [PATCH 09/17] Renamed testClasses to reflect their usecase better. --- tests/test_nc_attrs.py | 4 ++-- tests/test_nc_node.py | 4 ++-- tests/test_om_util.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) 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_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" From daeb5ec8fc5eb579f1727bd6019ced0ab9cf7216 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 21:51:03 -0800 Subject: [PATCH 10/17] Added tests for non-unique names and accessing shape attributes. --- tests/test_node_calculator.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) 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']) From 0204648582b748976ece65da35bfc481c1106124 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:08:36 -0800 Subject: [PATCH 11/17] sum(), average() and mult_matrix() deal with lists as args correctly now. --- node_calculator/base_operators.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/node_calculator/base_operators.py b/node_calculator/base_operators.py index d549d78..3208e39 100644 --- a/node_calculator/base_operators.py +++ b/node_calculator/base_operators.py @@ -671,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) @@ -681,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) @@ -1277,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) @@ -1294,6 +1299,9 @@ 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) @@ -2104,7 +2112,8 @@ def sum(*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 added up. Returns: NcNode: Instance with plusMinusAverage-node and output-attribute(s) @@ -2114,6 +2123,9 @@ def sum(*attrs): Op.average(Node("pCube.t"), [1, 2, 3]) """ + if len(attrs) == 1: + attrs = attrs[0] + return _create_operation_node("sum", attrs) From a94c5b6b78a1c2cb86fe6f3e4f85af9170cb5ea9 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:09:13 -0800 Subject: [PATCH 12/17] Updated changelog for Release 2.1.2. --- docs/changes.rst | 69 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 15 deletions(-) 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. From 16ee99abb2614cc02b93c62e52e497d4a1adf260 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:18:58 -0800 Subject: [PATCH 13/17] Unifying formatting of circleci yml. --- .circleci/config.yml | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b0f885..842e5df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,32 +8,32 @@ defaults: &defaults working_directory: ~/node_calculator default-run: &default-run - steps: - - checkout - - restore-cache: &d2-restore-cache - keys: - - a1-dependencies-{{ checksum "requirements.txt" }} - - a1-dependencies- - - run: - name: install git - command: yum -y install git - - run: &d-set-up-mayapy - name: set up mayapy and install deps - command: make install-deps - - save-cache: &d2-save-cache - paths: - - ~/venv/ - key: a1-dependencies-{{ checksum "requirements.txt" }} - - run: &run-tests - name: run tests - command: | - echo 'export MAYA_MODULE_PATH =~/cmt:$MAYA_MODULE_PATH' >> $BASH_ENV - make test-unit - - store_test_results: &store-results - path: ~/test-results - - store_artifacts: &store-artifacts - path: ~/.node_calculator - destination: ~/test-results + steps: + - checkout + - restore-cache: &d2-restore-cache + keys: + - a1-dependencies-{{ checksum "requirements.txt" }} + - a1-dependencies- + - run: + name: install git + command: yum -y install git + - run: &d-set-up-mayapy + name: set up mayapy and install deps + command: make install-deps + - save-cache: &d2-save-cache + paths: + - ~/venv/ + key: a1-dependencies-{{ checksum "requirements.txt" }} + - run: &run-tests + name: run tests + command: | + echo 'export MAYA_MODULE_PATH =~/cmt:$MAYA_MODULE_PATH' >> $BASH_ENV + make test-unit + - store_test_results: &store-results + path: ~/test-results + - store_artifacts: &store-artifacts + path: ~/.node_calculator + destination: ~/test-results jobs: From 22c4ccee3836969f8ad0969c8d9cc4e001ba7e33 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:24:19 -0800 Subject: [PATCH 14/17] Removing empty build from jobs. --- .circleci/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 842e5df..8ff482d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -37,8 +37,6 @@ default-run: &default-run jobs: - build: - maya2018: docker: - image: daemonecles/anvil:maya2018 From 1d1a6fe41b6af6a7eff1226762075e6709fb4b25 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:26:17 -0800 Subject: [PATCH 15/17] Renaming back to build. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8ff482d..ecea0e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -37,7 +37,7 @@ default-run: &default-run jobs: - maya2018: + build: docker: - image: daemonecles/anvil:maya2018 <<: *defaults From 9fb78f1b0fba98f06716ce584ad4630f5d4472a4 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:28:07 -0800 Subject: [PATCH 16/17] Formatting indentation again.. --- .circleci/config.yml | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ecea0e0..9b5ac5f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,32 +8,32 @@ defaults: &defaults working_directory: ~/node_calculator default-run: &default-run - steps: - - checkout - - restore-cache: &d2-restore-cache - keys: - - a1-dependencies-{{ checksum "requirements.txt" }} - - a1-dependencies- - - run: - name: install git - command: yum -y install git - - run: &d-set-up-mayapy - name: set up mayapy and install deps - command: make install-deps - - save-cache: &d2-save-cache - paths: - - ~/venv/ - key: a1-dependencies-{{ checksum "requirements.txt" }} - - run: &run-tests - name: run tests - command: | - echo 'export MAYA_MODULE_PATH =~/cmt:$MAYA_MODULE_PATH' >> $BASH_ENV - make test-unit - - store_test_results: &store-results - path: ~/test-results - - store_artifacts: &store-artifacts - path: ~/.node_calculator - destination: ~/test-results + steps: + - checkout + - restore-cache: &d2-restore-cache + keys: + - a1-dependencies-{{ checksum "requirements.txt" }} + - a1-dependencies- + - run: + name: install git + command: yum -y install git + - run: &d-set-up-mayapy + name: set up mayapy and install deps + command: make install-deps + - save-cache: &d2-save-cache + paths: + - ~/venv/ + key: a1-dependencies-{{ checksum "requirements.txt" }} + - run: &run-tests + name: run tests + command: | + echo 'export MAYA_MODULE_PATH =~/cmt:$MAYA_MODULE_PATH' >> $BASH_ENV + make test-unit + - store_test_results: &store-results + path: ~/test-results + - store_artifacts: &store-artifacts + path: ~/.node_calculator + destination: ~/test-results jobs: From ef8b9c694a6f57e26d7dd7e2a2e56b5b6d77edc5 Mon Sep 17 00:00:00 2001 From: Mischa Kolbe Date: Sat, 29 Dec 2018 22:29:04 -0800 Subject: [PATCH 17/17] More indentation adjustment.... --- .circleci/config.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b5ac5f..e2d646c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,29 +11,29 @@ default-run: &default-run steps: - checkout - restore-cache: &d2-restore-cache - keys: - - a1-dependencies-{{ checksum "requirements.txt" }} - - a1-dependencies- + keys: + - a1-dependencies-{{ checksum "requirements.txt" }} + - a1-dependencies- - run: - name: install git - command: yum -y install git + name: install git + command: yum -y install git - run: &d-set-up-mayapy - name: set up mayapy and install deps - command: make install-deps + name: set up mayapy and install deps + command: make install-deps - save-cache: &d2-save-cache - paths: - - ~/venv/ - key: a1-dependencies-{{ checksum "requirements.txt" }} + paths: + - ~/venv/ + key: a1-dependencies-{{ checksum "requirements.txt" }} - run: &run-tests - name: run tests - command: | - echo 'export MAYA_MODULE_PATH =~/cmt:$MAYA_MODULE_PATH' >> $BASH_ENV - make test-unit + name: run tests + command: | + echo 'export MAYA_MODULE_PATH =~/cmt:$MAYA_MODULE_PATH' >> $BASH_ENV + make test-unit - store_test_results: &store-results - path: ~/test-results + path: ~/test-results - store_artifacts: &store-artifacts - path: ~/.node_calculator - destination: ~/test-results + path: ~/.node_calculator + destination: ~/test-results jobs: