Files
OrcaSlicer/docs/CAD/ux/interaction-model.md
T
Tommaso BianchiandClaude Opus 5 3f52166e32 A sketch should not look like plate preparation
Three cues, because one is missed. A teal banner across the top of the viewport names the session
("Editing: Sketch N") and where its exits are; the printer bed is muted for the duration, since 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; and N looks straight down the plane normal at the
current zoom, with the plane's own y axis as up, because no hand-orbit lands exactly square and a
sketch read at an angle is one whose right angles do not look like right angles.

The banner is an INDICATOR. Finish and Cancel stay on the single ribbon action bar — the tab had
three competing confirm surfaces once and that is not being reopened for a strip of colour. It
sits above the canvas rather than floating inside it: a child window over a wxGLCanvas is a native
window on GTK with no reliable stacking over GL, and being unmissable beats being clever.

The bed checkbox stays the stored preference and is restored on leaving the sketch; ticking it
mid-sketch still shows the bed, because that is a deliberate act and this is only a default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
2026-09-05 11:45:29 +02:00

7.9 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 / OnRightUpDesignCanvas::set_on_context_menu, bound after GLCanvas3D's own handlers so it can consume the event before them:

RIGHT_DOWN: remember the press position and the clock, then Skip()   // the canvas still seeds the orbit

RIGHT_UP:   terminated = sketch_tool.take_right_consumed();          // read-and-clear, always
            is_click   = drift <= 3 px && dt <= 200 ms;              // both budgets, or 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

Two independent budgets because the two failure modes are independent: drift alone still popped a menu at the end of a slow, careful orbit. 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: past 3 px or 200 ms 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.