[Part::Mirroring] Add support for mirror plane reference object

This commit is contained in:
mwganson
2023-11-25 15:47:31 -06:00
committed by Chris Hennes
parent 3ebf841300
commit 26f23c945b
6 changed files with 421 additions and 56 deletions

View File

@@ -23,13 +23,28 @@
#include "PreCompiled.h"
#ifndef _PreComp_
# include <BRepBuilderAPI_Transform.hxx>
# include <BRep_Tool.hxx>
# include <gp_Ax2.hxx>
# include <gp_Circ.hxx>
# include <gp_Dir.hxx>
# include <gp_Pnt.hxx>
# include <gp_Trsf.hxx>
# include <BRepAdaptor_Curve.hxx>
# include <BRepAdaptor_Surface.hxx>
# include <gp_Pln.hxx>
# include <Geom_Plane.hxx>
# include <TopoDS.hxx>
# include <TopoDS_Face.hxx>
# include <TopExp_Explorer.hxx>
#endif
#include <Mod/Part/App/PrimitiveFeature.h>
#include <App/Link.h>
#include <App/OriginFeature.h>
#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<std::string> 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<App::Plane>() || (strstr(refObject->getNameInDocument(), "Plane")
&& refObject->isDerivedFrom(Part::Datum::getClassTypeId()))) {
Part::Feature* plane = static_cast<Part::Feature*>(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<Part::Feature>() || refObject->isDerivedFrom<App::Link>()) {
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);

View File

@@ -41,6 +41,7 @@ public:
App::PropertyLink Source;
App::PropertyPosition Base;
App::PropertyDirection Normal;
App::PropertyLinkSub MirrorPlane;
/** @name methods override feature */
//@{

View File

@@ -29,10 +29,22 @@
# define _USE_MATH_DEFINES
# include <cmath>
# include <gp_Ax2.hxx>
# include <gp_Circ.hxx>
# include <gp_Dir.hxx>
# include <gp_Pnt.hxx>
# include <BRepAdaptor_Curve.hxx>
# include <BRepAdaptor_Surface.hxx>
# include <Geom_Plane.hxx>
# include <TopoDS.hxx>
# include <TopoDS_Face.hxx>
# include <TopExp_Explorer.hxx>
# include <cfloat>
# include <QMessageBox>
# include <QRegularExpression>
# include <QTreeWidget>
# include <QComboBox>
#endif
#include <Base/Tools.h>
@@ -50,13 +62,119 @@
#include <Gui/ViewProvider.h>
#include <Gui/WaitCursor.h>
#include <Mod/Part/App/PartFeature.h>
#include <Mod/Part/App/PrimitiveFeature.h>
#include <Mod/Part/App/DatumFeature.h>
#include <App/OriginFeature.h>
#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<App::Plane>()
|| (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<Part::Feature>() || pObj->isDerivedFrom<App::Link>()) {
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<QTreeWidgetItem *> 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<Gui::SelectionObject> 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 #<number>)" 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;
}

View File

@@ -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_Mirroring> ui;
};

View File

@@ -14,54 +14,7 @@
<string>Mirroring</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QTreeWidget" name="shapes">
<property name="editTriggers">
<set>QAbstractItemView::CurrentChanged|QAbstractItemView::EditKeyPressed</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Shapes</string>
</property>
</column>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Mirror plane:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox">
<item>
<property name="text">
<string>XY plane</string>
</property>
</item>
<item>
<property name="text">
<string>XZ plane</string>
</property>
</item>
<item>
<property name="text">
<string>YZ plane</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="3" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Base point</string>
@@ -139,6 +92,81 @@
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Mirror plane:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox">
<item>
<property name="text">
<string>XY plane</string>
</property>
</item>
<item>
<property name="text">
<string>XZ plane</string>
</property>
</item>
<item>
<property name="text">
<string>YZ plane</string>
</property>
</item>
<item>
<property name="text">
<string>Use selected reference</string>
</property>
</item>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QTreeWidget" name="shapes">
<property name="editTriggers">
<set>QAbstractItemView::CurrentChanged|QAbstractItemView::EditKeyPressed</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Shapes</string>
</property>
</column>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="selectButton">
<property name="text">
<string>Selecting</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="referenceLineEdit">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="placeholderText">
<string>Mirror plane reference</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>

View File

@@ -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<Part::Mirroring*>(getObject());
Part::Feature* ref = static_cast<Part::Feature*>(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<Part::Mirroring*>(getObject());
Part::Feature* ref = static_cast<Part::Feature*>(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();