Material: Expanded Python API (#13829)

Expands the Python API to allow for material creation. With test cases.
This commit is contained in:
David Carter
2024-05-06 16:34:51 +00:00
committed by GitHub
parent de683708ba
commit 20e7deb86a
8 changed files with 358 additions and 46 deletions

View File

@@ -47,5 +47,10 @@
<UserDocu>Get a list of materials implementing the specified model, with values for all properties</UserDocu>
</Documentation>
</Methode>
<Methode Name="save" Keyword="true">
<Documentation>
<UserDocu>Save the material in the specified library</UserDocu>
</Documentation>
</Methode>
</PythonExport>
</GenerateMaterial>

View File

@@ -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<MaterialPy*>(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>(*(material->getMaterialPtr()));
std::shared_ptr<MaterialLibrary> 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;
}

View File

@@ -33,7 +33,7 @@
</Documentation>
<Parameter Name="LibraryIcon" Type="String"/>
</Attribute>
<Attribute Name="Name" ReadOnly="true">
<Attribute Name="Name" ReadOnly="false">
<Documentation>
<UserDocu>Model name.</UserDocu>
</Documentation>
@@ -47,23 +47,23 @@
</Attribute>
<Attribute Name="UUID" ReadOnly="true">
<Documentation>
<UserDocu>Unique model identifier.</UserDocu>
<UserDocu>Unique model identifier. This is only valid after the material is saved.</UserDocu>
</Documentation>
<Parameter Name="UUID" Type="String"/>
</Attribute>
<Attribute Name="Description" ReadOnly="true">
<Attribute Name="Description" ReadOnly="false">
<Documentation>
<UserDocu>Description of the material.</UserDocu>
</Documentation>
<Parameter Name="Description" Type="String"/>
</Attribute>
<Attribute Name="URL" ReadOnly="true">
<Attribute Name="URL" ReadOnly="false">
<Documentation>
<UserDocu>URL to a material reference.</UserDocu>
</Documentation>
<Parameter Name="URL" Type="String"/>
</Attribute>
<Attribute Name="Reference" ReadOnly="true">
<Attribute Name="Reference" ReadOnly="false">
<Documentation>
<UserDocu>Reference for material data.</UserDocu>
</Documentation>
@@ -81,13 +81,13 @@
</Documentation>
<Parameter Name="AuthorAndLicense" Type="String"/>
</Attribute>
<Attribute Name="Author" ReadOnly="true">
<Attribute Name="Author" ReadOnly="false">
<Documentation>
<UserDocu>Author information.</UserDocu>
</Documentation>
<Parameter Name="Author" Type="String"/>
</Attribute>
<Attribute Name="License" ReadOnly="true">
<Attribute Name="License" ReadOnly="false">
<Documentation>
<UserDocu>License information.</UserDocu>
</Documentation>
@@ -111,11 +111,31 @@
</Documentation>
<Parameter Name="Tags" Type="List"/>
</Attribute>
<Methode Name="addPhysicalModel" ReadOnly="false">
<Documentation>
<UserDocu>Add the physical model with the given UUID</UserDocu>
</Documentation>
</Methode>
<Methode Name="removePhysicalModel" ReadOnly="false">
<Documentation>
<UserDocu>Remove the physical model with the given UUID</UserDocu>
</Documentation>
</Methode>
<Methode Name="hasPhysicalModel" ReadOnly="true">
<Documentation>
<UserDocu>Check if the material implements the physical model with the given UUID</UserDocu>
</Documentation>
</Methode>
<Methode Name="addAppearanceModel" ReadOnly="false">
<Documentation>
<UserDocu>Add the appearance model with the given UUID</UserDocu>
</Documentation>
</Methode>
<Methode Name="removeAppearanceModel" ReadOnly="false">
<Documentation>
<UserDocu>Remove the appearance model with the given UUID</UserDocu>
</Documentation>
</Methode>
<Methode Name="hasAppearanceModel" ReadOnly="true">
<Documentation>
<UserDocu>Check if the material implements the appearance model with the given UUID</UserDocu>
@@ -175,10 +195,20 @@
<UserDocu>Get the value associated with the property</UserDocu>
</Documentation>
</Methode>
<Methode Name="setPhysicalValue" ReadOnly="true">
<Documentation>
<UserDocu>Set the value associated with the property</UserDocu>
</Documentation>
</Methode>
<Methode Name="getAppearanceValue" ReadOnly="true">
<Documentation>
<UserDocu>Get the value associated with the property</UserDocu>
</Documentation>
</Methode>
<Methode Name="setAppearanceValue" ReadOnly="true">
<Documentation>
<UserDocu>Set the value associated with the property</UserDocu>
</Documentation>
</Methode>
</PythonExport>
</GenerateModel>

View File

@@ -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<std::string> &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 << "<Material at " << getMaterialPtr() << ">";
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;
}

View File

@@ -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)) {

View File

@@ -285,6 +285,7 @@ set(MaterialTest_Files
materialtests/__init__.py
materialtests/TestModels.py
materialtests/TestMaterials.py
materialtests/TestMaterialCreation.py
)
ADD_CUSTOM_TARGET(MaterialTest ALL

View File

@@ -28,3 +28,4 @@ import Materials
from materialtests.TestModels import ModelTestCases
from materialtests.TestMaterials import MaterialTestCases
from materialtests.TestMaterialCreation import MaterialCreationTestCases

View File

@@ -0,0 +1,137 @@
#**************************************************************************
# Copyright (c) 2023 David Carter <dcarter@davidcarter.ca> *
# *
# 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))