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