mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 02:11:18 +00:00
CAD: extrude a sketch region with its holes, and stop crashing at startup
A rectangle with a circle inside it, drawn in ONE sketch, could not be extruded to a plate with a bore from the GUI. Five defects were in the way. Each was found by driving the app on a headless rig and measuring the result — the code reads correctly at every one of these points, which is why they survived. 1. Wire orientation (kernel). SketchEngine::wires_to_face added every hole as wires[i].Reversed(), which is only right when the sketch happens to wind both loops the same way. A circle drawn clockwise inside a counter-clockwise rectangle came out matching the outer boundary, OCCT swept it as a SECOND contour, and the prism was the plate with its bore filled and the disc's volume counted twice. Measured: bbox 67.17 x 219.67 x 10 with volume 152088 mm3 against a solid box of 147542 — a body larger than its own bounding box, which is the signature. Holes are now added as-is and ShapeFix_Face::FixOrientation() classifies them; that is winding-independent and is the idiom make_extrude_regions already used for imported glyphs, which is why holed TEXT always extruded correctly while a holed SKETCH never did. After the fix: 142996 mm3, implied bore radius 12.03 mm against the circle drawn. 2. The live-sketch click threw the picked region away. region_at() served only as a yes/no gate and on_face_selected() carried no argument, so Extrude fell back to whichever loop the resolver found first — clicking the material of a plate-with-a-hole extruded the disc. The region is now carried through, and DesignPanel also hands it to the tool with set_loop_pick(), AFTER open_tool() because that re-derives selection state, since extrude_uses_loop() reads selected_loop_entities() and that lives on the tool. 3. region_at() had no hole awareness and no innermost preference: it returned the first polygon containing the point. It now skips a region when the point lies inside one of that region's holes, and picks the smallest containing loop, so a click in the bore selects the disc and a click on the material selects the plate. 4. Startup segfault. DesignCanvas::request_repaint probed the GL backend via OpenGLManager::get_gl_info().get_renderer() before the canvas had initialised GL — glGetString with no context current and, before init_opengl(), no loaded function pointers. Anything that asked for a repaint while the panel was still being built landed there, with no window and nothing in the log. It now bails at the top on !is_initialized() and asks for a Refresh instead. Note the crash was in the PROBE, not in render(), which already guards itself. 5. A holed sketch on a plane whose normal points -Z came out as the full box PLUS a disc — 220274 mm3 where 163726 was due (192000 + 28274). wires_to_face took a SketchPlane parameter it never used and let OCCT infer a surface from the outer wire; when the inferred normal disagreed with the sketch's, the hole classification produced no hole. Every face is now built on the sketch's own gp_Pln. Also in this change, from the same rig session: - A right-click that only clears the sketch selection no longer reports itself as consumed, so it stops suppressing the offer menu. With any geometry in a live sketch there was no menu route left to add a second entity. - Escape no longer discards a live sketch that holds drawn geometry; it says so and keeps the work (live_sketch_has_work()). - The holed-region fill is an even-odd scanline instead of a keyhole bridge, so no corridor triangle leaks from the bore to the nearest corner. - Cyan is reserved for the selection: an unselected region no longer wears a shade one step off the selected one. - The origin planes follow the mode, so pressing Sketch on a document that already has a body offers them again instead of naming a plane you cannot see. Tests: three [holes] cases over add_extrude_entities asserting the plate-with-bore volume, solid and face counts on both a +Z and a -Z sketch plane, and the by-name refusal of two disjoint regions. Full [CadDocument] suite green.
This commit is contained in:
@@ -7802,3 +7802,109 @@ TEST_CASE("an invalid pair names which side is wrong", "[CadDocument][mate]")
|
||||
for (const auto& o : doc.mate_options(-5, a))
|
||||
REQUIRE_CONTAINS(o.reason, "connector A");
|
||||
}
|
||||
|
||||
// A rectangular plate 120 x 160 x 10 centred on the origin with a radius-30 bore through
|
||||
// the middle: one solid whose volume is the box minus the cylinder, and whose topology is a
|
||||
// plate-with-a-bore (1 solid, 7 faces) rather than a plate plus a plug.
|
||||
TEST_CASE("add_extrude_entities builds a plate with a bore", "[CadDocument][holes]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
|
||||
const Vec2d A(-60, -80), B(60, -80), C(60, 80), D(-60, 80);
|
||||
std::vector<SketchEntity> entities = {
|
||||
{SketchEntity::Type::Line, A, B},
|
||||
{SketchEntity::Type::Line, B, C},
|
||||
{SketchEntity::Type::Line, C, D},
|
||||
{SketchEntity::Type::Line, D, A},
|
||||
};
|
||||
SketchEntity bore;
|
||||
bore.type = SketchEntity::Type::Circle;
|
||||
bore.center = Vec2d(0, 0);
|
||||
bore.radius = 30.0;
|
||||
entities.push_back(bore);
|
||||
|
||||
doc.add_extrude_entities(entities, SketchPlane::XY(), 10.0, false, BooleanMode::New, "Extrude1");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(doc.display_mesh.facets_count() > 0);
|
||||
|
||||
auto mp = doc.body_mass_properties(0);
|
||||
REQUIRE(mp.valid);
|
||||
const double expected_vol = 120.0 * 160.0 * 10.0 - M_PI * 30.0 * 30.0 * 10.0;
|
||||
REQUIRE_THAT(mp.volume, WithinAbs(expected_vol, 1.0));
|
||||
|
||||
// One solid, seven faces: top + bottom (each a planar face with a hole), four side
|
||||
// walls, and the single cylindrical bore wall.
|
||||
int face_count = 0, solid_count = 0;
|
||||
for (TopExp_Explorer fe(doc.bodies.back().shape, TopAbs_FACE); fe.More(); fe.Next()) ++face_count;
|
||||
for (TopExp_Explorer se(doc.bodies.back().shape, TopAbs_SOLID); se.More(); se.Next()) ++solid_count;
|
||||
INFO("volume mm^3 = " << mp.volume << ", faces = " << face_count << ", solids = " << solid_count);
|
||||
REQUIRE(solid_count == 1);
|
||||
REQUIRE(face_count == 7);
|
||||
}
|
||||
|
||||
TEST_CASE("two disjoint circles are refused by name", "[CadDocument][holes]")
|
||||
{
|
||||
CadDocument doc;
|
||||
|
||||
SketchEntity a;
|
||||
a.type = SketchEntity::Type::Circle; a.center = Vec2d(-50, 0); a.radius = 20.0;
|
||||
SketchEntity b;
|
||||
b.type = SketchEntity::Type::Circle; b.center = Vec2d( 50, 0); b.radius = 20.0;
|
||||
|
||||
doc.add_extrude_entities({a, b}, SketchPlane::XY(), 10.0, false, BooleanMode::New, "Extrude1");
|
||||
|
||||
REQUIRE_FALSE(doc.recompute());
|
||||
REQUIRE_CONTAINS(doc.error, "disjoint");
|
||||
}
|
||||
|
||||
// Same plate-with-a-bore, but the bore circle is wound CLOCKWISE (geometric winding opposite
|
||||
// the CCW rectangle). The old wires_to_face reversed every hole wire unconditionally
|
||||
// (wires[i].Reversed()), which only produced a correct hole when the circle was wound the same
|
||||
// way as the outer loop; a clockwise circle got reversed into matching the outer boundary and
|
||||
// the prism swept it solid — measured 220274 mm^3 = box (192000) + disc (28274), the filled-bore
|
||||
// signature. The current ShapeFix_Face::FixOrientation path is winding-independent. Flipping the
|
||||
// plane normal reverses gp_Circ's parametrisation (gp_Ax2(center, -Z) sweeps clockwise seen from
|
||||
// +Z) while the rectangle's 2D coordinates stay CCW.
|
||||
TEST_CASE("add_extrude_entities builds a plate with a bore (clockwise circle)", "[CadDocument][holes]")
|
||||
{
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
CadDocument doc;
|
||||
|
||||
SketchPlane cw_plane = SketchPlane::XY();
|
||||
cw_plane.normal = Vec3d(0, 0, -1);
|
||||
|
||||
const Vec2d A(-60, -80), B(60, -80), C(60, 80), D(-60, 80);
|
||||
std::vector<SketchEntity> entities = {
|
||||
{SketchEntity::Type::Line, A, B},
|
||||
{SketchEntity::Type::Line, B, C},
|
||||
{SketchEntity::Type::Line, C, D},
|
||||
{SketchEntity::Type::Line, D, A},
|
||||
};
|
||||
SketchEntity bore;
|
||||
bore.type = SketchEntity::Type::Circle;
|
||||
bore.center = Vec2d(0, 0);
|
||||
bore.radius = 30.0;
|
||||
entities.push_back(bore);
|
||||
|
||||
doc.add_extrude_entities(entities, cw_plane, 10.0, false, BooleanMode::New, "Extrude1");
|
||||
REQUIRE(doc.recompute());
|
||||
REQUIRE(doc.error.empty());
|
||||
REQUIRE(doc.display_mesh.facets_count() > 0);
|
||||
|
||||
auto mp = doc.body_mass_properties(0);
|
||||
REQUIRE(mp.valid);
|
||||
const double expected_vol = 120.0 * 160.0 * 10.0 - M_PI * 30.0 * 30.0 * 10.0;
|
||||
REQUIRE_THAT(mp.volume, WithinAbs(expected_vol, 1.0));
|
||||
|
||||
int face_count = 0, solid_count = 0;
|
||||
for (TopExp_Explorer fe(doc.bodies.back().shape, TopAbs_FACE); fe.More(); fe.Next()) ++face_count;
|
||||
for (TopExp_Explorer se(doc.bodies.back().shape, TopAbs_SOLID); se.More(); se.Next()) ++solid_count;
|
||||
INFO("volume mm^3 = " << mp.volume << ", faces = " << face_count << ", solids = " << solid_count);
|
||||
REQUIRE(solid_count == 1);
|
||||
REQUIRE(face_count == 7);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user