From 938b6d5d6fa08833691c93401d4050f9774320a2 Mon Sep 17 00:00:00 2001 From: Jeremy Hu Date: Sun, 26 May 2019 08:54:24 +0930 Subject: [PATCH] Add color solubility setting If one part configured color solubility, the generated color texture seam between the neighbor parts will be gradient filled using this part's color with the specified solubility. Demo: https://twitter.com/jeremyhu2016/status/1132159910815227904 --- src/document.cpp | 20 +++++ src/document.h | 2 + src/documentwindow.cpp | 1 + src/meshgenerator.cpp | 25 ++++++- src/outcome.h | 1 + src/parttreewidget.cpp | 11 +++ src/parttreewidget.h | 1 + src/partwidget.cpp | 26 ++++++- src/partwidget.h | 1 + src/skeletondocument.h | 9 ++- src/texturegenerator.cpp | 90 +++++++++++++++++++++-- thirdparty/nodemesh/nodemesh/cgalmesh.h | 7 ++ thirdparty/nodemesh/nodemesh/combiner.cpp | 14 ++++ thirdparty/nodemesh/nodemesh/combiner.h | 2 + 14 files changed, 201 insertions(+), 9 deletions(-) diff --git a/src/document.cpp b/src/document.cpp index 74e2891f..17764743 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -915,6 +915,8 @@ void Document::toSnapshot(Snapshot *snapshot, const std::set &limitNodeId part["dirty"] = partIt.second.dirty ? "true" : "false"; if (partIt.second.hasColor) part["color"] = partIt.second.color.name(); + if (partIt.second.colorSolubilityAdjusted()) + part["colorSolubility"] = QString::number(partIt.second.colorSolubility); if (partIt.second.deformThicknessAdjusted()) part["deformThickness"] = QString::number(partIt.second.deformThickness); if (partIt.second.deformWidthAdjusted()) @@ -1190,6 +1192,9 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) part.color = QColor(colorIt->second); part.hasColor = true; } + const auto &colorSolubilityIt = partKv.second.find("colorSolubility"); + if (colorSolubilityIt != partKv.second.end()) + part.colorSolubility = colorSolubilityIt->second.toFloat(); const auto &deformThicknessIt = partKv.second.find("deformThickness"); if (deformThicknessIt != partKv.second.end()) part.setDeformThickness(deformThicknessIt->second.toFloat()); @@ -2311,6 +2316,21 @@ void Document::setPartTarget(QUuid partId, PartTarget target) emit skeletonChanged(); } +void Document::setPartColorSolubility(QUuid partId, float solubility) +{ + auto part = partMap.find(partId); + if (part == partMap.end()) { + qDebug() << "Part not found:" << partId; + return; + } + if (qFuzzyCompare(part->second.colorSolubility, solubility)) + return; + part->second.colorSolubility = solubility; + part->second.dirty = true; + emit partColorSolubilityChanged(partId); + emit skeletonChanged(); +} + void Document::setPartCutRotation(QUuid partId, float cutRotation) { auto part = partMap.find(partId); diff --git a/src/document.h b/src/document.h index 9b970ee2..0a58f03e 100644 --- a/src/document.h +++ b/src/document.h @@ -406,6 +406,7 @@ signals: void partMaterialIdChanged(QUuid partId); void partChamferStateChanged(QUuid partId); void partTargetChanged(QUuid partId); + void partColorSolubilityChanged(QUuid partId); void componentCombineModeChanged(QUuid componentId); void cleanup(); void originChanged(); @@ -565,6 +566,7 @@ public slots: void setPartMaterialId(QUuid partId, QUuid materialId); void setPartChamferState(QUuid partId, bool chamfered); void setPartTarget(QUuid partId, PartTarget target); + void setPartColorSolubility(QUuid partId, float solubility); void setComponentCombineMode(QUuid componentId, CombineMode combineMode); void moveComponentUp(QUuid componentId); void moveComponentDown(QUuid componentId); diff --git a/src/documentwindow.cpp b/src/documentwindow.cpp index bb2794be..e29003af 100644 --- a/src/documentwindow.cpp +++ b/src/documentwindow.cpp @@ -860,6 +860,7 @@ DocumentWindow::DocumentWindow() : connect(m_document, &Document::partCutRotationChanged, partTreeWidget, &PartTreeWidget::partCutRotationChanged); connect(m_document, &Document::partCutFaceChanged, partTreeWidget, &PartTreeWidget::partCutFaceChanged); connect(m_document, &Document::partMaterialIdChanged, partTreeWidget, &PartTreeWidget::partMaterialIdChanged); + connect(m_document, &Document::partColorSolubilityChanged, partTreeWidget, &PartTreeWidget::partColorSolubilityChanged); connect(m_document, &Document::partRemoved, partTreeWidget, &PartTreeWidget::partRemoved); connect(m_document, &Document::cleanup, partTreeWidget, &PartTreeWidget::removeAllContent); connect(m_document, &Document::partChecked, partTreeWidget, &PartTreeWidget::partChecked); diff --git a/src/meshgenerator.cpp b/src/meshgenerator.cpp index c83a36a2..3e72ce8b 100644 --- a/src/meshgenerator.cpp +++ b/src/meshgenerator.cpp @@ -308,6 +308,11 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt if (!materialIdString.isEmpty()) materialId = QUuid(materialIdString); + float colorSolubility = 0; + QString colorSolubilityString = valueOfKeyInMapOrEmpty(part, "colorSolubility"); + if (!colorSolubilityString.isEmpty()) + colorSolubility = colorSolubilityString.toFloat(); + auto &partCache = m_cacheContext->parts[partIdString]; partCache.outcomeNodes.clear(); partCache.outcomeNodeVertices.clear(); @@ -400,6 +405,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt outcomeNode.radius = nodeInfo.radius; outcomeNode.color = partColor; outcomeNode.materialId = materialId; + outcomeNode.colorSolubility = colorSolubility; outcomeNode.boneMark = nodeInfo.boneMark; outcomeNode.mirroredByPartId = mirroredPartIdString; partCache.outcomeNodes.push_back(outcomeNode); @@ -622,6 +628,10 @@ QString MeshGenerator::componentColorName(const std::map *comp return QString(); } auto &part = findPart->second; + QString colorSolubility = valueOfKeyInMapOrEmpty(part, "colorSolubility"); + if (!colorSolubility.isEmpty()) { + return QString("+"); + } QString colorName = valueOfKeyInMapOrEmpty(part, "color"); if (colorName.isEmpty()) return QString("-"); @@ -682,11 +692,15 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineComponentMesh(const QString &com // Firstly, group by combine mode int currentGroupIndex = -1; auto lastCombineMode = CombineMode::Count; + bool foundColorSolubilitySetting = false; for (const auto &childIdString: valueOfKeyInMapOrEmpty(*component, "children").split(",")) { if (childIdString.isEmpty()) continue; const auto &child = findComponent(childIdString); QString colorName = componentColorName(child); + if (colorName == "+") { + foundColorSolubilitySetting = true; + } auto combineMode = componentCombineMode(child); if (lastCombineMode != combineMode || lastCombineMode == CombineMode::Inversion) { qDebug() << "New group[" << currentGroupIndex << "] for combine mode[" << CombineModeToString(combineMode) << "]"; @@ -748,7 +762,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineComponentMesh(const QString &com } multipleMeshes.push_back({childMesh, CombineMode::Normal}); } - nodemesh::Combiner::Mesh *subGroupMesh = combineMultipleMeshes(multipleMeshes, false); + nodemesh::Combiner::Mesh *subGroupMesh = combineMultipleMeshes(multipleMeshes, foundColorSolubilitySetting); if (nullptr == subGroupMesh) continue; groupMeshes.push_back({subGroupMesh, group.first}); @@ -806,6 +820,10 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineMultipleMeshes(const std::vector } } } + if (nullptr != mesh && mesh->isNull()) { + delete mesh; + mesh = nullptr; + } return mesh; } @@ -830,6 +848,11 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineComponentChildGroupMesh(const st componentCache.outcomeNodes.push_back(it); for (const auto &it: childComponentCache.outcomeNodeVertices) componentCache.outcomeNodeVertices.push_back(it); + + if (nullptr == subMesh || subMesh->isNull()) { + delete subMesh; + continue; + } multipleMeshes.push_back({subMesh, childCombineMode}); } diff --git a/src/outcome.h b/src/outcome.h index ff328118..3714a1aa 100644 --- a/src/outcome.h +++ b/src/outcome.h @@ -18,6 +18,7 @@ struct OutcomeNode QVector3D origin; float radius = 0; QColor color; + float colorSolubility = 0; QUuid materialId; QUuid mirrorFromPartId; QUuid mirroredByPartId; diff --git a/src/parttreewidget.cpp b/src/parttreewidget.cpp index 52f81a13..4bd8bbd1 100644 --- a/src/parttreewidget.cpp +++ b/src/parttreewidget.cpp @@ -1003,6 +1003,17 @@ void PartTreeWidget::partMaterialIdChanged(QUuid partId) widget->updateColorButton(); } +void PartTreeWidget::partColorSolubilityChanged(QUuid partId) +{ + auto item = m_partItemMap.find(partId); + if (item == m_partItemMap.end()) { + qDebug() << "Part item not found:" << partId; + return; + } + PartWidget *widget = (PartWidget *)itemWidget(item->second, 0); + widget->updateColorButton(); +} + void PartTreeWidget::partChecked(QUuid partId) { auto item = m_partItemMap.find(partId); diff --git a/src/parttreewidget.h b/src/parttreewidget.h index eb6a48ea..e9c12f95 100644 --- a/src/parttreewidget.h +++ b/src/parttreewidget.h @@ -67,6 +67,7 @@ public slots: void partCutRotationChanged(QUuid partId); void partCutFaceChanged(QUuid partId); void partMaterialIdChanged(QUuid partId); + void partColorSolubilityChanged(QUuid partId); void partChecked(QUuid partId); void partUnchecked(QUuid partId); void groupChanged(QTreeWidgetItem *item, int column); diff --git a/src/partwidget.cpp b/src/partwidget.cpp index 6cbcd4fd..b93f96b3 100644 --- a/src/partwidget.cpp +++ b/src/partwidget.cpp @@ -166,6 +166,7 @@ PartWidget::PartWidget(const Document *document, QUuid partId) : connect(this, &PartWidget::setPartCutFaceLinkedId, m_document, &Document::setPartCutFaceLinkedId); connect(this, &PartWidget::setPartColorState, m_document, &Document::setPartColorState); connect(this, &PartWidget::setPartMaterialId, m_document, &Document::setPartMaterialId); + connect(this, &PartWidget::setPartColorSolubility, m_document, &Document::setPartColorSolubility); connect(this, &PartWidget::checkPart, m_document, &Document::checkPart); connect(this, &PartWidget::enableBackgroundBlur, m_document, &Document::enableBackgroundBlur); connect(this, &PartWidget::disableBackgroundBlur, m_document, &Document::disableBackgroundBlur); @@ -375,8 +376,31 @@ void PartWidget::showColorSettingPopup(const QPoint &pos) } }); + FloatNumberWidget *colorSolubilityWidget = new FloatNumberWidget; + colorSolubilityWidget->setItemName(tr("Solubility")); + colorSolubilityWidget->setRange(0.0, 1.0); + colorSolubilityWidget->setValue(part->colorSolubility); + + connect(colorSolubilityWidget, &FloatNumberWidget::valueChanged, [=](float value) { + emit setPartColorSolubility(m_partId, value); + emit groupOperationAdded(); + }); + + QPushButton *colorSolubilityEraser = new QPushButton(QChar(fa::eraser)); + initToolButton(colorSolubilityEraser); + + connect(colorSolubilityEraser, &QPushButton::clicked, [=]() { + colorSolubilityWidget->setValue(0.0); + emit groupOperationAdded(); + }); + + QHBoxLayout *colorSolubilityLayout = new QHBoxLayout; + colorSolubilityLayout->addWidget(colorSolubilityEraser); + colorSolubilityLayout->addWidget(colorSolubilityWidget); + QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addLayout(colorLayout); + mainLayout->addLayout(colorSolubilityLayout); if (m_document->materialIdList.empty()) { InfoLabel *infoLabel = new InfoLabel; @@ -712,7 +736,7 @@ void PartWidget::updateColorButton() qDebug() << "Part not found:" << m_partId; return; } - if (part->hasColor || part->materialAdjusted()) + if (part->hasColor || part->materialAdjusted() || part->colorSolubilityAdjusted()) updateButton(m_colorButton, QChar(fa::eyedropper), true); else updateButton(m_colorButton, QChar(fa::eyedropper), false); diff --git a/src/partwidget.h b/src/partwidget.h index acc9367d..8a60fee9 100644 --- a/src/partwidget.h +++ b/src/partwidget.h @@ -25,6 +25,7 @@ signals: void setPartCutFace(QUuid partId, CutFace cutFace); void setPartCutFaceLinkedId(QUuid partId, QUuid linkedId); void setPartMaterialId(QUuid partId, QUuid materialId); + void setPartColorSolubility(QUuid partId, float colorSolubility); void movePartUp(QUuid partId); void movePartDown(QUuid partId); void movePartToTop(QUuid partId); diff --git a/src/skeletondocument.h b/src/skeletondocument.h index 48bb7419..c6774b82 100644 --- a/src/skeletondocument.h +++ b/src/skeletondocument.h @@ -93,6 +93,7 @@ public: QUuid cutFaceLinkedId; QUuid materialId; PartTarget target; + float colorSolubility; SkeletonPart(const QUuid &withId=QUuid()) : visible(true), locked(false), @@ -110,7 +111,8 @@ public: dirty(true), cutRotation(0.0), cutFace(CutFace::Quad), - target(PartTarget::Model) + target(PartTarget::Model), + colorSolubility(0.0) { id = withId.isNull() ? QUuid::createUuid() : withId; } @@ -164,6 +166,10 @@ public: { return deformThicknessAdjusted() || deformWidthAdjusted(); } + bool colorSolubilityAdjusted() const + { + return fabs(colorSolubility - 0.0) >= 0.01; + } bool cutRotationAdjusted() const { return fabs(cutRotation - 0.0) >= 0.01; @@ -206,6 +212,7 @@ public: dirty = other.dirty; materialId = other.materialId; target = other.target; + colorSolubility = other.colorSolubility; } void updatePreviewMesh(MeshLoader *previewMesh) { diff --git a/src/texturegenerator.cpp b/src/texturegenerator.cpp index 7e09ee07..c2f5a943 100644 --- a/src/texturegenerator.cpp +++ b/src/texturegenerator.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "texturegenerator.h" #include "theme.h" #include "util.h" @@ -231,9 +232,11 @@ void TextureGenerator::generate() std::map partColorMap; std::map, const OutcomeNode *> nodeMap; + std::map partColorSolubilityMap; for (const auto &item: m_outcome->nodes) { nodeMap.insert({{item.partId, item.nodeId}, &item}); partColorMap.insert({item.partId, item.color}); + partColorSolubilityMap.insert({item.partId, item.colorSolubility}); } auto createImageBeginTime = countTimeConsumed.elapsed(); @@ -316,6 +319,81 @@ void TextureGenerator::generate() } } + auto drawGradient = [&](const QUuid &partId, size_t triangleIndex, size_t firstVertexIndex, size_t secondVertexIndex, + const QUuid &neighborPartId) { + const std::vector &uv = triangleVertexUvs[triangleIndex]; + const auto &allRects = partUvRects.find(partId); + if (allRects == partUvRects.end()) { + qDebug() << "Found part uv rects failed"; + return; + } + const auto &firstPoint = uv[firstVertexIndex]; + const auto &secondPoint = uv[secondVertexIndex]; + auto edgeLength = firstPoint.distanceToPoint(secondPoint); + auto middlePoint = (firstPoint + secondPoint) / 2.0; + const auto &findColor = partColorMap.find(partId); + if (findColor == partColorMap.end()) + return; + const auto &findNeighborColorSolubility = partColorSolubilityMap.find(neighborPartId); + if (findNeighborColorSolubility == partColorSolubilityMap.end()) + return; + if (qFuzzyIsNull(findNeighborColorSolubility->second)) + return; + const auto &findNeighborColor = partColorMap.find(neighborPartId); + if (findNeighborColor == partColorMap.end()) + return; + for (const auto &it: allRects->second) { + if (it.contains(firstPoint.x(), firstPoint.y()) || + it.contains(secondPoint.x(), secondPoint.y())) { + float finalRadius = (it.width() + it.height()) * 0.5 * findNeighborColorSolubility->second; + if (finalRadius < edgeLength) + finalRadius = edgeLength; + QRadialGradient gradient(QPointF(middlePoint.x() * TextureGenerator::m_textureSize, + middlePoint.y() * TextureGenerator::m_textureSize), + finalRadius * TextureGenerator::m_textureSize); + gradient.setColorAt(0.0, findNeighborColor->second); + gradient.setColorAt(1.0, Qt::transparent); + QRectF fillTarget((middlePoint.x() - finalRadius), + (middlePoint.y() - finalRadius), + (finalRadius + finalRadius), + (finalRadius + finalRadius)); + auto clippedRect = it.intersected(fillTarget); + texturePainter.fillRect(clippedRect.left() * TextureGenerator::m_textureSize, + clippedRect.top() * TextureGenerator::m_textureSize, + clippedRect.width() * TextureGenerator::m_textureSize, + clippedRect.height() * TextureGenerator::m_textureSize, + gradient); + break; + } + } + }; + + std::map, std::tuple> halfEdgeToTriangleMap; + for (size_t i = 0; i < m_outcome->triangles.size(); ++i) { + const auto &triangleIndices = m_outcome->triangles[i]; + if (triangleIndices.size() != 3) { + qDebug() << "Found invalid triangle indices"; + continue; + } + for (size_t j = 0; j < triangleIndices.size(); ++j) { + size_t k = (j + 1) % triangleIndices.size(); + halfEdgeToTriangleMap.insert(std::make_pair(std::make_pair(triangleIndices[j], triangleIndices[k]), + std::make_tuple(i, j, k))); + } + } + for (const auto &it: halfEdgeToTriangleMap) { + auto oppositeHalfEdge = std::make_pair(it.first.second, it.first.first); + const auto &opposite = halfEdgeToTriangleMap.find(oppositeHalfEdge); + if (opposite == halfEdgeToTriangleMap.end()) + continue; + const std::pair &source = triangleSourceNodes[std::get<0>(it.second)]; + const std::pair &oppositeSource = triangleSourceNodes[std::get<0>(opposite->second)]; + if (source.first == oppositeSource.first) + continue; + drawGradient(source.first, std::get<0>(it.second), std::get<1>(it.second), std::get<2>(it.second), oppositeSource.first); + drawGradient(oppositeSource.first, std::get<0>(opposite->second), std::get<1>(opposite->second), std::get<2>(opposite->second), source.first); + } + for (auto i = 0u; i < triangleVertexUvs.size(); i++) { QPainterPath path; const std::vector &uv = triangleVertexUvs[i]; @@ -337,12 +415,12 @@ void TextureGenerator::generate() texturePainter.drawImage(0, 0, findColorTextureResult->second); texturePainter.setClipping(false); } else { - auto findSourceNodeResult = nodeMap.find(source); - if (findSourceNodeResult != nodeMap.end() && nullptr != findSourceNodeResult->second) { - texturePainter.fillPath(path, QBrush(findSourceNodeResult->second->color)); - } else { - texturePainter.fillPath(path, QBrush(m_defaultTextureColor)); - } + //auto findSourceNodeResult = nodeMap.find(source); + //if (findSourceNodeResult != nodeMap.end() && nullptr != findSourceNodeResult->second) { + // texturePainter.fillPath(path, QBrush(findSourceNodeResult->second->color)); + //} else { + // texturePainter.fillPath(path, QBrush(m_defaultTextureColor)); + //} } // Copy normal texture if there is one auto findNormalTextureResult = m_partNormalTextureMap.find(source.first); diff --git a/thirdparty/nodemesh/nodemesh/cgalmesh.h b/thirdparty/nodemesh/nodemesh/cgalmesh.h index 5d8d4232..7a4d555a 100644 --- a/thirdparty/nodemesh/nodemesh/cgalmesh.h +++ b/thirdparty/nodemesh/nodemesh/cgalmesh.h @@ -84,5 +84,12 @@ void fetchFromCgalMesh(typename CGAL::Surface_mesh *me } } +template +bool isNullCgalMesh(typename CGAL::Surface_mesh *mesh) +{ + typename CGAL::Surface_mesh::Face_range faceRage = mesh->faces(); + return faceRage.begin() == faceRage.end(); +} + #endif diff --git a/thirdparty/nodemesh/nodemesh/combiner.cpp b/thirdparty/nodemesh/nodemesh/combiner.cpp index ced2908f..93028acf 100644 --- a/thirdparty/nodemesh/nodemesh/combiner.cpp +++ b/thirdparty/nodemesh/nodemesh/combiner.cpp @@ -47,12 +47,14 @@ Combiner::Mesh::Mesh(const std::vector &vertices, const std::vector(exactMesh)) { + delete exactMesh; + m_privateData = nullptr; + } +} + } diff --git a/thirdparty/nodemesh/nodemesh/combiner.h b/thirdparty/nodemesh/nodemesh/combiner.h index 19be4d54..36e91bcd 100644 --- a/thirdparty/nodemesh/nodemesh/combiner.h +++ b/thirdparty/nodemesh/nodemesh/combiner.h @@ -38,6 +38,8 @@ public: private: void *m_privateData = nullptr; bool m_isSelfIntersected = false; + + void validate(); }; static Mesh *combine(const Mesh &firstMesh, const Mesh &secondMesh, Method method,