Design: the offer ships — right-click the geometry, get what applies to it

Row order is RATIFIED (charter 4.1, 2026-07-31) and this is the first working
implementation of it: right-click in the Design viewport and a vertical list
opens at the pointer with the eight families in their fixed order, the verbs
that apply live, and the ones that do not disabled IN PLACE carrying their
reason.

THE MAP EXISTS ONCE. DesignOffer.hpp is GENERATED from docs/ux/tool_atlas.json
by docs/ux/mockups/gen_offer_table.py — the same file the 113 mockups are drawn
from. A drawing and the product therefore cannot drift apart, which is the only
way row constancy survives contact with a codebase. Never hand-edit the header.

NOTHING IS RE-IMPLEMENTED. Each row routes to the code that already runs that
verb: "key:S+E" through m_keys_feature, "key:L" through m_keys_sketch,
"fly:material#4" through the feature flyout's own action, "btn:colour" through
the standalone button. The offer is a second door onto the same room, so the
toolbar, the shortcuts and the menu cannot drift into three behaviours. The 8
verbs with kernel support but no GUI path show disabled, which is honest and
matches section 10 of the charter.

Right-click only fires the offer when the canvas is IDLE. Right-click already
ends a polyline chain and finishes the move gizmo; taking those over would
break two working interactions to add a third.

Two things the running build corrected, both found by looking at screenshots:

- THE REASON MUST BE TRUE FOR WHAT IS IN FRONT OF THE USER. Taking the first
  refusal in a family printed "Transform needs a body — add or import one
  first" on a document that HAS a body, because the real obstacle was that
  nothing was selected. Now the reason comes from a verb that accepts the
  current selection and fails only on document state; if no verb in the family
  accepts this selection at all, it says "select something first" or says
  nothing. A menu whose whole value is telling the truth cannot ship a lie.
- Classification follows the level the pick cycle has REACHED, not the face the
  ray happened to hit, so the header cannot name a face while the whole body is
  lit. Sketching on the face you merely clicked is untouched — that path is
  sketch_plane_from_selection (snaporca-3a2).

Verified on :11 end to end: nothing selected shows Sketch live with Shift+S and
seven greyed rows each explaining itself; a selected solid shows Move directly
with Shift+Y (one applicable verb, so no submenu and no extra click) and five
families as submenus. Both forks compile and link.

Fork parity re-checked after the port: DesignPanel.cpp 30 divergent lines,
DesignCanvas.cpp 16, every other CAD file byte-identical — the invariant exactly.

