Pattern/Cut/Boolean: grey the button when there is no body, and say why

Tommaso reported the array controls as missing. They were not — Shift+N opens a
Pattern card with every control correct — but the report was fair. With no body
the button accepts the click, opens nothing, and writes its refusal somewhere
other than where the click happened. From the user's seat that is
indistinguishable from a dead button, and the icon is one unlabelled glyph among
fourteen, which is how I mis-clicked it into Section view while reproducing this.

A control that cannot act should look like it cannot act, before it is pressed.
The three FEATURE buttons carrying a body-count guard — Pattern and Cut at one
body, Boolean at two — are now greyed below their threshold with a tooltip
naming what is missing.

Only those three. The same guard shape also appears on rows INSIDE the flyouts,
and those stay live: a drawer holds sketch-only entries too, so disabling the
drawer would hide tools that are perfectly usable. The keyboard shortcuts keep
running the guarded action rather than being gated — a key press has no
greyed-out state to see, so the sentence is the only feedback there is.

Re-evaluated in feed_bodies(), before its viewport early-return since this is
about the toolbar and not the canvas, and once after the toolbar is built: an
empty document is the state the bug was reported in and feed_bodies has not run
yet on a fresh tab.

snaporca-o9j. Reviewed and compiled (RC=0), not exercised.
This commit is contained in:
Tommaso Bianchi
2026-08-13 08:49:18 +02:00
parent 13922a52b6
commit 498c7ff8d1
2 changed files with 42 additions and 0 deletions
+37
View File
@@ -751,6 +751,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
b_pattern->Bind(wxEVT_BUTTON, [act_pattern](wxCommandEvent&) { act_pattern(); });
m_keys_feature[SHIFT('N')] = act_pattern;
fadd("pattern", b_pattern);
m_body_gates.push_back({b_pattern, 1, b_pattern->GetToolTipText(),
_L("Pattern — needs a solid body to replicate")});
// Surface: sheet-body tools (extrude / revolve / loft / fill / offset / thicken)
// The drawer BUTTON is design_surface, not design_extrude: sharing a face with the
@@ -949,6 +951,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
b_boolean->Bind(wxEVT_BUTTON, [act_boolean](wxCommandEvent&) { act_boolean(); });
m_keys_feature[SHIFT('B')] = act_boolean;
fadd("boolean", b_boolean);
m_body_gates.push_back({b_boolean, 2, b_boolean->GetToolTipText(),
_L("Boolean — needs two solid bodies to combine")});
auto* b_cut = icon_btn("design_cut", _L("Cut (split a body with a plane)"));
std::function<void()> act_cut = [this] {
@@ -966,6 +970,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
b_cut->Bind(wxEVT_BUTTON, [act_cut](wxCommandEvent&) { act_cut(); });
m_keys_feature[SHIFT('X')] = act_cut;
fadd("cut", b_cut);
m_body_gates.push_back({b_cut, 1, b_cut->GetToolTipText(),
_L("Cut — needs a solid body to slice")});
// Color — override the selected body's display colour (per-body, survives recompute).
auto* b_color = icon_btn("color_palette", _L("Color — set the selected body's display colour"));
@@ -1559,6 +1565,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
tbrow->Add(m_tb_action, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 5);
tbrow->AddSpacer(8);
m_toolbar->SetSizer(tbrow);
// Apply the body gates once now: an empty document is exactly the state the bug was reported
// in, and feed_bodies() has not run yet on a fresh tab.
update_body_gates();
// Onshape-style dialog-card header: feature icon + bold title. out receives
// the title control so open_tool() can retitle it per feature.
@@ -6278,6 +6287,9 @@ void DesignPanel::rebuild_disp_meshes()
void DesignPanel::feed_bodies()
{
// The body count just changed, so the tools that consume a body may have become reachable or
// unreachable. Before the early return below: the gating is about the toolbar, not the canvas.
update_body_gates();
// Rebuild the transformed meshes first so every display-refresh path (recompute, tint,
// visibility, live move) shows the bodies at their current Move offsets. The solid-pick
// keeps a STABLE pointer to m_disp_pick_mesh / m_body_visible / m_body_xform (rebuilt in
@@ -8930,6 +8942,31 @@ void DesignPanel::update_fillet_gizmo()
}
// Push the active Hole card's plane + position + diameter/depth to the viewport gizmo.
// Grey the FEATURE buttons whose tool cannot run yet, and say why in the tooltip (snaporca-o9j).
// Tommaso reported the array controls as MISSING; they were not, but Pattern with no body
// accepted the click, opened nothing, and wrote its refusal somewhere other than where the click
// happened — from the user's seat that is indistinguishable from a dead button. A control that
// cannot act should look like it cannot act, before it is pressed.
//
// Only the three top-level buttons that carry a body-count guard are gated. The same guard also
// appears on rows INSIDE the flyouts, and those stay live: a drawer holds sketch-only entries too,
// so disabling the drawer would hide tools that are perfectly usable. Their refusal message is
// still written, and now next to the geometry.
//
// The keyboard shortcuts (Shift+N / Shift+X / Shift+B) deliberately keep running the guarded
// action rather than being gated: a key press has no greyed-out state to see, so the sentence is
// the only feedback there is.
void DesignPanel::update_body_gates()
{
const int n = int(m_doc.bodies.size());
for (const BodyGate& g : m_body_gates) {
if (g.btn == nullptr) continue;
const bool live = n >= g.min_bodies;
g.btn->Enable(live);
g.btn->SetToolTip(live ? g.tip_live : g.tip_gated);
}
}
// Self-gates: clears the gizmo unless the Hole card is open.
void DesignPanel::update_hole_gizmo()
{
+5
View File
@@ -272,6 +272,11 @@ private:
void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card)
void sync_dressup_target(); // Dressup card: show picked edge vs group, gate the combo
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.
struct BodyGate { wxWindow* btn{nullptr}; int min_bodies{1}; wxString tip_live, tip_gated; };
std::vector<BodyGate> m_body_gates;
void update_body_gates(); // re-evaluate them against the current body count
void update_thread_gizmo(); // footprint circle + radius/length arrows (Thread card)
void update_shell_gizmo(); // inward thickness arrow on the picked face (Shell card)
void update_revolve_gizmo(); // angle-arc around the axis (Revolve card)