diff --git a/src/Mod/Material/App/MaterialManagerPy.xml b/src/Mod/Material/App/MaterialManagerPy.xml
index af9579ee75..4ad5991e22 100644
--- a/src/Mod/Material/App/MaterialManagerPy.xml
+++ b/src/Mod/Material/App/MaterialManagerPy.xml
@@ -47,5 +47,10 @@
Get a list of materials implementing the specified model, with values for all properties
+
+
+ Save the material in the specified library
+
+
diff --git a/src/Mod/Material/App/MaterialManagerPyImpl.cpp b/src/Mod/Material/App/MaterialManagerPyImpl.cpp
index e5af5059e4..bdc9113fc5 100644
--- a/src/Mod/Material/App/MaterialManagerPyImpl.cpp
+++ b/src/Mod/Material/App/MaterialManagerPyImpl.cpp
@@ -84,7 +84,8 @@ PyObject* MaterialManagerPy::getMaterialByPath(PyObject* args)
if (!libPath.isEmpty()) {
try {
auto material =
- getMaterialManagerPtr()->getMaterialByPath(QString::fromUtf8(utf8Path.c_str()), libPath);
+ getMaterialManagerPtr()->getMaterialByPath(QString::fromUtf8(utf8Path.c_str()),
+ libPath);
return new MaterialPy(new Material(*material));
}
catch (const MaterialNotFound&) {
@@ -98,7 +99,8 @@ PyObject* MaterialManagerPy::getMaterialByPath(PyObject* args)
}
try {
- auto material = getMaterialManagerPtr()->getMaterialByPath(QString::fromUtf8(utf8Path.c_str()));
+ auto material =
+ getMaterialManagerPtr()->getMaterialByPath(QString::fromUtf8(utf8Path.c_str()));
return new MaterialPy(new Material(*material));
}
catch (const MaterialNotFound&) {
@@ -195,3 +197,64 @@ PyObject* MaterialManagerPy::materialsWithModelComplete(PyObject* args)
return dict;
}
+
+PyObject* MaterialManagerPy::save(PyObject* args, PyObject* kwds)
+{
+ char* libraryName {};
+ PyObject* obj {};
+ char* path {};
+ PyObject* overwrite = Py_False;
+ PyObject* saveAsCopy = Py_False;
+ PyObject* saveInherited = Py_False;
+ static char* kwds_save[] =
+ {"library", "material", "path", "overwrite", "saveAsCopy", "saveInherited", nullptr};
+ if (!PyArg_ParseTupleAndKeywords(args,
+ kwds,
+ "etOet|O!O!O!",
+ kwds_save,
+ "utf-8", &libraryName,
+ &obj,
+ "utf-8", &path,
+ &PyBool_Type, &overwrite,
+ &PyBool_Type, &saveAsCopy,
+ &PyBool_Type, &saveInherited)) {
+ return nullptr;
+ }
+
+ Base::Console().Log("library name %s\n", libraryName);
+ Base::Console().Log("path %s\n", path);
+
+ MaterialPy* material;
+ if (QLatin1String(obj->ob_type->tp_name) == QLatin1String("Materials.Material")) {
+ material = static_cast(obj);
+ }
+ else {
+ PyErr_Format(PyExc_TypeError, "Material expected not '%s'", obj->ob_type->tp_name);
+ return nullptr;
+ }
+ if (!material) {
+ PyErr_SetString(PyExc_TypeError, "Invalid material object");
+ return nullptr;
+ }
+ auto sharedMaterial = std::make_shared(*(material->getMaterialPtr()));
+
+ std::shared_ptr library;
+ try {
+ library = getMaterialManagerPtr()->getLibrary(QString::fromUtf8(libraryName));
+ }
+ catch (const LibraryNotFound&) {
+ PyErr_SetString(PyExc_LookupError, "Unknown library");
+ return nullptr;
+ }
+
+
+ getMaterialManagerPtr()->saveMaterial(library,
+ sharedMaterial,
+ QString::fromUtf8(path),
+ PyObject_IsTrue(overwrite),
+ PyObject_IsTrue(saveAsCopy),
+ PyObject_IsTrue(saveInherited));
+ material->getMaterialPtr()->setUUID(sharedMaterial->getUUID()); // Make sure they match
+
+ return Py_None;
+}
diff --git a/src/Mod/Material/App/MaterialPy.xml b/src/Mod/Material/App/MaterialPy.xml
index 8d3d7f14d5..edd21c5890 100644
--- a/src/Mod/Material/App/MaterialPy.xml
+++ b/src/Mod/Material/App/MaterialPy.xml
@@ -33,7 +33,7 @@
-
+
Model name.
@@ -47,23 +47,23 @@
- Unique model identifier.
+ Unique model identifier. This is only valid after the material is saved.
-
+
Description of the material.
-
+
URL to a material reference.
-
+
Reference for material data.
@@ -81,13 +81,13 @@
-
+
Author information.
-
+
License information.
@@ -111,11 +111,31 @@
+
+
+ Add the physical model with the given UUID
+
+
+
+
+ Remove the physical model with the given UUID
+
+
Check if the material implements the physical model with the given UUID
+
+
+ Add the appearance model with the given UUID
+
+
+
+
+ Remove the appearance model with the given UUID
+
+
Check if the material implements the appearance model with the given UUID
@@ -175,10 +195,20 @@
Get the value associated with the property
+
+
+ Set the value associated with the property
+
+
Get the value associated with the property
+
+
+ Set the value associated with the property
+
+
diff --git a/src/Mod/Material/App/MaterialPyImpl.cpp b/src/Mod/Material/App/MaterialPyImpl.cpp
index bf959bb72d..3e4c72c079 100644
--- a/src/Mod/Material/App/MaterialPyImpl.cpp
+++ b/src/Mod/Material/App/MaterialPyImpl.cpp
@@ -48,43 +48,8 @@ static Py::List getList(const QVariant& value);
// returns a string which represents the object e.g. when printed in python
std::string MaterialPy::representation() const
{
- MaterialPy::PointerType ptr = getMaterialPtr();
- std::stringstream str;
- str << "Property [Name=(";
- str << ptr->getName().toStdString();
- str << "), UUID=(";
- str << ptr->getUUID().toStdString();
- auto library = ptr->getLibrary();
- if (library) {
- str << "), Library Name=(";
- str << ptr->getLibrary()->getName().toStdString();
- str << "), Library Root=(";
- str << ptr->getLibrary()->getDirectoryPath().toStdString();
- str << "), Library Icon=(";
- str << ptr->getLibrary()->getIconPath().toStdString();
- }
- str << "), Directory=(";
- str << ptr->getDirectory().toStdString();
- // str << "), URL=(";
- // str << ptr->getURL();
- // str << "), DOI=(";
- // str << ptr->getDOI();
- // str << "), Description=(";
- // str << ptr->getDescription();
- // str << "), Inherits=[";
- // const std::vector &inherited = getMaterialPtr()->getInheritance();
- // for (auto it = inherited.begin(); it != inherited.end(); it++)
- // {
- // std::string uuid = *it;
- // if (it != inherited.begin())
- // str << "), UUID=(";
- // else
- // str << "UUID=(";
- // str << uuid << ")";
- // }
- // str << "]]";
- str << ")]";
-
+ std::ostringstream str;
+ str << "";
return str.str();
}
@@ -123,6 +88,11 @@ Py::String MaterialPy::getName() const
return {getMaterialPtr()->getName().toStdString()};
}
+void MaterialPy::setName(Py::String arg)
+{
+ getMaterialPtr()->setName(QString::fromStdString(arg));
+}
+
Py::String MaterialPy::getDirectory() const
{
return {getMaterialPtr()->getDirectory().toStdString()};
@@ -138,16 +108,31 @@ Py::String MaterialPy::getDescription() const
return {getMaterialPtr()->getDescription().toStdString()};
}
+void MaterialPy::setDescription(Py::String arg)
+{
+ getMaterialPtr()->setDescription(QString::fromStdString(arg));
+}
+
Py::String MaterialPy::getURL() const
{
return {getMaterialPtr()->getURL().toStdString()};
}
+void MaterialPy::setURL(Py::String arg)
+{
+ getMaterialPtr()->setURL(QString::fromStdString(arg));
+}
+
Py::String MaterialPy::getReference() const
{
return {getMaterialPtr()->getReference().toStdString()};
}
+void MaterialPy::setReference(Py::String arg)
+{
+ getMaterialPtr()->setReference(QString::fromStdString(arg));
+}
+
Py::String MaterialPy::getParent() const
{
return {getMaterialPtr()->getParentUUID().toStdString()};
@@ -163,11 +148,21 @@ Py::String MaterialPy::getAuthor() const
return {getMaterialPtr()->getAuthor().toStdString()};
}
+void MaterialPy::setAuthor(Py::String arg)
+{
+ getMaterialPtr()->setAuthor(QString::fromStdString(arg));
+}
+
Py::String MaterialPy::getLicense() const
{
return {getMaterialPtr()->getLicense().toStdString()};
}
+void MaterialPy::setLicense(Py::String arg)
+{
+ getMaterialPtr()->setLicense(QString::fromStdString(arg));
+}
+
Py::List MaterialPy::getPhysicalModels() const
{
auto models = getMaterialPtr()->getPhysicalModels();
@@ -214,6 +209,30 @@ int MaterialPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/)
return 0;
}
+PyObject* MaterialPy::addPhysicalModel(PyObject* args)
+{
+ char* uuid;
+ if (!PyArg_ParseTuple(args, "s", &uuid)) {
+ return nullptr;
+ }
+
+ getMaterialPtr()->addPhysical(QString::fromStdString(uuid));
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+PyObject* MaterialPy::removePhysicalModel(PyObject* args)
+{
+ char* uuid;
+ if (!PyArg_ParseTuple(args, "s", &uuid)) {
+ return nullptr;
+ }
+
+ getMaterialPtr()->removePhysical(QString::fromStdString(uuid));
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
PyObject* MaterialPy::hasPhysicalModel(PyObject* args)
{
char* uuid;
@@ -225,6 +244,30 @@ PyObject* MaterialPy::hasPhysicalModel(PyObject* args)
return PyBool_FromLong(hasProperty ? 1 : 0);
}
+PyObject* MaterialPy::addAppearanceModel(PyObject* args)
+{
+ char* uuid;
+ if (!PyArg_ParseTuple(args, "s", &uuid)) {
+ return nullptr;
+ }
+
+ getMaterialPtr()->addAppearance(QString::fromStdString(uuid));
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+PyObject* MaterialPy::removeAppearanceModel(PyObject* args)
+{
+ char* uuid;
+ if (!PyArg_ParseTuple(args, "s", &uuid)) {
+ return nullptr;
+ }
+
+ getMaterialPtr()->removeAppearance(QString::fromStdString(uuid));
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
PyObject* MaterialPy::hasAppearanceModel(PyObject* args)
{
char* uuid;
@@ -472,6 +515,20 @@ PyObject* MaterialPy::getPhysicalValue(PyObject* args)
return _pyObjectFromVariant(value);
}
+PyObject* MaterialPy::setPhysicalValue(PyObject* args)
+{
+ char* name;
+ char* value;
+ if (!PyArg_ParseTuple(args, "ss", &name, &value)) {
+ return nullptr;
+ }
+
+ getMaterialPtr()->setPhysicalValue(QString::fromStdString(name),
+ QString::fromStdString(value));
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
PyObject* MaterialPy::getAppearanceValue(PyObject* args)
{
char* name;
@@ -482,3 +539,17 @@ PyObject* MaterialPy::getAppearanceValue(PyObject* args)
QVariant value = getMaterialPtr()->getAppearanceValue(QString::fromStdString(name));
return _pyObjectFromVariant(value);
}
+
+PyObject* MaterialPy::setAppearanceValue(PyObject* args)
+{
+ char* name;
+ char* value;
+ if (!PyArg_ParseTuple(args, "ss", &name, &value)) {
+ return nullptr;
+ }
+
+ getMaterialPtr()->setAppearanceValue(QString::fromStdString(name),
+ QString::fromStdString(value));
+ Py_INCREF(Py_None);
+ return Py_None;
+}
diff --git a/src/Mod/Material/App/ModelLibrary.cpp b/src/Mod/Material/App/ModelLibrary.cpp
index 40df9b3366..115a04846b 100644
--- a/src/Mod/Material/App/ModelLibrary.cpp
+++ b/src/Mod/Material/App/ModelLibrary.cpp
@@ -50,6 +50,10 @@ bool LibraryBase::operator==(const LibraryBase& library) const
QString LibraryBase::getLocalPath(const QString& path) const
{
QString filePath = getDirectoryPath();
+ if (!(filePath.endsWith(QLatin1String("/")) || filePath.endsWith(QLatin1String("\\")))) {
+ filePath += QLatin1String("/");
+ }
+
QString cleanPath = QDir::cleanPath(path);
QString prefix = QString::fromStdString("/") + getName();
if (cleanPath.startsWith(prefix)) {
diff --git a/src/Mod/Material/CMakeLists.txt b/src/Mod/Material/CMakeLists.txt
index a9ee1296d4..fc6113bd21 100644
--- a/src/Mod/Material/CMakeLists.txt
+++ b/src/Mod/Material/CMakeLists.txt
@@ -285,6 +285,7 @@ set(MaterialTest_Files
materialtests/__init__.py
materialtests/TestModels.py
materialtests/TestMaterials.py
+ materialtests/TestMaterialCreation.py
)
ADD_CUSTOM_TARGET(MaterialTest ALL
diff --git a/src/Mod/Material/TestMaterialsApp.py b/src/Mod/Material/TestMaterialsApp.py
index c53634d312..b368b8c4a7 100644
--- a/src/Mod/Material/TestMaterialsApp.py
+++ b/src/Mod/Material/TestMaterialsApp.py
@@ -28,3 +28,4 @@ import Materials
from materialtests.TestModels import ModelTestCases
from materialtests.TestMaterials import MaterialTestCases
+from materialtests.TestMaterialCreation import MaterialCreationTestCases
diff --git a/src/Mod/Material/materialtests/TestMaterialCreation.py b/src/Mod/Material/materialtests/TestMaterialCreation.py
new file mode 100644
index 0000000000..faf1a48f38
--- /dev/null
+++ b/src/Mod/Material/materialtests/TestMaterialCreation.py
@@ -0,0 +1,137 @@
+#**************************************************************************
+# 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 unittest
+import FreeCAD
+import Materials
+
+import os
+
+parseQuantity = FreeCAD.Units.parseQuantity
+
+class MaterialCreationTestCases(unittest.TestCase):
+ """
+ Test class for FreeCAD material creation and APIs
+ """
+
+ def setUp(self):
+ """ Setup function to initialize test data """
+ self.ModelManager = Materials.ModelManager()
+ self.MaterialManager = Materials.MaterialManager()
+ self.uuids = Materials.UUIDs()
+
+ def tearDown(self):
+ try:
+ material = self.MaterialManager.getMaterialByPath("Example/Frakenstein.FCMat", "User")
+ os.remove(material.LibraryRoot + "/Example/Frakenstein.FCMat")
+ try:
+ os.rmdir(material.LibraryRoot + "/Example")
+ except OSError:
+ # Can't remove directories that aren't empty
+ pass
+ except LookupError:
+ pass
+
+ def checkNewMaterial(self, material):
+ """ Check the state of a newly created material """
+ self.assertEqual(len(material.UUID), 0)
+ self.assertEqual(len(material.Name), 0)
+ self.assertEqual(len(material.Author), 0)
+ self.assertEqual(len(material.License), 0)
+ self.assertEqual(len(material.Description), 0)
+ self.assertEqual(len(material.URL), 0)
+ self.assertEqual(len(material.Reference), 0)
+ self.assertEqual(len(material.Parent), 0)
+ self.assertEqual(len(material.Tags), 0)
+
+ def testCreateMaterial(self):
+ """ Create a material with properties """
+ material = Materials.Material()
+ self.checkNewMaterial(material)
+
+ material.Name = "Frankenstein"
+ material.Author = "Mary Shelley"
+ material.License = "CC-BY-3.0"
+ material.Description = "The sad story of a boy afraid of fire"
+ material.URL = "https://www.example.com"
+ material.Reference = "ISBN 978-1673287882"
+
+ # UUID isn't valid until the file is saved
+ self.assertEqual(material.UUID, '')
+
+ self.assertEqual(material.Name, "Frankenstein")
+ self.assertEqual(material.Author, "Mary Shelley")
+ self.assertEqual(material.License, "CC-BY-3.0")
+ self.assertEqual(material.Description, "The sad story of a boy afraid of fire")
+ self.assertEqual(material.URL, "https://www.example.com")
+ self.assertEqual(material.Reference, "ISBN 978-1673287882")
+
+ self.assertEqual(len(material.PhysicalModels), 0)
+ self.assertEqual(len(material.AppearanceModels), 0)
+
+ material.addAppearanceModel(self.uuids.TextureRendering)
+ self.assertEqual(len(material.AppearanceModels), 1)
+ # TextureRendering inherits BasicRendering
+ self.assertTrue(material.hasAppearanceModel(self.uuids.BasicRendering))
+ self.assertTrue(material.hasAppearanceModel(self.uuids.TextureRendering))
+
+ # Colors are tuples of 3 (rgb) or 4 (rgba) values between 0 and 1
+ # All values are set with strings
+ material.setAppearanceValue("DiffuseColor", "(1.0, 1.0, 1.0)")
+ material.setAppearanceValue("SpecularColor", "(0, 0, 0, 1.0)")
+
+ self.assertEqual(material.AppearanceProperties["DiffuseColor"], "(1.0, 1.0, 1.0)")
+ self.assertEqual(material.getAppearanceValue("DiffuseColor"), "(1.0, 1.0, 1.0)")
+
+ self.assertEqual(material.AppearanceProperties["SpecularColor"], "(0, 0, 0, 1.0)")
+ self.assertEqual(material.getAppearanceValue("SpecularColor"), "(0, 0, 0, 1.0)")
+
+ # Properties without a value will return None
+ self.assertIsNone(material.getAppearanceValue("AmbientColor"))
+ self.assertIsNone(material.getAppearanceValue("EmissiveColor"))
+ self.assertIsNone(material.getAppearanceValue("Shininess"))
+ self.assertIsNone(material.getAppearanceValue("Transparency"))
+ self.assertIsNone(material.getAppearanceValue("TexturePath"))
+ self.assertIsNone(material.getAppearanceValue("TextureImage"))
+ self.assertIsNone(material.getAppearanceValue("TextureScaling"))
+
+ material.addPhysicalModel(self.uuids.Density)
+ self.assertEqual(len(material.PhysicalModels), 1)
+ self.assertTrue(material.hasPhysicalModel(self.uuids.Density))
+
+ # Quantity properties require units
+ material.setPhysicalValue("Density", "99.9 kg/m^3")
+ self.assertEqual(material.getPhysicalValue("Density").UserString, parseQuantity("99.90 kg/m^3").UserString)
+
+ # MaterialManager is unaware of the material until it is saved
+ self.MaterialManager.save("User", material, "Example/Frakenstein.FCMat")
+
+ # Now the UUID is valid
+ uuid = material.UUID
+ self.assertEqual(len(material.UUID), 36)
+ self.assertIn(uuid, self.MaterialManager.Materials)
+ self.assertIsNotNone(self.MaterialManager.getMaterialByPath("Example/Frakenstein.FCMat", "User"))
+ self.assertIsNotNone(self.MaterialManager.getMaterial(uuid))