PartDesign: Center holes on sketch points as well as circles and arcs (#20583)

Co-authored-by: Kacper Donat <kadet1090@gmail.com>
This commit is contained in:
theo-vt
2025-04-21 17:30:57 -04:00
committed by GitHub
parent 9aad08a646
commit 774ec2cc93
9 changed files with 250 additions and 58 deletions

View File

@@ -62,6 +62,7 @@ void DlgSettingsGeneral::saveSettings()
ui->checkSketchBaseRefine->onSave();
ui->checkObjectNaming->onSave();
ui->checkAllowCompoundBody->onSave();
ui->comboDefaultProfileTypeForHole->onSave();
}
void DlgSettingsGeneral::loadSettings()
@@ -71,6 +72,7 @@ void DlgSettingsGeneral::loadSettings()
ui->checkSketchBaseRefine->onRestore();
ui->checkObjectNaming->onRestore();
ui->checkAllowCompoundBody->onRestore();
ui->comboDefaultProfileTypeForHole->onRestore();
}
/**

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>550</width>
<height>333</height>
<height>450</height>
</rect>
</property>
<property name="windowTitle">
@@ -21,11 +21,11 @@
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="checkBooleanCheck">
<property name="text">
<widget class="Gui::PrefCheckBox" name="checkBooleanCheck" native="true">
<property name="text" stdset="0">
<string>Automatically check model after boolean operation</string>
</property>
<property name="checked">
<property name="checked" stdset="0">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -37,11 +37,11 @@
</widget>
</item>
<item row="1" column="0">
<widget class="Gui::PrefCheckBox" name="checkBooleanRefine">
<property name="text">
<widget class="Gui::PrefCheckBox" name="checkBooleanRefine" native="true">
<property name="text" stdset="0">
<string>Automatically refine model after boolean operation</string>
</property>
<property name="checked">
<property name="checked" stdset="0">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -53,11 +53,11 @@
</widget>
</item>
<item row="2" column="0">
<widget class="Gui::PrefCheckBox" name="checkSketchBaseRefine">
<property name="text">
<widget class="Gui::PrefCheckBox" name="checkSketchBaseRefine" native="true">
<property name="text" stdset="0">
<string>Automatically refine model after sketch-based operation</string>
</property>
<property name="checked">
<property name="checked" stdset="0">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
@@ -84,8 +84,8 @@
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="checkObjectNaming">
<property name="text">
<widget class="Gui::PrefCheckBox" name="checkObjectNaming" native="true">
<property name="text" stdset="0">
<string>Add name of base object</string>
</property>
<property name="prefEntry" stdset="0">
@@ -99,6 +99,56 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Features settings</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Default profile type for holes</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefComboBox" name="comboDefaultProfileTypeForHole">
<property name="currentIndex">
<number>1</number>
</property>
<property name="prefEntry" stdset="0">
<cstring>defaultBaseTypeHole</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/PartDesign</cstring>
</property>
<item>
<property name="text">
<string>Circles and arcs</string>
</property>
</item>
<item>
<property name="text">
<string>Points, circles and arcs</string>
</property>
</item>
<item>
<property name="text">
<string>Points</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxExperimental">
<property name="enabled">
@@ -154,13 +204,13 @@
</widget>
<customwidgets>
<customwidget>
<class>Gui::PrefCheckBox</class>
<extends>QCheckBox</extends>
<class>Gui::PrefComboBox</class>
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<class>Gui::PrefCheckBox</class>
<extends>QWidget</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>

View File

@@ -24,6 +24,7 @@
#include "PreCompiled.h"
#ifndef _PreComp_
# include <limits>
# include <gp_Circ.hxx>
# include <gp_Dir.hxx>
# include <BRep_Builder.hxx>
# include <Mod/Part/App/FCBRepAlgoAPI_Cut.h>
@@ -37,6 +38,7 @@
# include <BRepClass3d_SolidClassifier.hxx>
# include <BRepOffsetAPI_MakePipeShell.hxx>
# include <BRepPrimAPI_MakeRevol.hxx>
# include <BRepAdaptor_Curve.hxx>
# include <Geom_Circle.hxx>
# include <GC_MakeArcOfCircle.hxx>
# include <Geom_TrimmedCurve.hxx>
@@ -60,6 +62,8 @@
#include "FeatureHole.h"
#include "json.hpp"
#include <numbers>
FC_LOG_LEVEL_INIT("PartDesign", true, true);
namespace PartDesign {
@@ -813,6 +817,9 @@ Hole::Hole()
ADD_PROPERTY_TYPE(CustomThreadClearance, (0.0), "Hole", App::Prop_None, "Custom thread clearance (overrides ThreadClass)");
// Defaults to circles & arcs so that older files are kept intact
// while new file get points, circles and arcs set in setupObject()
ADD_PROPERTY_TYPE(BaseProfileType, (BaseProfileTypeOptions::OnCirclesArcs), "Hole", App::Prop_None, "Which profile feature to base the holes on");
}
void Hole::updateHoleCutParams()
@@ -1676,6 +1683,19 @@ void Hole::onChanged(const App::Property* prop)
ProfileBased::onChanged(prop);
}
void Hole::setupObject()
{
// Set the BaseProfileType to the user defined value
// here so that new objects use points, but older files
// keep the default value of "Circles and Arcs"
Base::Reference<ParameterGrp> hGrp = App::GetApplication().GetUserParameter()
.GetGroup("BaseApp")->GetGroup("Preferences")->GetGroup("Mod/PartDesign");
BaseProfileType.setValue(baseProfileOption_idxToBitmask(hGrp->GetInt("defaultBaseTypeHole", 1)));
ProfileBased::setupObject();
}
/**
* Computes 2D intersection between the lines (pa1, pa2) and (pb1, pb2).
@@ -1752,7 +1772,8 @@ short Hole::mustExecute() const
UseCustomThreadClearance.isTouched() ||
CustomThreadClearance.isTouched() ||
ThreadDepthType.isTouched() ||
ThreadDepth.isTouched()
ThreadDepth.isTouched() ||
BaseProfileType.isTouched()
)
return 1;
return ProfileBased::mustExecute();
@@ -1788,6 +1809,7 @@ void Hole::updateProps()
onChanged(&CustomThreadClearance);
onChanged(&ThreadDepthType);
onChanged(&ThreadDepth);
onChanged(&BaseProfileType);
}
static gp_Pnt toPnt(gp_Vec dir)
@@ -1796,14 +1818,8 @@ static gp_Pnt toPnt(gp_Vec dir)
}
App::DocumentObjectExecReturn* Hole::execute()
{
TopoShape profileshape;
try {
profileshape = getTopoShapeVerifiedFace();
}
catch (const Base::Exception& e) {
return new App::DocumentObjectExecReturn(e.what());
}
{
TopoShape profileshape = getProfileShape(/*needSubElement*/ false);
// Find the base shape
TopoShape base;
@@ -1826,10 +1842,6 @@ App::DocumentObjectExecReturn* Hole::execute()
TopLoc_Location invObjLoc = this->getLocation().Inverted();
base.move(invObjLoc);
if (profileshape.isNull())
return new App::DocumentObjectExecReturn(
QT_TRANSLATE_NOOP("Exception", "Hole error: Creating a face from sketch failed"));
profileshape.move(invObjLoc);
/* Build the prototype hole */
@@ -2177,33 +2189,59 @@ TopoShape Hole::findHoles(std::vector<TopoShape> &holes,
{
TopoShape result(0);
for(const auto &profileEdge : profileshape.getSubTopoShapes(TopAbs_EDGE)) {
Standard_Real c_start;
Standard_Real c_end;
TopoDS_Edge edge = TopoDS::Edge(profileEdge.getShape());
Handle(Geom_Curve) c = BRep_Tool::Curve(edge, c_start, c_end);
// Circle?
if (c->DynamicType() != STANDARD_TYPE(Geom_Circle))
continue;
Handle(Geom_Circle) circle = Handle(Geom_Circle)::DownCast(c);
gp_Pnt loc = circle->Axis().Location();
auto addHole = [&](Part::TopoShape const& baseshape, gp_Pnt loc) {
gp_Trsf localSketchTransformation;
localSketchTransformation.SetTranslation( gp_Pnt( 0, 0, 0 ),
gp_Pnt(loc.X(), loc.Y(), loc.Z()) );
Part::ShapeMapper mapper;
mapper.populate(Part::MappingStatus::Modified, profileEdge, TopoShape(protoHole).getSubTopoShapes(TopAbs_FACE));
mapper.populate(Part::MappingStatus::Modified, baseshape, TopoShape(protoHole).getSubTopoShapes(TopAbs_FACE));
TopoShape hole(-getID());
hole.makeShapeWithElementMap(protoHole, mapper, {profileEdge});
hole.makeShapeWithElementMap(protoHole, mapper, {baseshape});
// transform and generate element map.
hole = hole.makeElementTransform(localSketchTransformation);
holes.push_back(hole);
};
int baseProfileType = BaseProfileType.getValue();
// Iterate over edges and filter out non-circle/non-arc types
if (baseProfileType & BaseProfileTypeOptions::OnCircles ||
baseProfileType & BaseProfileTypeOptions::OnArcs) {
for (const auto &profileEdge : profileshape.getSubTopoShapes(TopAbs_EDGE)) {
TopoDS_Edge edge = TopoDS::Edge(profileEdge.getShape());
BRepAdaptor_Curve adaptor(edge);
// Circle base?
if (adaptor.GetType() != GeomAbs_Circle) {
continue;
}
// Filter for circles
if (!(baseProfileType & BaseProfileTypeOptions::OnCircles) && adaptor.IsClosed()) {
continue;
}
// Filter for arcs
if (!(baseProfileType & BaseProfileTypeOptions::OnArcs) && !adaptor.IsClosed()) {
continue;
}
gp_Circ circle = adaptor.Circle();
addHole(profileEdge, circle.Axis().Location());
}
}
// To avoid breaking older files which where not made with
// holes on points
if (baseProfileType & BaseProfileTypeOptions::OnPoints) {
// Iterate over vertices while avoiding edges so that curve handles are ignored
for (const auto &profileVertex : profileshape.getSubTopoShapes(TopAbs_VERTEX, TopAbs_EDGE)) {
TopoDS_Vertex vertex = TopoDS::Vertex(profileVertex.getShape());
addHole(profileVertex, BRep_Tool::Pnt(vertex));
}
}
return TopoShape().makeElementCompound(holes);
}
@@ -2577,4 +2615,37 @@ void Hole::readCutDefinitions()
}
}
int Hole::baseProfileOption_idxToBitmask(int index)
{
// Translate combobox index to bitmask value
// More options could be made available
if (index == 0) {
return PartDesign::Hole::BaseProfileTypeOptions::OnCirclesArcs;
}
if (index == 1) {
return PartDesign::Hole::BaseProfileTypeOptions::OnPointsCirclesArcs;
}
if (index == 2) {
return PartDesign::Hole::BaseProfileTypeOptions::OnPoints;
}
Base::Console().Error("Unexpected hole base profile combobox index: %i", index);
return 0;
}
int Hole::baseProfileOption_bitmaskToIdx(int bitmask)
{
if (bitmask == PartDesign::Hole::BaseProfileTypeOptions::OnCirclesArcs) {
return 0;
}
if (bitmask == PartDesign::Hole::BaseProfileTypeOptions::OnPointsCirclesArcs) {
return 1;
}
if (bitmask == PartDesign::Hole::BaseProfileTypeOptions::OnPoints) {
return 2;
}
Base::Console().Error("Unexpected hole base profile bitmask: %i", bitmask);
return -1;
}
} // namespace PartDesign

