diff --git a/docs/CAD/design_tab.md b/docs/CAD/design_tab.md index 3904df1fff..e6563bdfa9 100644 --- a/docs/CAD/design_tab.md +++ b/docs/CAD/design_tab.md @@ -227,6 +227,12 @@ Grouped in the toolbar by what they do, one concept per drawer. Extrude offers blind, symmetric, two-sided, through-all and up-to-face end conditions, plus a draft angle on the side wall, and can add, subtract, intersect or start a new body. +Revolve turns the profile about the sketch plane's X or Y axis, or about any line of the same +sketch: a construction centerline drawn beside the half-profile (picked for you when the sketch +has exactly one), or an edge of the profile itself. The axis is drawn dashed while the card is +open. A profile on both sides of the axis would sweep through itself, and is refused with that +reason. Surface Revolve takes the same axes. + ### Surface Sheet bodies — surfaces with no thickness — for shapes that are easier to build as skins and solidify afterwards. diff --git a/src/libslic3r/CAD/CadDocument.cpp b/src/libslic3r/CAD/CadDocument.cpp index f77c4ceeaf..8d4e765c46 100644 --- a/src/libslic3r/CAD/CadDocument.cpp +++ b/src/libslic3r/CAD/CadDocument.cpp @@ -2438,6 +2438,26 @@ static std::string open_loop_message(const CadFeature& sketch, return msg; } +// World axis of a Revolve / Surface Revolve whose profile is `sk`: the Line of that sketch named +// by revolve_axis_entity, else the sketch plane's X or Y axis through its origin. +static gp_Ax1 revolve_axis_of(const CadFeature& f, const CadFeature& sk) +{ + if (f.revolve_axis_entity >= 0) { + if (f.revolve_axis_entity >= int(sk.entities.size()) + || sk.entities[f.revolve_axis_entity].type != SketchEntity::Type::Line) + throw std::runtime_error("revolve: the axis line is no longer in the sketch — pick the axis again"); + const SketchEntity& e = sk.entities[f.revolve_axis_entity]; + const Vec3d a = sk.plane.to_world(e.p0), b = sk.plane.to_world(e.p1); + if ((b - a).norm() < 1e-9) + throw std::runtime_error("revolve: the axis line has no length"); + const Vec3d d = (b - a).normalized(); + return gp_Ax1(gp_Pnt(a.x(), a.y(), a.z()), gp_Dir(d.x(), d.y(), d.z())); + } + const Vec3d& adir = (f.revolve_axis == 1) ? sk.plane.y_axis : sk.plane.x_axis; + return gp_Ax1(gp_Pnt(sk.plane.origin.x(), sk.plane.origin.y(), sk.plane.origin.z()), + gp_Dir(adir.x(), adir.y(), adir.z())); +} + TopoDS_Wire CadDocument::build_sketch_wire(const CadFeature& sketch, bool closed_only) const { if (!sketch.entities.empty()) { @@ -2651,7 +2671,7 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, ? features[f.sketch_ref] : f; TopoDS_Wire wire = build_sketch_wire(sk, true); const double ang = f.flip ? -f.revolve_angle : f.revolve_angle; - TopoDS_Shape tool = SketchEngine::make_revolve(wire, sk.plane, ang, f.revolve_axis); + TopoDS_Shape tool = SketchEngine::make_revolve(wire, revolve_axis_of(f, sk), ang); if (!have_body || f.mode == BooleanMode::New) { result = tool; have_body = true; @@ -2693,10 +2713,7 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, throw std::runtime_error("surface-revolve: ref is not a sketch"); TopoDS_Wire wire = build_sketch_wire(sk); if (wire.IsNull()) throw std::runtime_error("surface-revolve: empty profile"); - const Vec3d& adir = (f.revolve_axis == 1) ? sk.plane.y_axis : sk.plane.x_axis; - gp_Pnt o(sk.plane.origin.x(), sk.plane.origin.y(), sk.plane.origin.z()); - gp_Dir xd(adir.x(), adir.y(), adir.z()); - gp_Ax1 axis(o, xd); + gp_Ax1 axis = revolve_axis_of(f, sk); // Same angle rules as the solid Revolve (flip reverses it, and a negative sweep is a // positive one about the reversed axis — MakeRevol wants (0, 2 pi]); this surface // version used to ignore both. diff --git a/src/libslic3r/CAD/CadDocument.hpp b/src/libslic3r/CAD/CadDocument.hpp index 78473af560..251d2e2496 100644 --- a/src/libslic3r/CAD/CadDocument.hpp +++ b/src/libslic3r/CAD/CadDocument.hpp @@ -180,6 +180,9 @@ struct CadFeature { // target_body. revolve_axis: 0 = plane X axis, 1 = plane Y axis. double revolve_angle{360}; // sweep angle in degrees (1..360) int revolve_axis{0}; // 0 = plane X, 1 = plane Y + // A Line of the profile sketch to revolve about instead (index into its entities: a + // centerline, usually construction, or an edge of the profile itself); -1 = revolve_axis. + int revolve_axis_entity{-1}; // Sweep: profile carried by sketch_ref / entities (like Extrude); the spine is a // second Sketch referenced by sweep_path_ref (an open or closed wire). Reuses @@ -404,7 +407,8 @@ struct CadFeature { coordsys_face_kind, coordsys_face_edges, thread_major_nominal, pattern_inclusive, dressup_edges, - text_string, text_font, text_height); + text_string, text_font, text_height, + revolve_axis_entity); } template void load(Archive& ar) { @@ -446,7 +450,8 @@ struct CadFeature { coordsys_face_kind, coordsys_face_edges, thread_major_nominal, pattern_inclusive, dressup_edges, - text_string, text_font, text_height); + text_string, text_font, text_height, + revolve_axis_entity); imported_solid = brep_from_string(brep); } // The pre-framing (v4) layout, FROZEN. A v4 recipe is one flat stream with no per-feature diff --git a/src/libslic3r/CAD/SketchEngine.cpp b/src/libslic3r/CAD/SketchEngine.cpp index 812e6fa703..13f5a9485a 100644 --- a/src/libslic3r/CAD/SketchEngine.cpp +++ b/src/libslic3r/CAD/SketchEngine.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -335,20 +336,35 @@ TopoDS_Shape SketchEngine::make_extrude_regions( return count == 1 ? last : TopoDS_Shape(comp); // avoid a compound-of-one } -TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane, - double angle_deg, int axis_sel) +TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& axis_in, double angle_deg) { BRepBuilderAPI_MakeFace faceMaker(wire); if (!faceMaker.IsDone()) throw std::runtime_error("Failed to make face from wire"); TopoDS_Face face = faceMaker.Face(); - // Revolution axis lies in the sketch plane through its origin: X (0) or Y (1). - const Vec3d& adir = (axis_sel == 1) ? plane.y_axis : plane.x_axis; - gp_Pnt o(plane.origin.x(), plane.origin.y(), plane.origin.z()); - gp_Dir xd(adir.x(), adir.y(), adir.z()); - gp_Ax1 axis(o, xd); - + // A profile on both sides of the axis sweeps through itself; MakeRevol then fails with no + // reason, or builds an invalid solid. Sample every edge and name the cause instead. + { + const gp_Pnt o = axis_in.Location(); + const gp_Dir d = axis_in.Direction(); + bool pos = false, neg = false; + gp_Vec side_ref; + for (TopExp_Explorer ex(wire, TopAbs_EDGE); ex.More(); ex.Next()) { + BRepAdaptor_Curve c(TopoDS::Edge(ex.Current())); + for (int i = 0; i <= 16; ++i) { + const gp_Pnt p = c.Value(c.FirstParameter() + (c.LastParameter() - c.FirstParameter()) * i / 16.0); + const gp_Vec off = gp_Vec(o, p) - gp_Vec(d) * gp_Vec(o, p).Dot(gp_Vec(d)); // from the axis + if (off.Magnitude() < 1e-6) + continue; + if (side_ref.Magnitude() == 0.0) { side_ref = off; pos = true; continue; } + (off.Dot(side_ref) > 0.0 ? pos : neg) = true; + } + } + if (pos && neg) + throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it"); + } + gp_Ax1 axis = axis_in; double angle_rad = angle_deg * M_PI / 180.0; // A negative angle is expressed as a positive sweep about the reversed axis, // since BRepPrimAPI_MakeRevol expects an angle in (0, 2*pi]. @@ -356,6 +372,8 @@ TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const SketchPla BRepPrimAPI_MakeRevol rev(face, axis, angle_rad); if (!rev.IsDone()) throw std::runtime_error("Failed to revolve"); + if (!BRepCheck_Analyzer(rev.Shape()).IsValid()) + throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it"); return rev.Shape(); } diff --git a/src/libslic3r/CAD/SketchEngine.hpp b/src/libslic3r/CAD/SketchEngine.hpp index 785102c205..ea3b6888b8 100644 --- a/src/libslic3r/CAD/SketchEngine.hpp +++ b/src/libslic3r/CAD/SketchEngine.hpp @@ -6,6 +6,7 @@ #include "libslic3r/CAD/GeometryEngine.hpp" #include +#include #include #include #include @@ -262,12 +263,10 @@ public: const std::vector>>& regions, const SketchPlane& plane, double length, bool symmetric = false); - // Revolve a planar profile wire about an axis lying in the sketch plane and - // passing through the plane origin: axis_sel 0 = plane X axis, 1 = plane Y axis. - // A negative angle_deg sweeps the opposite direction (Flip). The profile must - // lie to one side of the axis (Onshape rule); a straddling profile self-intersects. - static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane, - double angle_deg = 360.0, int axis_sel = 0); + // Revolve the closed profile wire about `axis` (world) by angle_deg; a negative angle sweeps + // the other way (Flip). The profile must lie to one side of the axis (Onshape rule): one that + // straddles it sweeps through itself, and that is refused rather than returned broken. + static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const gp_Ax1& axis, double angle_deg = 360.0); // Sweep a planar profile wire along a path (spine) wire. The profile is turned // into a face and swept with BRepOffsetAPI_MakePipe, which keeps the profile diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index 7feda3e9bd..6c3516b619 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -862,9 +862,9 @@ void DesignCanvas::set_on_shell_thickness_changed(std::function cb } void DesignCanvas::begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, - int axis_sel, double angle, bool flip) + const Vec3d& axis_origin, const Vec3d& axis_dir, double angle, bool flip) { - m_sketch_tool.set_revolve_gizmo(plane, centroid, axis_sel, angle, flip); + m_sketch_tool.set_revolve_gizmo(plane, centroid, axis_origin, axis_dir, angle, flip); request_repaint(); } diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index 21357021c7..50b2cab46c 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -161,11 +161,11 @@ public: void clear_shell_gizmo(); bool shelling() const; void set_on_shell_thickness_changed(std::function cb); - // Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + axis - // (0=plane X, 1=plane Y) + angle + flip while its Revolve card is open; drag/edit fire the - // angle callback. + // Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + the + // world axis (a point on it, unit direction) + angle + flip while its Revolve card is open; + // drag/edit fire the angle callback. void begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, - int axis_sel, double angle, bool flip); + const Vec3d& axis_origin, const Vec3d& axis_dir, double angle, bool flip); void clear_revolve_gizmo(); bool revolving() const; void set_on_revolve_angle_changed(std::function cb); diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index e913beaa33..2ca216260d 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -667,6 +667,7 @@ DesignPanel::DesignPanel(wxWindow* parent) set_status(StatusKind::Error, _L("Create a sketch profile to revolve first")); return; } + fill_revolve_axes(m_revolve_axis, m_revolve_axis_ents, m_revolve_sketch_ref, 0, -2); open_tool(Tool::Revolve); }, SHIFT('R')}, {"design_sweep", _L("Sweep"), _L("Sweep a profile along a path"), @@ -796,6 +797,7 @@ DesignPanel::DesignPanel(wxWindow* parent) set_status(StatusKind::Error, _L("Create a sketch profile to revolve first")); return; } + fill_revolve_axes(m_surf_revolve_axis, m_surf_revolve_axis_ents, m_surf_revolve_sketch_ref, 0, -2); open_tool(Tool::SurfaceRevolve); }, SHIFT('J')}, {"design_loft", _L("Surface Loft"), _L("Loft (skin) between 2+ profiles, open (no end caps)"), @@ -5372,9 +5374,11 @@ void DesignPanel::on_add_revolve() return; } m_feature_counter++; - m_doc.add_revolve(m_revolve_sketch_ref, m_revolve_angle->GetValue(), - m_revolve_axis->GetSelection(), m_revolve_flip->GetValue(), - mode, feature_name(_L("Revolve"))); + int axis = 0, axis_entity = -1; + read_revolve_axis(m_revolve_axis, m_revolve_axis_ents, axis, axis_entity); + const int idx = m_doc.add_revolve(m_revolve_sketch_ref, m_revolve_angle->GetValue(), axis, + m_revolve_flip->GetValue(), mode, feature_name(_L("Revolve"))); + m_doc.features[idx].revolve_axis_entity = axis_entity; if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -5465,9 +5469,11 @@ void DesignPanel::on_add_surface_revolve() return; } m_feature_counter++; - m_doc.add_surface_revolve(m_surf_revolve_sketch_ref, m_surf_revolve_angle->GetValue(), - m_surf_revolve_axis->GetSelection(), - feature_name(_L("Surface Revolve"))); + int axis = 0, axis_entity = -1; + read_revolve_axis(m_surf_revolve_axis, m_surf_revolve_axis_ents, axis, axis_entity); + const int idx = m_doc.add_surface_revolve(m_surf_revolve_sketch_ref, m_surf_revolve_angle->GetValue(), + axis, feature_name(_L("Surface Revolve"))); + m_doc.features[idx].revolve_axis_entity = axis_entity; if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -9199,10 +9205,10 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f) break; case CadFeatureType::Revolve: m_revolve_angle->SetValue(f.revolve_angle); - m_revolve_axis->SetSelection(f.revolve_axis); m_revolve_mode->SetSelection(static_cast(f.mode)); m_revolve_flip->SetValue(f.flip); m_revolve_sketch_ref = f.sketch_ref; + fill_revolve_axes(m_revolve_axis, m_revolve_axis_ents, f.sketch_ref, f.revolve_axis, f.revolve_axis_entity); break; case CadFeatureType::Sweep: m_sweep_profile_ref = f.sketch_ref; @@ -9316,9 +9322,9 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f) break; case CadFeatureType::SurfaceRevolve: m_surf_revolve_angle->SetValue(f.revolve_angle); - m_surf_revolve_axis->SetSelection(f.revolve_axis); m_surf_revolve_flip->SetValue(f.flip); m_surf_revolve_sketch_ref = f.sketch_ref; + fill_revolve_axes(m_surf_revolve_axis, m_surf_revolve_axis_ents, f.sketch_ref, f.revolve_axis, f.revolve_axis_entity); if (m_surf_revolve_sketch_ref >= 0 && m_surf_revolve_sketch_ref < int(m_doc.features.size())) m_surf_revolve_sketch_label->SetLabel(_L("Sketch: ") + wxString::FromUTF8(m_doc.features[m_surf_revolve_sketch_ref].name)); @@ -9850,7 +9856,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const f.type = CadFeatureType::Revolve; f.sketch_ref = m_revolve_sketch_ref; f.revolve_angle = m_revolve_angle->GetValue(); - f.revolve_axis = m_revolve_axis->GetSelection(); + read_revolve_axis(m_revolve_axis, m_revolve_axis_ents, f.revolve_axis, f.revolve_axis_entity); f.flip = m_revolve_flip->GetValue(); f.mode = static_cast(m_revolve_mode->GetSelection()); break; @@ -9934,7 +9940,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const f.type = CadFeatureType::SurfaceRevolve; f.sketch_ref = m_surf_revolve_sketch_ref; f.revolve_angle = m_surf_revolve_angle->GetValue(); - f.revolve_axis = m_surf_revolve_axis->GetSelection(); + read_revolve_axis(m_surf_revolve_axis, m_surf_revolve_axis_ents, f.revolve_axis, f.revolve_axis_entity); f.flip = m_surf_revolve_flip->GetValue(); break; case Tool::SurfaceLoft: { @@ -10185,6 +10191,40 @@ void DesignPanel::update_shell_gizmo() m_viewport->begin_shell_gizmo(c, (-n).normalized(), m_shell_thickness->GetValue()); } +void DesignPanel::fill_revolve_axes(ComboBox* combo, std::vector& ents, int sketch_ref, int axis, int entity) +{ + combo->Clear(); + ents.clear(); + combo->Append(_L("Plane X")); + combo->Append(_L("Plane Y")); + int centerline = -1, centerlines = 0; + if (sketch_ref >= 0 && sketch_ref < int(m_doc.features.size())) { + const std::vector& es = m_doc.features[sketch_ref].entities; + for (int i = 0; i < int(es.size()); ++i) { + if (es[i].type != SketchEntity::Type::Line) + continue; + // Named as the constraint list names entities (E0, E1, …), so the two agree. + combo->Append(es[i].construction ? wxString::Format(_L("Centerline E%d"), i) + : wxString::Format(_L("Line E%d"), i)); + ents.push_back(i); + if (es[i].construction) { centerline = i; ++centerlines; } + } + } + if (entity == -2) // fresh revolve: a lone centerline is what the profile was drawn around + entity = centerlines == 1 ? centerline : -1; + int sel = axis == 1 ? 1 : 0; + for (int k = 0; k < int(ents.size()); ++k) + if (ents[k] == entity) sel = 2 + k; + combo->SetSelection(sel); +} + +void DesignPanel::read_revolve_axis(ComboBox* combo, const std::vector& ents, int& axis, int& entity) +{ + const int sel = combo->GetSelection(); + axis = sel == 1 ? 1 : 0; + entity = sel >= 2 && sel - 2 < int(ents.size()) ? ents[sel - 2] : -1; +} + void DesignPanel::update_revolve_gizmo() { if (!m_viewport) return; @@ -10200,6 +10240,7 @@ void DesignPanel::update_revolve_gizmo() if (!sk.entities.empty()) { Vec2d acc(0, 0); int n = 0; for (const SketchEntity& e : sk.entities) { + if (e.construction) continue; // a centerline is the axis, not part of the profile switch (e.type) { case SketchEntity::Type::Line: acc += 0.5 * (e.p0 + e.p1); ++n; break; case SketchEntity::Type::Arc: @@ -10220,7 +10261,16 @@ void DesignPanel::update_revolve_gizmo() for (const Vec2d& p : sk.profile.points) centroid += p; centroid /= double(sk.profile.points.size()); } - m_viewport->begin_revolve_gizmo(sk.plane, centroid, m_revolve_axis->GetSelection(), + // The axis as the kernel resolves it (revolve_axis_of): the picked line, else plane X / Y. + int axis = 0, axis_entity = -1; + read_revolve_axis(m_revolve_axis, m_revolve_axis_ents, axis, axis_entity); + Vec3d ax_o = sk.plane.origin, ax_d = axis == 1 ? sk.plane.y_axis : sk.plane.x_axis; + if (axis_entity >= 0 && axis_entity < int(sk.entities.size()) + && (sk.entities[axis_entity].p1 - sk.entities[axis_entity].p0).norm() > 1e-9) { + ax_o = sk.plane.to_world(sk.entities[axis_entity].p0); + ax_d = sk.plane.to_world(sk.entities[axis_entity].p1) - ax_o; + } + m_viewport->begin_revolve_gizmo(sk.plane, centroid, ax_o, ax_d.normalized(), m_revolve_angle->GetValue(), m_revolve_flip->GetValue()); } diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index c7e20186df..b52942ff73 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -551,7 +551,8 @@ private: // Revolve controls (sweep a sketch profile about an in-plane axis). wxStaticText* m_revolve_sketch_label{nullptr}; wxSpinCtrlDouble* m_revolve_angle{nullptr}; - ComboBox* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y + ComboBox* m_revolve_axis{nullptr}; // Plane X, Plane Y, then the sketch's lines + std::vector m_revolve_axis_ents; // entity index of each line entry, in order ComboBox* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect CheckBox* m_revolve_flip{nullptr}; int m_revolve_sketch_ref{-1}; @@ -578,7 +579,14 @@ private: // Surface Revolve controls (sheet from sketch about axis). wxStaticText* m_surf_revolve_sketch_label{nullptr}; wxSpinCtrlDouble* m_surf_revolve_angle{nullptr}; - ComboBox* m_surf_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y + ComboBox* m_surf_revolve_axis{nullptr}; // as m_revolve_axis + std::vector m_surf_revolve_axis_ents; + // Fill a revolve axis combo for the profile sketch `sketch_ref`: "Plane X", "Plane Y", then + // one entry per Line of the sketch (entity index in `ents`), and select axis/entity. A fresh + // revolve passes entity = -2: the sketch's only centerline when it has exactly one, else X. + void fill_revolve_axes(ComboBox* combo, std::vector& ents, int sketch_ref, int axis, int entity); + // The combo's selection as CadFeature::revolve_axis + revolve_axis_entity. + static void read_revolve_axis(ComboBox* combo, const std::vector& ents, int& axis, int& entity); CheckBox* m_surf_revolve_flip{nullptr}; int m_surf_revolve_sketch_ref{-1}; diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index f06fcb1a81..675bdea216 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -6405,12 +6405,13 @@ void DesignSketchTool::drag_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& ev } void DesignSketchTool::set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, - int axis_sel, double angle, bool flip) + const Vec3d& axis_origin, const Vec3d& axis_dir, + double angle, bool flip) { - const Vec3d ax = (axis_sel == 1 ? plane.y_axis : plane.x_axis).normalized(); + const Vec3d ax = axis_dir.normalized(); const Vec3d cw = plane.to_world(centroid); - const double axial = (cw - plane.origin).dot(ax); - m_rv_center = plane.origin + axial * ax; // foot of the centroid on the axis line + const double axial = (cw - axis_origin).dot(ax); + m_rv_center = axis_origin + axial * ax; // foot of the centroid on the axis line Vec3d ref = cw - m_rv_center; // perpendicular to ax by construction double r = ref.norm(); if (r < 1e-6) { ref = plane.normal.normalized(); r = std::max(plane.normal.norm(), 1.0); } @@ -6477,6 +6478,16 @@ void DesignSketchTool::render_revolve_gizmo() draw_strokes(m_rv_stroke_model, segs, std::max(0.8 * upp, 1e-4), arcc); DimAnnot da; da.kind = DimType::Angle; da.value = m_rv_angle; draw_text(m_line_model, dim_text(da), tip * 1.14, th, arcc); + // The axis itself, dashed, past both ends of the sweep: which line is being revolved about + // is the one thing the card's list cannot show. + SketchPlane ap; ap.origin = m_rv_center; ap.x_axis = m_rv_axis; ap.y_axis = m_rv_ref; + ap.normal = m_rv_axis.cross(m_rv_ref); + m_plane = ap; + std::vector> dashes; + const double L = 1.6 * r, dash = std::max(r * 0.08, th * 0.5); + for (double u = -L; u < L; u += 2.0 * dash) + dashes.emplace_back(Vec2d(u, 0.0), Vec2d(std::min(u + dash, L), 0.0)); + draw_strokes(m_rv_stroke_model, dashes, std::max(0.8 * upp, 1e-4), ColorRGBA(1.0f, 0.55f, 0.1f, 1.0f)); m_plane = saved; } diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index 7228e3b362..56c1ac1a35 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -374,12 +374,12 @@ public: void set_mate_links(std::vector> l) { m_mate_links = std::move(l); } void clear_mate_connectors() { m_mate_connectors.clear(); m_mate_links.clear(); } - // Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X, - // 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the - // revolve plane at the profile radius. Dragging the tip sweeps the angle, a stationary click + // Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + the world axis + // (a point on it and its direction) + angle + flip while its Revolve card is open; an + // angle-arc is drawn in the revolve plane at the profile radius, and the axis dashed. Dragging the tip sweeps the angle, a stationary click // edits it; both fire on_revolve_angle_changed. void set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid, - int axis_sel, double angle, bool flip); + const Vec3d& axis_origin, const Vec3d& axis_dir, double angle, bool flip); void clear_revolve_gizmo(); bool revolving() const { return m_rv_active; } std::function on_revolve_angle_changed; diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index fe788d0ffa..480bd929e0 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -2351,11 +2351,12 @@ TEST_CASE("a truncated feature keeps what it could read", "[CadDocument][recipe] // Shorten feature 1 so it ends right after coordsys_face_kind: drop coordsys_face_edges // (4 bytes), the two flags appended after it (thread_major_nominal, pattern_inclusive: // 1 byte each), the empty dressup_edges list and the two empty text strings (an 8-byte size - // tag each) and text_height (a double). Rewrite its length prefix and erase the tail bytes. + // tag each), text_height (a double) and revolve_axis_entity (an int). Rewrite its length prefix and erase the tail bytes. // The reader then runs out inside fa(f), throws, and keeps everything it had already // assigned — that is the whole point of the try/catch. (Cut on a field boundary: a field // cut in half is read as whatever half arrived.) - const size_t drop = sizeof(uint32_t) + 2 * sizeof(bool) + 3 * sizeof(cereal::size_type) + sizeof(double); + const size_t drop = sizeof(uint32_t) + 2 * sizeof(bool) + 3 * sizeof(cereal::size_type) + sizeof(double) + + sizeof(int); REQUIRE(f_len[1] > drop); std::string shortened = blob; shortened.erase(f_off[1] + 4 + f_len[1] - drop, drop); @@ -8948,3 +8949,93 @@ TEST_CASE("display edges: every real edge once, no seams, no degenerate apex", " const auto cone = GeometryEngine::display_edges(BRepPrimAPI_MakeCone(5., 0., 8.).Shape(), 0.01); CHECK(cone.size() == 1); } + +namespace { +// A 10 x 20 rectangle standing on the plane X axis between u = 5 and u = 15, and a construction +// centerline x = 0 from (0,0) to (0,20): the half-profile of a tube, drawn the usual way. +Slic3r::CadFeature tube_half_profile() +{ + using namespace Slic3r; + CadFeature sk; + sk.type = CadFeatureType::Sketch; + sk.plane = SketchPlane::XY(); + auto line = [](Vec2d a, Vec2d b, bool c) { + SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = a; e.p1 = b; e.construction = c; return e; }; + sk.entities = { line({5, 0}, {15, 0}, false), line({15, 0}, {15, 20}, false), + line({15, 20}, {5, 20}, false), line({5, 20}, {5, 0}, false), + line({0, 0}, {0, 20}, true) }; + return sk; +} +} // namespace + +TEST_CASE("revolve about a line of the sketch", "[CadDocument][revolve]") +{ + using namespace Slic3r; + + SECTION("a construction centerline: the profile sweeps into a tube around it") { + CadDocument doc; + doc.features.push_back(tube_half_profile()); + const int r = doc.add_revolve(0, 360.0, 0, false, BooleanMode::New, "Rev"); + doc.features[r].revolve_axis_entity = 4; + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.bodies.size() == 1); + GProp_GProps props; + BRepGProp::VolumeProperties(doc.bodies[0].shape, props); + CHECK(props.Mass() == Approx(M_PI * (15. * 15. - 5. * 5.) * 20.).epsilon(1e-6)); // 4000 pi + Bnd_Box box; + BRepBndLib::Add(doc.bodies[0].shape, box); + double x0, y0, z0, x1, y1, z1; + box.Get(x0, y0, z0, x1, y1, z1); + CHECK(y0 == Approx(0.).margin(0.01)); // the axis runs along Y, as drawn + CHECK(y1 == Approx(20.).margin(0.01)); + CHECK(x1 == Approx(15.).margin(0.01)); + } + + SECTION("an edge of the profile itself: a solid cylinder") { + CadDocument doc; + doc.features.push_back(tube_half_profile()); + const int r = doc.add_revolve(0, 360.0, 0, false, BooleanMode::New, "Rev"); + doc.features[r].revolve_axis_entity = 3; // the rectangle's left side, u = 5 + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + GProp_GProps props; + BRepGProp::VolumeProperties(doc.bodies[0].shape, props); + CHECK(props.Mass() == Approx(M_PI * 10. * 10. * 20.).epsilon(1e-6)); + } + + SECTION("an axis through the profile is refused with the reason") { + CadDocument doc; + CadFeature sk = tube_half_profile(); + sk.entities[4].p0 = Vec2d(10, 0); + sk.entities[4].p1 = Vec2d(10, 20); // straight through the middle of the rectangle + doc.features.push_back(sk); + const int r = doc.add_revolve(0, 360.0, 0, false, BooleanMode::New, "Rev"); + doc.features[r].revolve_axis_entity = 4; + CHECK_FALSE(doc.recompute()); + INFO(doc.error); + CHECK(doc.error.find("crosses the revolve axis") != std::string::npos); + } + + SECTION("an axis index that no longer names a line fails with a reason") { + CadDocument doc; + doc.features.push_back(tube_half_profile()); + const int r = doc.add_revolve(0, 360.0, 0, false, BooleanMode::New, "Rev"); + doc.features[r].revolve_axis_entity = 9; + CHECK_FALSE(doc.recompute()); + CHECK(doc.error.find("axis line") != std::string::npos); + } + + SECTION("the axis line survives save and load") { + CadDocument doc; + doc.features.push_back(tube_half_profile()); + const int r = doc.add_revolve(0, 270.0, 0, false, BooleanMode::New, "Rev"); + doc.features[r].revolve_axis_entity = 4; + REQUIRE(doc.recompute()); + CadDocument loaded; + REQUIRE(loaded.deserialize_recipe(doc.serialize_recipe())); + REQUIRE(loaded.features.size() == 2); + CHECK(loaded.features[1].revolve_axis_entity == 4); + CHECK(loaded.features[1].revolve_angle == Approx(270.)); + } +}