diff --git a/dust3d.pro b/dust3d.pro index 656dd464..e094470f 100644 --- a/dust3d.pro +++ b/dust3d.pro @@ -295,6 +295,15 @@ HEADERS += src/cutdocument.h SOURCES += src/cutface.cpp HEADERS += src/cutface.h +SOURCES += src/parttarget.cpp +HEADERS += src/parttarget.h + +SOURCES += src/cutfacewidget.cpp +HEADERS += src/cutfacewidget.h + +SOURCES += src/cutfacelistwidget.cpp +HEADERS += src/cutfacelistwidget.h + SOURCES += src/remoteioserver.cpp HEADERS += src/remoteioserver.h diff --git a/shaders/default.vert b/shaders/default.vert index 1145ccd5..49ef89fa 100644 --- a/shaders/default.vert +++ b/shaders/default.vert @@ -33,7 +33,7 @@ void main() vert = (modelMatrix * vertex).xyz; vertNormal = normalize((modelMatrix * vec4(normal, 1.0)).xyz); vertColor = color; - cameraPos = vec3(0, 0, -2.1); + cameraPos = vec3(0, 0, -4.0); firstLightPos = vec3(5.0, 5.0, 5.0); secondLightPos = vec3(-5.0, 5.0, 5.0); diff --git a/src/cutface.cpp b/src/cutface.cpp index cc34bae9..208396e6 100644 --- a/src/cutface.cpp +++ b/src/cutface.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include "cutface.h" IMPL_CutFaceFromString @@ -38,3 +40,45 @@ void normalizeCutFacePoints(std::vector *points) position.setY((position.y() - yMiddle) * 2 / longSize); } } + +void cutFacePointsFromNodes(std::vector &points, const std::vector> &nodes) +{ + if (nodes.size() < 2) + return; + std::vector edges; + for (size_t i = 1; i < nodes.size(); ++i) { + const auto &previous = nodes[i - 1]; + const auto ¤t = nodes[i]; + edges.push_back((QVector2D(std::get<1>(current), std::get<2>(current)) - + QVector2D(std::get<1>(previous), std::get<2>(previous))).normalized()); + } + std::vector nodeDirections; + nodeDirections.push_back(edges[0]); + for (size_t i = 1; i < nodes.size() - 1; ++i) { + const auto &previousEdge = edges[i - 1]; + const auto &nextEdge = edges[i]; + nodeDirections.push_back((previousEdge + nextEdge).normalized()); + } + nodeDirections.push_back(edges[edges.size() - 1]); + std::vector> cutPoints; + for (size_t i = 0; i < nodes.size(); ++i) { + const auto ¤t = nodes[i]; + const auto &direction = nodeDirections[i]; + const auto &radius = std::get<0>(current); + QVector3D origin = QVector3D(std::get<1>(current), std::get<2>(current), 0); + QVector3D pointer = QVector3D(direction.x(), direction.y(), 0); + QVector3D rotateAxis = QVector3D(0, 0, 1); + QVector3D u = QVector3D::crossProduct(pointer, rotateAxis).normalized(); + QVector3D upPoint = origin + u * radius; + QVector3D downPoint = origin - u * radius; + cutPoints.push_back({QVector2D(upPoint.x(), upPoint.y()), + QVector2D(downPoint.x(), downPoint.y())}); + } + for (const auto &it: cutPoints) { + points.push_back(it.first); + } + for (auto it = cutPoints.rbegin(); it != cutPoints.rend(); ++it) { + points.push_back(it->second); + } + normalizeCutFacePoints(&points); +} diff --git a/src/cutface.h b/src/cutface.h index 4f1c9472..20d3ac84 100644 --- a/src/cutface.h +++ b/src/cutface.h @@ -11,7 +11,7 @@ enum class CutFace Pentagon, Hexagon, Triangle, - //UserDefined, + UserDefined, Count }; @@ -28,6 +28,8 @@ CutFace CutFaceFromString(const char *faceString) \ return CutFace::Hexagon; \ if (face == "Triangle") \ return CutFace::Triangle; \ + if (face == "UserDefined") \ + return CutFace::UserDefined; \ return CutFace::Quad; \ } QString CutFaceToString(CutFace cutFace); @@ -43,6 +45,8 @@ QString CutFaceToString(CutFace cutFace) \ return "Hexagon"; \ case CutFace::Triangle: \ return "Triangle"; \ + case CutFace::UserDefined: \ + return "UserDefined"; \ default: \ return ""; \ } \ @@ -93,5 +97,6 @@ std::vector CutFaceToPoints(CutFace cutFace) \ } void normalizeCutFacePoints(std::vector *points); +void cutFacePointsFromNodes(std::vector &points, const std::vector> &nodes); #endif diff --git a/src/cutfacelistwidget.cpp b/src/cutfacelistwidget.cpp new file mode 100644 index 00000000..4418cee9 --- /dev/null +++ b/src/cutfacelistwidget.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include "cutfacelistwidget.h" + +CutFaceListWidget::CutFaceListWidget(const Document *document, QWidget *parent) : + QTreeWidget(parent), + m_document(document) +{ + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + setFocusPolicy(Qt::NoFocus); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setAutoScroll(false); + + setHeaderHidden(true); + + QPalette palette = this->palette(); + palette.setColor(QPalette::Window, Qt::transparent); + palette.setColor(QPalette::Base, Qt::transparent); + setPalette(palette); + + setStyleSheet("QTreeView {qproperty-indentation: 0;}"); + + setContentsMargins(0, 0, 0, 0); + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QTreeWidget::customContextMenuRequested, this, &CutFaceListWidget::showContextMenu); + + std::set cutFacePartIds; + for (const auto &it: m_document->partMap) { + if (PartTarget::CutFace == it.second.target) { + if (cutFacePartIds.find(it.first) != cutFacePartIds.end()) + continue; + cutFacePartIds.insert(it.first); + m_cutFacePartIdList.push_back(it.first); + } + if (!it.second.cutFaceLinkedId.isNull()) { + if (cutFacePartIds.find(it.second.cutFaceLinkedId) != cutFacePartIds.end()) + continue; + cutFacePartIds.insert(it.second.cutFaceLinkedId); + m_cutFacePartIdList.push_back(it.second.cutFaceLinkedId); + } + } + + reload(); +} + +bool CutFaceListWidget::isEmpty() +{ + return m_cutFacePartIdList.empty(); +} + +void CutFaceListWidget::enableMultipleSelection(bool enabled) +{ + m_multipleSelectionEnabled = enabled; +} + +void CutFaceListWidget::updateCutFaceSelectState(QUuid partId, bool selected) +{ + auto findItemResult = m_itemMap.find(partId); + if (findItemResult == m_itemMap.end()) { + qDebug() << "Find part item failed:" << partId; + return; + } + CutFaceWidget *cutFaceWidget = (CutFaceWidget *)itemWidget(findItemResult->second.first, findItemResult->second.second); + cutFaceWidget->updateCheckedState(selected); +} + +void CutFaceListWidget::selectCutFace(QUuid partId, bool multiple) +{ + if (multiple) { + if (!m_currentSelectedPartId.isNull()) { + m_selectedPartIds.insert(m_currentSelectedPartId); + m_currentSelectedPartId = QUuid(); + } + if (m_selectedPartIds.find(partId) != m_selectedPartIds.end()) { + updateCutFaceSelectState(partId, false); + m_selectedPartIds.erase(partId); + } else { + if (m_multipleSelectionEnabled || m_selectedPartIds.empty()) { + updateCutFaceSelectState(partId, true); + m_selectedPartIds.insert(partId); + } + } + if (m_selectedPartIds.size() > 1) { + return; + } + if (m_selectedPartIds.size() == 1) + partId = *m_selectedPartIds.begin(); + else { + partId = QUuid(); + emit currentSelectedCutFaceChanged(partId); + } + } + if (!m_selectedPartIds.empty()) { + for (const auto &id: m_selectedPartIds) { + updateCutFaceSelectState(id, false); + } + m_selectedPartIds.clear(); + } + if (m_currentSelectedPartId != partId) { + if (!m_currentSelectedPartId.isNull()) { + updateCutFaceSelectState(m_currentSelectedPartId, false); + } + m_currentSelectedPartId = partId; + if (!m_currentSelectedPartId.isNull()) { + updateCutFaceSelectState(m_currentSelectedPartId, true); + } + emit currentSelectedCutFaceChanged(m_currentSelectedPartId); + } +} + +void CutFaceListWidget::mousePressEvent(QMouseEvent *event) +{ + QModelIndex itemIndex = indexAt(event->pos()); + QTreeView::mousePressEvent(event); + if (event->button() == Qt::LeftButton) { + bool multiple = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier); + if (itemIndex.isValid()) { + QTreeWidgetItem *item = itemFromIndex(itemIndex); + auto partId = QUuid(item->data(itemIndex.column(), Qt::UserRole).toString()); + if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier)) { + bool startAdd = false; + bool stopAdd = false; + std::vector waitQueue; + for (const auto &childId: m_cutFacePartIdList) { + if (m_shiftStartPartId == childId || partId == childId) { + if (startAdd) { + stopAdd = true; + } else { + startAdd = true; + } + } + if (startAdd) + waitQueue.push_back(childId); + if (stopAdd) + break; + } + if (stopAdd && !waitQueue.empty()) { + if (!m_selectedPartIds.empty()) { + for (const auto &id: m_selectedPartIds) { + updateCutFaceSelectState(id, false); + } + m_selectedPartIds.clear(); + } + if (!m_currentSelectedPartId.isNull()) { + m_currentSelectedPartId = QUuid(); + } + for (const auto &waitId: waitQueue) { + selectCutFace(waitId, true); + } + } + return; + } else { + m_shiftStartPartId = partId; + } + selectCutFace(partId, multiple); + return; + } + if (!multiple) + selectCutFace(QUuid()); + } +} + +bool CutFaceListWidget::isCutFaceSelected(QUuid partId) +{ + return (m_currentSelectedPartId == partId || + m_selectedPartIds.find(partId) != m_selectedPartIds.end()); +} + +void CutFaceListWidget::showContextMenu(const QPoint &pos) +{ + if (!m_hasContextMenu) + return; +} + +void CutFaceListWidget::resizeEvent(QResizeEvent *event) +{ + QTreeWidget::resizeEvent(event); + if (calculateColumnCount() != columnCount()) + reload(); +} + +int CutFaceListWidget::calculateColumnCount() +{ + if (nullptr == parentWidget()) + return 0; + + int columns = parentWidget()->width() / Theme::cutFacePreviewImageSize; + if (0 == columns) + columns = 1; + return columns; +} + +void CutFaceListWidget::reload() +{ + removeAllContent(); + + int columns = calculateColumnCount(); + if (0 == columns) + return; + + int columnWidth = parentWidget()->width() / columns; + + //qDebug() << "parentWidth:" << parentWidget()->width() << "columnWidth:" << columnWidth << "columns:" << columns; + + setColumnCount(columns); + for (int i = 0; i < columns; i++) + setColumnWidth(i, columnWidth); + + decltype(m_cutFacePartIdList.size()) cutFaceIndex = 0; + while (cutFaceIndex < m_cutFacePartIdList.size()) { + QTreeWidgetItem *item = new QTreeWidgetItem(this); + item->setFlags((item->flags() | Qt::ItemIsEnabled) & ~(Qt::ItemIsSelectable) & ~(Qt::ItemIsEditable)); + for (int col = 0; col < columns && cutFaceIndex < m_cutFacePartIdList.size(); col++, cutFaceIndex++) { + const auto &partId = m_cutFacePartIdList[cutFaceIndex]; + item->setSizeHint(col, QSize(columnWidth, CutFaceWidget::preferredHeight() + 2)); + item->setData(col, Qt::UserRole, partId.toString()); + CutFaceWidget *widget = new CutFaceWidget(m_document, partId); + setItemWidget(item, col, widget); + widget->reload(); + widget->updateCheckedState(isCutFaceSelected(partId)); + m_itemMap[partId] = std::make_pair(item, col); + } + invisibleRootItem()->addChild(item); + } +} + +void CutFaceListWidget::setHasContextMenu(bool hasContextMenu) +{ + m_hasContextMenu = hasContextMenu; +} + +void CutFaceListWidget::removeAllContent() +{ + m_itemMap.clear(); + clear(); +} diff --git a/src/cutfacelistwidget.h b/src/cutfacelistwidget.h new file mode 100644 index 00000000..4825ef62 --- /dev/null +++ b/src/cutfacelistwidget.h @@ -0,0 +1,41 @@ +#ifndef DUST3D_CUT_FACE_LIST_WIDGET_H +#define DUST3D_CUT_FACE_LIST_WIDGET_H +#include +#include +#include +#include "document.h" +#include "cutfacewidget.h" + +class CutFaceListWidget : public QTreeWidget +{ + Q_OBJECT +signals: + void currentSelectedCutFaceChanged(QUuid partId); +public: + CutFaceListWidget(const Document *document, QWidget *parent=nullptr); + bool isCutFaceSelected(QUuid partId); + void enableMultipleSelection(bool enabled); + bool isEmpty(); +public slots: + void reload(); + void removeAllContent(); + void showContextMenu(const QPoint &pos); + void selectCutFace(QUuid partId, bool multiple=false); + void setHasContextMenu(bool hasContextMenu); +protected: + void resizeEvent(QResizeEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; +private: + int calculateColumnCount(); + void updateCutFaceSelectState(QUuid partId, bool selected); + const Document *m_document = nullptr; + std::map> m_itemMap; + std::set m_selectedPartIds; + QUuid m_currentSelectedPartId; + QUuid m_shiftStartPartId; + bool m_hasContextMenu = false; + bool m_multipleSelectionEnabled = false; + std::vector m_cutFacePartIdList; +}; + +#endif diff --git a/src/cutfacewidget.cpp b/src/cutfacewidget.cpp new file mode 100644 index 00000000..90614677 --- /dev/null +++ b/src/cutfacewidget.cpp @@ -0,0 +1,60 @@ +#include "cutfacewidget.h" + +CutFaceWidget::CutFaceWidget(const Document *document, QUuid partId) : + m_partId(partId), + m_document(document) +{ + setObjectName("CutFaceFrame"); + + m_previewWidget = new ModelWidget(this); + m_previewWidget->setAttribute(Qt::WA_TransparentForMouseEvents); + m_previewWidget->setFixedSize(Theme::cutFacePreviewImageSize, Theme::cutFacePreviewImageSize); + m_previewWidget->enableMove(false); + m_previewWidget->enableZoom(false); + + setFixedSize(Theme::cutFacePreviewImageSize, CutFaceWidget::preferredHeight()); + + connect(document, &Document::partPreviewChanged, this, &CutFaceWidget::updatePreview); +} + +void CutFaceWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + m_previewWidget->move((width() - Theme::cutFacePreviewImageSize) / 2, 0); +} + +int CutFaceWidget::preferredHeight() +{ + return Theme::cutFacePreviewImageSize; +} + +void CutFaceWidget::reload() +{ + updatePreview(m_partId); +} + +void CutFaceWidget::updatePreview(QUuid partId) +{ + if (partId != m_partId) + return; + const SkeletonPart *part = m_document->findPart(m_partId); + if (!part) { + qDebug() << "Part not found:" << m_partId; + return; + } + MeshLoader *previewMesh = part->takePreviewMesh(); + m_previewWidget->updateMesh(previewMesh); +} + +void CutFaceWidget::updateCheckedState(bool checked) +{ + if (checked) + setStyleSheet("#CutFaceFrame {border: 1px solid " + Theme::red.name() + ";}"); + else + setStyleSheet("#CutFaceFrame {border: 1px solid transparent;}"); +} + +ModelWidget *CutFaceWidget::previewWidget() +{ + return m_previewWidget; +} diff --git a/src/cutfacewidget.h b/src/cutfacewidget.h new file mode 100644 index 00000000..223a32ff --- /dev/null +++ b/src/cutfacewidget.h @@ -0,0 +1,28 @@ +#ifndef DUST3D_CUT_FACE_WIDGET_H +#define DUST3D_CUT_FACE_WIDGET_H +#include +#include +#include +#include "document.h" +#include "modelwidget.h" + +class CutFaceWidget : public QFrame +{ + Q_OBJECT +public: + CutFaceWidget(const Document *document, QUuid partId); + static int preferredHeight(); + ModelWidget *previewWidget(); +protected: + void resizeEvent(QResizeEvent *event) override; +public slots: + void reload(); + void updatePreview(QUuid partId); + void updateCheckedState(bool checked); +private: + QUuid m_partId; + const Document *m_document = nullptr; + ModelWidget *m_previewWidget = nullptr; +}; + +#endif diff --git a/src/document.cpp b/src/document.cpp index c4cc1e4c..86236b01 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -147,6 +147,7 @@ void Document::removeEdge(QUuid edgeId) QUuid oldPartId = oldPart->id; std::vector> groups; splitPartByEdge(&groups, edgeId); + std::vector> newPartNodeNumMap; for (auto groupIt = groups.begin(); groupIt != groups.end(); groupIt++) { const auto newUuid = QUuid::createUuid(); SkeletonPart &part = partMap[newUuid]; @@ -171,6 +172,7 @@ void Document::removeEdge(QUuid edgeId) } } addPartToComponent(part.id, findComponentParentId(part.componentId)); + newPartNodeNumMap.push_back({part.id, part.nodeIds.size()}); emit partAdded(part.id); } for (auto nodeIdIt = edge->nodeIds.begin(); nodeIdIt != edge->nodeIds.end(); nodeIdIt++) { @@ -186,6 +188,14 @@ void Document::removeEdge(QUuid edgeId) emit edgeRemoved(edgeId); removePart(oldPartId); + if (!newPartNodeNumMap.empty()) { + std::sort(newPartNodeNumMap.begin(), newPartNodeNumMap.end(), [&]( + const std::pair &first, const std::pair &second) { + return first.second > second.second; + }); + updateLinkedPart(oldPartId, newPartNodeNumMap[0].first); + } + emit skeletonChanged(); } @@ -207,6 +217,7 @@ void Document::removeNode(QUuid nodeId) QUuid oldPartId = oldPart->id; std::vector> groups; splitPartByNode(&groups, nodeId); + std::vector> newPartNodeNumMap; for (auto groupIt = groups.begin(); groupIt != groups.end(); groupIt++) { const auto newUuid = QUuid::createUuid(); SkeletonPart &part = partMap[newUuid]; @@ -231,6 +242,7 @@ void Document::removeNode(QUuid nodeId) } } addPartToComponent(part.id, findComponentParentId(part.componentId)); + newPartNodeNumMap.push_back({part.id, part.nodeIds.size()}); emit partAdded(part.id); } for (auto edgeIdIt = node->edgeIds.begin(); edgeIdIt != node->edgeIds.end(); edgeIdIt++) { @@ -253,6 +265,14 @@ void Document::removeNode(QUuid nodeId) emit nodeRemoved(nodeId); removePart(oldPartId); + if (!newPartNodeNumMap.empty()) { + std::sort(newPartNodeNumMap.begin(), newPartNodeNumMap.end(), [&]( + const std::pair &first, const std::pair &second) { + return first.second > second.second; + }); + updateLinkedPart(oldPartId, newPartNodeNumMap[0].first); + } + emit skeletonChanged(); } @@ -543,12 +563,23 @@ void Document::addEdge(QUuid fromNodeId, QUuid toNodeId) emit edgeAdded(edge.id); if (toPartRemoved) { + updateLinkedPart(toPartId, fromNode->partId); removePart(toPartId); } emit skeletonChanged(); } +void Document::updateLinkedPart(QUuid oldPartId, QUuid newPartId) +{ + for (auto &partIt: partMap) { + if (partIt.second.cutFaceLinkedId == oldPartId) { + partIt.second.dirty = true; + partIt.second.setCutFaceLinkedId(newPartId); + } + } +} + const Component *Document::findComponent(QUuid componentId) const { if (componentId.isNull()) @@ -865,10 +896,19 @@ void Document::toSnapshot(Snapshot *snapshot, const std::set &limitNodeId part["zMirrored"] = partIt.second.zMirrored ? "true" : "false"; part["rounded"] = partIt.second.rounded ? "true" : "false"; part["chamfered"] = partIt.second.chamfered ? "true" : "false"; + if (PartTarget::Model != partIt.second.target) + part["target"] = PartTargetToString(partIt.second.target); if (partIt.second.cutRotationAdjusted()) part["cutRotation"] = QString::number(partIt.second.cutRotation); - if (partIt.second.cutFaceAdjusted()) - part["cutFace"] = CutFaceToString(partIt.second.cutFace); + if (partIt.second.cutFaceAdjusted()) { + if (CutFace::UserDefined == partIt.second.cutFace) { + if (!partIt.second.cutFaceLinkedId.isNull()) { + part["cutFace"] = partIt.second.cutFaceLinkedId.toString(); + } + } else { + part["cutFace"] = CutFaceToString(partIt.second.cutFace); + } + } part["dirty"] = partIt.second.dirty ? "true" : "false"; if (partIt.second.hasColor) part["color"] = partIt.second.color.name(); @@ -1110,6 +1150,7 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) materialIdList.push_back(newMaterialId); emit materialAdded(newMaterialId); } + std::map cutFaceLinkedIdModifyMap; for (const auto &partKv: snapshot.parts) { const auto newUuid = QUuid::createUuid(); SkeletonPart &part = partMap[newUuid]; @@ -1124,12 +1165,20 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) part.zMirrored = isTrueValueString(valueOfKeyInMapOrEmpty(partKv.second, "zMirrored")); part.rounded = isTrueValueString(valueOfKeyInMapOrEmpty(partKv.second, "rounded")); part.chamfered = isTrueValueString(valueOfKeyInMapOrEmpty(partKv.second, "chamfered")); + part.target = PartTargetFromString(valueOfKeyInMapOrEmpty(partKv.second, "target").toUtf8().constData()); const auto &cutRotationIt = partKv.second.find("cutRotation"); if (cutRotationIt != partKv.second.end()) part.setCutRotation(cutRotationIt->second.toFloat()); const auto &cutFaceIt = partKv.second.find("cutFace"); - if (cutFaceIt != partKv.second.end()) - part.setCutFace(CutFaceFromString(cutFaceIt->second.toUtf8().constData())); + if (cutFaceIt != partKv.second.end()) { + QUuid cutFaceLinkedId = QUuid(cutFaceIt->second); + if (cutFaceLinkedId.isNull()) { + part.setCutFace(CutFaceFromString(cutFaceIt->second.toUtf8().constData())); + } else { + part.setCutFaceLinkedId(cutFaceLinkedId); + cutFaceLinkedIdModifyMap.insert({part.id, cutFaceLinkedId}); + } + } if (isTrueValueString(valueOfKeyInMapOrEmpty(partKv.second, "inverse"))) inversePartIds.insert(part.id); const auto &colorIt = partKv.second.find("color"); @@ -1148,6 +1197,14 @@ void Document::addFromSnapshot(const Snapshot &snapshot, bool fromPaste) part.materialId = oldNewIdMap[QUuid(materialIdIt->second)]; newAddedPartIds.insert(part.id); } + for (const auto &it: cutFaceLinkedIdModifyMap) { + SkeletonPart &part = partMap[it.first]; + auto findNewLinkedId = oldNewIdMap.find(it.second); + if (findNewLinkedId == oldNewIdMap.end()) + part.setCutFaceLinkedId(QUuid()); + else + part.setCutFaceLinkedId(findNewLinkedId->second); + } for (const auto &nodeKv: snapshot.nodes) { if (nodeKv.second.find("radius") == nodeKv.second.end() || nodeKv.second.find("x") == nodeKv.second.end() || @@ -2220,6 +2277,21 @@ void Document::setPartChamferState(QUuid partId, bool chamfered) emit skeletonChanged(); } +void Document::setPartTarget(QUuid partId, PartTarget target) +{ + auto part = partMap.find(partId); + if (part == partMap.end()) { + qDebug() << "Part not found:" << partId; + return; + } + if (part->second.target == target) + return; + part->second.target = target; + part->second.dirty = true; + emit partTargetChanged(partId); + emit skeletonChanged(); +} + void Document::setPartCutRotation(QUuid partId, float cutRotation) { auto part = partMap.find(partId); @@ -2250,6 +2322,22 @@ void Document::setPartCutFace(QUuid partId, CutFace cutFace) emit skeletonChanged(); } +void Document::setPartCutFaceLinkedId(QUuid partId, QUuid linkedId) +{ + auto part = partMap.find(partId); + if (part == partMap.end()) { + qDebug() << "Part not found:" << partId; + return; + } + if (part->second.cutFace == CutFace::UserDefined && + part->second.cutFaceLinkedId == linkedId) + return; + part->second.setCutFaceLinkedId(linkedId); + part->second.dirty = true; + emit partCutFaceChanged(partId); + emit skeletonChanged(); +} + void Document::setPartColorState(QUuid partId, bool hasColor, QColor color) { auto part = partMap.find(partId); diff --git a/src/document.h b/src/document.h index aff9610a..abf1d414 100644 --- a/src/document.h +++ b/src/document.h @@ -404,6 +404,7 @@ signals: void partCutFaceChanged(QUuid partId); void partMaterialIdChanged(QUuid partId); void partChamferStateChanged(QUuid partId); + void partTargetChanged(QUuid partId); void componentCombineModeChanged(QUuid componentId); void cleanup(); void originChanged(); @@ -558,8 +559,10 @@ public slots: void setPartColorState(QUuid partId, bool hasColor, QColor color); void setPartCutRotation(QUuid partId, float cutRotation); void setPartCutFace(QUuid partId, CutFace cutFace); + void setPartCutFaceLinkedId(QUuid partId, QUuid linkedId); void setPartMaterialId(QUuid partId, QUuid materialId); void setPartChamferState(QUuid partId, bool chamfered); + void setPartTarget(QUuid partId, PartTarget target); void setComponentCombineMode(QUuid componentId, CombineMode combineMode); void moveComponentUp(QUuid componentId); void moveComponentDown(QUuid componentId); @@ -632,6 +635,7 @@ private: void resetDirtyFlags(); void markAllDirty(); void removeRigResults(); + void updateLinkedPart(QUuid oldPartId, QUuid newPartId); private: // need initialize bool m_isResultMeshObsolete; MeshGenerator *m_meshGenerator; diff --git a/src/documentwindow.cpp b/src/documentwindow.cpp index c2149479..f81179a3 100644 --- a/src/documentwindow.cpp +++ b/src/documentwindow.cpp @@ -831,6 +831,7 @@ DocumentWindow::DocumentWindow() : connect(partTreeWidget, &PartTreeWidget::setPartLockState, m_document, &Document::setPartLockState); connect(partTreeWidget, &PartTreeWidget::setPartVisibleState, m_document, &Document::setPartVisibleState); connect(partTreeWidget, &PartTreeWidget::setComponentCombineMode, m_document, &Document::setComponentCombineMode); + connect(partTreeWidget, &PartTreeWidget::setPartTarget, m_document, &Document::setPartTarget); connect(partTreeWidget, &PartTreeWidget::hideDescendantComponents, m_document, &Document::hideDescendantComponents); connect(partTreeWidget, &PartTreeWidget::showDescendantComponents, m_document, &Document::showDescendantComponents); connect(partTreeWidget, &PartTreeWidget::lockDescendantComponents, m_document, &Document::lockDescendantComponents); diff --git a/src/materialwidget.cpp b/src/materialwidget.cpp index 3483ee70..ed9ce133 100644 --- a/src/materialwidget.cpp +++ b/src/materialwidget.cpp @@ -63,12 +63,14 @@ int MaterialWidget::preferredHeight() void MaterialWidget::reload() { - updatePreview(); - updateName(); + updatePreview(m_materialId); + updateName(m_materialId); } -void MaterialWidget::updatePreview() +void MaterialWidget::updatePreview(QUuid materialId) { + if (materialId != m_materialId) + return; const Material *material = m_document->findMaterial(m_materialId); if (!material) { qDebug() << "Material not found:" << m_materialId; @@ -78,8 +80,10 @@ void MaterialWidget::updatePreview() m_previewWidget->updateMesh(previewMesh); } -void MaterialWidget::updateName() +void MaterialWidget::updateName(QUuid materialId) { + if (materialId != m_materialId) + return; const Material *material = m_document->findMaterial(m_materialId); if (!material) { qDebug() << "Material not found:" << m_materialId; diff --git a/src/materialwidget.h b/src/materialwidget.h index 63ec68af..c877ac9a 100644 --- a/src/materialwidget.h +++ b/src/materialwidget.h @@ -21,8 +21,8 @@ protected: void resizeEvent(QResizeEvent *event) override; public slots: void reload(); - void updatePreview(); - void updateName(); + void updatePreview(QUuid materialId); + void updateName(QUuid materialId); void updateCheckedState(bool checked); void setCornerButtonVisible(bool visible); private: diff --git a/src/meshgenerator.cpp b/src/meshgenerator.cpp index 28b01ba6..adfac26d 100644 --- a/src/meshgenerator.cpp +++ b/src/meshgenerator.cpp @@ -11,6 +11,8 @@ #include "util.h" #include "trianglesourcenoderesolve.h" #include "cutface.h" +#include "parttarget.h" +#include "theme.h" MeshGenerator::MeshGenerator(Snapshot *snapshot) : m_snapshot(snapshot) @@ -83,6 +85,20 @@ bool MeshGenerator::checkIsPartDirty(const QString &partIdString) return isTrueValueString(valueOfKeyInMapOrEmpty(findPart->second, "dirty")); } +bool MeshGenerator::checkIsPartDependencyDirty(const QString &partIdString) +{ + auto findPart = m_snapshot->parts.find(partIdString); + if (findPart == m_snapshot->parts.end()) { + qDebug() << "Find part failed:" << partIdString; + return false; + } + QString cutFaceString = valueOfKeyInMapOrEmpty(findPart->second, "cutFace"); + QUuid cutFaceLinkedPartId = QUuid(cutFaceString); + if (cutFaceLinkedPartId.isNull()) + return false; + return checkIsPartDirty(cutFaceString); +} + bool MeshGenerator::checkIsComponentDirty(const QString &componentIdString) { bool isDirty = false; @@ -108,6 +124,11 @@ bool MeshGenerator::checkIsComponentDirty(const QString &componentIdString) m_dirtyPartIds.insert(partId); isDirty = true; } + if (!isDirty) { + if (checkIsPartDependencyDirty(partId)) { + isDirty = true; + } + } } for (const auto &childId: valueOfKeyInMapOrEmpty(*component, "children").split(",")) { @@ -149,12 +170,122 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt float deformThickness = 1.0; float deformWidth = 1.0; float cutRotation = 0.0; + auto target = PartTargetFromString(valueOfKeyInMapOrEmpty(part, "target").toUtf8().constData()); - CutFace cutFace = CutFaceFromString(valueOfKeyInMapOrEmpty(part, "cutFace").toUtf8().constData()); - std::vector cutTemplate = CutFaceToPoints(cutFace); + std::vector cutTemplate; + QString cutFaceString = valueOfKeyInMapOrEmpty(part, "cutFace"); + QUuid cutFaceLinkedPartId = QUuid(cutFaceString); + if (!cutFaceLinkedPartId.isNull()) { + auto findCutFaceLinkedPart = m_snapshot->parts.find(cutFaceString); + if (findCutFaceLinkedPart == m_snapshot->parts.end()) { + qDebug() << "Find cut face linked part failed:" << cutFaceString; + } else { + // Build node info map + std::map> cutFaceNodeMap; + for (const auto &nodeIdString: m_partNodeIds[cutFaceString]) { + auto findNode = m_snapshot->nodes.find(nodeIdString); + if (findNode == m_snapshot->nodes.end()) { + qDebug() << "Find node failed:" << nodeIdString; + continue; + } + auto &node = findNode->second; + float radius = valueOfKeyInMapOrEmpty(node, "radius").toFloat(); + float x = (valueOfKeyInMapOrEmpty(node, "x").toFloat() - m_mainProfileMiddleX); + float y = (m_mainProfileMiddleY - valueOfKeyInMapOrEmpty(node, "y").toFloat()); + cutFaceNodeMap.insert({nodeIdString, {radius, x, y}}); + } + // Build edge link + std::map> cutFaceNodeLinkMap; + for (const auto &edgeIdString: m_partEdgeIds[cutFaceString]) { + auto findEdge = m_snapshot->edges.find(edgeIdString); + if (findEdge == m_snapshot->edges.end()) { + qDebug() << "Find edge failed:" << edgeIdString; + continue; + } + auto &edge = findEdge->second; + QString fromNodeIdString = valueOfKeyInMapOrEmpty(edge, "from"); + QString toNodeIdString = valueOfKeyInMapOrEmpty(edge, "to"); + cutFaceNodeLinkMap[fromNodeIdString].push_back(toNodeIdString); + cutFaceNodeLinkMap[toNodeIdString].push_back(fromNodeIdString); + } + // Find endpoint + QString endPointNodeIdString; + std::vector>> endpointNodes; + for (const auto &it: cutFaceNodeLinkMap) { + if (1 == it.second.size()) { + const auto &findNode = cutFaceNodeMap.find(it.first); + if (findNode != cutFaceNodeMap.end()) + endpointNodes.push_back({it.first, findNode->second}); + } + } + if (!endpointNodes.empty()) { + std::sort(endpointNodes.begin(), endpointNodes.end(), []( + const std::pair> &first, + const std::pair> &second) { + const auto &firstX = std::get<1>(first.second); + const auto &secondX = std::get<1>(second.second); + if (firstX < secondX) { + return true; + } else if (firstX > secondX) { + return false; + } else { + const auto &firstY = std::get<2>(first.second); + const auto &secondY = std::get<2>(second.second); + if (firstY > secondY) { + return true; + } else if (firstY < secondY) { + return false; + } else { + const auto &firstRadius = std::get<0>(first.second); + const auto &secondRadius = std::get<0>(second.second); + if (firstRadius < secondRadius) { + return true; + } else if (firstRadius > secondRadius) { + return false; + } else { + return true; + } + } + } + }); + endPointNodeIdString = endpointNodes[0].first; + } + // Loop all linked nodes + std::vector> cutFaceNodes; + std::set cutFaceVisitedNodeIds; + std::function loopNodeLink; + loopNodeLink = [&](const QString &fromNodeIdString) { + auto findCutFaceNode = cutFaceNodeMap.find(fromNodeIdString); + if (findCutFaceNode == cutFaceNodeMap.end()) + return; + if (cutFaceVisitedNodeIds.find(fromNodeIdString) != cutFaceVisitedNodeIds.end()) + return; + cutFaceVisitedNodeIds.insert(fromNodeIdString); + cutFaceNodes.push_back(findCutFaceNode->second); + auto findNeighbor = cutFaceNodeLinkMap.find(fromNodeIdString); + if (findNeighbor == cutFaceNodeLinkMap.end()) + return; + for (const auto &it: findNeighbor->second) { + if (cutFaceVisitedNodeIds.find(it) == cutFaceVisitedNodeIds.end()) { + loopNodeLink(it); + break; + } + } + }; + if (!endPointNodeIdString.isEmpty()) { + loopNodeLink(endPointNodeIdString); + } + // Fetch points from linked nodes + cutFacePointsFromNodes(cutTemplate, cutFaceNodes); + } + } + if (cutTemplate.size() < 3) { + CutFace cutFace = CutFaceFromString(cutFaceString.toUtf8().constData()); + cutTemplate = CutFaceToPoints(cutFace); + } if (chamfered) nodemesh::chamferFace2D(&cutTemplate); - //normalizeCutFacePoints(&cutTemplate); + QString cutRotationString = valueOfKeyInMapOrEmpty(part, "cutRotation"); if (!cutRotationString.isEmpty()) { cutRotation = cutRotationString.toFloat(); @@ -327,6 +458,7 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt partCache.outcomeNodeVertices.push_back({position, {partIdString, nodeIdString}}); } + bool hasMeshError = false; nodemesh::Combiner::Mesh *mesh = nullptr; if (buildSucceed) { @@ -360,17 +492,17 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt delete mesh; mesh = newMesh; } else { - m_isSucceed = false; + hasMeshError = true; qDebug() << "Xmirrored mesh generate failed"; delete newMesh; } } } else { - m_isSucceed = false; + hasMeshError = true; qDebug() << "Mesh built is uncombinable"; } } else { - m_isSucceed = false; + hasMeshError = true; qDebug() << "Mesh build failed"; } @@ -392,6 +524,9 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt } nodemesh::trim(&partPreviewVertices, true); + for (auto &it: partPreviewVertices) { + it *= 2.0; + } std::vector partPreviewTriangleNormals; for (const auto &face: partCache.previewTriangles) { partPreviewTriangleNormals.push_back(QVector3D::normal( @@ -406,6 +541,8 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt partPreviewTriangleNormals, &partPreviewTriangleVertexNormals); if (!partCache.previewTriangles.empty()) { + if (target == PartTarget::CutFace) + partPreviewColor = Theme::red; m_partPreviewMeshes[partId] = new MeshLoader(partPreviewVertices, partCache.previewTriangles, partPreviewTriangleVertexNormals, @@ -425,6 +562,15 @@ nodemesh::Combiner::Mesh *MeshGenerator::combinePartMesh(const QString &partIdSt mesh = nullptr; } + if (target != PartTarget::Model) { + delete mesh; + mesh = nullptr; + } + + if (hasMeshError && target == PartTarget::Model) { + m_isSucceed = false; + } + return mesh; } @@ -620,12 +766,10 @@ nodemesh::Combiner::Mesh *MeshGenerator::combineMultipleMeshes(const std::vector nodemesh::Combiner::Mesh *subMesh = it.first; qDebug() << "Combine mode:" << CombineModeToString(childCombineMode); if (nullptr == subMesh) { - m_isSucceed = false; qDebug() << "Child mesh is null"; continue; } if (subMesh->isNull()) { - m_isSucceed = false; qDebug() << "Child mesh is uncombinable"; delete subMesh; continue; @@ -906,7 +1050,6 @@ void MeshGenerator::collectUncombinedComponent(const QString &componentIdString) const auto &componentCache = m_cacheContext->components[componentIdString]; if (nullptr == componentCache.mesh || componentCache.mesh->isNull()) { qDebug() << "Uncombined mesh is null"; - m_isSucceed = false; return; } diff --git a/src/meshgenerator.h b/src/meshgenerator.h index 683b6513..d9bd0980 100644 --- a/src/meshgenerator.h +++ b/src/meshgenerator.h @@ -87,6 +87,7 @@ private: void collectParts(); bool checkIsComponentDirty(const QString &componentIdString); bool checkIsPartDirty(const QString &partIdString); + bool checkIsPartDependencyDirty(const QString &partIdString); void checkDirtyFlags(); nodemesh::Combiner::Mesh *combinePartMesh(const QString &partIdString); nodemesh::Combiner::Mesh *combineComponentMesh(const QString &componentIdString, CombineMode *combineMode); diff --git a/src/modelwidget.cpp b/src/modelwidget.cpp index b86ca3fa..0101f7b0 100644 --- a/src/modelwidget.cpp +++ b/src/modelwidget.cpp @@ -35,6 +35,7 @@ ModelWidget::ModelWidget(QWidget *parent) : setFormat(fmt); } setContextMenuPolicy(Qt::CustomContextMenu); + zoom(200); } int ModelWidget::xRot() @@ -128,7 +129,7 @@ void ModelWidget::initializeGL() // Our camera never changes in this example. m_camera.setToIdentity(); // FIXME: if change here, please also change the camera pos in PBR shader - m_camera.translate(0, 0, -2.1); + m_camera.translate(0, 0, -4.0); // Light position is fixed. // FIXME: PBR render no longer use this parameter diff --git a/src/parttarget.cpp b/src/parttarget.cpp new file mode 100644 index 00000000..308f84d0 --- /dev/null +++ b/src/parttarget.cpp @@ -0,0 +1,5 @@ +#include "parttarget.h" + +IMPL_PartTargetFromString +IMPL_PartTargetToString +IMPL_PartTargetToDispName \ No newline at end of file diff --git a/src/parttarget.h b/src/parttarget.h new file mode 100644 index 00000000..6a96b98e --- /dev/null +++ b/src/parttarget.h @@ -0,0 +1,50 @@ +#ifndef DUST3D_PART_TARGET_H +#define DUST3D_PART_TARGET_H +#include +#include + +enum class PartTarget +{ + Model = 0, + CutFace, + Count +}; +PartTarget PartTargetFromString(const char *targetString); +#define IMPL_PartTargetFromString \ +PartTarget PartTargetFromString(const char *targetString) \ +{ \ + QString target = targetString; \ + if (target == "Model") \ + return PartTarget::Model; \ + if (target == "CutFace") \ + return PartTarget::CutFace; \ + return PartTarget::Model; \ +} +const char *PartTargetToString(PartTarget target); +#define IMPL_PartTargetToString \ +const char *PartTargetToString(PartTarget target) \ +{ \ + switch (target) { \ + case PartTarget::Model: \ + return "Model"; \ + case PartTarget::CutFace: \ + return "CutFace"; \ + default: \ + return "Model"; \ + } \ +} +QString PartTargetToDispName(PartTarget target); +#define IMPL_PartTargetToDispName \ +QString PartTargetToDispName(PartTarget target) \ +{ \ + switch (target) { \ + case PartTarget::Model: \ + return QObject::tr("Model"); \ + case PartTarget::CutFace: \ + return QObject::tr("Cut Face"); \ + default: \ + return QObject::tr("Model"); \ + } \ +} + +#endif diff --git a/src/parttreewidget.cpp b/src/parttreewidget.cpp index b612a96f..57ef186e 100644 --- a/src/parttreewidget.cpp +++ b/src/parttreewidget.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "parttreewidget.h" #include "partwidget.h" #include "skeletongraphicswidget.h" @@ -276,20 +277,37 @@ void PartTreeWidget::showContextMenu(const QPoint &pos) combineModeSelectBox->addItem(CombineModeToDispName(mode)); } combineModeSelectBox->setCurrentIndex((int)component->combineMode); - connect(combineModeSelectBox, static_cast(&QComboBox::currentIndexChanged), this, [=](int index) { emit setComponentCombineMode(component->id, (CombineMode)index); }); - QHBoxLayout *combineModeLayout = new QHBoxLayout; - combineModeLayout->setAlignment(Qt::AlignCenter); - combineModeLayout->setContentsMargins(0, 0, 0, 0); - combineModeLayout->setSpacing(0); - combineModeLayout->addWidget(combineModeSelectBox); + QComboBox *partTargetSelectBox = nullptr; + if (nullptr != part && nullptr != partWidget) { + partTargetSelectBox = new QComboBox; + for (size_t i = 0; i < (size_t)PartTarget::Count; ++i) { + PartTarget target = (PartTarget)i; + partTargetSelectBox->addItem(PartTargetToDispName(target)); + } + partTargetSelectBox->setCurrentIndex((int)part->target); + connect(partTargetSelectBox, static_cast(&QComboBox::currentIndexChanged), this, [=](int index) { + emit setPartTarget(part->id, (PartTarget)index); + }); + } + + //QHBoxLayout *combineModeLayout = new QHBoxLayout; + //combineModeLayout->setAlignment(Qt::AlignCenter); + //combineModeLayout->setContentsMargins(0, 0, 0, 0); + //combineModeLayout->setSpacing(0); + //combineModeLayout->addWidget(combineModeSelectBox); + + QFormLayout *componentSettingsLayout = new QFormLayout; + if (nullptr != partTargetSelectBox) + componentSettingsLayout->addRow(tr("Target"), partTargetSelectBox); + componentSettingsLayout->addRow(tr("Mode"), combineModeSelectBox); QVBoxLayout *newLayout = new QVBoxLayout; newLayout->addLayout(layout); - newLayout->addLayout(combineModeLayout); + newLayout->addLayout(componentSettingsLayout); widget->setLayout(newLayout); } else { widget->setLayout(layout); @@ -687,6 +705,11 @@ void PartTreeWidget::componentCombineModeChanged(QUuid componentId) updateComponentAppearance(componentId); } +void PartTreeWidget::componentTargetChanged(QUuid componentId) +{ + updateComponentAppearance(componentId); +} + void PartTreeWidget::addComponentChildrenToItem(QUuid componentId, QTreeWidgetItem *parentItem) { const Component *parentComponent = m_document->findComponent(componentId); diff --git a/src/parttreewidget.h b/src/parttreewidget.h index 9e697741..20125617 100644 --- a/src/parttreewidget.h +++ b/src/parttreewidget.h @@ -21,6 +21,7 @@ signals: void setComponentExpandState(QUuid componentId, bool expanded); void setComponentSmoothAll(QUuid componentId, float toSmoothAll); void setComponentSmoothSeam(QUuid componentId, float toSmoothSeam); + void setPartTarget(QUuid partId, PartTarget target); void moveComponent(QUuid componentId, QUuid toParentId); void removeComponent(QUuid componentId); void hideOtherComponents(QUuid componentId); @@ -50,6 +51,7 @@ public slots: void componentAdded(QUuid componentId); void componentExpandStateChanged(QUuid componentId); void componentCombineModeChanged(QUuid componentId); + void componentTargetChanged(QUuid componentId); void partRemoved(QUuid partId); void partPreviewChanged(QUuid partid); void partLockStateChanged(QUuid partId); diff --git a/src/partwidget.cpp b/src/partwidget.cpp index fb939338..6cbcd4fd 100644 --- a/src/partwidget.cpp +++ b/src/partwidget.cpp @@ -18,6 +18,8 @@ #include "skeletongraphicswidget.h" #include "shortcuts.h" #include "graphicscontainerwidget.h" +#include "flowlayout.h" +#include "cutfacelistwidget.h" PartWidget::PartWidget(const Document *document, QUuid partId) : m_document(document), @@ -73,7 +75,7 @@ PartWidget::PartWidget(const Document *document, QUuid partId) : initButton(m_colorButton); m_cutRotationButton = new QPushButton; - m_cutRotationButton->setToolTip(tr("Cut rotation")); + m_cutRotationButton->setToolTip(tr("Cut face")); m_cutRotationButton->setSizePolicy(retainSizePolicy); initButton(m_cutRotationButton); @@ -161,6 +163,7 @@ PartWidget::PartWidget(const Document *document, QUuid partId) : connect(this, &PartWidget::setPartChamferState, m_document, &Document::setPartChamferState); connect(this, &PartWidget::setPartCutRotation, m_document, &Document::setPartCutRotation); connect(this, &PartWidget::setPartCutFace, m_document, &Document::setPartCutFace); + 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::checkPart, m_document, &Document::checkPart); @@ -434,11 +437,16 @@ void PartWidget::showCutRotationSettingPopup(const QPoint &pos) rotationLayout->addWidget(rotationEraser); rotationLayout->addWidget(rotationWidget); - QHBoxLayout *facesLayout = new QHBoxLayout; + QHBoxLayout *standardFacesLayout = new QHBoxLayout; QPushButton *buttons[(int)CutFace::Count] = {0}; + CutFaceListWidget *cutFaceListWidget = new CutFaceListWidget(m_document); + size_t cutFaceTypeCount = (size_t)CutFace::Count; + if (cutFaceListWidget->isEmpty()) + cutFaceTypeCount = (size_t)CutFace::UserDefined; + auto updateCutFaceButtonState = [&](size_t index) { - for (size_t i = 0; i < (size_t)CutFace::Count; ++i) { + for (size_t i = 0; i < (size_t)cutFaceTypeCount; ++i) { auto button = buttons[i]; if (i == index) { button->setFlat(true); @@ -448,8 +456,28 @@ void PartWidget::showCutRotationSettingPopup(const QPoint &pos) button->setEnabled(true); } } + if (index != (int)CutFace::UserDefined) + cutFaceListWidget->selectCutFace(QUuid()); }; - for (size_t i = 0; i < (size_t)CutFace::Count; ++i) { + + cutFaceListWidget->enableMultipleSelection(false); + cutFaceListWidget->selectCutFace(part->cutFaceLinkedId); + connect(cutFaceListWidget, &CutFaceListWidget::currentSelectedCutFaceChanged, this, [=](QUuid partId) { + if (partId.isNull()) { + CutFace cutFace = CutFace::Quad; + updateCutFaceButtonState((int)cutFace); + emit setPartCutFace(m_partId, cutFace); + emit groupOperationAdded(); + } else { + updateCutFaceButtonState((int)CutFace::UserDefined); + emit setPartCutFaceLinkedId(m_partId, partId); + emit groupOperationAdded(); + } + }); + if (cutFaceListWidget->isEmpty()) + cutFaceListWidget->hide(); + + for (size_t i = 0; i < (size_t)cutFaceTypeCount; ++i) { CutFace cutFace = (CutFace)i; QString iconFilename = ":/resources/" + CutFaceToString(cutFace).toLower() + ".png"; QPixmap pixmap(iconFilename); @@ -462,7 +490,7 @@ void PartWidget::showCutRotationSettingPopup(const QPoint &pos) emit setPartCutFace(m_partId, cutFace); emit groupOperationAdded(); }); - facesLayout->addWidget(button); + standardFacesLayout->addWidget(button); buttons[i] = button; } updateCutFaceButtonState((size_t)part->cutFace); @@ -470,7 +498,8 @@ void PartWidget::showCutRotationSettingPopup(const QPoint &pos) QVBoxLayout *popupLayout = new QVBoxLayout; popupLayout->addLayout(rotationLayout); popupLayout->addSpacing(10); - popupLayout->addLayout(facesLayout); + popupLayout->addLayout(standardFacesLayout); + popupLayout->addWidget(cutFaceListWidget); popup->setLayout(popupLayout); diff --git a/src/partwidget.h b/src/partwidget.h index 18177b51..acc9367d 100644 --- a/src/partwidget.h +++ b/src/partwidget.h @@ -23,6 +23,7 @@ signals: void setPartColorState(QUuid partId, bool hasColor, QColor color); void setPartCutRotation(QUuid partId, float cutRotation); void setPartCutFace(QUuid partId, CutFace cutFace); + void setPartCutFaceLinkedId(QUuid partId, QUuid linkedId); void setPartMaterialId(QUuid partId, QUuid materialId); void movePartUp(QUuid partId); void movePartDown(QUuid partId); diff --git a/src/skeletondocument.h b/src/skeletondocument.h index b9a85244..8b971217 100644 --- a/src/skeletondocument.h +++ b/src/skeletondocument.h @@ -10,6 +10,7 @@ #include "theme.h" #include "meshloader.h" #include "cutface.h" +#include "parttarget.h" class SkeletonNode { @@ -87,7 +88,9 @@ public: bool dirty; float cutRotation; CutFace cutFace; + QUuid cutFaceLinkedId; QUuid materialId; + PartTarget target; SkeletonPart(const QUuid &withId=QUuid()) : visible(true), locked(false), @@ -99,11 +102,12 @@ public: deformWidth(1.0), rounded(false), chamfered(false), - color(Theme::white), + color(Qt::white), hasColor(false), dirty(true), cutRotation(0.0), - cutFace(CutFace::Quad) + cutFace(CutFace::Quad), + target(PartTarget::Model) { id = withId.isNull() ? QUuid::createUuid() : withId; } @@ -134,6 +138,16 @@ public: void setCutFace(CutFace face) { cutFace = face; + cutFaceLinkedId = QUuid(); + } + void setCutFaceLinkedId(const QUuid &linkedId) + { + if (linkedId.isNull()) { + setCutFace(CutFace::Quad); + return; + } + cutFace = CutFace::UserDefined; + cutFaceLinkedId = linkedId; } bool deformThicknessAdjusted() const { @@ -183,9 +197,11 @@ public: hasColor = other.hasColor; cutRotation = other.cutRotation; cutFace = other.cutFace; + cutFaceLinkedId = other.cutFaceLinkedId; componentId = other.componentId; dirty = other.dirty; materialId = other.materialId; + target = other.target; } void updatePreviewMesh(MeshLoader *previewMesh) { diff --git a/src/theme.cpp b/src/theme.cpp index 09fe58d2..53cbb791 100644 --- a/src/theme.cpp +++ b/src/theme.cpp @@ -39,6 +39,7 @@ int Theme::miniIconFontSize = 0; int Theme::miniIconSize = 0; int Theme::partPreviewImageSize = 0; int Theme::materialPreviewImageSize = 0; +int Theme::cutFacePreviewImageSize = 0; int Theme::posePreviewImageSize = 0; int Theme::motionPreviewImageSize = 0; int Theme::sidebarPreferredWidth = 0; @@ -54,6 +55,7 @@ void Theme::initAwsomeBaseSizes() Theme::miniIconSize = (int)(Theme::miniIconFontSize * 1.67); Theme::partPreviewImageSize = (Theme::miniIconSize * 3); Theme::materialPreviewImageSize = 75; + Theme::cutFacePreviewImageSize = 75; Theme::posePreviewImageSize = 75; Theme::motionPreviewImageSize = 75; Theme::sidebarPreferredWidth = 200; diff --git a/src/theme.h b/src/theme.h index c5094e04..49ff4733 100644 --- a/src/theme.h +++ b/src/theme.h @@ -35,6 +35,7 @@ public: static int toolIconFontSize; static int toolIconSize; static int materialPreviewImageSize; + static int cutFacePreviewImageSize; static int posePreviewImageSize; static int partPreviewImageSize; static int motionPreviewImageSize; diff --git a/thirdparty/nodemesh/nodemesh/builder.cpp b/thirdparty/nodemesh/nodemesh/builder.cpp index 88bf6f5e..f4f1683c 100644 --- a/thirdparty/nodemesh/nodemesh/builder.cpp +++ b/thirdparty/nodemesh/nodemesh/builder.cpp @@ -616,7 +616,6 @@ void Builder::makeCut(const QVector3D &position, const QVector3D &traverseDirection, std::vector &resultCut) { - baseNormal = revisedBaseNormalAcordingToCutNormal(baseNormal, cutNormal); auto finalCutTemplate = cutTemplate; auto finalCutNormal = cutNormal; float degree = 0; @@ -624,14 +623,9 @@ void Builder::makeCut(const QVector3D &position, degree = m_cutRotation * 180; } if (QVector3D::dotProduct(cutNormal, traverseDirection) <= 0) { - baseNormal = -baseNormal; finalCutNormal = -finalCutNormal; std::reverse(finalCutTemplate.begin(), finalCutTemplate.end()); - degree = ((int)degree + 180) % 360; - //for (auto &it: finalCutTemplate) { - // it.setX(-it.x()); - // it.setY(-it.y()); - //} + std::rotate(finalCutTemplate.begin(), finalCutTemplate.begin() + finalCutTemplate.size() - 1, finalCutTemplate.end()); } QVector3D u = QVector3D::crossProduct(finalCutNormal, baseNormal).normalized(); QVector3D v = QVector3D::crossProduct(u, finalCutNormal).normalized();