mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
Sketch: a profile may hold more than one closed loop — a plate with a hole extrudes
entities_to_wire handled exactly two shapes of sketch: one lone Circle/Ellipse, or
any number of Line/Arc/EllipseArc/BSpline pushed into a single MakeWire. Everything
else fell off the end as a null wire, so a circle drawn inside a rectangle — the
most ordinary thing in this whole program — refused with "not supported yet". Two
separate closed polygons were quietly worse: both went into one MakeWire, which
does not mean "two loops" to OCCT.
entities_to_wires now returns one wire per loop. A Circle or Ellipse is a loop on
its own; chain entities are grouped by shared endpoints (union-find, 1e-6 in sketch
coordinates), and an open chain still comes back as a wire because a sweep path is
legitimately open. It is all-or-nothing: one loop that fails to build poisons the
whole result, because a partial profile would extrude a shape the user did not draw
— the failure 2e6a8f9e91 was written to stop.
entities_to_wire survives as a two-line wrapper returning the single wire when
there is exactly one loop and a null wire otherwise, so all nine of its call sites
keep their exact contract and Revolve/Sweep/Loft/Surface* are untouched. What a
holed profile means for each of those is a separate question.
wires_to_face takes the largest-area loop as the outer boundary and adds the rest
reversed, which is how OCCT is told a wire is a hole. Containment is CHECKED with
BRepClass_FaceClassifier, not assumed: a loop outside the largest one is a second
island, and one sketch producing several solids is a much bigger feature, so it is
refused by name ("two disjoint regions") rather than guessed at.
Only the Extrude case consumes the new face. Tapered extrudes of a holed profile
are refused — offsetting inner loops has to go the opposite way — and the guard
counts wires on the face already built rather than rebuilding every wire to ask how
many there are, which is also the more honest test: what matters is the profile
being extruded.
Tests: six new [CadDocument][sketchwire] cases, proved by VOLUME rather than by not
throwing — plate-with-hole, two holes, and two regression guards that a lone circle
and a lone polygon extrude exactly as before. Suite 177 cases / 2366 assertions.
No serialized field, recipe version untouched, golden fixtures unchanged.
snaporca-88v.
This commit is contained in:
@@ -2064,6 +2064,17 @@ TopoDS_Wire CadDocument::build_sketch_wire(const CadFeature& sketch) const
|
||||
return prof.to_occt_wire(sketch.plane);
|
||||
}
|
||||
|
||||
TopoDS_Face CadDocument::build_sketch_face(const CadFeature& sketch) const
|
||||
{
|
||||
if (!sketch.entities.empty()) {
|
||||
const std::vector<TopoDS_Wire> loops = SketchEngine::entities_to_wires(sketch.entities, sketch.plane);
|
||||
if (loops.empty())
|
||||
throw std::runtime_error("sketch entities do not form a closed loop");
|
||||
return SketchEngine::wires_to_face(loops, sketch.plane);
|
||||
}
|
||||
return BRepBuilderAPI_MakeFace(build_sketch_wire(sketch)).Face();
|
||||
}
|
||||
|
||||
void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
const TopoDS_Shape& context, const CadFeature& f) const
|
||||
{
|
||||
@@ -2109,8 +2120,8 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
|| features[f.sketch_ref].type == CadFeatureType::Project))
|
||||
? features[f.sketch_ref] : f;
|
||||
// Imported rigid art (Text/SVG) extrudes via the faces-with-holes path
|
||||
// (with its placement transform applied); otherwise build a single wire
|
||||
// from entities/profile/shape.
|
||||
// (with its placement transform applied); otherwise build the sketch's planar
|
||||
// region (outer loop + holes) from entities/profile/shape.
|
||||
tool = !sk.imported_regions.empty()
|
||||
? SketchEngine::make_extrude_regions(
|
||||
transform_regions(sk.imported_regions, sk.import_offset,
|
||||
@@ -2119,17 +2130,30 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
f.extrude_end == ExtrudeEnd::ThroughAll ? 1e5 : signed_d,
|
||||
f.extrude_end == ExtrudeEnd::ThroughAll ? true : (sym || f.extrude_end == ExtrudeEnd::TwoSided))
|
||||
: [&]() {
|
||||
TopoDS_Wire wire = build_sketch_wire(sk);
|
||||
const TopoDS_Face profile = build_sketch_face(sk);
|
||||
// A tapered extrude offsets the profile; a holed profile would have to
|
||||
// offset its inner loops the opposite way, which is not implemented yet.
|
||||
auto taper = [&](double L) -> TopoDS_Shape {
|
||||
// Count the loops off the face we ALREADY built, rather than rebuilding
|
||||
// every wire from the entities a second time to ask how many there are.
|
||||
// It is also the more honest test: what matters is whether the profile
|
||||
// being extruded has holes, not what the entity list could produce.
|
||||
int nloops = 0;
|
||||
for (TopExp_Explorer ex(profile, TopAbs_WIRE); ex.More(); ex.Next()) ++nloops;
|
||||
if (nloops > 1)
|
||||
throw std::runtime_error("tapered extrude of a sketch with holes is not supported yet");
|
||||
return SketchEngine::make_extrude_taper(build_sketch_wire(sk), sk.plane, L, f.taper_deg);
|
||||
};
|
||||
TopoDS_Shape t;
|
||||
switch (f.extrude_end) {
|
||||
case ExtrudeEnd::Blind:
|
||||
t = (std::abs(f.taper_deg) > 1e-6)
|
||||
? SketchEngine::make_extrude_taper(wire, sk.plane, signed_d, f.taper_deg)
|
||||
: SketchEngine::make_extrude(wire, sk.plane, signed_d, false);
|
||||
? taper(signed_d)
|
||||
: SketchEngine::make_extrude(profile, sk.plane, signed_d, false);
|
||||
break;
|
||||
case ExtrudeEnd::Symmetric: t = SketchEngine::make_extrude(wire, sk.plane, f.distance, true); break;
|
||||
case ExtrudeEnd::TwoSided: t = SketchEngine::make_extrude_two_sided(wire, sk.plane, f.distance, f.distance2); break;
|
||||
case ExtrudeEnd::ThroughAll: t = SketchEngine::make_extrude(wire, sk.plane, 1.0e5, true); break;
|
||||
case ExtrudeEnd::Symmetric: t = SketchEngine::make_extrude(profile, sk.plane, f.distance, true); break;
|
||||
case ExtrudeEnd::TwoSided: t = SketchEngine::make_extrude_two_sided(profile, sk.plane, f.distance, f.distance2); break;
|
||||
case ExtrudeEnd::ThroughAll: t = SketchEngine::make_extrude(profile, sk.plane, 1.0e5, true); break;
|
||||
case ExtrudeEnd::UpToFace: {
|
||||
const TopoDS_Face tgt = GeometryEngine::face_by_index(context, f.up_to_face);
|
||||
double L = signed_d;
|
||||
@@ -2138,16 +2162,16 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
L = (c - sk.plane.origin).dot(sk.plane.normal);
|
||||
}
|
||||
t = (std::abs(f.taper_deg) > 1e-6)
|
||||
? SketchEngine::make_extrude_taper(wire, sk.plane, L, f.taper_deg)
|
||||
: SketchEngine::make_extrude(wire, sk.plane, L, false);
|
||||
? taper(L)
|
||||
: SketchEngine::make_extrude(profile, sk.plane, L, false);
|
||||
break;
|
||||
}
|
||||
case ExtrudeEnd::UpToVertex: {
|
||||
const double L = (f.up_to_point - sk.plane.origin).dot(sk.plane.normal);
|
||||
t = SketchEngine::make_extrude(wire, sk.plane, L, false);
|
||||
t = SketchEngine::make_extrude(profile, sk.plane, L, false);
|
||||
break;
|
||||
}
|
||||
default: t = SketchEngine::make_extrude(wire, sk.plane, signed_d, false); break;
|
||||
default: t = SketchEngine::make_extrude(profile, sk.plane, signed_d, false); break;
|
||||
}
|
||||
return t;
|
||||
}();
|
||||
|
||||
@@ -703,6 +703,10 @@ public:
|
||||
|
||||
private:
|
||||
TopoDS_Wire build_sketch_wire(const CadFeature& sketch) const;
|
||||
// The planar region an Extrude sweeps: the sketch's outer loop with its inner loops as
|
||||
// holes. Falls back to a face over build_sketch_wire() for the legacy profile/shape paths,
|
||||
// which have no concept of a second loop.
|
||||
TopoDS_Face build_sketch_face(const CadFeature& sketch) const;
|
||||
// Apply a single feature to (result, have_body), throwing std::runtime_error on
|
||||
// failure. `context` is the body whose faces/edges the feature reads (face-extrude
|
||||
// source, up-to-face target, dress-up, hole) — it differs from `result` only when the
|
||||
|
||||
+222
-73
@@ -7,6 +7,10 @@
|
||||
#include <BRepBuilderAPI_MakeWire.hxx>
|
||||
#include <BRepBuilderAPI_MakeEdge.hxx>
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepGProp.hxx>
|
||||
#include <GProp_GProps.hxx>
|
||||
#include <BRepClass_FaceClassifier.hxx>
|
||||
#include <TopoDS_Vertex.hxx>
|
||||
#include <GC_MakeArcOfCircle.hxx>
|
||||
#include <GC_MakeArcOfEllipse.hxx>
|
||||
#include <Geom_TrimmedCurve.hxx>
|
||||
@@ -168,12 +172,18 @@ static TopoDS_Shape extrude_face_internal(const TopoDS_Face& face, const gp_Dir&
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double length, bool symmetric, double /*taper_deg*/)
|
||||
double length, bool symmetric, double taper_deg)
|
||||
{
|
||||
BRepBuilderAPI_MakeFace fm(wire);
|
||||
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
|
||||
return make_extrude(fm.Face(), plane, length, symmetric, taper_deg);
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_extrude(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double length, bool symmetric, double /*taper_deg*/)
|
||||
{
|
||||
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
return extrude_face_internal(fm.Face(), dir, length, symmetric);
|
||||
return extrude_face_internal(face, dir, length, symmetric);
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
@@ -181,13 +191,19 @@ TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Wire& wire, const
|
||||
{
|
||||
BRepBuilderAPI_MakeFace fm(wire);
|
||||
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
|
||||
return make_extrude_two_sided(fm.Face(), plane, up, down);
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double up, double down)
|
||||
{
|
||||
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
const double u = std::abs(up), d = std::abs(down);
|
||||
if (u < 1e-9 && d < 1e-9) return TopoDS_Shape();
|
||||
if (d < 1e-9) { BRepPrimAPI_MakePrism p(fm.Face(), gp_Vec(dir) * u); return p.Shape(); }
|
||||
if (u < 1e-9) { BRepPrimAPI_MakePrism p(fm.Face(), gp_Vec(dir) * -d); return p.Shape(); }
|
||||
BRepPrimAPI_MakePrism pos(fm.Face(), gp_Vec(dir) * u);
|
||||
BRepPrimAPI_MakePrism neg(fm.Face(), gp_Vec(dir) * -d);
|
||||
if (d < 1e-9) { BRepPrimAPI_MakePrism p(face, gp_Vec(dir) * u); return p.Shape(); }
|
||||
if (u < 1e-9) { BRepPrimAPI_MakePrism p(face, gp_Vec(dir) * -d); return p.Shape(); }
|
||||
BRepPrimAPI_MakePrism pos(face, gp_Vec(dir) * u);
|
||||
BRepPrimAPI_MakePrism neg(face, gp_Vec(dir) * -d);
|
||||
BRepAlgoAPI_Fuse fuse(pos.Shape(), neg.Shape());
|
||||
if (!fuse.IsDone()) throw std::runtime_error("two-sided extrude fuse failed");
|
||||
return fuse.Shape();
|
||||
@@ -514,16 +530,19 @@ TriangleMesh SketchEngine::tessellate(const TopoDS_Shape& shape,
|
||||
return TriangleMesh(std::move(its));
|
||||
}
|
||||
|
||||
TopoDS_Wire SketchEngine::entities_to_wire(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane)
|
||||
std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane)
|
||||
{
|
||||
std::vector<const SketchEntity*> valid;
|
||||
for (const auto& e : entities) {
|
||||
struct Item { const SketchEntity* e; size_t idx; };
|
||||
std::vector<Item> valid;
|
||||
valid.reserve(entities.size());
|
||||
for (size_t i = 0; i < entities.size(); ++i) {
|
||||
const SketchEntity& e = entities[i];
|
||||
if (e.construction) continue;
|
||||
if (e.type == SketchEntity::Type::Point) continue;
|
||||
valid.push_back(&e);
|
||||
valid.push_back({&e, i});
|
||||
}
|
||||
if (valid.empty()) return TopoDS_Wire{};
|
||||
if (valid.empty()) return {};
|
||||
|
||||
// Build an OCCT ellipse (gp_Elips) in the sketch plane from an Ellipse(Arc)
|
||||
// entity. Major-axis direction = plane-rotated (cos phi, sin phi). Enforces
|
||||
@@ -562,75 +581,205 @@ TopoDS_Wire SketchEngine::entities_to_wire(const std::vector<SketchEntity>& enti
|
||||
return new Geom_BSplineCurve(poles, knots, mults, p);
|
||||
};
|
||||
|
||||
bool has_closed_single = false; // Circle or full Ellipse (stand-alone closed)
|
||||
bool has_chain = false; // Line / Arc / EllipseArc
|
||||
for (const auto* e : valid) {
|
||||
if (e->type == SketchEntity::Type::Circle || e->type == SketchEntity::Type::Ellipse)
|
||||
has_closed_single = true;
|
||||
else
|
||||
has_chain = true;
|
||||
}
|
||||
auto is_chain = [](const SketchEntity& e) {
|
||||
return e.type == SketchEntity::Type::Line || e.type == SketchEntity::Type::Arc ||
|
||||
e.type == SketchEntity::Type::EllipseArc || e.type == SketchEntity::Type::BSpline;
|
||||
};
|
||||
|
||||
// Case 1: exactly one closed entity (Circle or Ellipse) and nothing else
|
||||
if (has_closed_single && !has_chain && valid.size() == 1) {
|
||||
const SketchEntity& c = *valid[0];
|
||||
TopoDS_Edge e;
|
||||
if (c.type == SketchEntity::Type::Ellipse) {
|
||||
if (c.radius <= 1e-9 || c.rminor <= 1e-9) return TopoDS_Wire{};
|
||||
e = BRepBuilderAPI_MakeEdge(make_elips(c)).Edge();
|
||||
} else {
|
||||
Vec3d c3 = plane.to_world(c.center);
|
||||
gp_Pnt center(c3.x(), c3.y(), c3.z());
|
||||
gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
gp_Circ circ(gp_Ax2(center, n), c.radius);
|
||||
e = BRepBuilderAPI_MakeEdge(circ).Edge();
|
||||
// Endpoints of a chain entity in SKETCH coordinates (before to_world). False on a
|
||||
// degenerate (fewer than two control points) BSpline, which can never close a loop.
|
||||
auto endpoints = [&](const SketchEntity& e, Vec2d& a, Vec2d& b) -> bool {
|
||||
if (e.type == SketchEntity::Type::BSpline) {
|
||||
if (e.ctrl.size() < 2) return false;
|
||||
a = e.ctrl.front(); b = e.ctrl.back();
|
||||
return true;
|
||||
}
|
||||
a = e.p0; b = e.p1;
|
||||
return true;
|
||||
};
|
||||
|
||||
const double EPS = 1e-6;
|
||||
auto same = [&](const Vec2d& p, const Vec2d& q) { return (p - q).norm() < EPS; };
|
||||
|
||||
// Union-find over the valid index list: chain entities sharing an endpoint belong to one loop.
|
||||
std::vector<int> parent(valid.size());
|
||||
for (size_t i = 0; i < valid.size(); ++i) parent[i] = int(i);
|
||||
auto find = [&](int x) {
|
||||
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
|
||||
return x;
|
||||
};
|
||||
auto unite = [&](int x, int y) {
|
||||
int rx = find(x), ry = find(y);
|
||||
if (rx != ry) parent[rx] = ry;
|
||||
};
|
||||
|
||||
std::vector<size_t> chain_idx;
|
||||
chain_idx.reserve(valid.size());
|
||||
for (size_t i = 0; i < valid.size(); ++i)
|
||||
if (is_chain(*valid[i].e)) chain_idx.push_back(i);
|
||||
|
||||
for (size_t a = 0; a < chain_idx.size(); ++a) {
|
||||
const size_t i = chain_idx[a];
|
||||
Vec2d i0, i1;
|
||||
if (!endpoints(*valid[i].e, i0, i1)) continue;
|
||||
for (size_t b = a + 1; b < chain_idx.size(); ++b) {
|
||||
const size_t j = chain_idx[b];
|
||||
Vec2d j0, j1;
|
||||
if (!endpoints(*valid[j].e, j0, j1)) continue;
|
||||
if (same(i0, j0) || same(i0, j1) || same(i1, j0) || same(i1, j1))
|
||||
unite(int(i), int(j));
|
||||
}
|
||||
BRepBuilderAPI_MakeWire wm(e);
|
||||
if (!wm.IsDone()) return TopoDS_Wire{};
|
||||
return wm.Wire();
|
||||
}
|
||||
|
||||
// Case 2: closed chain of Line/Arc/EllipseArc entities (no closed-single)
|
||||
if (!has_closed_single && has_chain) {
|
||||
BRepBuilderAPI_MakeWire builder;
|
||||
for (const SketchEntity* e : valid) {
|
||||
if (e->type == SketchEntity::Type::Line) {
|
||||
Vec3d p0 = plane.to_world(e->p0);
|
||||
Vec3d p1 = plane.to_world(e->p1);
|
||||
gp_Pnt pa(p0.x(), p0.y(), p0.z());
|
||||
gp_Pnt pb(p1.x(), p1.y(), p1.z());
|
||||
builder.Add(BRepBuilderAPI_MakeEdge(pa, pb).Edge());
|
||||
} else if (e->type == SketchEntity::Type::EllipseArc) {
|
||||
if (e->radius <= 1e-9 || e->rminor <= 1e-9) return TopoDS_Wire{};
|
||||
GC_MakeArcOfEllipse arc_maker(make_elips(*e), e->start_angle, e->end_angle, Standard_True);
|
||||
if (!arc_maker.IsDone()) return TopoDS_Wire{};
|
||||
builder.Add(BRepBuilderAPI_MakeEdge(arc_maker.Value()).Edge());
|
||||
} else if (e->type == SketchEntity::Type::Arc) {
|
||||
Vec3d p0 = plane.to_world(e->p0);
|
||||
Vec3d p1 = plane.to_world(e->p1);
|
||||
double mid_angle = (e->start_angle + e->end_angle) * 0.5;
|
||||
Vec2d mid_2d(e->center.x() + e->radius * std::cos(mid_angle),
|
||||
e->center.y() + e->radius * std::sin(mid_angle));
|
||||
Vec3d mid_3d = plane.to_world(mid_2d);
|
||||
gp_Pnt pa(p0.x(), p0.y(), p0.z());
|
||||
gp_Pnt pm(mid_3d.x(), mid_3d.y(), mid_3d.z());
|
||||
gp_Pnt pb(p1.x(), p1.y(), p1.z());
|
||||
GC_MakeArcOfCircle arc_maker(pa, pm, pb);
|
||||
if (!arc_maker.IsDone()) return TopoDS_Wire{};
|
||||
Handle(Geom_TrimmedCurve) curve = arc_maker.Value();
|
||||
builder.Add(BRepBuilderAPI_MakeEdge(curve).Edge());
|
||||
} else if (e->type == SketchEntity::Type::BSpline) {
|
||||
Handle(Geom_BSplineCurve) crv = make_bspline(*e);
|
||||
if (crv.IsNull()) return TopoDS_Wire{};
|
||||
builder.Add(BRepBuilderAPI_MakeEdge(crv).Edge());
|
||||
// Group chain entities by connected-component root.
|
||||
std::map<int, std::vector<size_t>> comps;
|
||||
for (size_t i = 0; i < valid.size(); ++i) {
|
||||
if (!is_chain(*valid[i].e)) continue;
|
||||
comps[find(int(i))].push_back(i);
|
||||
}
|
||||
|
||||
// Loop descriptors: each Circle/Ellipse is its own loop; each chain component is a loop.
|
||||
struct Loop { size_t min_idx{0}; bool closed_single{false}; size_t member{0}; std::vector<size_t> members; };
|
||||
std::vector<Loop> loops;
|
||||
for (size_t i = 0; i < valid.size(); ++i) {
|
||||
const SketchEntity& e = *valid[i].e;
|
||||
if (e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Ellipse) {
|
||||
Loop l; l.min_idx = valid[i].idx; l.closed_single = true; l.member = i;
|
||||
loops.push_back(l);
|
||||
}
|
||||
}
|
||||
for (const auto& kv : comps) {
|
||||
Loop l; l.closed_single = false; l.members = kv.second;
|
||||
size_t mn = std::numeric_limits<size_t>::max();
|
||||
for (size_t m : kv.second) mn = std::min(mn, valid[m].idx);
|
||||
l.min_idx = mn;
|
||||
loops.push_back(l);
|
||||
}
|
||||
// Deterministic order: by the index of each loop's first entity.
|
||||
std::sort(loops.begin(), loops.end(), [](const Loop& x, const Loop& y) { return x.min_idx < y.min_idx; });
|
||||
|
||||
// Build each loop. All-or-nothing: one failed loop poisons the whole result.
|
||||
std::vector<TopoDS_Wire> out;
|
||||
out.reserve(loops.size());
|
||||
for (const Loop& loop : loops) {
|
||||
BRepBuilderAPI_MakeWire wm;
|
||||
if (loop.closed_single) {
|
||||
const SketchEntity& c = *valid[loop.member].e;
|
||||
TopoDS_Edge e;
|
||||
if (c.type == SketchEntity::Type::Ellipse) {
|
||||
if (c.radius <= 1e-9 || c.rminor <= 1e-9) return {};
|
||||
e = BRepBuilderAPI_MakeEdge(make_elips(c)).Edge();
|
||||
} else {
|
||||
Vec3d c3 = plane.to_world(c.center);
|
||||
gp_Pnt center(c3.x(), c3.y(), c3.z());
|
||||
gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
gp_Circ circ(gp_Ax2(center, n), c.radius);
|
||||
e = BRepBuilderAPI_MakeEdge(circ).Edge();
|
||||
}
|
||||
wm.Add(e);
|
||||
} else {
|
||||
for (size_t m : loop.members) {
|
||||
const SketchEntity* e = valid[m].e;
|
||||
if (e->type == SketchEntity::Type::Line) {
|
||||
Vec3d p0 = plane.to_world(e->p0);
|
||||
Vec3d p1 = plane.to_world(e->p1);
|
||||
gp_Pnt pa(p0.x(), p0.y(), p0.z());
|
||||
gp_Pnt pb(p1.x(), p1.y(), p1.z());
|
||||
wm.Add(BRepBuilderAPI_MakeEdge(pa, pb).Edge());
|
||||
} else if (e->type == SketchEntity::Type::EllipseArc) {
|
||||
if (e->radius <= 1e-9 || e->rminor <= 1e-9) return {};
|
||||
GC_MakeArcOfEllipse arc_maker(make_elips(*e), e->start_angle, e->end_angle, Standard_True);
|
||||
if (!arc_maker.IsDone()) return {};
|
||||
wm.Add(BRepBuilderAPI_MakeEdge(arc_maker.Value()).Edge());
|
||||
} else if (e->type == SketchEntity::Type::Arc) {
|
||||
Vec3d p0 = plane.to_world(e->p0);
|
||||
Vec3d p1 = plane.to_world(e->p1);
|
||||
double mid_angle = (e->start_angle + e->end_angle) * 0.5;
|
||||
Vec2d mid_2d(e->center.x() + e->radius * std::cos(mid_angle),
|
||||
e->center.y() + e->radius * std::sin(mid_angle));
|
||||
Vec3d mid_3d = plane.to_world(mid_2d);
|
||||
gp_Pnt pa(p0.x(), p0.y(), p0.z());
|
||||
gp_Pnt pm(mid_3d.x(), mid_3d.y(), mid_3d.z());
|
||||
gp_Pnt pb(p1.x(), p1.y(), p1.z());
|
||||
GC_MakeArcOfCircle arc_maker(pa, pm, pb);
|
||||
if (!arc_maker.IsDone()) return {};
|
||||
Handle(Geom_TrimmedCurve) curve = arc_maker.Value();
|
||||
wm.Add(BRepBuilderAPI_MakeEdge(curve).Edge());
|
||||
} else if (e->type == SketchEntity::Type::BSpline) {
|
||||
Handle(Geom_BSplineCurve) crv = make_bspline(*e);
|
||||
if (crv.IsNull()) return {};
|
||||
wm.Add(BRepBuilderAPI_MakeEdge(crv).Edge());
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.Build();
|
||||
if (!builder.IsDone()) return TopoDS_Wire{};
|
||||
return builder.Wire();
|
||||
wm.Build();
|
||||
if (!wm.IsDone()) return {};
|
||||
out.push_back(wm.Wire());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
TopoDS_Wire SketchEngine::entities_to_wire(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane)
|
||||
{
|
||||
const std::vector<TopoDS_Wire> w = entities_to_wires(entities, plane);
|
||||
return w.size() == 1 ? w[0] : TopoDS_Wire{};
|
||||
}
|
||||
|
||||
TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
const SketchPlane& /*plane*/)
|
||||
{
|
||||
if (wires.empty()) throw std::runtime_error("sketch has no closed loop");
|
||||
|
||||
if (wires.size() == 1) {
|
||||
BRepBuilderAPI_MakeFace fm(wires[0]);
|
||||
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
|
||||
return fm.Face();
|
||||
}
|
||||
|
||||
return TopoDS_Wire{};
|
||||
// Two or more loops: build a face per wire and let the largest area be the outer
|
||||
// boundary; every other loop is a candidate hole inside it.
|
||||
std::vector<TopoDS_Face> faces;
|
||||
faces.reserve(wires.size());
|
||||
std::vector<double> areas;
|
||||
areas.reserve(wires.size());
|
||||
for (const TopoDS_Wire& w : wires) {
|
||||
BRepBuilderAPI_MakeFace fm(w);
|
||||
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
|
||||
faces.push_back(fm.Face());
|
||||
GProp_GProps props;
|
||||
BRepGProp::SurfaceProperties(faces.back(), props);
|
||||
areas.push_back(props.Mass());
|
||||
}
|
||||
|
||||
size_t outer = 0;
|
||||
for (size_t i = 1; i < areas.size(); ++i)
|
||||
if (areas[i] > areas[outer]) outer = i;
|
||||
|
||||
// Note: NOT MakeFace(faces[outer], wires[outer]) — that constructor copies the outer face
|
||||
// (including its existing boundary wire) and then adds the wire again, doubling the outer
|
||||
// boundary. The wire-only constructor starts clean and the reversed holes follow.
|
||||
BRepBuilderAPI_MakeFace fm(wires[outer]);
|
||||
for (size_t i = 0; i < wires.size(); ++i) {
|
||||
if (i == outer) continue;
|
||||
// Containment is checked, not assumed: a vertex of the inner wire must lie strictly
|
||||
// inside the outer face. A loop outside the largest one is a second island, not a hole.
|
||||
gp_Pnt p;
|
||||
bool got = false;
|
||||
for (TopExp_Explorer ex(wires[i], TopAbs_VERTEX); ex.More(); ex.Next()) {
|
||||
p = BRep_Tool::Pnt(TopoDS::Vertex(ex.Current()));
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if (!got) throw std::runtime_error("sketch loop does not bound a face");
|
||||
BRepClass_FaceClassifier fc(faces[outer], p, 1e-7);
|
||||
if (fc.State() != TopAbs_IN)
|
||||
throw std::runtime_error("sketch has two disjoint regions; put each in its own sketch");
|
||||
// A reversed wire tells OCCT this loop is a hole, not a second boundary.
|
||||
fm.Add(TopoDS::Wire(wires[i].Reversed()));
|
||||
}
|
||||
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
|
||||
return fm.Face();
|
||||
}
|
||||
|
||||
std::vector<SketchEntity> SketchEngine::mirror_entities(
|
||||
|
||||
@@ -155,6 +155,8 @@ class SketchEngine
|
||||
public:
|
||||
static TopoDS_Shape make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double length, bool symmetric = false, double taper_deg = 0.0);
|
||||
static TopoDS_Shape make_extrude(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double length, bool symmetric = false, double taper_deg = 0.0);
|
||||
// Asymmetric two-sided prism: extrude the wire's face by `up` along +normal and `down`
|
||||
// along -normal, fused into one solid. up/down are non-negative magnitudes.
|
||||
// Tapered (draft) extrude of a planar wire: the top profile is the base wire offset in its
|
||||
@@ -164,6 +166,8 @@ public:
|
||||
double length, double taper_deg);
|
||||
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double up, double down);
|
||||
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double up, double down);
|
||||
static TopoDS_Shape make_extrude_face(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double length, bool symmetric = false, double taper_deg = 0.0);
|
||||
|
||||
@@ -212,6 +216,20 @@ public:
|
||||
static TopoDS_Wire entities_to_wire(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane);
|
||||
|
||||
// Every closed loop the sketch holds, in the order each loop's FIRST entity appears in
|
||||
// `entities`. A Circle or Ellipse is a loop on its own; Line/Arc/EllipseArc/BSpline
|
||||
// entities are grouped into loops by shared endpoints. An OPEN chain is returned too —
|
||||
// a sweep path is legitimately open, so open-ness is not an error here.
|
||||
// Empty vector = nothing usable; the caller decides whether that is an error.
|
||||
static std::vector<TopoDS_Wire> entities_to_wires(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane);
|
||||
|
||||
// A planar face from a set of coplanar loops: the largest-area loop is the outer boundary
|
||||
// and every other loop is a hole in it. Throws std::runtime_error with a message naming the
|
||||
// problem when the loops do not describe one such region.
|
||||
static TopoDS_Face wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
const SketchPlane& plane);
|
||||
|
||||
static std::vector<SketchEntity> mirror_entities(
|
||||
const std::vector<SketchEntity>& src, const Vec2d& a, const Vec2d& b);
|
||||
|
||||
|
||||
@@ -7057,20 +7057,134 @@ TEST_CASE("An entity sketch that forms no wire fails instead of extruding a defa
|
||||
SketchPlane::XY(), "Sketch");
|
||||
doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude");
|
||||
CHECK_FALSE(doc.recompute());
|
||||
CHECK(doc.error.find("do not form a single closed wire") != std::string::npos);
|
||||
CHECK(doc.error.find("does not bound a face") != std::string::npos);
|
||||
CHECK(doc.bodies.empty());
|
||||
}
|
||||
|
||||
SECTION("two circles are rejected too") {
|
||||
SECTION("two disjoint circles are rejected too") {
|
||||
CadDocument doc;
|
||||
int sk = doc.add_sketch_entities({ circle({-20, 0}, 8.0), circle({20, 0}, 8.0) },
|
||||
SketchPlane::XY(), "Sketch");
|
||||
doc.add_extrude(sk, 5.0, false, BooleanMode::New, "Extrude");
|
||||
CHECK_FALSE(doc.recompute());
|
||||
CHECK(doc.error.find("do not form a single closed wire") != std::string::npos);
|
||||
CHECK(doc.error.find("disjoint") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<SketchEntity> rect_entities(double w, double h)
|
||||
{
|
||||
const double hw = w * 0.5, hh = h * 0.5;
|
||||
return {
|
||||
{SketchEntity::Type::Line, Vec2d(-hw, -hh), Vec2d( hw, -hh)},
|
||||
{SketchEntity::Type::Line, Vec2d( hw, -hh), Vec2d( hw, hh)},
|
||||
{SketchEntity::Type::Line, Vec2d( hw, hh), Vec2d(-hw, hh)},
|
||||
{SketchEntity::Type::Line, Vec2d(-hw, hh), Vec2d(-hw, -hh)},
|
||||
};
|
||||
}
|
||||
|
||||
SketchEntity circle_entity(const Vec2d& c, double r)
|
||||
{
|
||||
SketchEntity e;
|
||||
e.type = SketchEntity::Type::Circle;
|
||||
e.center = c; e.p0 = c; e.radius = r;
|
||||
return e;
|
||||
}
|
||||
|
||||
CadDocument plate_doc(const std::vector<SketchEntity>& entities, double distance)
|
||||
{
|
||||
CadDocument doc;
|
||||
CadFeature sk;
|
||||
sk.type = CadFeatureType::Sketch;
|
||||
sk.name = "sketch";
|
||||
sk.plane = SketchPlane::XY();
|
||||
sk.entities = entities;
|
||||
doc.features.push_back(sk);
|
||||
CadFeature ex;
|
||||
ex.type = CadFeatureType::Extrude;
|
||||
ex.name = "extrude";
|
||||
ex.sketch_ref = 0;
|
||||
ex.distance = distance;
|
||||
ex.mode = BooleanMode::New;
|
||||
doc.features.push_back(ex);
|
||||
return doc;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// snaporca-88v: a sketch may hold more than one closed loop. The Extrude path builds the
|
||||
// sketch's planar region via SketchEngine::entities_to_wires + wires_to_face: the largest loop
|
||||
// is the outer boundary, every other loop a hole. Volumes are the proof — a plate with a hole
|
||||
// must subtract the hole, not merely "not throw".
|
||||
TEST_CASE("a circle inside a rectangle extrudes to a plate with a hole", "[CadDocument][sketchwire]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = rect_entities(40, 30);
|
||||
ents.push_back(circle_entity({0, 0}, 5.0));
|
||||
CadDocument doc = plate_doc(ents, 10.0);
|
||||
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
const double expected = 40.0 * 30.0 * 10.0 - M_PI * 25.0 * 10.0;
|
||||
REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(expected, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("two holes are both subtracted", "[CadDocument][sketchwire]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = rect_entities(40, 30);
|
||||
ents.push_back(circle_entity({ 5, 0}, 3.0));
|
||||
ents.push_back(circle_entity({-5, 0}, 3.0));
|
||||
CadDocument doc = plate_doc(ents, 10.0);
|
||||
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
const double expected = 40.0 * 30.0 * 10.0 - 2.0 * M_PI * 9.0 * 10.0;
|
||||
REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(expected, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("a lone circle still extrudes exactly as before", "[CadDocument][sketchwire]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { circle_entity({0, 0}, 8.0) };
|
||||
CadDocument doc = plate_doc(ents, 5.0);
|
||||
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
const double expected = M_PI * 64.0 * 5.0;
|
||||
REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(expected, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("a closed polygon still extrudes exactly as before", "[CadDocument][sketchwire]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = rect_entities(20, 20);
|
||||
CadDocument doc = plate_doc(ents, 10.0);
|
||||
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
const double expected = 20.0 * 20.0 * 10.0;
|
||||
REQUIRE_THAT(double(doc.display_mesh.volume()), Catch::Matchers::WithinRel(expected, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("two disjoint regions are refused, not guessed", "[CadDocument][sketchwire]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = { circle_entity({-10, 0}, 5.0), circle_entity({10, 0}, 5.0) };
|
||||
CadDocument doc = plate_doc(ents, 10.0);
|
||||
|
||||
CHECK_FALSE(doc.recompute());
|
||||
CHECK(doc.error.find("disjoint") != std::string::npos);
|
||||
CHECK(doc.bodies.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("entities_to_wires returns one wire per loop", "[CadDocument][sketchwire]")
|
||||
{
|
||||
std::vector<SketchEntity> ents = rect_entities(40, 30);
|
||||
ents.push_back(circle_entity({0, 0}, 5.0));
|
||||
|
||||
const std::vector<TopoDS_Wire> wires = SketchEngine::entities_to_wires(ents, SketchPlane::XY());
|
||||
REQUIRE(wires.size() == 2);
|
||||
REQUIRE_FALSE(wires[0].IsNull());
|
||||
REQUIRE_FALSE(wires[1].IsNull());
|
||||
}
|
||||
|
||||
// Sketching on a picked face is the most common gesture in solid modelling, and it was impossible:
|
||||
// the plane came from a combo of base + datum planes only, so the sole route onto a face was to
|
||||
// build a Coincident datum plane first. plane_of_face is the shared derivation that makes the
|
||||
|
||||
Reference in New Issue
Block a user