snaporca-96r.
This commit is contained in:
Tommaso Bianchi
2026-07-31 12:20:07 +02:00
parent 00948cf767
commit 7114e316ea
8 changed files with 1792 additions and 200 deletions
+142
View File
@@ -0,0 +1,142 @@
// GENERATED FILE — DO NOT EDIT.
// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py
//
// The object-driven tool offer (charter 4.1): every verb has ONE row index, that index
// is the same in every selection it appears in, and verbs that do not apply are shown
// disabled in place with their reason rather than removed. Row order was ratified
// 2026-07-31; changing an index is a breaking change to every user's muscle memory.
#ifndef slic3r_GUI_DesignOffer_hpp_
#define slic3r_GUI_DesignOffer_hpp_
#include <cstdint>
namespace Slic3r { namespace GUI {
// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in
// OfferVerb::accepts indexes these.
enum class OfferSel : int {
None = 0,
FacePlanar = 1,
FaceCyl = 2,
FaceOther = 3,
EdgeStr = 4,
EdgeCirc = 5,
Vertex = 6,
BodySolid = 7,
BodySheet = 8,
Bodies2 = 9,
DatumPlane = 10,
DatumAxis = 11,
CoordSys = 12,
Art = 13,
SkLoop = 14,
SkNone = 15,
SkLine = 16,
SkArc = 17,
SkPoint = 18,
Sk2Ent = 19,
Count = 20
};
inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
// One row of the offer. `action` routes to the code that already implements the verb:
// "key:S+E" -> m_keys_feature[SHIFT('E')]
// "key:L" -> m_keys_sketch['L']
// "fly:material#4" -> row 4 of the "material" feature flyout
// "btn:delete" -> a standalone toolbar button
// nullptr -> kernel support exists, no GUI path yet (row shows disabled)
struct OfferVerb {
const char* id;
const char* name; // drawing-office word (L10); translated at use with wxGetTranslation
int row; // 0..7, the ratified index — NEVER reorder
const char* key; // shortcut shown in the row, or nullptr
const char* action;
const char* refusal; // why this row is greyed, in the product's own words
uint32_t accepts; // bitmask over OfferSel
int need_bodies;
int need_sketches;
bool need_sheet;
bool sketch_mode; // belongs to the sketch-mode vocabulary, not the model one
};
// Row labels, in ratified order.
static const char* const kOfferRowNames[] = {
"Create",
"Add material",
"Remove",
"Dress-up",
"Repeat",
"Transform",
"Reference",
"Modify",
};
static const int kOfferRowCount = 8;
static const OfferVerb kOfferVerbs[] = {
{"sketch", "Sketch", 0, "Shift+S", "key:S+S", "Click a face or a reference plane in the viewport, then a sketch tool", 0x00000403u, 0, 0, false, false},
{"extrude", "Extrude", 1, "Shift+E", "key:S+E", "Create a sketch, or pick a solid face, first", 0x00004002u, 0, 0, false, false},
{"revolve", "Revolve", 1, "Shift+R", "key:S+R", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false},
{"sweep", "Sweep", 1, "Shift+W", "key:S+W", "Create a profile sketch to sweep first", 0x00004000u, 0, 2, false, false},
{"loft", "Loft", 1, "Shift+L", "key:S+L", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false},
{"thicken", "Thicken", 1, nullptr, "fly:material#4", "Thicken needs a solid body — add or import one first", 0x0000000au, 1, 0, false, false},
{"rib", "Rib", 1, nullptr, "fly:material#5", "Rib needs a solid body — add or import one first", 0x00010000u, 1, 0, false, false},
{"boolean", "Combine", 1, "Shift+B", "key:S+B", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false},
{"surf_extrude", "Surface Extrude", 1, "Shift+G", "key:S+G", "Create a sketch first", 0x00004000u, 0, 0, false, false},
{"surf_revolve", "Surface Revolve", 1, nullptr, "fly:surface#1", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false},
{"surf_loft", "Surface Loft", 1, nullptr, "fly:surface#2", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false},
{"surf_fill", "Surface Fill", 1, nullptr, "fly:surface#3", "Create a closed sketch first", 0x00004000u, 0, 0, false, false},
{"thicken_surf", "Thicken Surface", 1, nullptr, "fly:surface#5", "target is not a sheet body", 0x00000100u, 0, 0, true, false},
{"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false},
{"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false},
{"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false},
{"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x00000480u, 1, 0, false, false},
{"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x00000080u, 1, 0, false, false},
{"fillet", "Fillet", 3, "Shift+F", "key:S+F", "Pick an edge to round", 0x000000b2u, 1, 0, false, false},
{"chamfer", "Chamfer", 3, nullptr, "key:S+F", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false},
{"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false},
{"surf_offset", "Surface Offset", 3, nullptr, "fly:surface#4", "target is not a sheet body", 0x00000100u, 0, 0, true, false},
{"pattern", "Pattern", 4, "Shift+N", "key:S+N", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false},
{"mirror", "Mirror", 4, "Shift+Z", "key:S+Z", "Mirror needs a body — add or import one first", 0x00000480u, 1, 0, false, false},
{"pat_curve", "Pattern on Curve", 4, nullptr, nullptr, "Pattern on curve needs a body and a curve", 0x00000090u, 1, 0, false, false},
{"transform", "Move", 5, "Shift+Y", "key:S+Y", "Transform needs a body — add or import one first", 0x00002180u, 1, 0, false, false},
{"mate", "Mate", 5, nullptr, "fly:placement#2", "A mate needs two coordinate systems", 0x00001202u, 2, 0, false, false},
{"align", "Align to", 5, nullptr, nullptr, "Align needs a body", 0x00000002u, 1, 0, false, false},
{"plane", "Plane", 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false},
{"axis", "Axis", 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false},
{"coordsys_v", "Coord Sys", 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false},
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false},
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false},
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false},
{"mass_props", "Mass", 6, nullptr, nullptr, nullptr, 0x00000080u, 1, 0, false, false},
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false},
{"edit_feature", "Edit", 7, nullptr, nullptr, nullptr, 0x00007d8eu, 0, 0, false, false},
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false},
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x00000180u, 1, 0, false, false},
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7f80u, 0, 0, false, false},
{"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_rect", "Rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_circle", "Circle", 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_arc_t", "Arc", 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_slot", "Slot", 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_ellipse", "Ellipse", 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_spline", "Spline", 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_polygon", "Polygon", 0, "G", "key:G", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_point_t", "Point", 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_offset", "Offset", 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true},
{"sk_trim", "Trim", 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true},
{"sk_fillet", "Fillet", 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true},
{"sk_chamfer", "Chamfer", 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true},
{"sk_mirror", "Mirror", 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true},
{"sk_move", "Move", 5, nullptr, nullptr, nullptr, 0x000f0000u, 0, 0, false, true},
{"sk_dimension", "Dimension", 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true},
{"sk_constrain", "Constrain", 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true},
{"sk_construct", "Construction", 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true},
{"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true},
{"sk_delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f0000u, 0, 0, false, true},
};
static const int kOfferVerbCount = 60;
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_DesignOffer_hpp_