Refactored code to support local and external material sources This is the first PR in a series to support external modules. External modules allow materials to be stored in external data sources such as databases or web services. No new functionality is introduced in this PR, rather it is a refactoring of code that will allow for changes to be introduced in future PRs. Minor performance improvements have also been made in the model and material managers. The Python API has been enhanced for many data types to allow for modification within Python.
582 lines
22 KiB
C++
582 lines
22 KiB
C++
/***************************************************************************
|
|
* Copyright (c) 2023 David Carter <dcarter@david.carter.ca> *
|
|
* *
|
|
* This file is part of FreeCAD. *
|
|
* *
|
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
|
* under the terms of the GNU Lesser General Public License as *
|
|
* published by the Free Software Foundation, either version 2.1 of the *
|
|
* License, or (at your option) any later version. *
|
|
* *
|
|
* 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 *
|
|
* Lesser General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Lesser General Public *
|
|
* License along with FreeCAD. If not, see *
|
|
* <https://www.gnu.org/licenses/>. *
|
|
* *
|
|
**************************************************************************/
|
|
|
|
#include "PreCompiled.h"
|
|
#ifndef _PreComp_
|
|
#include <QDirIterator>
|
|
#include <QFileInfo>
|
|
#include <QList>
|
|
#include <QMetaType>
|
|
#include <QRegularExpression>
|
|
#include <QString>
|
|
#endif
|
|
|
|
#include <App/Application.h>
|
|
#include <Base/Interpreter.h>
|
|
#include <Base/Stream.h>
|
|
#include <Gui/MetaTypes.h>
|
|
|
|
#include "Materials.h"
|
|
|
|
#include "MaterialConfigLoader.h"
|
|
#include "MaterialLibrary.h"
|
|
#include "MaterialLoader.h"
|
|
#include "Model.h"
|
|
#include "ModelManager.h"
|
|
|
|
|
|
using namespace Materials;
|
|
|
|
MaterialEntry::MaterialEntry(const std::shared_ptr<MaterialLibraryLocal>& library,
|
|
const QString& modelName,
|
|
const QString& dir,
|
|
const QString& modelUuid)
|
|
: _library(library)
|
|
, _name(modelName)
|
|
, _directory(dir)
|
|
, _uuid(modelUuid)
|
|
{}
|
|
|
|
MaterialYamlEntry::MaterialYamlEntry(const std::shared_ptr<MaterialLibraryLocal>& library,
|
|
const QString& modelName,
|
|
const QString& dir,
|
|
const QString& modelUuid,
|
|
const YAML::Node& modelData)
|
|
: MaterialEntry(library, modelName, dir, modelUuid)
|
|
, _model(modelData)
|
|
{}
|
|
|
|
QString MaterialYamlEntry::yamlValue(const YAML::Node& node,
|
|
const std::string& key,
|
|
const std::string& defaultValue)
|
|
{
|
|
if (node[key]) {
|
|
return QString::fromStdString(node[key].as<std::string>());
|
|
}
|
|
return QString::fromStdString(defaultValue);
|
|
}
|
|
|
|
std::shared_ptr<QList<QVariant>> MaterialYamlEntry::readList(const YAML::Node& node,
|
|
bool isImageList)
|
|
{
|
|
auto list = std::make_shared<QList<QVariant>>();
|
|
for (auto it = node.begin(); it != node.end(); it++) {
|
|
QVariant nodeValue;
|
|
if (isImageList) {
|
|
nodeValue = QString::fromStdString(it->as<std::string>())
|
|
.remove(QRegularExpression(QStringLiteral("[\r\n]")));
|
|
}
|
|
else {
|
|
nodeValue = QString::fromStdString(it->as<std::string>());
|
|
}
|
|
list->append(nodeValue);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
std::shared_ptr<QList<QVariant>> MaterialYamlEntry::readImageList(const YAML::Node& node)
|
|
{
|
|
return readList(node, true);
|
|
}
|
|
|
|
std::shared_ptr<Array2D> MaterialYamlEntry::read2DArray(const YAML::Node& node, int columns)
|
|
{
|
|
auto array2d = std::make_shared<Array2D>();
|
|
array2d->setColumns(columns);
|
|
|
|
if (node.size() == 1 || node.size() == 2) {
|
|
// There used to be a default value. Ignore it.
|
|
auto yamlArray = node[0];
|
|
if (node.size() == 2) {
|
|
yamlArray = node[1];
|
|
}
|
|
|
|
for (std::size_t i = 0; i < yamlArray.size(); i++) {
|
|
auto yamlRow = yamlArray[i];
|
|
|
|
auto row = std::make_shared<QList<QVariant>>();
|
|
for (std::size_t j = 0; j < yamlRow.size(); j++) {
|
|
Base::Quantity qq = Base::Quantity::parse(yamlRow[j].as<std::string>());
|
|
qq.setFormat(MaterialValue::getQuantityFormat());
|
|
row->push_back(QVariant::fromValue(qq));
|
|
}
|
|
array2d->addRow(row);
|
|
}
|
|
}
|
|
|
|
return array2d;
|
|
}
|
|
|
|
std::shared_ptr<Array3D> MaterialYamlEntry::read3DArray(const YAML::Node& node, int columns)
|
|
{
|
|
auto array3d = std::make_shared<Array3D>();
|
|
array3d->setColumns(columns - 1); // First column is third dimension
|
|
|
|
if (node.size() == 1 || node.size() == 2) {
|
|
// There used to be a default value. Ignore it.
|
|
auto yamlArray = node[0];
|
|
if (node.size() == 2) {
|
|
yamlArray = node[1];
|
|
}
|
|
|
|
for (std::size_t depth = 0; depth < yamlArray.size(); depth++) {
|
|
auto yamlDepth = yamlArray[depth];
|
|
for (auto it = yamlDepth.begin(); it != yamlDepth.end(); it++) {
|
|
auto depthValue = Base::Quantity::parse(it->first.as<std::string>());
|
|
depthValue.setFormat(MaterialValue::getQuantityFormat());
|
|
array3d->addDepth(depth, depthValue);
|
|
|
|
auto yamlTable = it->second;
|
|
for (std::size_t i = 0; i < yamlTable.size(); i++) {
|
|
auto yamlRow = yamlTable[i];
|
|
|
|
auto row = std::make_shared<QList<Base::Quantity>>();
|
|
for (std::size_t j = 0; j < yamlRow.size(); j++) {
|
|
auto qq = Base::Quantity::parse(yamlRow[j].as<std::string>());
|
|
qq.setFormat(MaterialValue::getQuantityFormat());
|
|
row->push_back(qq);
|
|
}
|
|
array3d->addRow(depth, row);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return array3d;
|
|
}
|
|
|
|
void MaterialYamlEntry::addToTree(
|
|
std::shared_ptr<std::map<QString, std::shared_ptr<Material>>> materialMap)
|
|
{
|
|
std::set<QString> exclude;
|
|
exclude.insert(QStringLiteral("General"));
|
|
exclude.insert(QStringLiteral("Inherits"));
|
|
|
|
auto yamlModel = getModel();
|
|
auto library = getLibrary();
|
|
auto name = getName();
|
|
auto directory = getDirectory();
|
|
QString uuid = getUUID();
|
|
|
|
QString author = yamlValue(yamlModel["General"], "Author", "");
|
|
QString license = yamlValue(yamlModel["General"], "License", "");
|
|
QString description = yamlValue(yamlModel["General"], "Description", "");
|
|
|
|
std::shared_ptr<Material> finalModel =
|
|
std::make_shared<Material>(library, directory, uuid, name);
|
|
finalModel->setAuthor(author);
|
|
finalModel->setLicense(license);
|
|
finalModel->setDescription(description);
|
|
|
|
// Add inheritance list
|
|
if (yamlModel["Inherits"]) {
|
|
auto inherits = yamlModel["Inherits"];
|
|
for (auto it = inherits.begin(); it != inherits.end(); it++) {
|
|
auto nodeName = it->second["UUID"].as<std::string>();
|
|
|
|
finalModel->setParentUUID(
|
|
QString::fromStdString(nodeName)); // Should only be one. Need to check
|
|
}
|
|
}
|
|
|
|
// Add material models
|
|
if (yamlModel["Models"]) {
|
|
auto models = yamlModel["Models"];
|
|
for (auto it = models.begin(); it != models.end(); it++) {
|
|
auto modelName = (it->first).as<std::string>();
|
|
|
|
// Add the model uuid
|
|
auto modelNode = models[modelName];
|
|
auto modelUUID = modelNode["UUID"].as<std::string>();
|
|
finalModel->addPhysical(QString::fromStdString(modelUUID));
|
|
|
|
// Add the property values
|
|
auto properties = yamlModel["Models"][modelName];
|
|
for (auto itp = properties.begin(); itp != properties.end(); itp++) {
|
|
auto propertyName = (itp->first).as<std::string>();
|
|
if (finalModel->hasPhysicalProperty(QString::fromStdString(propertyName))) {
|
|
auto prop =
|
|
finalModel->getPhysicalProperty(QString::fromStdString(propertyName));
|
|
auto type = prop->getType();
|
|
|
|
try {
|
|
if (type == MaterialValue::List || type == MaterialValue::FileList) {
|
|
auto list = readList(itp->second);
|
|
finalModel->setPhysicalValue(QString::fromStdString(propertyName),
|
|
list);
|
|
}
|
|
else if (type == MaterialValue::ImageList) {
|
|
auto list = readImageList(itp->second);
|
|
finalModel->setPhysicalValue(QString::fromStdString(propertyName),
|
|
list);
|
|
}
|
|
else if (type == MaterialValue::Array2D) {
|
|
auto array2d = read2DArray(itp->second, prop->columns());
|
|
finalModel->setPhysicalValue(QString::fromStdString(propertyName),
|
|
array2d);
|
|
}
|
|
else if (type == MaterialValue::Array3D) {
|
|
auto array3d = read3DArray(itp->second, prop->columns());
|
|
finalModel->setPhysicalValue(QString::fromStdString(propertyName),
|
|
array3d);
|
|
}
|
|
else {
|
|
QString propertyValue =
|
|
QString::fromStdString((itp->second).as<std::string>());
|
|
if (type == MaterialValue::Image) {
|
|
propertyValue = propertyValue.remove(
|
|
QRegularExpression(QStringLiteral("[\r\n]")));
|
|
}
|
|
finalModel->setPhysicalValue(QString::fromStdString(propertyName),
|
|
propertyValue);
|
|
}
|
|
}
|
|
catch (const YAML::BadConversion& e) {
|
|
Base::Console().Log("Exception %s <%s:%s> - ignored\n",
|
|
e.what(),
|
|
name.toStdString().c_str(),
|
|
propertyName.c_str());
|
|
}
|
|
}
|
|
else if (propertyName != "UUID") {
|
|
Base::Console().Log("\tProperty '%s' is not described by any model. Ignored\n",
|
|
propertyName.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add appearance models
|
|
if (yamlModel["AppearanceModels"]) {
|
|
auto models = yamlModel["AppearanceModels"];
|
|
for (auto it = models.begin(); it != models.end(); it++) {
|
|
auto modelName = (it->first).as<std::string>();
|
|
|
|
// Add the model uuid
|
|
auto modelNode = models[modelName];
|
|
auto modelUUID = modelNode["UUID"].as<std::string>();
|
|
finalModel->addAppearance(QString::fromStdString(modelUUID));
|
|
|
|
// Add the property values
|
|
auto properties = yamlModel["AppearanceModels"][modelName];
|
|
for (auto itp = properties.begin(); itp != properties.end(); itp++) {
|
|
auto propertyName = (itp->first).as<std::string>();
|
|
if (finalModel->hasAppearanceProperty(QString::fromStdString(propertyName))) {
|
|
auto prop =
|
|
finalModel->getAppearanceProperty(QString::fromStdString(propertyName));
|
|
auto type = prop->getType();
|
|
|
|
try {
|
|
if (type == MaterialValue::List || type == MaterialValue::FileList) {
|
|
auto list = readList(itp->second);
|
|
finalModel->setAppearanceValue(QString::fromStdString(propertyName),
|
|
list);
|
|
}
|
|
else if (type == MaterialValue::ImageList) {
|
|
auto list = readImageList(itp->second);
|
|
finalModel->setAppearanceValue(QString::fromStdString(propertyName),
|
|
list);
|
|
}
|
|
else if (type == MaterialValue::Array2D) {
|
|
auto array2d = read2DArray(itp->second, prop->columns());
|
|
finalModel->setAppearanceValue(QString::fromStdString(propertyName),
|
|
array2d);
|
|
}
|
|
else if (type == MaterialValue::Array3D) {
|
|
auto array3d = read3DArray(itp->second, prop->columns());
|
|
finalModel->setAppearanceValue(QString::fromStdString(propertyName),
|
|
array3d);
|
|
}
|
|
else {
|
|
QString propertyValue =
|
|
QString::fromStdString((itp->second).as<std::string>());
|
|
if (type == MaterialValue::Image) {
|
|
propertyValue = propertyValue.remove(
|
|
QRegularExpression(QStringLiteral("[\r\n]")));
|
|
}
|
|
finalModel->setAppearanceValue(QString::fromStdString(propertyName),
|
|
propertyValue);
|
|
}
|
|
}
|
|
catch (const YAML::BadConversion& e) {
|
|
Base::Console().Log("Exception %s <%s:%s> - ignored\n",
|
|
e.what(),
|
|
name.toStdString().c_str(),
|
|
propertyName.c_str());
|
|
}
|
|
}
|
|
else if (propertyName != "UUID") {
|
|
Base::Console().Log("\tProperty '%s' is not described by any model. Ignored\n",
|
|
propertyName.c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QString path = QDir(directory).absolutePath();
|
|
(*materialMap)[uuid] = library->addMaterial(finalModel, path);
|
|
}
|
|
|
|
//===
|
|
|
|
std::unique_ptr<std::map<QString, std::shared_ptr<MaterialEntry>>>
|
|
MaterialLoader::_materialEntryMap = nullptr;
|
|
|
|
MaterialLoader::MaterialLoader(
|
|
const std::shared_ptr<std::map<QString, std::shared_ptr<Material>>>& materialMap,
|
|
const std::shared_ptr<std::list<std::shared_ptr<MaterialLibrary>>>& libraryList)
|
|
: _materialMap(materialMap)
|
|
, _libraryList(libraryList)
|
|
{
|
|
loadLibraries(libraryList);
|
|
}
|
|
|
|
void MaterialLoader::addLibrary(const std::shared_ptr<MaterialLibraryLocal>& model)
|
|
{
|
|
_libraryList->push_back(model);
|
|
}
|
|
|
|
std::shared_ptr<MaterialEntry>
|
|
MaterialLoader::getMaterialFromYAML(const std::shared_ptr<MaterialLibraryLocal>& library,
|
|
YAML::Node& yamlroot,
|
|
const QString& path)
|
|
{
|
|
std::shared_ptr<MaterialEntry> model = nullptr;
|
|
|
|
try {
|
|
auto uuid = yamlroot["General"]["UUID"].as<std::string>();
|
|
|
|
// Always get the name from the filename
|
|
QFileInfo filepath(path);
|
|
QString name =
|
|
filepath.fileName().remove(QStringLiteral(".FCMat"), Qt::CaseInsensitive);
|
|
|
|
model = std::make_shared<MaterialYamlEntry>(library,
|
|
name,
|
|
path,
|
|
QString::fromStdString(uuid),
|
|
yamlroot);
|
|
}
|
|
catch (YAML::Exception const& e) {
|
|
Base::Console().Error("YAML parsing error: '%s'\n", path.toStdString().c_str());
|
|
Base::Console().Error("\t'%s'\n", e.what());
|
|
showYaml(yamlroot);
|
|
}
|
|
|
|
|
|
return model;
|
|
}
|
|
|
|
std::shared_ptr<MaterialEntry>
|
|
MaterialLoader::getMaterialFromPath(const std::shared_ptr<MaterialLibraryLocal>& library,
|
|
const QString& path) const
|
|
{
|
|
std::shared_ptr<MaterialEntry> model = nullptr;
|
|
auto materialLibrary =
|
|
reinterpret_cast<const std::shared_ptr<Materials::MaterialLibraryLocal>&>(library);
|
|
|
|
// Used for debugging
|
|
std::string pathName = path.toStdString();
|
|
|
|
if (MaterialConfigLoader::isConfigStyle(path)) {
|
|
auto material = MaterialConfigLoader::getMaterialFromPath(materialLibrary, path);
|
|
if (material) {
|
|
(*_materialMap)[material->getUUID()] = materialLibrary->addMaterial(material, path);
|
|
}
|
|
|
|
// Return the nullptr as there are no intermediate steps to take, such
|
|
// as checking inheritance
|
|
return model;
|
|
}
|
|
|
|
Base::FileInfo info(pathName);
|
|
Base::ifstream fin(info);
|
|
if (!fin) {
|
|
Base::Console().Error("YAML file open error: '%s'\n", pathName.c_str());
|
|
return model;
|
|
}
|
|
|
|
YAML::Node yamlroot;
|
|
try {
|
|
yamlroot = YAML::Load(fin);
|
|
|
|
model = getMaterialFromYAML(materialLibrary, yamlroot, path);
|
|
}
|
|
catch (YAML::Exception const& e) {
|
|
Base::Console().Error("YAML parsing error: '%s'\n", pathName.c_str());
|
|
Base::Console().Error("\t'%s'\n", e.what());
|
|
showYaml(yamlroot);
|
|
}
|
|
|
|
|
|
return model;
|
|
}
|
|
|
|
void MaterialLoader::showYaml(const YAML::Node& yaml)
|
|
{
|
|
std::stringstream out;
|
|
|
|
out << yaml;
|
|
std::string logData = out.str();
|
|
Base::Console().Log("%s\n", logData.c_str());
|
|
}
|
|
|
|
|
|
void MaterialLoader::dereference(
|
|
const std::shared_ptr<std::map<QString, std::shared_ptr<Material>>>& materialMap,
|
|
const std::shared_ptr<Material>& material)
|
|
{
|
|
// Avoid recursion
|
|
if (material->getDereferenced()) {
|
|
return;
|
|
}
|
|
|
|
auto parentUUID = material->getParentUUID();
|
|
if (parentUUID.size() > 0) {
|
|
std::shared_ptr<Material> parent;
|
|
try {
|
|
parent = materialMap->at(parentUUID);
|
|
}
|
|
catch (std::out_of_range&) {
|
|
Base::Console().Log(
|
|
"Unable to apply inheritance for material '%s', parent '%s' not found.\n",
|
|
material->getName().toStdString().c_str(),
|
|
parentUUID.toStdString().c_str());
|
|
return;
|
|
}
|
|
|
|
// Ensure the parent has been dereferenced
|
|
dereference(materialMap, parent);
|
|
|
|
// Add physical models
|
|
auto modelVector = parent->getPhysicalModels();
|
|
for (auto& model : *modelVector) {
|
|
if (!material->hasPhysicalModel(model)) {
|
|
material->addPhysical(model);
|
|
}
|
|
}
|
|
|
|
// Add appearance models
|
|
modelVector = parent->getAppearanceModels();
|
|
for (auto& model : *modelVector) {
|
|
if (!material->hasAppearanceModel(model)) {
|
|
material->addAppearance(model);
|
|
}
|
|
}
|
|
|
|
// Add values
|
|
auto properties = parent->getPhysicalProperties();
|
|
for (auto& itp : properties) {
|
|
auto name = itp.first;
|
|
auto property = itp.second;
|
|
|
|
if (material->getPhysicalProperty(name)->isNull()) {
|
|
material->getPhysicalProperty(name)->setValue(property->getValue());
|
|
}
|
|
}
|
|
|
|
properties = parent->getAppearanceProperties();
|
|
for (auto& itp : properties) {
|
|
auto name = itp.first;
|
|
auto property = itp.second;
|
|
|
|
if (material->getAppearanceProperty(name)->isNull()) {
|
|
material->getAppearanceProperty(name)->setValue(property->getValue());
|
|
}
|
|
}
|
|
}
|
|
|
|
material->markDereferenced();
|
|
}
|
|
|
|
void MaterialLoader::dereference(const std::shared_ptr<Material>& material)
|
|
{
|
|
dereference(_materialMap, material);
|
|
}
|
|
|
|
void MaterialLoader::loadLibrary(const std::shared_ptr<MaterialLibraryLocal>& library)
|
|
{
|
|
if (_materialEntryMap == nullptr) {
|
|
_materialEntryMap = std::make_unique<std::map<QString, std::shared_ptr<MaterialEntry>>>();
|
|
}
|
|
|
|
QDirIterator it(library->getDirectory(), QDirIterator::Subdirectories);
|
|
while (it.hasNext()) {
|
|
auto pathname = it.next();
|
|
QFileInfo file(pathname);
|
|
if (file.isFile()) {
|
|
if (file.suffix().toStdString() == "FCMat") {
|
|
try {
|
|
auto model = getMaterialFromPath(library, file.canonicalFilePath());
|
|
if (model) {
|
|
(*_materialEntryMap)[model->getUUID()] = model;
|
|
}
|
|
}
|
|
catch (const MaterialReadError&) {
|
|
// Ignore the file. Error messages should have already been logged
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& it : *_materialEntryMap) {
|
|
it.second->addToTree(_materialMap);
|
|
}
|
|
}
|
|
|
|
void MaterialLoader::loadLibraries(
|
|
const std::shared_ptr<std::list<std::shared_ptr<MaterialLibrary>>>& libraryList)
|
|
{
|
|
if (libraryList) {
|
|
for (auto& it : *libraryList) {
|
|
if (it->isLocal()) {
|
|
auto materialLibrary =
|
|
reinterpret_cast<const std::shared_ptr<Materials::MaterialLibraryLocal>&>(it);
|
|
loadLibrary(materialLibrary);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& it : *_materialMap) {
|
|
dereference(it.second);
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<std::list<QString>>
|
|
MaterialLoader::getMaterialFolders(const MaterialLibraryLocal& library)
|
|
{
|
|
std::shared_ptr<std::list<QString>> pathList = std::make_shared<std::list<QString>>();
|
|
QDirIterator it(library.getDirectory(), QDirIterator::Subdirectories);
|
|
while (it.hasNext()) {
|
|
auto pathname = it.next();
|
|
QFileInfo file(pathname);
|
|
if (file.isDir()) {
|
|
QString path = QDir(library.getDirectory()).relativeFilePath(file.absoluteFilePath());
|
|
if (!path.startsWith(QStringLiteral("."))) {
|
|
pathList->push_back(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
return pathList;
|
|
}
|