Files
OrcaSlicer/docs/CAD/ux/interaction-model.md
T
Claude c74ddf8bb5 Design tab: translatable offer, one vocabulary, reports in the status line
- 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
2026-09-30 08:34:25 +00:00

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 a wxGLCanvas is 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.
  • N looks 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.