diff --git a/python/ht/inline/api.py b/python/ht/inline/api.py index 7b6216ea..da6d9750 100644 --- a/python/ht/inline/api.py +++ b/python/ht/inline/api.py @@ -2663,10 +2663,9 @@ def get_multiparm_start_offset(parm): if not is_parm_multiparm(parm): raise ValueError("Parameter is not a multiparm.") - if isinstance(parm, hou.Parm): - parm = parm.tuple() + parm_template = parm.parmTemplate() - return _cpp_methods.getMultiParmStartOffset(parm.node(), parm.name()) + return int(parm_template.tags().get("multistartoffset", 1)) def get_multiparm_instance_index(parm): @@ -2778,6 +2777,88 @@ def get_multiparm_instance_values(parm): return tuple(all_values) +def eval_multiparm_instance(node, name, index): + """Evaluate a multiparm parameter by index. + + The name should include the # value which will be replaced by the index. + + The index should be the multiparm index, not including any start offset. + + This function raises an IndexError if the index exceeds the number of active + multiparm instances. + + Supports float, integer and string parameters which can also be tuples. + + You cannot try to evaluate a single component of a tuple parameter, evaluate + the entire tuple instead and get which values you need. + + # Float + >>> eval_multiparm_instance(node, "float#", 1) + 0.53 + # Float 3 + >>> eval_multiparm_instance(node, "vec#", 1) + (0.53, 1.0, 2.5) + + :param node: The node to evaluate the parameter on. + :type node: hou.Node + :param name: The base parameter name. + :type name: str + :param index: The multiparm index. + :type index: int + :return: The evaluated parameter value. + :rtype: type + + """ + if name.count('#') != 1: + raise ValueError("Name {} must contain a single '#' value".format(name)) + + ptg = node.parmTemplateGroup() + + parm_template = ptg.find(name) + + if parm_template is None: + raise ValueError("Name {} does not map to a parameter on {}".format(name, node.path())) + + containing_folder = ptg.containingFolder(name) + + folder_parm = node.parm(containing_folder.name()) + + if not is_parm_multiparm(folder_parm): + raise ValueError("Parameter is not inside a multiparm.") + + num_values = folder_parm.eval() + + # Check against the current number of available parms. + if index >= num_values: + raise IndexError("Invalid index {}".format(index)) + + # Need the start offset. + start_offset = get_multiparm_start_offset(folder_parm) + + data_type = parm_template.dataType() + + values = [] + + for component_index in range(parm_template.numComponents()): + if data_type == hou.parmData.Float: + values.append(_cpp_methods.eval_multiparm_instance_float(node, name, component_index, index, start_offset)) + + elif data_type == hou.parmData.Int: + values.append(_cpp_methods.eval_multiparm_instance_int(node, name, component_index, index, start_offset)) + + elif data_type == hou.parmData.String: + values.append(_cpp_methods.eval_multiparm_instance_string(node, name, component_index, index, start_offset)) + + else: + raise TypeError("Invalid parm data type {}".format(data_type)) + + # Return single value for non-tuple parms. + if len(values) == 1: + return values[0] + + return tuple(values) + + def disconnect_all_inputs(node): """Disconnect all of this node's inputs. diff --git a/python/ht/inline/lib.py b/python/ht/inline/lib.py index 4f0e582a..a96bc56e 100644 --- a/python/ht/inline/lib.py +++ b/python/ht/inline/lib.py @@ -1842,22 +1842,6 @@ } """, -""" -int -getMultiParmStartOffset(OP_Node *node, const char *parm_name) -{ - int offset; - - PRM_Parm *parm; - - PRM_Parm &multiparm = node->getParm(parm_name); - - offset = multiparm.getMultiStartOffset(); - - return offset; -} -""", - """ IntArray getMultiParmInstanceIndex(OP_Node *node, const char *parm_name) @@ -1924,6 +1908,51 @@ } """, +""" +float +eval_multiparm_instance_float(OP_Node *node, const char *parm_name, int component_index, int index, int start_offset) +{ + fpreal t = CHgetEvalTime(); + int instance_idx; + + UT_StringRef name(parm_name); + + instance_idx = index + start_offset; + return node->evalFloatInst(name, &instance_idx, component_index, t); +} +""", + +""" +int +eval_multiparm_instance_int(OP_Node *node, const char *parm_name, int component_index, int index, int start_offset) +{ + fpreal t = CHgetEvalTime(); + int instance_idx; + + UT_StringRef name(parm_name); + + instance_idx = index + start_offset; + return node->evalIntInst(name, &instance_idx, component_index, t); +} +""", + +""" +const char * +eval_multiparm_instance_string(OP_Node *node, const char *parm_name, int component_index, int index, int start_offset) +{ + fpreal t = CHgetEvalTime(); + int instance_idx; + + UT_StringRef name(parm_name); + UT_String value; + + instance_idx = index + start_offset; + node->evalStringInst(name, &instance_idx, value, component_index, t); + + return value; +} +""", + """ void buildLookatMatrix(UT_DMatrix3 *mat, diff --git a/tests/inline/test_api.py b/tests/inline/test_api.py index 3d146827..677f565d 100644 --- a/tests/inline/test_api.py +++ b/tests/inline/test_api.py @@ -14,8 +14,7 @@ # Python Imports import ctypes -from mock import MagicMock, PropertyMock, call, patch -import os +from mock import MagicMock, call, patch import unittest # Houdini Toolbox Imports @@ -4015,28 +4014,27 @@ def test_not_multiparm(self, mock_is_multiparm): with self.assertRaises(ValueError): api.get_multiparm_start_offset(mock_parm) - @patch("ht.inline.api._cpp_methods.getMultiParmStartOffset") @patch("ht.inline.api.is_parm_multiparm", return_value=True) - def test_parm(self, mock_is_multiparm, mock_get): - mock_parm_tuple = MagicMock(spec=hou.ParmTuple) + def test_default(self, mock_is_multiparm): + mock_template = MagicMock(spec=hou.ParmTemplate) + mock_template.tags.return_value = {} mock_parm = MagicMock(spec=hou.Parm) - mock_parm.tuple.return_value = mock_parm_tuple + mock_parm.parmTemplate.return_value = mock_template result = api.get_multiparm_start_offset(mock_parm) - self.assertEqual(result, mock_get.return_value) - - mock_get.assert_called_with(mock_parm_tuple.node.return_value, mock_parm_tuple.name.return_value) + self.assertEqual(result, 1) - @patch("ht.inline.api._cpp_methods.getMultiParmStartOffset") @patch("ht.inline.api.is_parm_multiparm", return_value=True) - def test_parm_tuple(self, mock_is_multiparm, mock_get): - mock_parm_tuple = MagicMock(spec=hou.ParmTuple) + def test_specific(self, mock_is_multiparm): + mock_template = MagicMock(spec=hou.ParmTemplate) + mock_template.tags.return_value = {"multistartoffset": "3"} - result = api.get_multiparm_start_offset(mock_parm_tuple) - self.assertEqual(result, mock_get.return_value) + mock_parm = MagicMock(spec=hou.Parm) + mock_parm.parmTemplate.return_value = mock_template - mock_get.assert_called_with(mock_parm_tuple.node.return_value, mock_parm_tuple.name.return_value) + result = api.get_multiparm_start_offset(mock_parm) + self.assertEqual(result, 3) class Test_get_multiparm_instance_index(unittest.TestCase): @@ -4243,6 +4241,287 @@ def test_parm_tuple(self, mock_is_multiparm, mock_get): mock_get.assert_called_with(mock_parm_tuple) +class Test_eval_multiparm_instance(unittest.TestCase): + """Test ht.inline.api.eval_multiparm_instance.""" + + def test_invalid_number_signs(self): + mock_node = MagicMock(spec=hou.Node) + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 2 + + mock_index = MagicMock(spec=int) + + with self.assertRaises(ValueError): + api.eval_multiparm_instance(mock_node, mock_name, mock_index) + + mock_name.count.assert_called_with('#') + + def test_invalid_parm_name(self): + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.find.return_value = None + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock(spec=int) + + with self.assertRaises(ValueError): + api.eval_multiparm_instance(mock_node, mock_name, mock_index) + + mock_name.count.assert_called_with('#') + + @patch("ht.inline.api.is_parm_multiparm", return_value=False) + def test_not_multiparm(self, mock_is_multiparm): + mock_parm = MagicMock(spec=hou.Parm) + + mock_template = MagicMock(spec=hou.ParmTemplate) + + mock_folder_template = MagicMock(spec=hou.FolderParmTemplate) + + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.containingFolder.return_value = mock_folder_template + mock_ptg.find.return_value = mock_template + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + mock_node.parm.return_value = mock_parm + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock(spec=int) + + with self.assertRaises(ValueError): + api.eval_multiparm_instance(mock_node, mock_name, mock_index) + + mock_name.count.assert_called_with('#') + + mock_ptg.containingFolder.assert_called_with(mock_name) + mock_node.parm.assert_called_with(mock_folder_template.name.return_value) + + mock_is_multiparm.assert_called_with(mock_parm) + + @patch("ht.inline.api.is_parm_multiparm", return_value=True) + def test_invalid_index(self, mock_is_multiparm): + mock_parm = MagicMock(spec=hou.Parm) + mock_parm.eval.return_value = MagicMock(spec=int) + + mock_template = MagicMock(spec=hou.ParmTemplate) + + mock_folder_template = MagicMock(spec=hou.FolderParmTemplate) + + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.containingFolder.return_value = mock_folder_template + mock_ptg.find.return_value = mock_template + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + mock_node.parm.return_value = mock_parm + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock() + mock_index.__ge__.return_value = True + + with self.assertRaises(IndexError): + api.eval_multiparm_instance(mock_node, mock_name, mock_index) + + mock_name.count.assert_called_with('#') + + mock_ptg.containingFolder.assert_called_with(mock_name) + mock_node.parm.assert_called_with(mock_folder_template.name.return_value) + + mock_is_multiparm.assert_called_with(mock_parm) + + mock_index.__ge__.assert_called_with(mock_parm.eval.return_value) + + @patch("ht.inline.api._cpp_methods.eval_multiparm_instance_float") + @patch("ht.inline.api.get_multiparm_start_offset") + @patch("ht.inline.api.is_parm_multiparm", return_value=True) + def test_float_single_component(self, mock_is_multiparm, mock_get, mock_eval): + mock_parm = MagicMock(spec=hou.Parm) + mock_parm.eval.return_value = MagicMock(spec=int) + + mock_template = MagicMock(spec=hou.ParmTemplate) + mock_template.dataType.return_value = hou.parmData.Float + mock_template.numComponents.return_value = 1 + + mock_folder_template = MagicMock(spec=hou.FolderParmTemplate) + + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.containingFolder.return_value = mock_folder_template + mock_ptg.find.return_value = mock_template + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + mock_node.parm.return_value = mock_parm + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock() + mock_index.__ge__.return_value = False + + result = api.eval_multiparm_instance(mock_node, mock_name, mock_index) + self.assertEqual(result, mock_eval.return_value) + + mock_name.count.assert_called_with('#') + + mock_ptg.containingFolder.assert_called_with(mock_name) + mock_node.parm.assert_called_with(mock_folder_template.name.return_value) + + mock_is_multiparm.assert_called_with(mock_parm) + + mock_index.__ge__.assert_called_with(mock_parm.eval.return_value) + + mock_get.assert_called_with(mock_parm) + + mock_eval.assert_called_with(mock_node, mock_name, 0, mock_index, mock_get.return_value) + + @patch("ht.inline.api._cpp_methods.eval_multiparm_instance_int") + @patch("ht.inline.api.get_multiparm_start_offset") + @patch("ht.inline.api.is_parm_multiparm", return_value=True) + def test_int_multiple_components(self, mock_is_multiparm, mock_get, mock_eval): + mock_parm = MagicMock(spec=hou.Parm) + mock_parm.eval.return_value = MagicMock(spec=int) + + mock_template = MagicMock(spec=hou.ParmTemplate) + mock_template.dataType.return_value = hou.parmData.Int + mock_template.numComponents.return_value = 2 + + mock_folder_template = MagicMock(spec=hou.FolderParmTemplate) + + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.containingFolder.return_value = mock_folder_template + mock_ptg.find.return_value = mock_template + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + mock_node.parm.return_value = mock_parm + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock() + mock_index.__ge__.return_value = False + + result = api.eval_multiparm_instance(mock_node, mock_name, mock_index) + self.assertEqual(result, (mock_eval.return_value, mock_eval.return_value)) + + mock_name.count.assert_called_with('#') + + mock_ptg.containingFolder.assert_called_with(mock_name) + mock_node.parm.assert_called_with(mock_folder_template.name.return_value) + + mock_is_multiparm.assert_called_with(mock_parm) + + mock_index.__ge__.assert_called_with(mock_parm.eval.return_value) + + mock_get.assert_called_with(mock_parm) + + mock_eval.assert_has_calls( + [ + call(mock_node, mock_name, 0, mock_index, mock_get.return_value), + call(mock_node, mock_name, 1, mock_index, mock_get.return_value), + ] + ) + + @patch("ht.inline.api._cpp_methods.eval_multiparm_instance_string") + @patch("ht.inline.api.get_multiparm_start_offset") + @patch("ht.inline.api.is_parm_multiparm", return_value=True) + def test_string_multiple_components(self, mock_is_multiparm, mock_get, mock_eval): + mock_parm = MagicMock(spec=hou.Parm) + mock_parm.eval.return_value = MagicMock(spec=int) + + mock_template = MagicMock(spec=hou.ParmTemplate) + mock_template.dataType.return_value = hou.parmData.String + mock_template.numComponents.return_value = 3 + + mock_folder_template = MagicMock(spec=hou.FolderParmTemplate) + + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.containingFolder.return_value = mock_folder_template + mock_ptg.find.return_value = mock_template + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + mock_node.parm.return_value = mock_parm + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock() + mock_index.__ge__.return_value = False + + result = api.eval_multiparm_instance(mock_node, mock_name, mock_index) + self.assertEqual(result, (mock_eval.return_value, mock_eval.return_value, mock_eval.return_value)) + + mock_name.count.assert_called_with('#') + + mock_ptg.containingFolder.assert_called_with(mock_name) + mock_node.parm.assert_called_with(mock_folder_template.name.return_value) + + mock_is_multiparm.assert_called_with(mock_parm) + + mock_index.__ge__.assert_called_with(mock_parm.eval.return_value) + + mock_get.assert_called_with(mock_parm) + + mock_eval.assert_has_calls( + [ + call(mock_node, mock_name, 0, mock_index, mock_get.return_value), + call(mock_node, mock_name, 1, mock_index, mock_get.return_value), + call(mock_node, mock_name, 2, mock_index, mock_get.return_value) + ] + ) + + @patch("ht.inline.api._cpp_methods.eval_multiparm_instance_string") + @patch("ht.inline.api.get_multiparm_start_offset") + @patch("ht.inline.api.is_parm_multiparm", return_value=True) + def test_invalid_type(self, mock_is_multiparm, mock_get, mock_eval): + mock_parm = MagicMock(spec=hou.Parm) + mock_parm.eval.return_value = MagicMock(spec=int) + + mock_template = MagicMock(spec=hou.ParmTemplate) + mock_template.dataType.return_value = hou.parmData.Data + mock_template.numComponents.return_value = 3 + + mock_folder_template = MagicMock(spec=hou.FolderParmTemplate) + + mock_ptg = MagicMock(spec=hou.ParmTemplateGroup) + mock_ptg.containingFolder.return_value = mock_folder_template + mock_ptg.find.return_value = mock_template + + mock_node = MagicMock(spec=hou.Node) + mock_node.parmTemplateGroup.return_value = mock_ptg + mock_node.parm.return_value = mock_parm + + mock_name = MagicMock(spec=str) + mock_name.count.return_value = 1 + + mock_index = MagicMock() + mock_index.__ge__.return_value = False + + with self.assertRaises(TypeError): + api.eval_multiparm_instance(mock_node, mock_name, mock_index) + + mock_name.count.assert_called_with('#') + + mock_ptg.containingFolder.assert_called_with(mock_name) + mock_node.parm.assert_called_with(mock_folder_template.name.return_value) + + mock_is_multiparm.assert_called_with(mock_parm) + + mock_index.__ge__.assert_called_with(mock_parm.eval.return_value) + + mock_get.assert_called_with(mock_parm) + + class Test_disconnect_all_inputs(unittest.TestCase): """Test ht.inline.api.disconnect_all_inputs.""" @@ -4893,4 +5172,3 @@ def test(self, mock_is_dummy): if __name__ == '__main__': unittest.main() - diff --git a/tests/inline/test_api_integration.py b/tests/inline/test_api_integration.py index 8fe26d74..2f3c0ebf 100755 --- a/tests/inline/test_api_integration.py +++ b/tests/inline/test_api_integration.py @@ -41,7 +41,7 @@ class TestInlineCpp(unittest.TestCase): @classmethod def setUpClass(cls): - hou.hipFile.load(os.path.join(THIS_DIR, "test_inline.hipnc")) + hou.hipFile.load(os.path.join(THIS_DIR, "test_inline.hipnc"), ignore_load_warnings=True) @classmethod def tearDownClass(cls): @@ -1614,6 +1614,30 @@ def test_get_multiparm_instance_values(self): self.assertEqual(values, target) + def test_eval_multiparm_instance(self): + node = OBJ.node("test_get_multiparm_instance_values/null1") + + # Ints + self.assertEqual(ht.inline.api.eval_multiparm_instance(node, "foo#", 0), 1) + self.assertEqual(ht.inline.api.eval_multiparm_instance(node, "foo#", 1), 5) + + with self.assertRaises(IndexError): + ht.inline.api.eval_multiparm_instance(node, "foo#", 2) + + # Floats + self.assertEqual(ht.inline.api.eval_multiparm_instance(node, "bar#", 0), (2.0, 3.0, 4.0)) + self.assertEqual(ht.inline.api.eval_multiparm_instance(node, "bar#", 1), (6.0, 7.0, 8.0)) + + with self.assertRaises(IndexError): + ht.inline.api.eval_multiparm_instance(node, "bar#", 2) + + # Strings + self.assertEqual(ht.inline.api.eval_multiparm_instance(node, "hello#", 0), "foo") + self.assertEqual(ht.inline.api.eval_multiparm_instance(node, "hello#", 1), "bar") + + with self.assertRaises(IndexError): + ht.inline.api.eval_multiparm_instance(node, "hello#", 2) + # ========================================================================= # NODES AND NODE TYPES # ========================================================================= diff --git a/tests/inline/test_inline.hipnc b/tests/inline/test_inline.hipnc index 41fb65d8..f646cea7 100644 Binary files a/tests/inline/test_inline.hipnc and b/tests/inline/test_inline.hipnc differ