Design tab: pick several solid edges and dress them in one feature

A solid edge could only be picked one at a time, and Fillet/Chamfer took
either that one edge or a whole face group. Rounding three chosen edges
meant three features, whose edge ids each resolve against a body the
previous one had already changed.

- Shift+click (or Ctrl+click) on an edge of the body already picked adds
  it to the selection, or removes it; the same modifiers that extend a
  sketch selection. The whole set is highlighted. A plain click replaces
  it, as before.
- Fillet/Chamfer dresses every picked edge in ONE feature at one size,
  all ids resolved against the same body. The card says "3 edges", the
  status line and the offer header name the count.
- CadFeature gains dressup_edges, appended at the end of the framed
  recipe, so existing projects load and rebuild unchanged. dressup_edge
  keeps the first edge, so an older build opening a newer project still
  dresses that edge instead of falling back to the face group.
- The MCP fillet/chamfer verbs take `edge` as one id or an array.

Tests: a fillet on the four picked top edges equals the Top face group
exactly; the list survives save/load; two opposite chamfers remove
exactly twice one; a missing id fails with a reason. 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:33 +00:00
parent f4cdeb1706
commit 3c218591bc
13 changed files with 297 additions and 42 deletions
+30 -4
View File
@@ -928,6 +928,19 @@ int CadDocument::add_fillet(double radius, int edge_id, const std::string& name)
return int(features.size()) - 1;
}
int CadDocument::add_fillet(double radius, const std::vector<int>& edge_ids, const std::string& name)
{
if (edge_ids.size() == 1) return add_fillet(radius, edge_ids.front(), name);
CadFeature f;
f.type = CadFeatureType::Fillet;
f.name = name;
f.dressup_size = radius;
f.dressup_edges = edge_ids;
f.dressup_edge = edge_ids.empty() ? -1 : edge_ids.front();
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_chamfer(double distance, FaceGroup faces, const std::string& name)
{
CadFeature f;
@@ -950,6 +963,19 @@ int CadDocument::add_chamfer(double distance, int edge_id, const std::string& na
return int(features.size()) - 1;
}
int CadDocument::add_chamfer(double distance, const std::vector<int>& edge_ids, const std::string& name)
{
if (edge_ids.size() == 1) return add_chamfer(distance, edge_ids.front(), name);
CadFeature f;
f.type = CadFeatureType::Chamfer;
f.name = name;
f.dressup_size = distance;
f.dressup_edges = edge_ids;
f.dressup_edge = edge_ids.empty() ? -1 : edge_ids.front();
features.push_back(f);
return int(features.size()) - 1;
}
int CadDocument::add_hole(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane,
const std::string& name)
@@ -2886,15 +2912,15 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
}
case CadFeatureType::Fillet:
if (!have_body) throw std::runtime_error("fillet needs a body");
if (f.dressup_edge >= 0)
result = GeometryEngine::apply_fillet(result, f.dressup_size, f.dressup_edge);
if (f.dressup_edge >= 0 || !f.dressup_edges.empty())
result = GeometryEngine::apply_fillet(result, f.dressup_size, f.dressup_edge_ids());
else
result = GeometryEngine::apply_fillet(result, f.dressup_size, f.face_group);
break;
case CadFeatureType::Chamfer:
if (!have_body) throw std::runtime_error("chamfer needs a body");
if (f.dressup_edge >= 0)
result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.dressup_edge);
if (f.dressup_edge >= 0 || !f.dressup_edges.empty())
result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.dressup_edge_ids());
else
result = GeometryEngine::apply_chamfer(result, f.dressup_size, f.face_group);
break;
+17 -2
View File
@@ -112,6 +112,17 @@ struct CadFeature {
double dressup_size{1.0}; // fillet radius or chamfer distance
FaceGroup face_group{FaceGroup::All};
int dressup_edge{-1}; // global edge id for edge-targeted fillet/chamfer; -1 = use face_group
// Several picked edges dressed in ONE operation, all resolved against the same body, so
// the ids cannot drift the way they do across a chain of single-edge features. When set,
// dressup_edge holds the first of them: a build that predates the list still dresses that
// one edge instead of falling back to the whole face group.
std::vector<int> dressup_edges;
// The edges this dress-up targets: the list, else the single edge, else none (face group).
std::vector<int> dressup_edge_ids() const {
if (!dressup_edges.empty()) return dressup_edges;
if (dressup_edge >= 0) return { dressup_edge };
return {};
}
// Hole params (positioned circular cut into the current body)
double hole_diameter{5};
@@ -383,7 +394,8 @@ struct CadFeature {
expr,
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
coordsys_face_kind, coordsys_face_edges,
thread_major_nominal, pattern_inclusive);
thread_major_nominal, pattern_inclusive,
dressup_edges);
}
template<class Archive>
void load(Archive& ar) {
@@ -423,7 +435,8 @@ struct CadFeature {
expr,
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
coordsys_face_kind, coordsys_face_edges,
thread_major_nominal, pattern_inclusive);
thread_major_nominal, pattern_inclusive,
dressup_edges);
imported_solid = brep_from_string(brep);
}
// The pre-framing (v4) layout, FROZEN. A v4 recipe is one flat stream with no per-feature
@@ -596,8 +609,10 @@ public:
BooleanMode mode, const std::string& name);
int add_fillet(double radius, FaceGroup faces, const std::string& name);
int add_fillet(double radius, int edge_id, const std::string& name);
int add_fillet(double radius, const std::vector<int>& edge_ids, const std::string& name);
int add_chamfer(double distance, FaceGroup faces, const std::string& name);
int add_chamfer(double distance, int edge_id, const std::string& name);
int add_chamfer(double distance, const std::vector<int>& edge_ids, const std::string& name);
int add_hole(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane,
const std::string& name);
+23 -9
View File
@@ -364,28 +364,42 @@ TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double dis
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, int edge_id)
{
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
return apply_fillet(solid, radius, std::vector<int>{ edge_id });
}
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
{
return apply_chamfer(solid, distance, std::vector<int>{ edge_id });
}
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, const std::vector<int>& edge_ids)
{
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
if (edge_ids.empty()) throw std::runtime_error("apply_fillet: no edge picked");
BRepFilletAPI_MakeFillet mk(solid);
mk.Add(radius, edge);
for (int id : edge_ids) {
TopoDS_Edge edge = edge_by_index(solid, id);
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
mk.Add(radius, edge);
}
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_fillet: OCCT fillet failed");
return mk.Shape();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, const std::vector<int>& edge_ids)
{
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
if (edge_ids.empty()) throw std::runtime_error("apply_chamfer: no edge picked");
BRepFilletAPI_MakeChamfer mk(solid);
mk.Add(distance, edge);
for (int id : edge_ids) {
TopoDS_Edge edge = edge_by_index(solid, id);
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
mk.Add(distance, edge);
}
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed");
+5
View File
@@ -120,6 +120,11 @@ public:
FaceGroup faces = FaceGroup::All);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
int edge_id);
// Several edges in one operation, all ids resolved against `solid`.
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
const std::vector<int>& edge_ids);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
const std::vector<int>& edge_ids);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
double linear_deflection = 0.003,
+5
View File
@@ -938,6 +938,11 @@ void DesignCanvas::set_on_pattern_changed(std::function<void(double)> cb)
m_sketch_tool.on_pattern_changed = std::move(cb);
}
std::vector<int> DesignCanvas::selected_solid_edges() const
{
return m_sketch_tool.selected_edges();
}
void DesignCanvas::set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb)
{
m_sketch_tool.on_solid_selection_changed = std::move(cb);
+1
View File
@@ -122,6 +122,7 @@ public:
const std::vector<bool>* visible = nullptr,
const std::vector<Transform3d>* xform = nullptr);
void set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb);
std::vector<int> selected_solid_edges() const; // the Shift/Ctrl+click edge set, last-clicked at the end
void select_body(int body); // Parts-list -> highlight a whole body by index
// Effective display colour of a body: the per-body override (Color tool) when set,
// otherwise the auto body-index palette. Single source of truth shared with reload().
+36 -13
View File
@@ -3663,6 +3663,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_sel_solid_body = (level >= 1) ? body : -1;
m_sel_solid_face = (level == 2) ? face : -1; // 4 = Vertex: a corner is not its face
m_sel_solid_edge = (level == 3) ? edge : -1;
m_sel_solid_edges = (level == 3) ? m_viewport->selected_solid_edges() : std::vector<int>();
m_sel_solid_vertex = (level == 4);
// Keep the hit face even at whole-body level: the cycle's first click means "this body",
// but the user pointed AT a face and a sketch should be able to use it. 3a2.
@@ -3851,7 +3852,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(level == 4 ? bodytag + _L("vertex selected — click again for the whole body")
: level == 1 ? bodytag + _L("selected (whole body) — right-click for what applies to it")
: level == 2 ? bodytag + wxString::Format(_L("face %d selected — right-click to push/pull it, or click again for the whole body"), face)
: level == 3 ? bodytag + wxString::Format(_L("edge %d selected — Fillet/Chamfer to dress it, or click again for the whole body"), edge)
: level == 3 && m_sel_solid_edges.size() > 1
? bodytag + wxString::Format(_L_PLURAL("%zu edge selected — Shift+click adds or removes one, Fillet/Chamfer dresses them all", "%zu edges selected — Shift+click adds or removes one, Fillet/Chamfer dresses them all", m_sel_solid_edges.size()), m_sel_solid_edges.size())
: level == 3 ? bodytag + wxString::Format(_L("edge %d selected — Shift+click to add edges, or click again for the whole body"), edge)
: _L("Nothing selected"));
m_status->Refresh();
});
@@ -5064,13 +5067,14 @@ void DesignPanel::on_add_dressup()
bool fillet = (m_dressup_type->GetSelection() == 0);
m_feature_counter++;
// A click-selected solid edge targets THAT edge; otherwise dress the whole face-group.
// Click-selected solid edges are the target; otherwise dress the whole face-group.
int didx = -1;
if (m_sel_solid_edge >= 0) {
const std::vector<int> edges = dressup_edges();
if (!edges.empty()) {
if (fillet)
didx = m_doc.add_fillet(sz, m_sel_solid_edge, feature_name(_L("Fillet")));
didx = m_doc.add_fillet(sz, edges, feature_name(_L("Fillet")));
else
didx = m_doc.add_chamfer(sz, m_sel_solid_edge, feature_name(_L("Chamfer")));
didx = m_doc.add_chamfer(sz, edges, feature_name(_L("Chamfer")));
} else if (fillet)
didx = m_doc.add_fillet(sz, fg, feature_name(_L("Fillet")));
else
@@ -6383,8 +6387,13 @@ wxString DesignPanel::offer_header(int kind) const
case OfferSel::FacePlanar: return wxString::Format(_L("Flat face %d of %s"), m_sel_solid_face, body());
case OfferSel::FaceCyl: return wxString::Format(_L("Cylindrical face %d of %s"), m_sel_solid_face, body());
case OfferSel::FaceOther: return wxString::Format(_L("Face %d of %s"), m_sel_solid_face, body());
case OfferSel::EdgeStr: return wxString::Format(_L("Straight edge %d of %s"), m_sel_solid_edge, body());
case OfferSel::EdgeCirc: return wxString::Format(_L("Circular edge %d of %s"), m_sel_solid_edge, body());
case OfferSel::EdgeStr:
case OfferSel::EdgeCirc:
if (dressup_edges().size() > 1)
return wxString::Format(_L_PLURAL("%zu edge of %s", "%zu edges of %s", dressup_edges().size()), dressup_edges().size(), body());
return OfferSel(kind) == OfferSel::EdgeStr
? wxString::Format(_L("Straight edge %d of %s"), m_sel_solid_edge, body())
: wxString::Format(_L("Circular edge %d of %s"), m_sel_solid_edge, body());
case OfferSel::Vertex: return wxString::Format(_L("Vertex of %s"), body());
case OfferSel::SkLoop: return _L("Sketch profile");
case OfferSel::SkNone: return _L("Sketch — nothing selected");
@@ -8999,7 +9008,8 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
m_dressup_type->SetSelection(f.type == CadFeatureType::Fillet ? 0 : 1);
m_dressup_size->SetValue(f.dressup_size);
m_face_group->SetSelection(static_cast<int>(f.face_group));
m_sel_solid_edge = f.dressup_edge; // preserve edge-targeting on re-edit
m_sel_solid_edges = f.dressup_edge_ids(); // preserve edge-targeting on re-edit
m_sel_solid_edge = m_sel_solid_edges.empty() ? -1 : m_sel_solid_edges.back();
m_sel_solid_body = f.target_body; // preserve which body on re-edit
sync_dressup_target(); // and say which of the two the re-edit is targeting
break;
@@ -9658,8 +9668,12 @@ CadFeature DesignPanel::build_candidate(Tool t) const
: CadFeatureType::Chamfer;
f.dressup_size = m_dressup_size->GetValue();
f.face_group = static_cast<FaceGroup>(m_face_group->GetSelection());
// A click-selected solid edge overrides the face-group: dress THAT edge.
f.dressup_edge = m_sel_solid_edge; // -1 when no edge picked
// Click-selected solid edges override the face-group: dress THOSE edges.
{
const std::vector<int> edges = dressup_edges();
f.dressup_edge = edges.empty() ? -1 : edges.front(); // -1 when no edge picked
f.dressup_edges = edges.size() > 1 ? edges : std::vector<int>();
}
break;
case Tool::Hole:
f.type = CadFeatureType::Hole;
@@ -9928,12 +9942,21 @@ CadFeature DesignPanel::build_candidate(Tool t) const
// user picked in the viewport, or the face-group. build_dressup reads m_sel_solid_edge first and
// only falls back to the group, so when an edge is picked the group combo is inert — grey it out
// rather than leave it showing a value it will not use.
std::vector<int> DesignPanel::dressup_edges() const
{
if (m_sel_solid_edge < 0) return {};
if (!m_sel_solid_edges.empty() && m_sel_solid_edges.back() == m_sel_solid_edge)
return m_sel_solid_edges;
return { m_sel_solid_edge };
}
void DesignPanel::sync_dressup_target()
{
if (m_dressup_edge_label == nullptr) return;
const bool have_edge = (m_sel_solid_edge >= 0);
m_dressup_edge_label->SetLabel(have_edge
? wxString::Format(_L("Edge %d"), m_sel_solid_edge)
const size_t n = dressup_edges().size();
const bool have_edge = n > 0;
m_dressup_edge_label->SetLabel(n > 1 ? wxString::Format(_L_PLURAL("%zu edge", "%zu edges", n), n)
: have_edge ? wxString::Format(_L("Edge %d"), m_sel_solid_edge)
: _L("(no edge picked — group below)"));
if (m_face_group != nullptr) m_face_group->Enable(!have_edge);
m_dressup_edge_label->Refresh();
+5
View File
@@ -329,6 +329,7 @@ private:
void update_extrude_gizmo();
void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card)
void sync_dressup_target(); // Dressup card: show picked edge vs group, gate the combo
std::vector<int> dressup_edges() const; // the picked edge(s) a dress-up targets; empty = face group
void update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card)
// A FEATURE button whose tool needs bodies it may not have yet. Greyed with an explanatory
// tooltip below min_bodies, rather than accepting the click and refusing afterwards.
@@ -716,6 +717,10 @@ private:
int m_sel_solid_body{-1}; // which body the face/edge selection is on
int m_sel_solid_face{-1};
int m_sel_solid_edge{-1};
// The picked edge set (Shift/Ctrl+click), m_sel_solid_edge last. Read through dressup_edges(),
// which drops it once m_sel_solid_edge moves on — the many places that reset the single edge
// then need not know the set exists.
std::vector<int> m_sel_solid_edges;
bool m_sel_solid_vertex{false}; // a corner is picked (body+point, no face/edge)
// The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level.
// The first click on a solid selects the WHOLE body, but the ray has already resolved which face
+51
View File
@@ -3445,12 +3445,22 @@ void DesignSketchTool::clear_solid_selection()
m_solid_sel = SolidSel::None;
m_sel_body = m_sel_face = m_sel_edge = -1;
m_sel_edge_pts.clear();
m_sel_edges_more.clear();
m_sel_edges_more_pts.clear();
// The pre-highlight names a face/edge/vertex by index into a shape that a recompute has just
// rebuilt, so it expires with the selection it was a promise about. Left behind it would keep
// glowing on whatever now sits at those indices — a real entity, but not the one meant.
m_pre = SolidPick{};
}
std::vector<int> DesignSketchTool::selected_edges() const
{
if (m_solid_sel != SolidSel::Edge || m_sel_edge < 0) return {};
std::vector<int> out = m_sel_edges_more;
out.push_back(m_sel_edge);
return out;
}
void DesignSketchTool::select_body(int body)
{
// Hidden bodies aren't highlighted (the tint overlay would otherwise draw over a
@@ -3463,6 +3473,8 @@ void DesignSketchTool::select_body(int body)
m_sel_body = body;
m_sel_face = m_sel_edge = -1;
m_sel_edge_pts.clear();
m_sel_edges_more.clear();
m_sel_edges_more_pts.clear();
m_solid_sel = SolidSel::Whole; // render_solid_highlight tints just this body
}
@@ -3683,6 +3695,42 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent
const int prev_body = m_sel_body, prev_face = m_sel_face, prev_edge = m_sel_edge;
const Vec3d prev_vtx = m_sel_vertex_pt;
// SHIFT/CTRL+CLICK ON AN EDGE BUILDS AN EDGE SET, the same modifiers that extend a sketch
// selection. Only edges of the one body already picked: a dress-up acts on one body, and a
// set spanning two could not be applied. An edge already in the set leaves it; the last one
// leaving clears the selection. No escalation to the whole body here — a modified click is
// always about the set.
const bool extend = evt.ShiftDown() || evt.ControlDown() || evt.CmdDown();
if (extend && p.kind == SolidSel::Edge && prev_kind == SolidSel::Edge && p.body == prev_body) {
auto more = std::find(m_sel_edges_more.begin(), m_sel_edges_more.end(), p.edge);
if (p.edge == prev_edge) {
if (m_sel_edges_more.empty()) {
clear_solid_selection();
} else { // the previous pick becomes the current one
m_sel_edge = m_sel_edges_more.back();
m_sel_edge_pts = std::move(m_sel_edges_more_pts.back());
m_sel_edges_more.pop_back();
m_sel_edges_more_pts.pop_back();
}
} else if (more != m_sel_edges_more.end()) {
const size_t k = size_t(more - m_sel_edges_more.begin());
m_sel_edges_more.erase(more);
m_sel_edges_more_pts.erase(m_sel_edges_more_pts.begin() + k);
} else {
m_sel_edges_more.push_back(prev_edge);
m_sel_edges_more_pts.push_back(std::move(m_sel_edge_pts));
m_sel_edge = p.edge;
m_sel_edge_pts = std::move(p.edge_pts);
m_sel_face = p.face;
}
dp_pick_trace("edge set -> %zu edge(s), current %d", selected_edges().size(), m_sel_edge);
if (on_solid_selection_changed)
on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
return true;
}
m_sel_edges_more.clear();
m_sel_edges_more_pts.clear();
m_sel_body = p.body;
m_sel_face = p.face;
m_sel_edge = p.edge;
@@ -3871,6 +3919,9 @@ void DesignSketchTool::render_solid_highlight()
render_solid_sel(m_solid_sel, m_sel_body, m_sel_face, m_sel_edge_pts, m_sel_vertex_pt,
sel_cyan, 1.0f);
if (m_solid_sel == SolidSel::Edge)
for (const std::vector<Vec3d>& pts : m_sel_edges_more_pts)
render_solid_sel(SolidSel::Edge, m_sel_body, -1, pts, Vec3d::Zero(), sel_cyan, 1.0f);
}
// Datum/reference planes (Plane feature) have no solid; draw each as a translucent indigo
+9
View File
@@ -200,6 +200,10 @@ public:
void set_pick_only_body(int b) { m_pick_only_body = b; }
void clear_solid_selection();
bool has_solid_selection() const { return m_solid_sel != SolidSel::None; }
// Every picked edge when the selection is an edge set (Shift/Ctrl+click adds and removes
// edges of the same body): the earlier picks first, the last-clicked edge at the end.
// Empty unless the selection is at edge level.
std::vector<int> selected_edges() const;
// Select a whole body by index (from the Parts list) — Whole-level highlight, no face/edge.
// body < 0 or out of range clears the selection.
void select_body(int body);
@@ -1194,6 +1198,11 @@ private:
int m_sel_face{-1};
int m_sel_edge{-1};
std::vector<Vec3d> m_sel_edge_pts;
// Edges picked BEFORE m_sel_edge in a Shift/Ctrl+click set, same body, with their world
// polylines for the highlight. m_sel_edge stays the last-clicked one, so everything that
// reads a single edge (the radius gizmo, the offer header) keeps working unchanged.
std::vector<int> m_sel_edges_more;
std::vector<std::vector<Vec3d>> m_sel_edges_more_pts;
Vec3d m_sel_vertex_pt{Vec3d::Zero()}; // world point of a picked vertex
bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // pick + notify
// What a click at (mx,my) WOULD take, resolved without touching the selection. One
+22 -8
View File
@@ -156,15 +156,15 @@ json describe_tools()
json{{"name", "profile"}, {"type", "array"}, {"default", json::array()}, {"description", "optional closed contour [[x,y],...] in plane mm; overrides width/height"}},
json{{"name", "boolean"}, {"type", "string"}, {"enum", json::array({"new", "union", "subtract", "intersect"})}, {"default", "new"}},
})}},
json{{"name", "fillet"}, {"summary", "Round a measured edge of a body (edge id from query_topology on that body)."},
json{{"name", "fillet"}, {"summary", "Round measured edges of a body (edge ids from query_topology on that body)."},
{"params", json::array({
json{{"name", "edge"}, {"type", "integer"}},
json{{"name", "edge"}, {"type", "integer|array"}, {"description", "one edge id, or an array of ids rounded together in one feature"}},
json{{"name", "radius"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}},
json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}},
})}},
json{{"name", "chamfer"}, {"summary", "Chamfer a measured edge of a body (edge id from query_topology on that body)."},
json{{"name", "chamfer"}, {"summary", "Chamfer measured edges of a body (edge ids from query_topology on that body)."},
{"params", json::array({
json{{"name", "edge"}, {"type", "integer"}},
json{{"name", "edge"}, {"type", "integer|array"}, {"description", "one edge id, or an array of ids chamfered together in one feature"}},
json{{"name", "distance"}, {"type", "number"}, {"unit", "mm"}, {"default", 1}, {"min", 0.01}},
json{{"name", "body"}, {"type", "integer"}, {"default", -1}, {"description", "target body; omit for the last body. edge id is resolved against THIS body."}},
})}},
@@ -867,16 +867,30 @@ int target_body_arg(const json& params, const CadDocument& doc)
return bi; // <0 -> kernel uses the last body
}
// `edge` is one id or an array of ids; an array becomes ONE feature, every id resolved
// against the same body (a chain of single-edge features would see the ids drift).
std::vector<int> edge_ids_arg(const json& params, const char* verb)
{
if (!params.contains("edge"))
throw std::runtime_error(std::string(verb) + " needs 'edge' (id or array of ids from query_topology)");
const json& e = params["edge"];
std::vector<int> ids;
if (e.is_array()) for (const json& v : e) ids.push_back(v.get<int>());
else ids.push_back(e.get<int>());
if (ids.empty()) throw std::runtime_error(std::string(verb) + ": 'edge' is an empty array");
return ids;
}
json action_fillet(DesignPanel* panel, const json& params)
{
if (!params.contains("edge")) throw std::runtime_error("fillet needs 'edge' (id from query_topology)");
const std::vector<int> edges = edge_ids_arg(params, "fillet");
const double radius = params.value("radius", 1.0);
if (radius <= 0) throw std::runtime_error("radius must be > 0");
CadDocument& doc = panel->mcp_doc();
if (doc.bodies.empty()) throw std::runtime_error("no body to fillet");
int bi = target_body_arg(params, doc);
doc.checkpoint();
int f = doc.add_fillet(radius, params["edge"].get<int>(), "Fillet");
int f = doc.add_fillet(radius, edges, "Fillet");
if (bi >= 0) doc.features[f].target_body = bi; // edge id resolved against THIS body's shape
bool ok = doc.recompute();
if (!ok) { const std::string why = doc.error; doc.undo(); doc.error = why; }
@@ -887,14 +901,14 @@ json action_fillet(DesignPanel* panel, const json& params)
json action_chamfer(DesignPanel* panel, const json& params)
{
if (!params.contains("edge")) throw std::runtime_error("chamfer needs 'edge' (id from query_topology)");
const std::vector<int> edges = edge_ids_arg(params, "chamfer");
const double dist = params.value("distance", 1.0);
if (dist <= 0) throw std::runtime_error("distance must be > 0");
CadDocument& doc = panel->mcp_doc();
if (doc.bodies.empty()) throw std::runtime_error("no body to chamfer");
int bi = target_body_arg(params, doc);
doc.checkpoint();
int c = doc.add_chamfer(dist, params["edge"].get<int>(), "Chamfer");
int c = doc.add_chamfer(dist, edges, "Chamfer");
if (bi >= 0) doc.features[c].target_body = bi; // edge id resolved against THIS body's shape
bool ok = doc.recompute();
if (!ok) { const std::string why = doc.error; doc.undo(); doc.error = why; }