diff --git a/src/Mod/Material/App/MaterialManagerPy.xml b/src/Mod/Material/App/MaterialManagerPy.xml index 4ad5991e22..f518062bc6 100644 --- a/src/Mod/Material/App/MaterialManagerPy.xml +++ b/src/Mod/Material/App/MaterialManagerPy.xml @@ -52,5 +52,15 @@ Save the material in the specified library + + + Returns a filtered material list + + + + + Refreshes the material tree. Use sparingly as this is an expensive operation. + + diff --git a/src/Mod/Material/App/MaterialManagerPyImpl.cpp b/src/Mod/Material/App/MaterialManagerPyImpl.cpp index 80f26cdaef..bd31992db6 100644 --- a/src/Mod/Material/App/MaterialManagerPyImpl.cpp +++ b/src/Mod/Material/App/MaterialManagerPyImpl.cpp @@ -22,6 +22,8 @@ #include "PreCompiled.h" #include "Exceptions.h" +#include "MaterialFilter.h" +#include "MaterialFilterPy.h" #include "MaterialManager.h" #include "MaterialManagerPy.h" #include "MaterialPy.h" @@ -256,5 +258,71 @@ PyObject* MaterialManagerPy::save(PyObject* args, PyObject* kwds) PyObject_IsTrue(saveInherited)); material->getMaterialPtr()->setUUID(sharedMaterial->getUUID()); // Make sure they match + Py_INCREF(Py_None); + return Py_None; +} + +void addMaterials(Py::List& list, + const std::shared_ptr>>& tree) +{ + for (auto& node : *tree) { + if (node.second->getType() == MaterialTreeNode::DataNode) { + auto material = node.second->getData(); + PyObject* materialPy = new MaterialPy(new Material(*material)); + list.append(Py::Object(materialPy, true)); + } + else { + addMaterials(list, node.second->getFolder()); + } + } +} + +PyObject* MaterialManagerPy::filterMaterials(PyObject* args, PyObject* kwds) +{ + PyObject* filterPy {}; + PyObject* includeLegacy = Py_False; + static char* kwds_save[] = {"filter", + "includeLegacy", + nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, + kwds, + // "O|O!", + "O!|O!", + kwds_save, + &MaterialFilterPy::Type, + &filterPy, + &PyBool_Type, + &includeLegacy)) { + return nullptr; + } + + MaterialFilterOptions options; + options.setIncludeFavorites(false); + options.setIncludeRecent(false); + options.setIncludeEmptyFolders(false); + options.setIncludeEmptyLibraries(false); + options.setIncludeLegacy(PyObject_IsTrue(includeLegacy)); + + auto filter = std::make_shared(*(static_cast(filterPy)->getMaterialFilterPtr())); + + auto libraries = getMaterialManagerPtr()->getMaterialLibraries(); + Py::List list; + + for (auto lib : *libraries) { + auto tree = getMaterialManagerPtr()->getMaterialTree(lib, filter, options); + if (tree->size() > 0) { + addMaterials(list, tree); + } + } + + Py_INCREF(*list); + return *list; +} + +PyObject* MaterialManagerPy::refresh(PyObject* /*args*/) +{ + getMaterialManagerPtr()->refresh(); + + Py_INCREF(Py_None); return Py_None; } diff --git a/src/Mod/Material/CMakeLists.txt b/src/Mod/Material/CMakeLists.txt index fc6113bd21..4464472aea 100644 --- a/src/Mod/Material/CMakeLists.txt +++ b/src/Mod/Material/CMakeLists.txt @@ -286,6 +286,7 @@ set(MaterialTest_Files materialtests/TestModels.py materialtests/TestMaterials.py materialtests/TestMaterialCreation.py + materialtests/TestMaterialFilter.py ) ADD_CUSTOM_TARGET(MaterialTest ALL @@ -297,6 +298,23 @@ fc_target_copy_resource(MaterialTest ${CMAKE_BINARY_DIR}/Mod/Material ${MaterialTest_Files}) +set(MaterialPythonTestData_Files + materialtests/Materials/TestAcrylicLegacy.FCMat + materialtests/Materials/TestAluminumAppearance.FCMat + materialtests/Materials/TestAluminumMixed.FCMat + materialtests/Materials/TestAluminumPhysical.FCMat + materialtests/Materials/TestBrassAppearance.FCMat +) + +ADD_CUSTOM_TARGET(MaterialPythonTestData ALL + SOURCES ${MaterialPythonTestData_Files} +) + +fc_target_copy_resource(MaterialPythonTestData + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_BINARY_DIR}/Mod/Material + ${MaterialPythonTestData_Files}) + ADD_CUSTOM_TARGET(MaterialScripts ALL SOURCES ${MaterialScripts_Files} ${Material_Ui_Files} ${Material_QRC_SRCS} ) @@ -382,6 +400,11 @@ INSTALL( DESTINATION Mod/Material/materialtests ) +INSTALL( + FILES ${MaterialPythonTestData_Files} + DESTINATION Mod/Material/materialtests/Materials +) + foreach(file ${MaterialLib_Files} ${FluidMaterial_Files} ${AppearanceLib_Files} ${PatternLib_Files} ${MaterialTestLib_Files} ${MaterialModel_Files}) get_filename_component(filepath ${file} DIRECTORY) INSTALL( diff --git a/src/Mod/Material/TestMaterialsApp.py b/src/Mod/Material/TestMaterialsApp.py index b368b8c4a7..effe1182c7 100644 --- a/src/Mod/Material/TestMaterialsApp.py +++ b/src/Mod/Material/TestMaterialsApp.py @@ -29,3 +29,4 @@ import Materials from materialtests.TestModels import ModelTestCases from materialtests.TestMaterials import MaterialTestCases from materialtests.TestMaterialCreation import MaterialCreationTestCases +from materialtests.TestMaterialFilter import MaterialFilterTestCases diff --git a/src/Mod/Material/materialtests/Materials/TestAcrylicLegacy.FCMat b/src/Mod/Material/materialtests/Materials/TestAcrylicLegacy.FCMat new file mode 100644 index 0000000000..a25c1f3e04 --- /dev/null +++ b/src/Mod/Material/materialtests/Materials/TestAcrylicLegacy.FCMat @@ -0,0 +1,13 @@ +; Acrylic +; Automatically generated by the Rocket Workbench +; information about the content of such cards can be found on the wiki: +; https://www.freecadweb.org/wiki/Material + +[General] +Name = TestAcrylicLegacy +Description = Acrylic +KindOfMaterial = Solid + +[Mechanical] +Density = 1190.0 kg/m^3 +Hardness = 10 diff --git a/src/Mod/Material/materialtests/Materials/TestAluminumAppearance.FCMat b/src/Mod/Material/materialtests/Materials/TestAluminumAppearance.FCMat new file mode 100644 index 0000000000..b374cce1e5 --- /dev/null +++ b/src/Mod/Material/materialtests/Materials/TestAluminumAppearance.FCMat @@ -0,0 +1,17 @@ +--- +# File created by ConvertFCMat.py +General: + UUID: "3c6d0407-66b3-48ea-a2e8-ee843edf0311" + Author: "David Carter" + License: "GPL-2.0-or-later" + Name: "TestAluminumAppearance" + Description: "Defines the Aluminum appearance properties" +AppearanceModels: + BasicRendering: + UUID: 'f006c7e4-35b7-43d5-bbf9-c5d572309e6e' + AmbientColor: "(0.3000, 0.3000, 0.3000, 1.0)" + DiffuseColor: "(0.3000, 0.3000, 0.3000, 1.0)" + EmissiveColor: "(0.0000, 0.0000, 0.0000, 1.0)" + Shininess: "0.0900" + SpecularColor: "(0.3000, 0.3000, 0.3000, 1.0)" + Transparency: "0.0" diff --git a/src/Mod/Material/materialtests/Materials/TestAluminumMixed.FCMat b/src/Mod/Material/materialtests/Materials/TestAluminumMixed.FCMat new file mode 100644 index 0000000000..400ac77812 --- /dev/null +++ b/src/Mod/Material/materialtests/Materials/TestAluminumMixed.FCMat @@ -0,0 +1,33 @@ +--- +# File created by ConvertFCMat.py +General: + UUID: "5f546608-fcbb-40db-98d7-d8e104eb33ce" + Author: "M. Münch" + License: "LGPL-2.0-or-later" + Name: "TestAluminumMixed" +Inherits: + Aluminum: + UUID: "3c6d0407-66b3-48ea-a2e8-ee843edf0311" +Models: + Father: + UUID: '9cdda8b6-b606-4778-8f13-3934d8668e67' + Father: "Metal" + MaterialStandard: + UUID: '1e2c0088-904a-4537-925f-64064c07d700' + KindOfMaterial: "Aluminium" + MaterialNumber: "3.3535.26" + StandardCode: "DIN 1725" + LinearElastic: + UUID: '7b561d1d-fb9b-44f6-9da9-56a4f74d7536' + Density: "2700 kg/m^3" + PoissonRatio: "0.3" + ShearModulus: "27000 MPa" + UltimateStrain: "5" + UltimateTensileStrength: "250 MPa" + YieldStrength: "180 MPa" + YoungsModulus: "70000 MPa" + Thermal: + UUID: '9959d007-a970-4ea7-bae4-3eb1b8b883c7' + SpecificHeat: "900 J/kg/K" + ThermalConductivity: "150 W/m/K" + ThermalExpansionCoefficient: "23 µm/m/K" diff --git a/src/Mod/Material/materialtests/Materials/TestAluminumPhysical.FCMat b/src/Mod/Material/materialtests/Materials/TestAluminumPhysical.FCMat new file mode 100644 index 0000000000..97312c28e6 --- /dev/null +++ b/src/Mod/Material/materialtests/Materials/TestAluminumPhysical.FCMat @@ -0,0 +1,30 @@ +--- +# File created by ConvertFCMat.py +General: + UUID: "a8e60089-550d-4370-8e7e-1734db12a3a9" + Author: "M. Münch" + License: "LGPL-2.0-or-later" + Name: "TestAluminumPhysical" +Models: + Father: + UUID: '9cdda8b6-b606-4778-8f13-3934d8668e67' + Father: "Metal" + MaterialStandard: + UUID: '1e2c0088-904a-4537-925f-64064c07d700' + KindOfMaterial: "Aluminium" + MaterialNumber: "3.3535.26" + StandardCode: "DIN 1725" + LinearElastic: + UUID: '7b561d1d-fb9b-44f6-9da9-56a4f74d7536' + Density: "2700 kg/m^3" + PoissonRatio: "0.3" + ShearModulus: "27000 MPa" + # UltimateStrain: "5" + UltimateTensileStrength: "250 MPa" + YieldStrength: "180 MPa" + YoungsModulus: "70000 MPa" + Thermal: + UUID: '9959d007-a970-4ea7-bae4-3eb1b8b883c7' + SpecificHeat: "900 J/kg/K" + ThermalConductivity: "150 W/m/K" + ThermalExpansionCoefficient: "23 µm/m/K" diff --git a/src/Mod/Material/materialtests/Materials/TestBrassAppearance.FCMat b/src/Mod/Material/materialtests/Materials/TestBrassAppearance.FCMat new file mode 100644 index 0000000000..d188a9d853 --- /dev/null +++ b/src/Mod/Material/materialtests/Materials/TestBrassAppearance.FCMat @@ -0,0 +1,17 @@ +--- +# File created by ConvertFCMat.py +General: + UUID: "fff3d5c8-98c3-4ee2-8fe5-7e17403c48fcc" + Author: "David Carter" + License: "GPL-2.0-or-later" + Name: "TestBrassAppearance" + Description: "Defines the Brass appearance properties" +AppearanceModels: + BasicRendering: + UUID: 'f006c7e4-35b7-43d5-bbf9-c5d572309e6e' + AmbientColor: "(0.3294, 0.2235, 0.0275, 1.0)" + DiffuseColor: "(0.7804, 0.5686, 0.1137, 1.0)" + EmissiveColor: "(0.0000, 0.0000, 0.0000, 1.0)" + Shininess: "0.2179" + SpecularColor: "(0.9922, 0.9412, 0.8078, 1.0)" + Transparency: "0.0" diff --git a/src/Mod/Material/materialtests/TestMaterialFilter.py b/src/Mod/Material/materialtests/TestMaterialFilter.py new file mode 100644 index 0000000000..a484cb2409 --- /dev/null +++ b/src/Mod/Material/materialtests/TestMaterialFilter.py @@ -0,0 +1,192 @@ +# ************************************************************************** +# Copyright (c) 2023 David Carter * +# * +# This file is part of the FreeCAD CAx development system. * +# * +# This program is free software; you can redistribute it and/or modify * +# it under the terms of the GNU Lesser General Public License (LGPL) * +# as published by the Free Software Foundation; either version 2 of * +# the License, or (at your option) any later version. * +# for detail see the LICENCE text file. * +# * +# FreeCAD is distributed in the hope that it will be useful, * +# but WITHOUT ANY WARRANTY; without even the implied warranty of * +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# GNU Library General Public License for more details. * +# * +# You should have received a copy of the GNU Library General Public * +# License along with FreeCAD; if not, write to the Free Software * +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# USA * +# ************************************************************************** + +""" +Test module for FreeCAD material cards and APIs +""" +import os + +import unittest +import FreeCAD +import Materials + +parseQuantity = FreeCAD.Units.parseQuantity + +UUIDAcrylicLegacy = "" # This can't be known until it is loaded +UUIDAluminumAppearance = "3c6d0407-66b3-48ea-a2e8-ee843edf0311" +UUIDAluminumMixed = "5f546608-fcbb-40db-98d7-d8e104eb33ce" +UUIDAluminumPhysical = "a8e60089-550d-4370-8e7e-1734db12a3a9" +UUIDBrassAppearance = "fff3d5c8-98c3-4ee2-8fe5-7e17403c48fcc" + + +class MaterialFilterTestCases(unittest.TestCase): + """ + Test class for FreeCAD material cards and APIs + """ + + def setUp(self): + """Setup function to initialize test data""" + self.ModelManager = Materials.ModelManager() + self.MaterialManager = Materials.MaterialManager() + self.uuids = Materials.UUIDs() + + # Use our test files as a custom directory + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material/Resources") + self.customDir = param.GetString("CustomMaterialsDir", "") + self.useBuiltInDir = param.GetBool("UseBuiltInMaterials", True) + self.useWorkbenchDir = param.GetBool("UseMaterialsFromWorkbenches", True) + self.useUserDir = param.GetBool("UseMaterialsFromConfigDir", True) + self.useCustomDir = param.GetBool("UseMaterialsFromCustomDir", False) + + filePath = os.path.dirname(__file__) + os.sep + testPath = filePath + "Materials" + param.SetString("CustomMaterialsDir", testPath) + param.SetBool("UseBuiltInMaterials", False) + param.SetBool("UseMaterialsFromWorkbenches", False) + param.SetBool("UseMaterialsFromConfigDir", False) + param.SetBool("UseMaterialsFromCustomDir", True) + + self.MaterialManager.refresh() + + def tearDown(self): + param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material/Resources") + + # Restore preferences + param.SetString("CustomMaterialsDir", self.customDir) + param.SetBool("UseBuiltInMaterials", self.useBuiltInDir) + param.SetBool("UseMaterialsFromWorkbenches", self.useWorkbenchDir) + param.SetBool("UseMaterialsFromConfigDir", self.useUserDir) + param.SetBool("UseMaterialsFromCustomDir", self.useCustomDir) + + self.MaterialManager.refresh() + + def testFilter(self): + """Test that our filter returns the correct materials""" + + # First check that our materials are loading + material = self.MaterialManager.getMaterial(UUIDAluminumAppearance) + self.assertIsNotNone(material) + self.assertEqual(material.Name, "TestAluminumAppearance") + self.assertEqual(material.UUID, UUIDAluminumAppearance) + + material = self.MaterialManager.getMaterial(UUIDAluminumMixed) + self.assertIsNotNone(material) + self.assertEqual(material.Name, "TestAluminumMixed") + self.assertEqual(material.UUID, UUIDAluminumMixed) + + material = self.MaterialManager.getMaterial(UUIDAluminumPhysical) + self.assertIsNotNone(material) + self.assertEqual(material.Name, "TestAluminumPhysical") + self.assertEqual(material.UUID, UUIDAluminumPhysical) + + material = self.MaterialManager.getMaterial(UUIDBrassAppearance) + self.assertIsNotNone(material) + self.assertEqual(material.Name, "TestBrassAppearance") + self.assertEqual(material.UUID, UUIDBrassAppearance) + + # Create an empty filter + filter = Materials.MaterialFilter() + self.assertEqual(len(self.MaterialManager.MaterialLibraries), 1) + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 4) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 5) + + # Create a basic rendering filter + filter.Name = "Basic Appearance" + filter.RequiredCompleteModels = [self.uuids.BasicRendering] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 3) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 3) + + # Create an advanced rendering filter + filter= Materials.MaterialFilter() + filter.Name = "Advanced Appearance" + filter.RequiredCompleteModels = [self.uuids.AdvancedRendering] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 0) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 0) + + # Create a Density filter + filter= Materials.MaterialFilter() + filter.Name = "Density" + filter.RequiredCompleteModels = [self.uuids.Density] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 2) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 3) + + # Create a Hardness filter + filter= Materials.MaterialFilter() + filter.Name = "Hardness" + filter.RequiredCompleteModels = [self.uuids.Hardness] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 0) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 0) + + # Create a Density and Basic Rendering filter + filter= Materials.MaterialFilter() + filter.Name = "Density and Basic Rendering" + filter.RequiredCompleteModels = [self.uuids.Density, self.uuids.BasicRendering] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 1) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 1) + + # Create a Linear Elastic filter + filter= Materials.MaterialFilter() + filter.Name = "Linear Elastic" + filter.RequiredCompleteModels = [self.uuids.LinearElastic] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 0) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + self.assertEquals(len(filtered), 0) + + filter= Materials.MaterialFilter() + filter.Name = "Linear Elastic - incomplete" + filter.RequiredModels = [self.uuids.LinearElastic] + + filtered = self.MaterialManager.filterMaterials(filter) + self.assertEquals(len(filtered), 2) + + filtered = self.MaterialManager.filterMaterials(filter, includeLegacy=True) + + def testErrorInput(self): + + self.assertRaises(TypeError, self.MaterialManager.filterMaterials, [])