Files
create/src/Mod/Material/App/MaterialValue.cpp
David Carter 3dd6a67804 Material: Material editor enhancements
Continues the work of the material subsystem improvements.

This merge covers the continued development of the material editor. The
primary improvements are the addition of new data types, a new
appearance preview UI, and changes in the array data types.

New data types were added to support more advanced workflows, such as
the Render Workbench.The Image datatype allows the material to embed
the image in the card instead of pointing to an image in an external
file. Multi-buyte strings span multiple lines as the name implies.
It preserves formatting accross those lines. Also several list types
are now supported, with the primary difference being the editors.
List is a list of strings, FileList is a list of file path names, and
ImageList is a list of embedded images.

For the appearance preview, the UI now uses the same Coin library as
is used in the documents, meaning the preview will look exactly the
same as the material will be shown in the documents.

The array data types are now more complete. The default value wasn't
being used as originially envisioned and was tehrefore removed. For
3D arrays, the Python API was implemented.

There were a lot of code clean ups. This involved removing logging
statements used for debugging during development, reduction of lint
warnings, and code refactoring.

The editor can automatically convert from previous format files to the
current format. This has been extended to material files generated by
the Render WB. Old format files are displayed in the editor with a
warning icon. Selecting one will require saving the file in the new
format before it can be used.
2023-12-06 08:48:34 -06:00

