mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-24 17:26:47 +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:
@@ -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