Design tab: one rule for mouse, Enter and Esc; honest status messages

Keyboard and mouse
- Esc drops what is pending (picks, a dimension's first point, an edit-op or transform) and
  never applies it; Enter applies a ready edit-op/transform, ends a polyline/spline chain,
  ends an armed tool, and confirms a feature card exactly when its ✓ is enabled.
- Right-click only abandons the gesture in progress; with nothing pending it opens the offer
  in every tool (Trim, edit-ops, transforms, Dimension, Constrain, TransformArt, move gizmo).
  Clicking empty space no longer commits. The offer needs no timing, only a still press.
- Delete removes only an explicit selection. Undo/redo inside a sketch go through the same
  route as the buttons (whole shapes, with redo); Edit > Undo follows the shown tab.
- The canvas no longer handles Delete/Esc/Ctrl+Z itself (Backspace in a value field deleted
  the geometry it measured); F is in the panel's key map.
- Value fields: a refused value keeps the field open with the reason; click outside and Tab
  commit; an untouched field commits the exact value; any decimal separator is accepted;
  lengths are always mm; validation is the same for every editor.
- Snapping: the marker shows only where the click will actually snap; pick tolerances are
  one set of pixel budgets (Constrain picks within reach; no mm floor on labels).

Messages and consistency
- set_status(kind, text) gives every status line its own colour and glyph; kernel errors are
  translated into sentences and formatted, not concatenated; sketch refusals go to the status
  line instead of the per-frame HUD that erased them.
- Hints describe the gestures that now work; Dimension shows its second step; Constrain uses
  the sketch palette (red means conflict only); the straight slot's value is its width.
- Hole/Thread/Project keep the user's pick or refuse up front; circular pattern opens with its
  own preview; thread fields use the nominal diameter and the ISO internal depth.

Integration
- MCP loads the project's recipe before touching the document, refuses to mutate it while the
  tab is busy, never runs a request that already timed out, only replaces a socket at its
  path, caps line length and removes the socket at exit; not started in the G-code viewer.
- Hiding a feature keeps later body references on their bodies; a design keeps its modeling
  origin across printer changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
Claude committed 2026-09-30 08:34:25 +00:00
1 parent a043979f44
commit 829124982d
15 files changed
+1166 -978

No files matched your search

+32 -29
View File
@@ -135,6 +135,8 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
[this, commit](double v) {
m_sketch_tool.set_inline_busy(false);
if (commit) commit(v);
// A refused value re-opens the same field (SketchInlineEditor::refuse).
if (m_inline_editor && m_inline_editor->is_open()) m_sketch_tool.set_inline_busy(true);
request_repaint();
},
[this, cancel]() {
@@ -941,11 +943,6 @@ void DesignCanvas::set_on_solid_selection_changed(std::function<void(int, int, i
m_sketch_tool.on_solid_selection_changed = std::move(cb);
}
void DesignCanvas::set_on_place_on_face(std::function<bool()> cb)
{
m_sketch_tool.on_place_on_face = std::move(cb);
}
void DesignCanvas::select_body(int body)
{
m_sketch_tool.select_body(body);
@@ -1058,9 +1055,9 @@ void DesignCanvas::set_on_sketch_exit_refused(std::function<void()> cb)
m_sketch_tool.on_exit_refused = std::move(cb);
}
void DesignCanvas::set_on_move_exit(std::function<void()> cb)
void DesignCanvas::set_on_sketch_notice(std::function<void(const std::string&, bool)> cb)
{
m_sketch_tool.on_move_exit = std::move(cb);
m_sketch_tool.on_notice = std::move(cb);
}
void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
@@ -1074,25 +1071,26 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
// through to the polyline-chain end and the move gizmo, which were there first.
// Right-drag pans. Without remembering where the press landed, every pan ended by popping
// the offer over wherever the camera stopped — the menu appearing as the reward for moving
// the view. The offer is the release of a STATIONARY right-click, at the same 8 px budget
// the left-click pick uses.
// the view. The offer is the release of a STATIONARY right-click (kCadRightClickDriftPx).
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition();
m_ctx_press_ms = wxGetLocalTimeMillis().GetValue();
m_ctx_press = e.GetPosition();
m_ctx_travelled = false;
e.Skip(); // the canvas still needs the press to seed the orbit
});
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) {
const wxPoint d = e.GetPosition() - m_ctx_press;
if (std::max(std::abs(d.x), std::abs(d.y)) > kCadRightClickDriftPx) m_ctx_travelled = true;
}
e.Skip();
});
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press;
const long long dt = wxGetLocalTimeMillis().GetValue() - m_ctx_press_ms;
// Always read-and-clear, even when another guard already rules the offer out, or a
// terminator recorded under one condition would still be pending under the next.
const bool terminated = m_sketch_tool.take_right_consumed();
// Click, or navigation? Both budgets must hold: a press that travelled orbited, and a
// press that was HELD was aiming to orbit even if the hand never quite moved. Two
// independent budgets because the two failure modes are independent — the drift one
// alone still popped a menu at the end of a slow, careful orbit.
const bool is_click = std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx
&& dt <= kCadRightClickMs;
// Click, or navigation? A press that travelled orbited; one that did not, did not.
const bool is_click = !m_ctx_travelled && std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx;
if (m_on_context_menu && !terminated && !inline_busy() && is_click) {
// The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not
// at the release, so the raycast uses the press position. Within a 3 px budget the
@@ -1110,11 +1108,6 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
});
}
void DesignCanvas::set_on_undo_redo(std::function<void(bool)> cb)
{
m_sketch_tool.on_undo_redo = std::move(cb);
}
void DesignCanvas::set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds)
{
m_sketch_tool.set_display_sketches(std::move(ds));
@@ -1352,10 +1345,12 @@ void DesignCanvas::set_body_hidden(bool on)
reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
}
void DesignCanvas::delete_selected_sketch_entities()
bool DesignCanvas::delete_selected_sketch_entities()
{
if (m_sketch_tool.selection().empty()) return false;
m_sketch_tool.delete_selected();
request_repaint();
return true;
}
bool DesignCanvas::inline_busy() const
@@ -1394,16 +1389,16 @@ bool DesignCanvas::live_sketch_has_work() const
return m_sketch_tool.live_sketch_has_work();
}
bool DesignCanvas::undo_last_sketch_entity()
bool DesignCanvas::redo_last_sketch_entity()
{
const bool did = m_sketch_tool.undo_last_entity();
const bool did = m_sketch_tool.redo_last_entity();
if (did) request_repaint();
return did;
}
bool DesignCanvas::delete_selected_or_last_sketch_entity()
bool DesignCanvas::undo_last_sketch_entity()
{
const bool did = m_sketch_tool.delete_selected_or_last();
const bool did = m_sketch_tool.undo_last_entity();
if (did) request_repaint();
return did;
}
@@ -1450,6 +1445,7 @@ void DesignCanvas::open_inline_value(double current, std::function<void(double)>
[this, commit](double v) {
m_sketch_tool.set_inline_busy(false);
if (commit) commit(v);
if (m_inline_editor && m_inline_editor->is_open()) m_sketch_tool.set_inline_busy(true);
request_repaint();
},
[this, cancel]() {
@@ -1560,6 +1556,13 @@ bool DesignCanvas::sketch_abort_gesture()
return true;
}
bool DesignCanvas::sketch_confirm_pending()
{
if (!m_sketch_tool.confirm_pending()) return false;
request_repaint();
return true;
}
bool DesignCanvas::sketch_disarm_tool()
{
if (!m_sketch_tool.disarm_tool()) return false;
@@ -1569,7 +1572,7 @@ bool DesignCanvas::sketch_disarm_tool()
bool DesignCanvas::drawing_in_progress() const
{
return m_sketch_tool.pending_points() > 0;
return m_sketch_tool.gesture_pending();
}
bool DesignCanvas::has_any_selection() const