- Offer table: user-facing strings carry the L() marker so xgettext
extracts them; DesignOffer.hpp, DesignSketchTool.cpp and
SketchInlineEditor.cpp are listed in localization/i18n/list.txt.
- Offer: model-mode Constrain sits in the same row as the sketch one;
Interference is wired; Rib shows its R key; a verb that accepts the
selection but is blocked by the document stays greyed with its reason
instead of vanishing from the submenu; one refusal wording per verb.
- Extrude infers Join when the profile touches a solid (new
CadDocument::body_touching_sketch) and on face push/pull; New body in
free space. Revolve/Sweep/Loft/Boolean use the same result words.
- Interference and volume/area reports go to the status line in mm3/mm2
instead of modal dialogs; Delete Body no longer asks (it is undoable).
- New feature names match the card header ("Extrude 3"), translated;
"Coordinate system", "Angle (°)", center/color spelling, translated
face and length readouts, slot hints say width.
- CAD gizmos in Prepare are selectable only with the CAD feature on; the
sketch auto-close setting is stored per design.
- Docs: confirm/cancel rules, enabling the feature and MCP in
design_tab.md; drift-only right-click in interaction-model.md; the
portability note rewritten to describe the integration as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
8.2 KiB
Design tab — interaction model
The contract for Esc, the right mouse button, and the states between them. Code that changes any of the three changes this file in the same commit.
1. The state machine
src/slic3r/GUI/CAD/DesignInteraction.hpp — a four-level LIFO stack. The enum value is the
depth, so "which level does this press belong to" is a comparison rather than a chain of
special cases spread over three files.
enum class CadLevel : int {
Idle = 0, // nothing transient is up: Esc clears the selection
Tool = 1, // a feature card / armed sketch tool / constrain session: Esc exits it
Gesture = 2, // an uncommitted delta (entity being drawn, body being dragged): Esc reverts it
Transient = 3, // a value field or a popup menu: Esc closes just that
};
struct CadInteractionState { // the four bits routing actually needs
bool value_field_open{false};
bool gesture_active{false};
bool tool_armed{false};
bool has_selection{false};
};
constexpr CadLevel cad_escape_level(const CadInteractionState& s)
{
if (s.value_field_open) return CadLevel::Transient;
if (s.gesture_active) return CadLevel::Gesture;
if (s.tool_armed) return CadLevel::Tool;
return CadLevel::Idle;
}
The rule is a constexpr free function over a POD, not a method on the panel, so the ordering
that is the entire contract is checkable without a window, a GL context or an event loop. Five
static_asserts in the header do exactly that, at compile time.
Strict invariant. No level of Esc deletes a feature, discards a sketch that holds geometry, or rolls history back. Destroying work needs a gesture that says so:
| To destroy | Gesture |
|---|---|
| a feature | Delete / Backspace on an explicit selection |
| a drawn sketch | the ribbon's ✗ Cancel, which asks first |
| the last committed change | Ctrl+Z |
2. Event routing
OnKeyDown(WXK_ESCAPE) — DesignPanel's wxEVT_CHAR_HOOK, one line:
if (key == WXK_ESCAPE) { escape(); return; }
Every Esc in the tab goes through it, whatever holds focus. DesignPanel::escape_level() answers
the four questions of CadInteractionState about this panel; DesignPanel::escape() acts on the
one level that answer names, and on no other:
| Level | What one press does | What it must not touch |
|---|---|---|
Transient |
close the value field (cancel_value / inline_cancel) |
the tool, which stays armed |
Gesture |
drop the clicks of the entity being drawn, or put a moved body back at the pose it had when the gizmo appeared | everything already committed |
Tool |
discard a feature card's candidate; drop an armed sketch tool to Select; end Constrain | committed features; entities already drawn |
Idle |
clear the selection (model and sketch); leave a sketch session only if it is empty | a sketch holding geometry — it is left through Finish or Cancel |
A sketch session is deliberately not a Tool. It is the environment the Idle level lives in,
which is what makes the destructive path unrepresentable rather than merely unlikely.
OnRightDown / OnRightUp — DesignCanvas::set_on_context_menu, bound after GLCanvas3D's
own handlers so it can consume the event before them:
RIGHT_DOWN: remember the press position, clear `travelled`, then Skip() // the canvas still seeds the orbit
MOTION: travelled |= drift from the press > 3 px // an orbit that came back still orbited
RIGHT_UP: terminated = sketch_tool.take_right_consumed(); // read-and-clear, always
is_click = !travelled && drift <= 3 px; // otherwise it was navigation
if (callback && !terminated && !inline_busy && is_click) {
select_at_screen(press.x, press.y); // raycast at the PRESS, not the release
on_context_menu(ClientToScreen(press));
return; // consumed
}
Skip(); // orbit / pan / the handlers underneath
Only distance decides, never time (charter §6.2: no timing-dependent gestures): a slow, deliberate
right-click is still a click. The drift is tracked over the whole press, not just at release, so a
careful orbit that ends where it started does not pop a menu. take_right_consumed() is how a right-click that
already meant something to the armed sketch tool (terminate a chain, drop an edit-op) declines to
also mean "open a menu".
3. Transition table
sel = something is picked. Blank = the input does nothing at that state.
| State | Left-click | Right-click | Esc | Enter |
|---|---|---|---|---|
| Idle — model view | pick / escalate the pick | offer menu for what is under the cursor | clear the selection | — |
| Idle — sketch, empty | pick | sketch offer menu | leave the session (nothing to lose) | Finish sketch |
| Idle — sketch, drawn | pick | sketch offer menu | clear the selection; status says the sketch is kept | Finish sketch |
| Tool — feature card | pick the card's next reference | offer menu | discard the candidate, close the card | commit the feature |
| Tool — sketch tool armed | place the first point | drop the tool to Select | drop the tool to Select | — |
| Tool — constrain | pick an entity | offer menu | end the session | apply |
| Gesture — drawing | place the next point | terminate the chain (keep what is drawn) | drop the in-progress entity, tool stays armed | commit the entity as drawn |
| Gesture — moving a body | drop the body here | end the move | revert to the pose at move-start | keep the placement |
| Transient — value field | — | — | close the field, tool stays armed | commit the value, advance the chain |
| Transient — popup menu | run the entry | — | close the menu | run the highlighted entry |
| any | — | — | never deletes, discards or rolls back | — |
Right-hold-and-drag is not in the table on purpose: once it has travelled past 3 px it is navigation, and navigation does not transition the state machine.
4. Visual scaffolding
Entering a sketch changes three things at once, so the state is legible from across the room:
- Banner. A teal strip across the top of the viewport:
Editing: Sketch N · N = look normal to the plane · Finish or Cancel in the toolbar. Indicator only — Confirm and Cancel stay on the one ribbon action bar, per the Design UX contract. It is a sibling above the canvas, not a floating child over it: a child window over awxGLCanvasis a native window on GTK and does not reliably stack over GL, and this banner's job is to be unmissable rather than clever. - The printer bed is muted. A plate grid and a sketch grid are the same visual language, and reading one as the other is how a sketch gets drawn against the wrong reference. The view checkbox remains the stored preference and is restored on the way out; ticking it mid-sketch still shows the bed, because that is a deliberate act and this is only a default.
Nlooks normal to the plane, keeping the current zoom, with the plane's own y axis as up. Sketch key map only — in Feature mode the navigator orb owns orientation.
5. Context menu content
The offer is generated from docs/CAD/ux/tool_atlas.json; its 8-row shape and permanent row
indices are ratified and are not changed here. Checked against the per-context vocabularies asked
for in the 2026-09-05 interaction brief, the atlas already carries all of them except two, both on
a planar face:
| Asked for | Status |
|---|---|
| Revolve on a planar face | not offered, and should not be: revolve accepts sk_loop only, because the kernel takes a sketch profile — a face is not one |
| Offset Face | offered as Thicken (thicken, accepts face_planar); surf_offset is the sheet-body verb and accepts body_sheet |
View and document actions — Zoom to Fit, View Isometric, Clear Selection, Finish Sketch, Normal to
Sketch — stay in chrome by the atlas's own rule: the offer describes verbs that consume a
selection, and these act on the document or the camera. Esc covers Clear Selection, N covers
Normal to Sketch, and the ribbon covers Finish.