839 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 <QRegularExpression>
#endif
#include <QMetaType>
#include <App/Application.h>
#include <Base/QtTools.h>
#include <Base/Quantity.h>
#include <Gui/MetaTypes.h>
#include "Exceptions.h"
#include "MaterialValue.h"
using namespace Materials;
/* TRANSLATOR Material::MaterialValue */
TYPESYSTEM_SOURCE(Materials::MaterialValue, Base::BaseClass)
MaterialValue::MaterialValue()
: _valueType(None)
{
this->setInitialValue(None);
}
MaterialValue::MaterialValue(const MaterialValue& other)
: _valueType(other._valueType)
, _value(other._value)
{}
MaterialValue::MaterialValue(ValueType type)
: _valueType(type)
{
this->setInitialValue(None);
}
MaterialValue::MaterialValue(ValueType type, ValueType inherited)
: _valueType(type)
{
this->setInitialValue(inherited);
}
MaterialValue& MaterialValue::operator=(const MaterialValue& other)
{
if (this == &other) {
return *this;
}
_valueType = other._valueType;
_value = other._value;
return *this;
}
bool MaterialValue::operator==(const MaterialValue& other) const
{
if (this == &other) {
return true;
}
return (_valueType == other._valueType) && (_value == other._value);
}
QString MaterialValue::escapeString(const QString& source)
{
QString res = source;
res.replace(QString::fromStdString("\\"), QString::fromStdString("\\\\"));
res.replace(QString::fromStdString("\""), QString::fromStdString("\\\""));
return res;
}
void MaterialValue::setInitialValue(ValueType inherited)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
if (_valueType == String || _valueType == MultiLineString) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::QString));
}
else if (_valueType == Boolean) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::Bool));
}
else if (_valueType == Integer) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::Int));
}
else if (_valueType == Float) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::Float));
}
else if (_valueType == URL) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::QString));
}
else if (_valueType == Color) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::QString));
}
else if (_valueType == File) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::QString));
}
else if (_valueType == Image) {
_value = QVariant(static_cast<QVariant::Type>(QMetaType::QString));
}
#else
if (_valueType == String || _valueType == MultiLineString) {
_value = QVariant(QMetaType(QMetaType::QString));
}
else if (_valueType == Boolean) {
_value = QVariant(QMetaType(QMetaType::Bool));
}
else if (_valueType == Integer) {
_value = QVariant(QMetaType(QMetaType::Int));
}
else if (_valueType == Float) {
_value = QVariant(QMetaType(QMetaType::Float));
}
else if (_valueType == URL) {
_value = QVariant(QMetaType(QMetaType::QString));
}
else if (_valueType == Color) {
_value = QVariant(QMetaType(QMetaType::QString));
}
else if (_valueType == File) {
_value = QVariant(QMetaType(QMetaType::QString));
}
else if (_valueType == Image) {
_value = QVariant(QMetaType(QMetaType::QString));
}
#endif
else if (_valueType == Quantity) {
Base::Quantity qu;
qu.setInvalid();
_value = QVariant::fromValue(qu);
}
else if (_valueType == List || _valueType == FileList || _valueType == ImageList) {
auto list = QList<QVariant>();
_value = QVariant::fromValue(list);
}
else if (_valueType == Array2D) {
if (_valueType != inherited) {
throw InvalidMaterialType("Initializing a regular material value as a 2D Array");
}
_value = QVariant(); // Uninitialized default value
}
else if (_valueType == Array3D) {
if (_valueType != inherited) {
throw InvalidMaterialType("Initializing a regular material value as a 3D Array");
}
_value = QVariant(); // Uninitialized default value
}
else {
// Default is to set the type to None and leave the variant uninitialized
_valueType = None;
_value = QVariant();
}
}
void MaterialValue::setList(const QList<QVariant>& value)
{
_value = QVariant::fromValue(value);
}
bool MaterialValue::isNull() const
{
if (_value.isNull()) {
return true;
}
if (_valueType == Quantity) {
return !_value.value<Base::Quantity>().isValid();
}
if (_valueType == List || _valueType == FileList || _valueType == ImageList) {
return _value.value<QList<QVariant>>().isEmpty();
}
return false;
}
QString MaterialValue::getYAMLStringImage() const
{
QString yaml;
yaml = QString::fromStdString(" |-2");
QString base64 = getValue().toString();
while (!base64.isEmpty()) {
yaml += QString::fromStdString("\n ") + base64.left(74);
base64.remove(0, 74);
}
return yaml;
}
QString MaterialValue::getYAMLStringList() const
{
QString yaml;
for (auto& it : getList()) {
yaml += QString::fromStdString("\n - \"") + escapeString(it.toString())
+ QString::fromStdString("\"");
}
return yaml;
}
QString MaterialValue::getYAMLStringImageList() const
{
QString yaml;
for (auto& it : getList()) {
yaml += QString::fromStdString("\n - |-2");
QString base64 = it.toString();
while (!base64.isEmpty()) {
yaml += QString::fromStdString("\n ") + base64.left(72);
base64.remove(0, 72);
}
}
return yaml;
}
QString MaterialValue::getYAMLStringMultiLine() const
{
QString yaml;
yaml = QString::fromStdString(" >2");
auto list =
getValue().toString().split(QRegExp(QString::fromStdString("[\r\n]")), Qt::SkipEmptyParts);
for (auto& it : list) {
yaml += QString::fromStdString("\n ") + it;
}
return yaml;
}
QString MaterialValue::getYAMLString() const
{
QString yaml;
if (!isNull()) {
if (getType() == MaterialValue::Image) {
return getYAMLStringImage();
}
if (getType() == MaterialValue::List || getType() == MaterialValue::FileList) {
return getYAMLStringList();
}
if (getType() == MaterialValue::ImageList) {
return getYAMLStringImageList();
}
if (getType() == MaterialValue::MultiLineString) {
return getYAMLStringMultiLine();
}
if (getType() == MaterialValue::Quantity) {
auto quantity = getValue().value<Base::Quantity>();
yaml += quantity.getUserString();
}
else if (getType() == MaterialValue::Float) {
auto value = getValue();
if (!value.isNull()) {
yaml += QString::fromLatin1("%1").arg(value.toFloat(), 0, 'g', 6);
}
}
else if (getType() == MaterialValue::MultiLineString) {
yaml = QString::fromLatin1(">2");
auto list =
getValue().toString().split(QRegularExpression(QString::fromLatin1("[\r\n]")),
Qt::SkipEmptyParts);
for (auto& it : list) {
yaml += QString::fromLatin1("\n ") + it;
}
return yaml;
}
else if (getType() == MaterialValue::List) {
for (auto& it : getList()) {
yaml += QString::fromLatin1("\n - \"") + escapeString(it.toString())
+ QString::fromLatin1("\"");
}
return yaml;
}
else {
yaml += getValue().toString();
}
}
yaml = QString::fromLatin1("\"") + escapeString(yaml) + QString::fromLatin1("\"");
return yaml;
}
//===
TYPESYSTEM_SOURCE(Materials::Material2DArray, Materials::MaterialValue)
Material2DArray::Material2DArray()
: MaterialValue(Array2D, Array2D)
, _columns(0)
{
// Initialize separatelt to prevent recursion
// setType(Array2D);
}
Material2DArray::Material2DArray(const Material2DArray& other)
: MaterialValue(other)
, _columns(other._columns)
{
deepCopy(other);
}
Material2DArray& Material2DArray::operator=(const Material2DArray& other)
{
if (this == &other) {
return *this;
}
MaterialValue::operator=(other);
_columns = other._columns;
deepCopy(other);
return *this;
}
void Material2DArray::deepCopy(const Material2DArray& other)
{
// Deep copy
for (auto& row : other._rows) {
QList<QVariant> vv;
for (auto& col : *row) {
QVariant newVariant(col);
vv.push_back(newVariant);
}
addRow(std::make_shared<QList<QVariant>>(vv));
}
}
bool Material2DArray::isNull() const
{
return rows() <= 0;
}
void Material2DArray::validateRow(int row) const
{
if (row < 0 || row >= rows()) {
throw InvalidIndex();
}
}
void Material2DArray::validateColumn(int column) const
{
if (column < 0 || column >= columns()) {
throw InvalidIndex();
}
}
std::shared_ptr<QList<QVariant>> Material2DArray::getRow(int row) const
{
validateRow(row);
try {
return _rows.at(row);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
std::shared_ptr<QList<QVariant>> Material2DArray::getRow(int row)
{
validateRow(row);
try {
return _rows.at(row);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
void Material2DArray::addRow(const std::shared_ptr<QList<QVariant>>& row)
{
_rows.push_back(row);
}
void Material2DArray::insertRow(int index, const std::shared_ptr<QList<QVariant>>& row)
{
_rows.insert(_rows.begin() + index, row);
}
void Material2DArray::deleteRow(int row)
{
if (row >= static_cast<int>(_rows.size()) || row < 0) {
throw InvalidIndex();
}
_rows.erase(_rows.begin() + row);
}
void Material2DArray::setValue(int row, int column, const QVariant& value)
{
validateRow(row);
validateColumn(column);
auto val = getRow(row);
try {
val->replace(column, value);
}
catch (const std::out_of_range&) {
throw InvalidIndex();
}
}
QVariant Material2DArray::getValue(int row, int column) const
{
validateColumn(column);
auto val = getRow(row);
try {
return val->at(column);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
void Material2DArray::dumpRow(const std::shared_ptr<QList<QVariant>>& row)
{
Base::Console().Log("row: ");
for (auto& column : *row) {
Base::Console().Log("'%s' ", column.toString().toStdString().c_str());
}
Base::Console().Log("\n");
}
void Material2DArray::dump() const
{
for (auto& row : _rows) {
dumpRow(row);
}
}
QString Material2DArray::getYAMLString() const
{
if (isNull()) {
return QString();
}
// Set the correct indentation. 9 chars in this case
QString pad;
pad.fill(QChar::fromLatin1(' '), 9);
// Save the array contents
QString yaml = QString::fromStdString("\n - [");
bool firstRow = true;
for (auto& row : _rows) {
if (!firstRow) {
// Each row is on its own line, padded for correct indentation
yaml += QString::fromStdString(",\n") + pad;
}
else {
firstRow = false;
}
yaml += QString::fromStdString("[");
bool first = true;
for (auto& column : *row) {
if (!first) {
// TODO: Fix for arrays with too many columns to fit on a single line
yaml += QString::fromStdString(", ");
}
else {
first = false;
}
yaml += QString::fromStdString("\"");
auto quantity = column.value<Base::Quantity>();
yaml += quantity.getUserString();
yaml += QString::fromStdString("\"");
}
yaml += QString::fromStdString("]");
}
yaml += QString::fromStdString("]");
return yaml;
}
//===
TYPESYSTEM_SOURCE(Materials::Material3DArray, Materials::MaterialValue)
Material3DArray::Material3DArray()
: MaterialValue(Array3D, Array3D)
, _currentDepth(0)
, _columns(0)
{
// Initialize separatelt to prevent recursion
// setType(Array3D);
}
bool Material3DArray::isNull() const
{
return depth() <= 0;
}
void Material3DArray::validateDepth(int level) const
{
if (level < 0 || level >= depth()) {
throw InvalidIndex();
}
}
void Material3DArray::validateColumn(int column) const
{
if (column < 0 || column >= columns()) {
throw InvalidIndex();
}
}
void Material3DArray::validateRow(int level, int row) const
{
validateDepth(level);
if (row < 0 || row >= rows(level)) {
throw InvalidIndex();
}
}
const std::shared_ptr<QList<std::shared_ptr<QList<Base::Quantity>>>>&
Material3DArray::getTable(const Base::Quantity& depth) const
{
for (auto& it : _rowMap) {
if (std::get<0>(it) == depth) {
return std::get<1>(it);
}
}
throw InvalidIndex();
}
const std::shared_ptr<QList<std::shared_ptr<QList<Base::Quantity>>>>&
Material3DArray::getTable(int depthIndex) const
{
try {
return std::get<1>(_rowMap.at(depthIndex));
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int depth, int row) const
{
validateRow(depth, row);
try {
return getTable(depth)->at(row);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int row) const
{
// Check if we can convert otherwise throw error
return getRow(_currentDepth, row);
}
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int depth, int row)
{
validateRow(depth, row);
try {
return getTable(depth)->at(row);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int row)
{
return getRow(_currentDepth, row);
}
void Material3DArray::addRow(int depth, const std::shared_ptr<QList<Base::Quantity>>& row)
{
try {
getTable(depth)->push_back(row);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
void Material3DArray::addRow(const std::shared_ptr<QList<Base::Quantity>>& row)
{
addRow(_currentDepth, row);
}
int Material3DArray::addDepth(int depth, const Base::Quantity& value)
{
if (depth == this->depth()) {
// Append to the end
return addDepth(value);
}
if (depth > this->depth()) {
throw InvalidIndex();
}
auto rowVector = std::make_shared<QList<std::shared_ptr<QList<Base::Quantity>>>>();
auto entry = std::make_pair(value, rowVector);
_rowMap.insert(_rowMap.begin() + depth, entry);
return depth;
}
int Material3DArray::addDepth(const Base::Quantity& value)
{
auto rowVector = std::make_shared<QList<std::shared_ptr<QList<Base::Quantity>>>>();
auto entry = std::make_pair(value, rowVector);
_rowMap.push_back(entry);
return depth() - 1;
}
void Material3DArray::deleteDepth(int depth)
{
deleteRows(depth); // This may throw an InvalidIndex
_rowMap.erase(_rowMap.begin() + depth);
}
void Material3DArray::insertRow(int depth,
int row,
const std::shared_ptr<QList<Base::Quantity>>& rowData)
{
try {
auto table = getTable(depth);
table->insert(table->begin() + row, rowData);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
void Material3DArray::insertRow(int row, const std::shared_ptr<QList<Base::Quantity>>& rowData)
{
insertRow(_currentDepth, row, rowData);
}
void Material3DArray::deleteRow(int depth, int row)
{
auto table = getTable(depth);
if (row >= static_cast<int>(table->size()) || row < 0) {
throw InvalidIndex();
}
table->erase(table->begin() + row);
}
void Material3DArray::deleteRow(int row)
{
deleteRow(_currentDepth, row);
}
void Material3DArray::deleteRows(int depth)
{
auto table = getTable(depth);
table->clear();
}
void Material3DArray::deleteRows()
{
deleteRows(_currentDepth);
}
int Material3DArray::rows(int depth) const
{
if (depth < 0 || (depth == 0 && this->depth() == 0)) {
return 0;
}
validateDepth(depth);
return getTable(depth)->size();
}
void Material3DArray::setValue(int depth, int row, int column, const Base::Quantity& value)
{
validateRow(depth, row);
validateColumn(column);
auto val = getRow(depth, row);
try {
val->replace(column, value);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
void Material3DArray::setValue(int row, int column, const Base::Quantity& value)
{
setValue(_currentDepth, row, column, value);
}
void Material3DArray::setDepthValue(int depth, const Base::Quantity& value)
{
try {
auto oldRows = getTable(depth);
_rowMap.replace(depth, std::pair(value, oldRows));
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
void Material3DArray::setDepthValue(const Base::Quantity& value)
{
setDepthValue(_currentDepth, value);
}
Base::Quantity Material3DArray::getValue(int depth, int row, int column) const
{
// getRow validates depth and row. Do that first
auto val = getRow(depth, row);
validateColumn(column);
try {
return val->at(column);
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
Base::Quantity Material3DArray::getValue(int row, int column) const
{
return getValue(_currentDepth, row, column);
}
Base::Quantity Material3DArray::getDepthValue(int depth) const
{
validateDepth(depth);
try {
return std::get<0>(_rowMap.at(depth));
}
catch (std::out_of_range const&) {
throw InvalidIndex();
}
}
int Material3DArray::currentDepth() const
{
return _currentDepth;
}
void Material3DArray::setCurrentDepth(int depth)
{
validateDepth(depth);
if (depth < 0 || _rowMap.empty()) {
_currentDepth = 0;
}
else if (depth >= static_cast<int>(_rowMap.size())) {
_currentDepth = _rowMap.size() - 1;
}
else {
_currentDepth = depth;
}
}
QString Material3DArray::getYAMLString() const
{
if (isNull()) {
return QString();
}
// Set the correct indentation. 7 chars + name length
QString pad;
pad.fill(QChar::fromLatin1(' '), 9);
// Save the array contents
QString yaml = QString::fromStdString("\n - [");
for (int depth = 0; depth < this->depth(); depth++) {
if (depth > 0) {
// Each row is on its own line, padded for correct indentation
yaml += QString::fromStdString(",\n") + pad;
}
yaml += QString::fromStdString("\"");
auto value = getDepthValue(depth).getUserString();
yaml += value;
yaml += QString::fromStdString("\": [");
QString pad2;
pad2.fill(QChar::fromLatin1(' '), 14 + value.length());
bool firstRow = true;
auto rows = getTable(depth);
for (auto& row : *rows) {
if (!firstRow) {
// Each row is on its own line, padded for correct indentation
yaml += QString::fromStdString(",\n") + pad2;
}
else {
firstRow = false;
}
yaml += QString::fromStdString("[");
bool first = true;
for (auto& column : *row) {
if (!first) {
// TODO: Fix for arrays with too many columns to fit on a single line
yaml += QString::fromStdString(", ");
}
else {
first = false;
}
yaml += QString::fromStdString("\"");
// Base::Quantity quantity = column.value<Base::Quantity>();
yaml += column.getUserString();
yaml += QString::fromStdString("\"");
}
yaml += QString::fromStdString("]");
}
yaml += QString::fromStdString("]");
}
yaml += QString::fromStdString("]");
return yaml;
}