Part: FaceMakerRing from RealThunder's branch.

Co-authored-by: Zheng Lei <realthunder.dev@gmail.com>
Co-authored-by: Pierre-Louis Boyer <pierrelouis.boyer@gmail.com>
This commit is contained in:
paddle
2025-06-12 09:57:27 +02:00
committed by Chris Hennes
parent c1d17bcc6b
commit 24efbf4234
4 changed files with 302 additions and 63 deletions

View File

@@ -425,6 +425,7 @@ PyMOD_INIT_FUNC(Part)
Part::FaceMakerCheese ::init();
Part::FaceMakerExtrusion ::init();
Part::FaceMakerBullseye ::init();
Part::FaceMakerRing ::init();
Attacher::AttachEngine ::init();
Attacher::AttachEngine3D ::init();

View File

@@ -25,6 +25,7 @@
# include <BRep_Builder.hxx>
# include <BRep_Tool.hxx>
# include <BRepAdaptor_Surface.hxx>
# include <BRepBndLib.hxx>
# include <BRepBuilderAPI_Copy.hxx>
# include <BRepBuilderAPI_MakeFace.hxx>
# include <BRepClass_FaceClassifier.hxx>
@@ -36,18 +37,21 @@
# include <TopoDS.hxx>
# include <TopExp_Explorer.hxx>
# include <QtGlobal>
# include <TopExp.hxx>
#endif
#include "FaceMakerBullseye.h"
#include "FaceMakerCheese.h"
#include "TopoShape.h"
#include "WireJoiner.h"
using namespace Part;
TYPESYSTEM_SOURCE(Part::FaceMakerBullseye, Part::FaceMakerPublic)
void FaceMakerBullseye::setPlane(const gp_Pln &plane)
void FaceMakerBullseye::setPlane(const gp_Pln& plane)
{
this->myPlane = gp_Pln(plane);
this->planeSupplied = true;
@@ -63,19 +67,26 @@ std::string FaceMakerBullseye::getBriefExplanation() const
return {tr("Supports making planar faces with holes with islands.").toStdString()};
}
bool FaceMakerBullseye::WireInfo::operator<(const WireInfo& other) const
{
return extent - other.extent > Precision::Confusion();
}
void FaceMakerBullseye::Build_Essence()
{
if (myWires.empty())
if (myWires.empty()) {
return;
}
//validity check
// validity check
for (TopoDS_Wire& w : myWires) {
if (!BRep_Tool::IsClosed(w))
throw Base::ValueError(QT_TRANSLATE_NOOP("Exception", "Wire is not closed."));
if (!BRep_Tool::IsClosed(w)) {
throw Base::ValueError("Wire is not closed.");
}
}
//find plane (at the same time, test that all wires are on the same plane)
// find plane (at the same time, test that all wires are on the same plane)
gp_Pln plane;
if (this->planeSupplied) {
plane = this->myPlane;
@@ -88,48 +99,85 @@ void FaceMakerBullseye::Build_Essence()
builder.Add(comp, BRepBuilderAPI_Copy(w).Shape());
}
BRepLib_FindSurface planeFinder(comp, -1, /*OnlyPlane=*/Standard_True);
if (!planeFinder.Found())
if (!planeFinder.Found()) {
throw Base::ValueError("Wires are not coplanar.");
}
plane = GeomAdaptor_Surface(planeFinder.Surface()).Plane();
}
//sort wires by length of diagonal of bounding box.
std::vector<TopoDS_Wire> wires = this->myWires;
std::stable_sort(wires.begin(), wires.end(), FaceMakerCheese::Wire_Compare());
std::vector<WireInfo> wireInfos;
for (const auto& w : this->myTopoWires) {
Bnd_Box box;
if (w.isNull()) {
continue;
}
BRepBndLib::AddOptimal(w.getShape(), box, Standard_False);
if (box.IsVoid()) {
continue;
}
wireInfos.emplace_back(w, box);
}
//add wires one by one to current set of faces.
//We go from last to first, to make it so that outer wires come before inner wires.
std::vector< std::unique_ptr<FaceDriller> > faces;
for (int i = static_cast<int>(wires.size()) - 1; i >= 0; --i) {
TopoDS_Wire& w = wires[i];
// Sort wires by length of diagonal of bounding box.
std::stable_sort(wireInfos.begin(), wireInfos.end());
//test if this wire is on any of existing faces (if yes, it's a hole;
// if no, it's a beginning of a new face).
//Since we are assuming the wires do not intersect, testing if one vertex of wire is in a face is enough.
gp_Pnt p = BRep_Tool::Pnt(TopoDS::Vertex(TopExp_Explorer(w, TopAbs_VERTEX).Current()));
FaceDriller* foundFace = nullptr;
for (std::unique_ptr<FaceDriller>& ff : faces) {
if (ff->hitTest(p)) {
foundFace = &(*ff);
break;
for (int i = 0; i < (reuseInnerWire ? 2 : 1); ++i) {
// add wires one by one to current set of faces.
std::vector<std::unique_ptr<FaceDriller>> faces;
for (auto it = wireInfos.begin(); it != wireInfos.end();) {
// test if this wire is on any of existing faces (if yes, it's a hole;
// if no, it's a beginning of a new face).
FaceDriller* foundFace = nullptr;
bool hitted = false;
for (auto rit = faces.rbegin(); rit != faces.rend(); ++rit) {
switch ((*rit)->hitTest(it->wire)) {
case FaceDriller::HitTest::Hit:
foundFace = rit->get();
hitted = true;
break;
case FaceDriller::HitTest::HitOuter:
// Shape in outer wire but not on face, which means it is
// within a hole. So it's a hit and we shall make a new face
// with the wire.
hitted = true;
break;
default:
break;
}
}
TopoDS_Wire w = TopoDS::Wire(it->wire.getShape());
if (foundFace) {
// wire is on a face.
if (reuseInnerWire) {
foundFace->addHole(*it, mySourceShapes);
}
else {
foundFace->addHole(w);
}
}
else {
// wire is not on a face. Start a new face.
faces.push_back(std::make_unique<FaceDriller>(plane, w));
}
if (i == 0 && reuseInnerWire && !hitted) {
// If reuseInnerWire, then discard the outer-most wire, and
// retry so that the previous hole (and nested hole) wires can
// become outer wire for new faces.
it = wireInfos.erase(it);
}
else {
++it;
}
}
if (foundFace) {
//wire is on a face.
foundFace->addHole(w);
// and we are done!
for (std::unique_ptr<FaceDriller>& ff : faces) {
this->myShapesToReturn.push_back(ff->Face());
}
else {
//wire is not on a face. Start a new face.
faces.push_back(std::make_unique<FaceDriller>(
plane, w
));
}
}
//and we are done!
for (std::unique_ptr<FaceDriller>& ff : faces) {
this->myShapesToReturn.push_back(ff->Face());
}
}
@@ -139,42 +187,154 @@ FaceMakerBullseye::FaceDriller::FaceDriller(const gp_Pln& plane, TopoDS_Wire out
this->myPlane = plane;
this->myFace = TopoDS_Face();
//Ensure correct orientation of the wire.
if (getWireDirection(myPlane, outerWire) < 0)
// Ensure correct orientation of the wire.
if (getWireDirection(myPlane, outerWire) < 0) {
outerWire.Reverse();
}
myHPlane = new Geom_Plane(this->myPlane);
BRep_Builder builder;
builder.MakeFace(this->myFace, myHPlane, Precision::Confusion());
builder.Add(this->myFace, outerWire);
this->myTopoFace = TopoShape(this->myFace);
}
bool FaceMakerBullseye::FaceDriller::hitTest(const gp_Pnt& point) const
FaceMakerBullseye::FaceDriller::HitTest
FaceMakerBullseye::FaceDriller::hitTest(const TopoShape& shape) const
{
double u, v;
GeomAPI_ProjectPointOnSurf(point, myHPlane).LowerDistanceParameters(u, v);
BRepClass_FaceClassifier cl(myFace, gp_Pnt2d(u, v), Precision::Confusion());
TopAbs_State ret = cl.State();
switch (ret) {
case TopAbs_UNKNOWN:
throw Base::ValueError("FaceMakerBullseye::FaceDriller::hitTest: result unknown.");
break;
default:
return ret == TopAbs_IN || ret == TopAbs_ON;
auto vertex = TopoDS::Vertex(shape.getSubShape(TopAbs_VERTEX, 1));
if (!myFaceBound.IsNull()) {
if (myTopoFaceBound.findShape(vertex) > 0) {
return HitTest::HitNone;
}
for (const auto& info : myHoles) {
if (info.wire.findShape(vertex)) {
return HitTest::Hit;
}
}
}
else if (myTopoFace.findShape(vertex) > 0) {
return HitTest::HitNone;
}
double tol = BRep_Tool::Tolerance(vertex);
auto point = BRep_Tool::Pnt(vertex);
double u, v;
GeomAPI_ProjectPointOnSurf(point, myHPlane).LowerDistanceParameters(u, v);
const char* err = "FaceMakerBullseye::FaceDriller::hitTest: result unknown.";
auto hit = HitTest::HitNone;
if (!myFaceBound.IsNull()) {
BRepClass_FaceClassifier cl(myFaceBound, gp_Pnt2d(u, v), tol);
switch (cl.State()) {
case TopAbs_OUT:
case TopAbs_ON:
return HitTest::HitNone;
case TopAbs_IN:
hit = HitTest::HitOuter;
break;
default:
throw Base::ValueError(err);
}
}
BRepClass_FaceClassifier cl(myFace, gp_Pnt2d(u, v), tol);
TopAbs_State ret = cl.State();
switch (ret) {
case TopAbs_IN:
return HitTest::Hit;
case TopAbs_ON:
if (hit == HitTest::HitOuter) {
// the given point is within the outer wire, but on some other wire
// of the face, which must be a hole wire, which means that two hole
// wires have shared vertex (or edge). We can deal with this if
// reuseInnerWire is on by merging these holes.
return HitTest::Hit;
}
return HitTest::HitNone;
case TopAbs_OUT:
return hit;
default:
throw Base::ValueError(err);
}
}
void FaceMakerBullseye::FaceDriller::copyFaceBound(TopoDS_Face& face,
TopoShape& topoFace,
const TopoShape& source)
{
face = BRepBuilderAPI_MakeFace(myHPlane, TopoDS::Wire(source.getSubShape(TopAbs_WIRE, 1)));
topoFace = TopoShape(face);
}
void FaceMakerBullseye::FaceDriller::addHole(TopoDS_Wire w)
{
//Ensure correct orientation of the wire.
if (getWireDirection(myPlane, w) > 0) //if wire is CCW..
w.Reverse(); //.. we want CW!
// Ensure correct orientation of the wire.
if (getWireDirection(myPlane, w) > 0) { // if wire is CCW..
w.Reverse(); //.. we want CW!
}
if (this->myFaceBound.IsNull()) {
copyFaceBound(this->myFaceBound, this->myTopoFaceBound, this->myTopoFace);
}
BRep_Builder builder;
builder.Add(this->myFace, w);
}
void FaceMakerBullseye::FaceDriller::addHole(const WireInfo& wireInfo,
std::vector<TopoShape>& sources)
{
if (this->myFaceBound.IsNull()) {
copyFaceBound(this->myFaceBound, this->myTopoFaceBound, this->myTopoFace);
}
if (!myJoiner) {
myJoiner.reset(new WireJoiner);
myJoiner->setOutline(true);
}
myJoiner->addShape(wireInfo.wire);
bool intersected = false;
for (const auto& info : myHoles) {
if (!info.bound.IsOut(wireInfo.bound) || !wireInfo.bound.IsOut(info.bound)) {
intersected = true;
break;
}
}
myHoles.push_back(wireInfo);
TopoShape wire = wireInfo.wire;
if (intersected) {
TopoShape hole;
// Join intersected wires and get their outline
myJoiner->getResultWires(hole);
// Check if the hole gets merged.
if (!hole.findShape(wireInfo.wire.getShape())) {
for (const auto& e : wireInfo.wire.getSubTopoShapes(TopAbs_EDGE)) {
if (hole.findShape(e.getShape()) > 0) {
continue;
}
for (const auto& e : hole.findSubShapesWithSharedVertex(e.getShape())) {
sources.push_back(e);
}
}
copyFaceBound(this->myFace, this->myTopoFace, this->myTopoFaceBound);
wire = hole;
}
}
BRep_Builder builder;
for (const auto& w : wire.getSubShapes(TopAbs_WIRE)) {
// Ensure correct orientation of the wire.
if (getWireDirection(myPlane, TopoDS::Wire(w)) > 0) { // if wire is CCW..
builder.Add(this->myFace, TopoDS::Wire(w.Reversed())); //.. we want CW!
}
else {
builder.Add(this->myFace, TopoDS::Wire(w));
}
}
}
int FaceMakerBullseye::FaceDriller::getWireDirection(const gp_Pln& plane, const TopoDS_Wire& wire)
{
//make a test face
@@ -194,3 +354,22 @@ int FaceMakerBullseye::FaceDriller::getWireDirection(const gp_Pln& plane, const
return normal_co ? 1 : -1;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
TYPESYSTEM_SOURCE(Part::FaceMakerRing, Part::FaceMakerBullseye)
FaceMakerRing::FaceMakerRing()
{
reuseInnerWire = true;
}
std::string FaceMakerRing::getUserFriendlyName() const
{
return {tr("Ring facemaker").toStdString()};
}
std::string FaceMakerRing::getBriefExplanation() const
{
return {tr("Supports making planar faces with holes and holes as faces.").toStdString()};
}

View File

@@ -27,11 +27,14 @@
#include <Geom_Surface.hxx>
#include <gp_Pln.hxx>
#include <Bnd_Box.hxx>
namespace Part
{
class WireJoiner;
/**
* @brief The FaceMakerBullseye class is a tool to make planar faces with holes,
* where there can be additional faces inside holes and they can have holes too
@@ -45,14 +48,15 @@ class PartExport FaceMakerBullseye: public FaceMakerPublic
{
TYPESYSTEM_HEADER_WITH_OVERRIDE();
public:
FaceMakerBullseye() = default;
FaceMakerBullseye():
planeSupplied(false){}
/**
* @brief setPlane: sets the plane to use when making faces. This is
* optional. If the plane was set, it is not tested that the wires are
* planar or on the supplied plane, potentially speeding things up.
* @param plane FIXME: the plane is not propagated if processing compounds.
*/
void setPlane(const gp_Pln& plane) override;
virtual void setPlane(const gp_Pln& plane) override;
std::string getUserFriendlyName() const override;
std::string getBriefExplanation() const override;
@@ -62,7 +66,22 @@ protected:
protected:
gp_Pln myPlane; //externally supplied plane (if any)
bool planeSupplied{false};
bool planeSupplied {false};
bool reuseInnerWire {false};
struct WireInfo
{
TopoShape wire;
Bnd_Box bound;
double extent;
WireInfo(const TopoShape& s, const Bnd_Box& b)
: wire(s)
, bound(b)
{
extent = bound.SquareExtent();
}
bool operator<(const WireInfo& other) const;
};
/**
* @brief The FaceDriller class is similar to BRepBuilderAPI_MakeFace,
@@ -74,13 +93,26 @@ protected:
public:
FaceDriller(const gp_Pln& plane, TopoDS_Wire outerWire);
/// Hit test result
enum class HitTest
{
/// Not hitting
HitNone = 0,
/// Hit inside face
Hit = 1,
/// Hit inside the out wire bound
HitOuter = 2,
};
/**
* @brief hitTest: returns True if point is on the face
* @param point
* @brief hitTest: returns True if a shape is on the face
* @param shape
*/
bool hitTest(const gp_Pnt& point) const;
HitTest hitTest(const TopoShape& shape) const;
void addHole(TopoDS_Wire w);
void addHole(const WireInfo& info, std::vector<TopoShape>& sources);
void copyFaceBound(TopoDS_Face& f, TopoShape& tf, const TopoShape& source);
const TopoDS_Face& Face() const {return myFace;}
public:
@@ -94,10 +126,35 @@ protected:
private:
gp_Pln myPlane;
TopoDS_Face myFace;
TopoDS_Face myFaceBound;
TopoShape myTopoFace;
TopoShape myTopoFaceBound;
std::vector<WireInfo> myHoles;
Handle(Geom_Surface) myHPlane;
std::unique_ptr<WireJoiner> myJoiner;
};
};
/**
* The FaceMakerRing is a tool to make faces with holes.
*
* The tool assumes the wires are closed and do not intersect with each other.
* Each wire will be used to as an outer wire to make a face, with inner wire as
* holes. Only the first level inner wire will be used for holes. Nested inner
* wires are ignored. Note that each inner wire will also be used as outer wire
* for making face, with the second level inner wires as holes, so on and so
* forth.
*/
class PartExport FaceMakerRing: public FaceMakerBullseye
{
TYPESYSTEM_HEADER_WITH_OVERRIDE();
public:
FaceMakerRing();
virtual std::string getUserFriendlyName() const override;
virtual std::string getBriefExplanation() const override;
};
}//namespace Part
#endif // PART_FACEMAKER_BULLSEYE_H

View File

@@ -474,16 +474,18 @@ Part::TopoShape SketchObject::buildInternals(const Part::TopoShape &edges) const
joiner.getResultWires(result, "SKF");
result = result.makeElementFace(result.getSubTopoShapes(TopAbs_WIRE),
/*op*/"",
/*maker*/"Part::FaceMakerBullseye",
/*maker*/"Part::FaceMakerRing",
/*pln*/nullptr
);
}
Part::TopoShape openWires(getID(), getDocument()->getStringHasher());
joiner.getOpenWires(openWires, "SKF");
if (openWires.isNull())
if (openWires.isNull()) {
return result; // No open wires, return either face or empty toposhape
if (result.isNull())
}
if (result.isNull()) {
return openWires; // No face, but we have open wires to return as a shape
}
return result.makeElementCompound({result, openWires}); // Compound and return both
} catch (Base::Exception &e) {
FC_WARN("Failed to make face for sketch: " << e.what());