diff --git a/src/Mod/Part/App/FeatureMirroring.cpp b/src/Mod/Part/App/FeatureMirroring.cpp index 3449410dfc..9cd077c72c 100644 --- a/src/Mod/Part/App/FeatureMirroring.cpp +++ b/src/Mod/Part/App/FeatureMirroring.cpp @@ -23,13 +23,28 @@ #include "PreCompiled.h" #ifndef _PreComp_ # include +# include # include +# include # include # include # include +# include +# include +# include +# include +# include +# include +# include #endif +#include +#include +#include + #include "FeatureMirroring.h" +#include "DatumFeature.h" + using namespace Part; @@ -41,6 +56,7 @@ Mirroring::Mirroring() ADD_PROPERTY(Source,(nullptr)); ADD_PROPERTY_TYPE(Base,(Base::Vector3d()),"Plane",App::Prop_None,"The base point of the plane"); ADD_PROPERTY_TYPE(Normal,(Base::Vector3d(0,0,1)),"Plane",App::Prop_None,"The normal of the plane"); + ADD_PROPERTY_TYPE(MirrorPlane,(nullptr),"Plane",App::Prop_None,"A reference for the mirroring plane, overrides Base and Normal if set, can be face or circle"); } short Mirroring::mustExecute() const @@ -51,13 +67,39 @@ short Mirroring::mustExecute() const return 1; if (Normal.isTouched()) return 1; + if (MirrorPlane.isTouched()) + return 1; return 0; } void Mirroring::onChanged(const App::Property* prop) { + /** + In the case the user has a reference plane object, then + Base and Normal are computed based on that object. We must + handle this by setting the changes to Base and Normal to not + trigger a recompute when they are computed and changed. We + should also set Base and Normal to readonly so not to confuse + the user, who might try to change them despite having a reference + object. We could also hide them, but they contain useful information. + */ if (!isRestoring()) { - if (prop == &Base || prop == &Normal) { + bool needsRecompute = false; + App::DocumentObject* refObject = MirrorPlane.getValue(); + if (!refObject){ + Base.setStatus(App::Property::ReadOnly, false); + Normal.setStatus(App::Property::ReadOnly, false); + if (prop == &Base || prop == &Normal) { + needsRecompute = true; + } + } else { + if (prop == &MirrorPlane){ + Base.setStatus(App::Property::ReadOnly, true); + Normal.setStatus(App::Property::ReadOnly, true); + needsRecompute = true; + } + } + if (needsRecompute){ try { App::DocumentObjectExecReturn *ret = recompute(); delete ret; @@ -95,13 +137,124 @@ App::DocumentObjectExecReturn *Mirroring::execute() App::DocumentObject* link = Source.getValue(); if (!link) return new App::DocumentObjectExecReturn("No object linked"); + + App::DocumentObject* refObject = MirrorPlane.getValue(); + + std::vector subStrings = MirrorPlane.getSubValues(); + + gp_Pnt axbase; + gp_Dir axdir; + /** + Support mirror plane reference objects: + DatumPlanes, Part::Planes, Origin planes, Faces, Circles + Can also be App::Links to such objects + */ + if (refObject){ + if (refObject->isDerivedFrom(Part::Plane::getClassTypeId()) || refObject->isDerivedFrom() || (strstr(refObject->getNameInDocument(), "Plane") + && refObject->isDerivedFrom(Part::Datum::getClassTypeId()))) { + Part::Feature* plane = static_cast(refObject); + Base::Vector3d base = plane->Placement.getValue().getPosition(); + axbase = gp_Pnt(base.x, base.y, base.z); + Base::Rotation rot = plane->Placement.getValue().getRotation(); + Base::Vector3d dir; + rot.multVec(Base::Vector3d(0,0,1), dir); + axdir = gp_Dir(dir.x, dir.y, dir.z); + // reference is an app::link or a part::feature or some subobject + } else if (refObject->isDerivedFrom() || refObject->isDerivedFrom()) { + if (subStrings.size() > 1){ + throw Base::ValueError(std::string(this->getFullLabel()) + ": Only 1 subobject is supported for Mirror Plane reference, either a plane face or a circle edge."); + + } + auto linked = MirrorPlane.getValue(); + bool isFace = false; //will be true if user selected face subobject or if object only has 1 face + bool isEdge = false; //will be true if user selected edge subobject or if object only has 1 edge + TopoDS_Shape shape; + if (!subStrings.empty() && subStrings[0].length() > 0){ + shape = Feature::getTopoShape(linked, subStrings[0].c_str(), true).getShape(); + if (strstr(subStrings[0].c_str(), "Face")){ + isFace = true; //was face subobject, e.g. Face3 + } else { + if (strstr(subStrings[0].c_str(), "Edge")){ + isEdge = true; //was edge subobject, e.g. Edge7 + } + } + } else { + shape = Feature::getShape(linked); //no subobjects were selected, so this is entire shape of feature + } + + // if there is only 1 face or 1 edge, then we don't need to force the user to select that face or edge + // instead we can infer what was intended + int faceCount = Part::TopoShape(shape).countSubShapes(TopAbs_FACE); + int edgeCount = Part::TopoShape(shape).countSubShapes(TopAbs_EDGE); + + TopoDS_Face face; + TopoDS_Edge edge; + + if (isFace) { //user selected a face, so use shape to get the TopoDS::Face + face = TopoDS::Face(shape); + } else { + if (faceCount == 1) { //entire feature selected, but it only has 1 face, so get that face + TopoDS_Shape tdface = Part::TopoShape(shape).getSubShape(std::string("Face1").c_str()); + face = TopoDS::Face(tdface); + isFace = true; + } + } + if (!isFace && isEdge){ //don't bother with edge if we already have a face to work with + edge = TopoDS::Edge(shape); //isEdge means an edge was selected + } else { + if (edgeCount == 1){ //we don't have a face yet and there were no edges in the subobject selection + //but since this object only has 1 edge, we use it + TopoDS_Shape tdedge = Part::TopoShape(shape).getSubShape(std::string("Edge1").c_str()); + edge = TopoDS::Edge(tdedge); + isEdge = true; + } + } + + if (isFace && face.IsNull()) { //ensure we have a good face to work with + throw Base::ValueError(std::string(this->getFullLabel()) + ": Failed to extract mirror plane because face is null"); + } + if (isEdge && edge.IsNull()){ //ensure we have a good edge to work with + throw Base::ValueError(std::string(this->getFullLabel()) + ": Failed to extract mirror plane because edge is null"); + } + if (!isFace && !isEdge){ + throw Base::ValueError(std::string(this->getFullLabel()) + ": Failed to extract mirror plane, unable to determine which face or edge to use."); + } + + if (isFace) { + BRepAdaptor_Surface adapt(face); + if (adapt.GetType() != GeomAbs_Plane) + throw Base::TypeError(std::string(this->getFullLabel()) + ": Mirror plane face must be planar"); + TopExp_Explorer exp; + exp.Init(face, TopAbs_VERTEX); + if (exp.More()) { + axbase = BRep_Tool::Pnt(TopoDS::Vertex(exp.Current())); + } + axdir = adapt.Plane().Axis().Direction(); + } else { + if (isEdge){ + BRepAdaptor_Curve curve(edge); + if (!(curve.GetType() == GeomAbs_Circle)) { + throw Base::TypeError(std::string(this->getFullLabel()) + ": Only circle edge types are supported"); + } + gp_Circ circle = curve.Circle(); + axdir = circle.Axis().Direction(); + axbase = circle.Location(); + } + } + } else { + throw Base::ValueError(std::string(this->getFullLabel()) + ": Mirror plane reference must be a face of a feature or a plane object or a circle"); + } + Base.setValue(axbase.X(), axbase.Y(), axbase.Z()); + Normal.setValue(axdir.X(), axdir.Y(), axdir.Z()); + } + Base::Vector3d base = Base.getValue(); Base::Vector3d norm = Normal.getValue(); try { const TopoDS_Shape& shape = Feature::getShape(link); if (shape.IsNull()) - Standard_Failure::Raise("Cannot mirroR empty shape"); + Standard_Failure::Raise(std::string(std::string(this->getFullLabel()) + ": Cannot mirror empty shape").c_str()); gp_Ax2 ax2(gp_Pnt(base.x,base.y,base.z), gp_Dir(norm.x,norm.y,norm.z)); gp_Trsf mat; mat.SetMirror(ax2); diff --git a/src/Mod/Part/App/FeatureMirroring.h b/src/Mod/Part/App/FeatureMirroring.h index d737acff7c..3a0be113ba 100644 --- a/src/Mod/Part/App/FeatureMirroring.h +++ b/src/Mod/Part/App/FeatureMirroring.h @@ -41,6 +41,7 @@ public: App::PropertyLink Source; App::PropertyPosition Base; App::PropertyDirection Normal; + App::PropertyLinkSub MirrorPlane; /** @name methods override feature */ //@{ diff --git a/src/Mod/Part/Gui/Mirroring.cpp b/src/Mod/Part/Gui/Mirroring.cpp index c41609ac1f..991c49e0dc 100644 --- a/src/Mod/Part/Gui/Mirroring.cpp +++ b/src/Mod/Part/Gui/Mirroring.cpp @@ -29,10 +29,22 @@ # define _USE_MATH_DEFINES # include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include + # include # include # include # include +# include #endif #include @@ -50,13 +62,119 @@ #include #include #include +#include +#include +#include #include "Mirroring.h" + #include "ui_Mirroring.h" using namespace PartGui; +namespace PartGui { +class MirrorPlaneSelection : public Gui::SelectionFilterGate +{ +public: + explicit MirrorPlaneSelection() + : Gui::SelectionFilterGate() + { + } + /** + * We can't simply check if the selection is a face or an edge because only certain faces + * and edges can work. Bspline faces won't work, and only circle edges are supported. But we + * also allow document object selections for part::plane, partdesign::plane, and origin planes, + * as well as any part::feature with only a single face or a single circle edge. App::Links are + * supported, provided the object they are linking to meets the above criteria. + */ + + bool allow(App::Document* /*pDoc*/, App::DocumentObject* pObj, const char* sSubName) override + { + std::string subString(sSubName); + + if (pObj->isDerivedFrom(Part::Plane::getClassTypeId()) || pObj->isDerivedFrom() + || (strstr(pObj->getNameInDocument(), "Plane") && pObj->isDerivedFrom(Part::Datum::getClassTypeId()))) { + return true; + // reference is an app::link or a part::feature or some subobject + } else if (pObj->isDerivedFrom() || pObj->isDerivedFrom()) { + bool isFace = false; //will be true if user selected face subobject or if object only has 1 face + bool isEdge = false; //will be true if user selected edge subobject or if object only has 1 edge + TopoDS_Shape shape; + if (subString.length() > 0){ + shape = Part::Feature::getTopoShape(pObj, subString.c_str(), true).getShape(); + if (strstr(subString.c_str(), "Face")){ + isFace = true; //was face subobject, e.g. Face3 + } else { + if (strstr(subString.c_str(), "Edge")){ + isEdge = true; //was edge subobject, e.g. Edge7 + } + } + } else { + shape = Part::Feature::getShape(pObj); //no subobjects were selected, so this is entire shape of feature + } + + // if there is only 1 face or 1 edge, then we don't need to force the user to select that face or edge + // instead we can infer what was intended + int faceCount = Part::TopoShape(shape).countSubShapes(TopAbs_FACE); + int edgeCount = Part::TopoShape(shape).countSubShapes(TopAbs_EDGE); + + TopoDS_Face face; + TopoDS_Edge edge; + + if (isFace) { //user selected a face, so use shape to get the TopoDS::Face + face = TopoDS::Face(shape); + } else { + if (faceCount == 1) { //entire feature selected, but it only has 1 face, so get that face + TopoDS_Shape tdface = Part::TopoShape(shape).getSubShape(std::string("Face1").c_str()); + face = TopoDS::Face(tdface); + isFace = true; + } + } + if (!isFace && isEdge){ //don't bother with edge if we already have a face to work with + edge = TopoDS::Edge(shape); //isEdge means an edge was selected + } else { + if (edgeCount == 1){ //we don't have a face yet and there were no edges in the subobject selection + //but since this object only has 1 edge, we use it + TopoDS_Shape tdedge = Part::TopoShape(shape).getSubShape(std::string("Edge1").c_str()); + edge = TopoDS::Edge(tdedge); + isEdge = true; + } + } + + if (isFace && face.IsNull()) { //ensure we have a good face to work with + return false; + } + if (isEdge && edge.IsNull()){ //ensure we have a good edge to work with + return false; + } + if (!isFace && !isEdge){ + return false; + } + + if (isFace) { + BRepAdaptor_Surface adapt(face); + if (adapt.GetType() != GeomAbs_Plane){ + return false; + } + return true; + } else { + if (isEdge){ + BRepAdaptor_Curve curve(edge); + if (!(curve.GetType() == GeomAbs_Circle)) { + return false; + } + return true; + } + } + } //end of if(derived from part::feature) + return true; + }//end of allow() + +}; //end of class +}; //end of namespace block + + /* TRANSLATOR PartGui::Mirroring */ Mirroring::Mirroring(QWidget* parent) @@ -75,6 +193,11 @@ Mirroring::Mirroring(QWidget* parent) sel.applyFrom(Gui::Selection().getObjectsOfType(Part::Feature::getClassTypeId())); sel.applyFrom(Gui::Selection().getObjectsOfType(App::Link::getClassTypeId())); sel.applyFrom(Gui::Selection().getObjectsOfType(App::Part::getClassTypeId())); + + connect(ui->selectButton, &QPushButton::clicked, this, &Mirroring::onSelectButtonClicked); + + MirrorPlaneSelection* gate = new MirrorPlaneSelection(); + Gui::Selection().addSelectionGate(gate); } /* @@ -82,6 +205,17 @@ Mirroring::Mirroring(QWidget* parent) */ Mirroring::~Mirroring() = default; +void Mirroring::onSelectButtonClicked(){ + if (!ui->selectButton->isChecked()){ + Gui::Selection().rmvSelectionGate(); + ui->selectButton->setText(tr("Select reference")); + } else { + MirrorPlaneSelection* gate = new MirrorPlaneSelection(); + Gui::Selection().addSelectionGate(gate); + ui->selectButton->setText(tr("Selecting")); + } +} + void Mirroring::changeEvent(QEvent *e) { if (e->type() == QEvent::LanguageChange) { @@ -90,6 +224,20 @@ void Mirroring::changeEvent(QEvent *e) QWidget::changeEvent(e); } +void Mirroring::onSelectionChanged(const Gui::SelectionChanges &msg) +{ + if (ui->selectButton->isChecked()) { + if (msg.Type == Gui::SelectionChanges::AddSelection) { + std::string objName(msg.pObjectName); + std::string subName(msg.pSubName); + std::stringstream refStr; + refStr << objName << " : [" << subName << "]"; + ui->referenceLineEdit->setText(QLatin1String(refStr.str().c_str())); + ui->comboBox->setCurrentIndex(3); + } + } +} + void Mirroring::findShapes() { App::Document* activeDoc = App::GetApplication().getActiveDocument(); @@ -119,6 +267,12 @@ void Mirroring::findShapes() } } +bool Mirroring::reject() +{ + Gui::Selection().rmvSelectionGate(); + return true; +} + bool Mirroring::accept() { if (ui->shapes->selectedItems().isEmpty()) { @@ -138,17 +292,26 @@ bool Mirroring::accept() unsigned int count = activeDoc->countObjectsOfType(Base::Type::fromName("Part::Mirroring")); activeDoc->openTransaction("Mirroring"); - QString shape, label; + QString shape, label, selectionString; QRegularExpression rx(QString::fromLatin1(R"( \(Mirror #\d+\)$)")); QList items = ui->shapes->selectedItems(); float normx=0, normy=0, normz=0; int index = ui->comboBox->currentIndex(); - if (index == 0) + std::string selection(""); //set MirrorPlane property to empty string unless + //user has selected Use selected reference in combobox + + if (index == 0){ normz = 1.0f; - else if (index == 1) + } else if (index == 1){ normy = 1.0f; - else + } else if (index == 2){ normx = 1.0f; + } else if (index == 3){ //use selected reference + std::vector selobjs = Gui::Selection().getSelectionEx(); + if (selobjs.size() == 1) { + selection = selobjs[0].getAsPropertyLinkSubString(); + } + } double basex = ui->baseX->value().getValue(); double basey = ui->baseY->value().getValue(); double basez = ui->baseZ->value().getValue(); @@ -156,6 +319,7 @@ bool Mirroring::accept() shape = item->data(0, Qt::UserRole).toString(); std::string escapedstr = Base::Tools::escapedUnicodeFromUtf8(item->text(0).toUtf8()); label = QString::fromStdString(escapedstr); + selectionString = QString::fromStdString(selection); // if we already have the suffix " (Mirror #)" remove it int pos = label.indexOf(rx); @@ -170,10 +334,12 @@ bool Mirroring::accept() "__doc__.ActiveObject.Label=u\"%3\"\n" "__doc__.ActiveObject.Normal=(%4,%5,%6)\n" "__doc__.ActiveObject.Base=(%7,%8,%9)\n" + "__doc__.ActiveObject.MirrorPlane=(%10)\n" "del __doc__") .arg(this->document, shape, label) .arg(normx).arg(normy).arg(normz) - .arg(basex).arg(basey).arg(basez); + .arg(basex).arg(basey).arg(basez) + .arg(selectionString); Gui::Command::runCommand(Gui::Command::App, code.toLatin1()); QByteArray from = shape.toLatin1(); Gui::Command::copyVisual("ActiveObject", "ShapeColor", from); @@ -183,6 +349,7 @@ bool Mirroring::accept() activeDoc->commitTransaction(); activeDoc->recompute(); + Gui::Selection().rmvSelectionGate(); return true; } diff --git a/src/Mod/Part/Gui/Mirroring.h b/src/Mod/Part/Gui/Mirroring.h index 27bde2dc5e..5d0ab74f44 100644 --- a/src/Mod/Part/Gui/Mirroring.h +++ b/src/Mod/Part/Gui/Mirroring.h @@ -35,7 +35,7 @@ class Property; namespace PartGui { class Ui_Mirroring; -class Mirroring : public QWidget +class Mirroring : public QWidget, public Gui::SelectionObserver { Q_OBJECT @@ -43,14 +43,17 @@ public: explicit Mirroring(QWidget* parent = nullptr); ~Mirroring() override; bool accept(); + bool reject(); protected: void changeEvent(QEvent *e) override; private: void findShapes(); + void onSelectButtonClicked(); private: + void onSelectionChanged(const Gui::SelectionChanges& msg) override; QString document; std::unique_ptr ui; }; diff --git a/src/Mod/Part/Gui/Mirroring.ui b/src/Mod/Part/Gui/Mirroring.ui index 61d885f659..e773146e95 100644 --- a/src/Mod/Part/Gui/Mirroring.ui +++ b/src/Mod/Part/Gui/Mirroring.ui @@ -14,54 +14,7 @@ Mirroring - - - - QAbstractItemView::CurrentChanged|QAbstractItemView::EditKeyPressed - - - QAbstractItemView::ExtendedSelection - - - false - - - false - - - - Shapes - - - - - - - - Mirror plane: - - - - - - - - XY plane - - - - - XZ plane - - - - - YZ plane - - - - - + Base point @@ -139,6 +92,81 @@ + + + + Mirror plane: + + + + + + + + XY plane + + + + + XZ plane + + + + + YZ plane + + + + + Use selected reference + + + + + + + + QAbstractItemView::CurrentChanged|QAbstractItemView::EditKeyPressed + + + QAbstractItemView::ExtendedSelection + + + false + + + false + + + + Shapes + + + + + + + + Selecting + + + true + + + true + + + + + + + true + + + Mirror plane reference + + + diff --git a/src/Mod/Part/Gui/ViewProviderMirror.cpp b/src/Mod/Part/Gui/ViewProviderMirror.cpp index b3f6dde3c2..617844c02c 100644 --- a/src/Mod/Part/Gui/ViewProviderMirror.cpp +++ b/src/Mod/Part/Gui/ViewProviderMirror.cpp @@ -74,9 +74,18 @@ ViewProviderMirror::~ViewProviderMirror() void ViewProviderMirror::setupContextMenu(QMenu* menu, QObject* receiver, const char* member) { + // don't add plane editor to context menu if MirrorPlane is set because it would override any changes, anyway + Part::Mirroring* mf = static_cast(getObject()); + Part::Feature* ref = static_cast(mf->MirrorPlane.getValue()); + bool enabled = true; + if (ref){ + enabled = false; + } QAction* act; act = menu->addAction(QObject::tr("Edit mirror plane"), receiver, member); + act->setEnabled(enabled); act->setData(QVariant((int)ViewProvider::Default)); + ViewProviderPart::setupContextMenu(menu, receiver, member); } @@ -85,6 +94,10 @@ bool ViewProviderMirror::setEdit(int ModNum) if (ModNum == ViewProvider::Default) { // get the properties from the mirror feature Part::Mirroring* mf = static_cast(getObject()); + Part::Feature* ref = static_cast(mf->MirrorPlane.getValue()); + if (ref) { //skip this editor if MirrorPlane property is set + return false; + } Base::BoundBox3d bbox = mf->Shape.getBoundingBox(); float len = (float)bbox.CalcDiagonalLength(); Base::Vector3d base = mf->Base.getValue();