Design tab: revolve about a line of the sketch

Reported: the Revolve axis could only be the sketch plane's X or Y axis
through its origin, so a half-profile drawn beside a centerline, the
usual way, could not be revolved about that centerline.

- CadFeature::revolve_axis_entity names a Line of the profile sketch to
  revolve about (a construction centerline, or an edge of the profile
  itself); -1 keeps revolve_axis. Appended at the end of the framed
  recipe, so existing projects load and rebuild unchanged. Revolve and
  Surface Revolve resolve their axis in one place (revolve_axis_of); an
  index that no longer names a line fails with a reason.
- SketchEngine::make_revolve takes the world axis. A profile with points
  on both sides of it is refused with "the profile crosses the revolve
  axis"; MakeRevol failed there with no reason.
- The Axis list of both cards reads Plane X, Plane Y, then every line of
  the sketch, named as the constraint list names them (Centerline E4,
  Line E3). A fresh revolve preselects the sketch's centerline when it
  has exactly one. The gizmo turns about the chosen axis and draws it
  dashed; construction lines no longer pull its centre.

Tests: a rectangle beside a construction centerline revolves into the
tube of the expected volume along that line; about its own edge, into a
cylinder; an axis through it is refused with the reason; a stale axis
index fails with a reason; the axis survives save and load. The
truncated-recipe test accounts for the new tail field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
Claude
2026-09-30 08:34:46 +00:00
parent b8d45c6bb1
commit a4387e93f2
12 changed files with 255 additions and 50 deletions
+6
View File
@@ -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.
+22 -5
View File
@@ -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.
+7 -2
View File
@@ -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<class Archive>
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
+26 -8
View File
@@ -9,6 +9,7 @@
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepCheck_Analyzer.hxx>
#include <BRepAdaptor_Curve.hxx>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <BRepClass_FaceClassifier.hxx>
@@ -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();
}
+5 -6
View File
@@ -6,6 +6,7 @@
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <gp_Pln.hxx>
#include <gp_Ax1.hxx>
#include <gp_Ax3.hxx>
#include <TopoDS_Wire.hxx>
#include <TopoDS_Shape.hxx>
@@ -262,12 +263,10 @@ public:
const std::vector<std::vector<std::vector<Vec2d>>>& 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
+2 -2
View File
@@ -862,9 +862,9 @@ void DesignCanvas::set_on_shell_thickness_changed(std::function<void(double)> 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();
}
+4 -4
View File
@@ -161,11 +161,11 @@ public:
void clear_shell_gizmo();
bool shelling() const;
void set_on_shell_thickness_changed(std::function<void(double)> 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<void(double)> cb);
+61 -11
View File
@@ -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<int>(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<BooleanMode>(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<int>& 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<SketchEntity>& 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<int>& 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());
}
+10 -2
View File
@@ -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<int> 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<int> 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<int>& 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<int>& ents, int& axis, int& entity);
CheckBox* m_surf_revolve_flip{nullptr};
int m_surf_revolve_sketch_ref{-1};
+15 -4
View File
@@ -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<std::pair<Vec2d, Vec2d>> 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;
}
+4 -4
View File
@@ -374,12 +374,12 @@ public:
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> 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<void(double angle)> on_revolve_angle_changed;
+93 -2
View File
@@ -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.));
}
}