View File

@@ -74,6 +74,19 @@ public:
App::PropertyAngle TaperedAngle;
App::PropertyBool UseCustomThreadClearance;
App::PropertyLength CustomThreadClearance;
App::PropertyInteger BaseProfileType;
enum BaseProfileTypeOptions {
OnPoints = 1 << 0,
OnCircles = 1 << 1,
OnArcs = 1 << 2,
// Common combos
OnPointsCirclesArcs = OnPoints | OnCircles | OnArcs,
OnCirclesArcs = OnCircles | OnArcs
};
static int baseProfileOption_idxToBitmask(int index);
static int baseProfileOption_bitmaskToIdx(int bitmask);
/** @name methods override feature */
//@{
@@ -113,6 +126,8 @@ public:
protected:
void onChanged(const App::Property* prop) override;
void setupObject() override;
static const App::PropertyAngle::Constraints floatAngle;
private:

View File

@@ -337,7 +337,7 @@ TopoDS_Shape ProfileBased::getVerifiedFace(bool silent) const {
}
else if (AllowMultiFace.getValue()) {
try {
auto shape = getProfileShape();
auto shape = getProfileShape(/*needSubElement*/ false);
if (shape.isNull())
err = "Linked shape object is empty";
else {
@@ -404,7 +404,7 @@ TopoDS_Shape ProfileBased::getVerifiedFace(bool silent) const {
return TopoDS_Face();
}
TopoShape ProfileBased::getProfileShape() const
TopoShape ProfileBased::getProfileShape(bool needSubElement) const
{
TopoShape shape;
const auto& subs = Profile.getSubValues();
@@ -416,7 +416,7 @@ TopoShape ProfileBased::getProfileShape() const
std::vector<TopoShape> shapes;
for (auto& sub : subs) {
shapes.push_back(
Part::Feature::getTopoShape(profile, sub.c_str(), /* needSubElement */ true));
Part::Feature::getTopoShape(profile, sub.c_str(), needSubElement));
}
shape = TopoShape(shape.Tag).makeElementCompound(shapes);
}
@@ -472,7 +472,7 @@ std::vector<TopoShape> ProfileBased::getTopoShapeProfileWires() const
// tessellations for some faces. Making an explicit copy of the linked
// shape seems to fix it. The error mostly happens when re-computing the
// shape but sometimes also for the first time
auto shape = getProfileShape().makeElementCopy();
auto shape = getProfileShape(/*needSubElement*/ false).makeElementCopy();
if (shape.hasSubShape(TopAbs_WIRE)) {
return shape.getSubTopoShapes(TopAbs_WIRE);
@@ -944,11 +944,14 @@ double ProfileBased::getThroughAllLength() const
{
TopoShape profileshape;
TopoShape base;
profileshape = getTopoShapeVerifiedFace();
profileshape = getTopoShapeVerifiedFace(true);
base = getBaseTopoShape();
Bnd_Box box;
BRepBndLib::Add(base.getShape(), box);
BRepBndLib::Add(profileshape.getShape(), box);
if (!profileshape.isNull()) {
BRepBndLib::Add(profileshape.getShape(), box);
}
box.SetGap(0.0);
// The diagonal of the bounding box, plus 1% extra to eliminate risk of
// co-planar issues, gives a length that is guaranteed to go through all.

View File

@@ -131,7 +131,7 @@ public:
virtual Base::Vector3d getProfileNormal() const;
TopoShape getProfileShape() const;
TopoShape getProfileShape(bool needSubElement = false) const;
/// retrieves the number of axes in the linked sketch (defined as construction lines)
int getSketchAxisCount() const;

View File

@@ -183,6 +183,8 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole* HoleView, QWidget* pare
std::string(pcHole->ThreadDepthType.getValueAsString()) == "Dimension"
);
ui->BaseProfileType->setCurrentIndex(PartDesign::Hole::baseProfileOption_bitmaskToIdx(pcHole->BaseProfileType.getValue()));
setCutDiagram();
// clang-format off
@@ -240,6 +242,8 @@ TaskHoleParameters::TaskHoleParameters(ViewProviderHole* HoleView, QWidget* pare
this, &TaskHoleParameters::threadDepthTypeChanged);
connect(ui->ThreadDepth, qOverload<double>(&Gui::QuantitySpinBox::valueChanged),
this, &TaskHoleParameters::threadDepthChanged);
connect(ui->BaseProfileType, qOverload<int>(&QComboBox::currentIndexChanged),
this, &TaskHoleParameters::baseProfileTypeChanged);
// clang-format on
getViewObject()->show();
@@ -321,7 +325,6 @@ void TaskHoleParameters::threadDepthTypeChanged(int index)
recomputeFeature();
}
}
void TaskHoleParameters::threadDepthChanged(double value)
{
if (auto hole = getObject<PartDesign::Hole>()) {
@@ -410,6 +413,13 @@ void TaskHoleParameters::holeCutTypeChanged(int index)
}
setCutDiagram();
}
void TaskHoleParameters::baseProfileTypeChanged(int index)
{
if (auto hole = getObject<PartDesign::Hole>()) {
hole->BaseProfileType.setValue(PartDesign::Hole::baseProfileOption_idxToBitmask(index));
recomputeFeature();
}
}
void TaskHoleParameters::setCutDiagram()
{
@@ -907,6 +917,9 @@ void TaskHoleParameters::changedObject(const App::Document&, const App::Property
else if (&Prop == &hole->ThreadDepth) {
ui->ThreadDepth->setEnabled(true);
updateSpinBox(ui->ThreadDepth, hole->ThreadDepth.getValue());
} else if (&Prop == &hole->BaseProfileType) {
ui->BaseProfileType->setEnabled(true);
updateComboBox(ui->BaseProfileType, PartDesign::Hole::baseProfileOption_bitmaskToIdx(hole->BaseProfileType.getValue()));
}
}
@@ -1056,7 +1069,10 @@ double TaskHoleParameters::getThreadDepth() const
{
return ui->ThreadDepth->value().getValue();
}
int TaskHoleParameters::getBaseProfileType() const
{
return PartDesign::Hole::baseProfileOption_idxToBitmask(ui->BaseProfileType->currentIndex());
}
void TaskHoleParameters::apply()
{
auto hole = getObject<PartDesign::Hole>();
@@ -1123,6 +1139,9 @@ void TaskHoleParameters::apply()
if (!hole->Tapered.isReadOnly()) {
FCMD_OBJ_CMD(hole, "Tapered = " << getTapered());
}
if (!hole->BaseProfileType.isReadOnly()) {
FCMD_OBJ_CMD(hole, "BaseProfileType = " << getBaseProfileType());
}
isApplying = false;
}

View File

@@ -79,6 +79,7 @@ public:
bool getModelThread() const;
long getThreadDepthType() const;
double getThreadDepth() const;
int getBaseProfileType() const;
private Q_SLOTS:
void threadedChanged();
@@ -108,6 +109,7 @@ private Q_SLOTS:
void updateViewChanged(bool isChecked);
void threadDepthTypeChanged(int index);
void threadDepthChanged(double value);
void baseProfileTypeChanged(int index);
void setCutDiagram();
private:

View File

@@ -33,7 +33,7 @@
<number>6</number>
</property>
<item>
<layout class="QGridLayout" name="headLayout" columnstretch="2,4">
<layout class="QGridLayout" name="headLayout" columnstretch="2,5">
<property name="topMargin">
<number>10</number>
</property>
@@ -918,7 +918,7 @@ Note that the calculation can take some time</string>
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<layout class="QHBoxLayout" name="horizontalLayout_11" stretch="2,5">
<property name="spacing">
<number>6</number>
</property>
@@ -967,7 +967,7 @@ Note that the calculation can take some time</string>
</item>
<item>
<widget class="QWidget" name="ThreadDepthDimensionWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="2,5">
<property name="leftMargin">
<number>0</number>
</property>
@@ -1011,7 +1011,7 @@ Note that the calculation can take some time</string>
</item>
<item>
<widget class="QWidget" name="CustomClearanceWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_13">
<layout class="QHBoxLayout" name="horizontalLayout_13" stretch="2,5">
<property name="spacing">
<number>6</number>
</property>
@@ -1074,6 +1074,36 @@ Note that the calculation can take some time</string>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="BaseProfileLayout" stretch="2,5">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Base profile types</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="BaseProfileType">
<item>
<property name="text">
<string>Circles and arcs</string>
</property>
</item>
<item>
<property name="text">
<string>Points, circles and arcs</string>
</property>
</item>
<item>
<property name="text">
<string>Points</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>