Design: slot Radius caption, keyboard offer, plane combo removal, mass props, docs

Mirror of snaporca ac85277bac..0e7cb3ec78 (six changes, applied as a patch to
DesignPanel.cpp rather than copied, so this fork's 30 permitted divergent lines survive
— parity re-checked afterwards: the five shared files are byte-identical, DesignCanvas.cpp
and DesignPanel.cpp differ by exactly 16 and 30 lines).

- The straight slot's inline field says Radius, which is what it sets. It stores the
  half-width and passed the typed number through unchanged, so 30 produced a 60 mm slot.
- The offer opens from the keyboard (Menu, Shift+F10), anchored on the viewport rather
  than wherever the pointer happens to be. The card hint names the new route.
- The sketch card's Plane combo is gone; the plane comes from the viewport. Also stops
  build_candidate collapsing a face plane to a base plane while editing.
- Mass properties and the dead Edit row are wired into the offer; DesignOffer.hpp is
  regenerated from tool_atlas.json, verified by re-running the generator and diffing.
- docs/rig_build_traps.md + scripts/rig-build.sh, which derives its fork identity from
  project() so it cannot be pointed at the other fork's image or volume.
- docs/design_tab.md refreshed (44 commits stale) + a PR description, with this fork's
  own merge-base and diff shape rather than snaporca's.

