mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-06 23:31:19 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user