Built green in the deps container with the new script and verified on the rig: Menu and
Shift+F10 both open the offer at the viewport centre with the pointer parked off-canvas,
and the sketch card shows no Plane row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyRwbuq6fjn3VV9U9UvhBM
This commit is contained in:
Tommaso Bianchi
2026-08-01 06:21:08 +02:00
co-authored by Claude Opus 5
parent 6e9910303b
commit fd1bc092d8
15 changed files with 499 additions and 43 deletions
+68 -15
View File
@@ -375,7 +375,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners"));
sk_key('C', DesignSketchTool::Mode::CenterCircle, _L("Circle — click center, then radius"));
sk_key('A', DesignSketchTool::Mode::ThreePointArc,_L("Arc — click start, end, then a point"));
sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then width"));
sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then end radius"));
sk_key('E', DesignSketchTool::Mode::Ellipse, _L("Ellipse — center, major end, minor point"));
sk_key('B', DesignSketchTool::Mode::BSpline, _L("Spline — click control points"));
sk_key('P', DesignSketchTool::Mode::Point, _L("Point — click to place"));
@@ -533,7 +533,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_status->Refresh();
if (m_sketch_hint) { // the card must agree with the status line, not argue with it
m_sketch_hint->SetLabel(have_plane
? wxString::Format(_L("Drawing on %s.\nPick a tool, or right-click for the list."), where)
? wxString::Format(_L("Drawing on %s.\nPick a tool, or press Menu for the list."), where)
: _L("Click a face or a reference plane, then a sketch tool."));
m_sketch_hint->Refresh();
m_cards->Layout();
@@ -542,7 +542,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// mode change has settled before a modal menu takes the loop; the menu carries each
// tool's shortcut, so pressing the key instead of picking a row costs nothing.
if (have_plane)
CallAfter([this] { show_offer_menu(wxGetMousePosition()); });
CallAfter([this] { show_offer_menu(offer_anchor()); });
};
b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); });
m_keys_feature[SHIFT('S')] = act_sketch;
@@ -898,6 +898,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
fadd("color", b_color);
m_verb_actions["btn:colour"] = [this] { on_set_body_color(); };
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
m_verb_actions["btn:edit"] = [this] { on_edit_feature(); };
m_verb_actions["btn:mass"] = [this] { on_mass_properties(); };
// Dress-up: finishing operations on the faces and edges of an existing solid — nothing
// that moves a body (see the Placement drawer) and nothing that creates geometry.
@@ -1132,7 +1134,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
{"design_tangentarc", DesignSketchTool::Mode::TangentArc, _L("Tangent arc"), _L("Click start (on the last entity) then end")},
{"design_arc_center", DesignSketchTool::Mode::CenterArc, _L("Center-point arc"), _L("Click center, then start, then a point for the end angle")} });
dropdown("design_slot", _L("Slot"), {
{"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for width")},
{"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for the end radius")},
{"design_slot_arc", DesignSketchTool::Mode::ArcSlot, _L("Arc slot"), _L("Click center, start, end, then a point for the width")} });
dropdown("design_ellipse", _L("Ellipse"), {
{"design_ellipse", DesignSketchTool::Mode::Ellipse, _L("Ellipse"), _L("Click center, a major-axis end, then a point for the minor axis")},
@@ -1419,13 +1421,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Shape")), 0, wxALIGN_CENTER_VERTICAL);
form->Add(m_shape, 0, wxEXPAND);
m_plane = make_combo(m_cards);
m_plane->Append(_L("XY"));
m_plane->Append(_L("XZ"));
m_plane->Append(_L("YZ"));
m_plane->SetSelection(0);
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
form->Add(m_plane, 0, wxEXPAND);
// NO plane row. A sketch takes its plane from what is picked in the VIEWPORT — a planar face
// on a solid, or one of the reference-plane ghosts clicked in 3D — resolved by
// sketch_plane_from_selection(). A three-row XY/XZ/YZ combo could not express either of those
// targets, so it displayed a value that was at best redundant and at worst false. snaporca-e1p.
m_width = make_spin(m_cards, 20);
form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Width / X")), 0, wxALIGN_CENTER_VERTICAL);
@@ -3473,6 +3472,20 @@ DesignPanel::DesignPanel(wxWindow* parent)
const bool dismissable = m_active != Tool::None || (m_viewport && m_viewport->moving_body());
if (key == WXK_ESCAPE && dismissable) { tool_cancel(); return; }
// The offer from the keyboard (charter 4.1): the Menu key, or Shift+F10 for keyboards that
// do not have one. Same menu the right-click opens — show_offer_menu already decides which
// half of the map applies via sketch_map_applies(), so nothing about the content is decided
// here. With no press to anchor it, offer_anchor() puts it on the viewport.
// WXK_MENU is the GTK code for the physical Menu key (GDK_KEY_Menu); wxMSW instead sends
// WXK_WINDOWS_MENU for VK_APPS and maps Alt to WXK_ALT, so accepting both is safe
// everywhere. CallAfter because the menu is modal: let the key event finish dispatching
// before a nested loop takes the queue, the same reason the Sketch entry does it.
if (!in_text && !ctrl
&& (key == WXK_MENU || key == WXK_WINDOWS_MENU || (key == WXK_F10 && e.ShiftDown()))) {
CallAfter([this] { show_offer_menu(offer_anchor()); });
return;
}
// Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y — undo/redo handled here (not only in the GL canvas) so
// it works even when the canvas lost keyboard focus. In a sketch, undo drops the last entity.
if (!in_text && ctrl && (key == 'Z' || key == 'z' || key == WXK_CONTROL_Z ||
@@ -4089,13 +4102,14 @@ void DesignPanel::on_add_sketch()
{
SketchShape shape = (m_shape->GetSelection() == 1) ? SketchShape::Circle
: SketchShape::Rectangle;
SketchPlane plane = plane_from_choice(m_plane->GetSelection());
wxString where; // named for the status line
SketchPlane plane = sketch_plane_from_selection(where); // picked face, else the 3D plane click
m_feature_counter++;
m_doc.add_sketch(shape, plane, m_width->GetValue(), m_height->GetValue(),
m_radius->GetValue(), "Sketch" + std::to_string(m_feature_counter));
m_doc.recompute(); // a lone sketch yields an empty body; that is expected
m_status->SetForegroundColour(wxNullColour);
m_status->SetLabel(_L("Sketch added — select it and Extrude"));
m_status->SetLabel(wxString::Format(_L("Sketch added on %s — select it and Extrude"), where));
refresh_tree();
}
@@ -4712,6 +4726,31 @@ void DesignPanel::on_check_interference()
wxMessageBox(msg, _L("Interference"), wxOK, this);
}
// Mass properties of the selected solid. A report, not a feature: it never checkpoints, never
// recomputes and never opens a card, which is why it sits beside the interference check rather
// than in the on_add_* family. The caller only reaches us with m_sel_solid_body in range.
void DesignPanel::on_mass_properties()
{
const auto mp = GeometryEngine::mass_properties(m_doc.bodies[m_sel_solid_body].shape);
if (!mp.valid) {
m_status->SetForegroundColour(wxColour(235, 110, 110));
m_status->SetLabel(_L("Mass properties could not be computed for this body"));
m_status->Refresh();
return;
}
// 1-based, and the body's own name when it has one — the same wording the parts list uses.
wxString name = wxString::Format(_L("Body %d"), m_sel_solid_body + 1);
if (!m_doc.bodies[m_sel_solid_body].name.empty())
name = wxString::FromUTF8(m_doc.bodies[m_sel_solid_body].name);
m_status->SetForegroundColour(wxNullColour);
m_status->SetLabel(wxString::Format(_L("%s: %.3f cm³, %.2f cm²"),
name, mp.volume / 1000.0, mp.surface_area / 100.0));
m_status->Refresh();
wxMessageBox(wxString::Format(_L("%s\n\nVolume: %.3f cm³\nSurface area: %.2f cm²"),
name, mp.volume / 1000.0, mp.surface_area / 100.0),
_L("Mass properties"), wxOK, this);
}
// The rows are only the SHEET bodies, so a row index is NOT a body index — with a solid at 0
// and a sheet at 1 the single row is row 0 but body 1. Every caller must therefore read the
// real body index out of the client data (3-arg Append; the 2-arg form takes a bitmap), never
@@ -5016,6 +5055,17 @@ void DesignPanel::run_offer_action(const char* action)
it->second();
}
wxPoint DesignPanel::offer_anchor() const
{
const wxPoint mouse = wxGetMousePosition();
if (!m_viewport)
return mouse;
const wxRect r = m_viewport->GetScreenRect();
if (r.Contains(mouse))
return mouse;
return wxPoint(r.x + r.width / 2, r.y + r.height / 2);
}
void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
{
const int kind = offer_selection_kind();
@@ -7167,7 +7217,6 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f)
switch (f.type) {
case CadFeatureType::Sketch:
m_shape->SetSelection(f.shape == SketchShape::Circle ? 1 : 0);
m_plane->SetSelection(index_from_plane(f.plane));
m_width->SetValue(f.width);
m_height->SetValue(f.height);
m_radius->SetValue(f.radius);
@@ -7770,7 +7819,11 @@ CadFeature DesignPanel::build_candidate(Tool t) const
case Tool::Sketch:
f.type = CadFeatureType::Sketch;
f.shape = (m_shape->GetSelection() == 0) ? SketchShape::Rectangle : SketchShape::Circle;
f.plane = plane_from_choice(m_plane->GetSelection());
// The plane is STRUCTURAL, like Extrude's profile source. While EDITING it is preserved
// from the seeded original — the card carries no plane control and the old combo silently
// collapsed a face plane to a base plane through the modeling origin. While ADDING it
// comes from what is picked in the viewport. snaporca-e1p.
if (!editing) { wxString where; f.plane = sketch_plane_from_selection(where); }
f.width = m_width->GetValue();
f.height = m_height->GetValue();
f.radius = m_radius->GetValue();