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
2026-09-30 08:34:25 +00:00
parent a043979f44
commit 829124982d
15 changed files with 1166 additions and 978 deletions
+3 -2
View File
@@ -47,8 +47,9 @@ tool"*. It is also where a refusal explains itself.
## The offer
Right-click on the geometry, released without moving the mouse (an 8 px budget — a
right-drag that orbits the camera does not open it). Left-click still only selects, so
Right-click on the geometry, released without moving the mouse (a 3 px budget — a
right-drag that orbits the camera does not open it). How long the button is held does not
matter. Left-click still only selects, so
pointing at things stays quiet.
The offer also opens by itself the moment you press Sketch on a face or plane, showing the
+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
+11 -9
View File
@@ -122,7 +122,6 @@ public:
const std::vector<bool>* visible = nullptr,
const std::vector<Transform3d>* xform = nullptr);
void set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb);
void set_on_place_on_face(std::function<bool()> cb); // F key: Place on Face
void select_body(int body); // Parts-list -> highlight a whole body by index
// Effective display colour of a body: the per-body override (Color tool) when set,
// otherwise the auto body-index palette. Single source of truth shared with reload().
@@ -213,7 +212,7 @@ public:
void set_on_datum_base_picked(std::function<void(int)> cb);
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
void set_on_sketch_exit_refused(std::function<void()> cb); // Esc declined: sketch has work
void set_on_undo_redo(std::function<void(bool /*redo*/)> cb); // Ctrl+Z / Ctrl+Shift+Z
void set_on_sketch_notice(std::function<void(const std::string&, bool)> cb); // tool refusals/side effects
// Persistently draw committed sketches (un-consumed ones stay visible).
void set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds);
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
@@ -235,13 +234,12 @@ public:
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
void set_xray_focus(int body); // >=0: fade+lock out every other body (CoordSys picking)
void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost
void set_on_move_exit(std::function<void()> cb); // right-click finished the move-body gizmo
// Right-click (or its platform equivalent) on the viewport with no tool running: open the
// object-driven offer there. Fires with SCREEN coordinates. Deliberately NOT fired while a
// tool is live — right-click already ends a polyline chain and finishes the move gizmo, and
// taking those over would break two working interactions in order to add a third.
void set_on_context_menu(std::function<void(const wxPoint&)> cb);
void delete_selected_sketch_entities();
bool delete_selected_sketch_entities(); // false when nothing was selected
bool inline_busy() const; // a sketch value field is open (guard keys)
bool inline_has_focus() const; // the field itself holds keyboard focus
void inline_commit(); // accept the typed value (Enter/Tab)
@@ -251,8 +249,10 @@ public:
// panel can do it when focus is not on the canvas.
void request_sketch_exit();
bool live_sketch_has_work() const; // the live sketch holds entities a cancel would destroy
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last entity
bool delete_selected_or_last_sketch_entity(); // Delete in a sketch: selected, else last
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last drawn shape
bool redo_last_sketch_entity(); // Ctrl+Y in a sketch: bring it back
bool can_undo_sketch_entity() const { return m_sketch_tool.can_undo_entity(); }
bool can_redo_sketch_entity() const { return m_sketch_tool.can_redo_entity(); }
void clear_sketch_selection();
// View toggles (keys P / A): origin planes, world axis triad. Each returns the new on/off
@@ -299,9 +299,10 @@ public:
// Esc routing (DesignInteraction.hpp). The panel decides WHICH level one press belongs to;
// these are the levels it can act on inside the canvas. Each returns whether it did anything,
// so the panel can fall through to the next level without asking twice.
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn, or the tool's picks
bool sketch_disarm_tool(); // CadLevel::Tool — armed sketch tool falls back to Select
bool drawing_in_progress() const;// an entity has clicks down but is not committed
bool sketch_confirm_pending(); // Enter — apply a ready edit-op or transform
bool drawing_in_progress() const;// clicks or picks are down but nothing is committed yet
bool has_any_selection() const; // model pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop both; true if anything was dropped
bool sketch_first_selected_type(SketchEntity::Type& out) const;
@@ -367,7 +368,8 @@ private:
std::function<void(const wxPoint&)> m_on_context_menu;
bool m_ctx_bound{false}; // bind the RIGHT_UP handler once, however often the cb is set
wxPoint m_ctx_press{0, 0}; // right-press origin: a right-DRAG orbits, it must not offer
long long m_ctx_press_ms{0}; // and a right-HOLD is navigation too, however still it is held
bool m_ctx_travelled{false}; // the right press wandered past the budget at ANY point,
// so an orbit that ends where it began is still an orbit
Bed3D m_bed;
// The half of the camera swap above that is NOT on screen: the editor tabs' view while
+4 -4
View File
@@ -48,10 +48,10 @@ static_assert(cad_escape_level({false, false, true, true}) == CadLevel::Tool,
static_assert(cad_escape_level({false, false, false, true}) == CadLevel::Idle, "selection is idle-level");
static_assert(cad_escape_level({false, false, false, false}) == CadLevel::Idle, "empty is idle");
// Right-click vs. right-hold-orbit. A press that stays put and is let go promptly is a click and
// summons the offer; anything longer or further was navigation, and navigation must never be
// rewarded with a menu over wherever the camera happened to stop.
inline constexpr int kCadRightClickMs = 200; // press->release budget
// Right-click vs. right-drag-orbit. A press that stays put summons the offer; one that travelled
// was navigation, and navigation must never be rewarded with a menu over wherever the camera
// happened to stop. Distance only, never duration: charter 6.2 rules out any gesture that
// depends on timing, and a press that did not move did not move the camera either.
inline constexpr int kCadRightClickDriftPx = 3; // cursor drift budget, max(|dx|,|dy|)
}} // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+22 -3
View File
@@ -87,6 +87,13 @@ public:
// Defined out of line in DesignPanel.cpp: it needs kOfferVerbs, which this header deliberately
// does not include (the table is generated and belongs to the offer-menu code).
bool mcp_run_verb(const char* verb_id);
// Would a request from the control socket collide with what the GUI is doing — a rebuild
// in progress, an open feature card, a sketch session? `sketch_method` = it drives the live
// sketch rather than the feature list. Fills `why` when it would.
bool mcp_busy(bool sketch_method, std::string& why) const;
// Load the project's recipe into an empty document, as showing the tab does. The control
// socket may be the first thing to touch the Design tab after a project was opened.
void hydrate_from_model();
private:
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix, Mate };
@@ -115,6 +122,7 @@ private:
// used to be handled in four places that could not see each other, and that is how two
// presses in a row reached past a tool and discarded the sketch under it.
CadLevel escape_level() const;
bool confirm_enabled() const; // would the ✓ act right now (and is it not greyed)?
void escape();
void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active
@@ -125,7 +133,7 @@ private:
void on_add_hole();
void on_add_thread();
void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection
void infer_thread_spec(double diameter); // nearest M-standard from a picked cylinder diameter
void infer_thread_spec(double diameter, bool internal); // nearest standard from a picked cylinder
void on_add_revolve();
void on_add_sweep();
void on_add_loft();
@@ -275,6 +283,12 @@ private:
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport. With a tool/dialog open it
// cancels that (Esc-like); otherwise it undoes/redoes the committed feature history.
void do_undo_redo(bool redo);
public:
// Edit > Undo / Redo while this tab is shown: the same route and the same gate as the keys
// and the toolbar buttons.
void menu_undo_redo(bool redo) { do_undo_redo(redo); }
bool menu_can_undo_redo(bool redo) const;
private:
// The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown.
SketchPlane hole_plane() const;
// The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown.
@@ -718,8 +732,13 @@ private:
std::map<std::string, std::function<void()>> m_verb_actions;
// Append an offer row with its toolbar glyph. The bitmap must be set BEFORE Append —
// wxGTK builds the GtkMenuItem there and only makes an image item if one is present.
// Every status write goes through here so long hints wrap instead of clipping.
void set_status(const wxString& text);
// Every status write goes through here so long hints wrap instead of clipping. The kind
// sets the colour AND a leading glyph, so a message's meaning never rests on colour alone
// (charter 6.2), and every message sets its own kind instead of inheriting the last one's.
enum class StatusKind { Info, Ok, Warning, Error };
void set_status(StatusKind kind, const wxString& text);
void set_status(const wxString& text) { set_status(StatusKind::Info, text); }
static wxString kernel_error_text(const std::string& err); // a kernel message, in words a modeller reads
wxString idle_hint() const; // what to say when nothing is selected
// Reason detect_mate_conflicts() recorded for a feature, or nullptr. Marks the tree row and
// feeds the status line; a conflict is a diagnostic, not a document error.
File diff suppressed because it is too large Load Diff
+46 -29
View File
@@ -67,6 +67,17 @@ public:
// row off arms a NEIGHBOURING tool and then grades whatever that drew. ekt9.
Mode mode() const { return m_mode; }
int pending_points() const { return int(m_points.size()); }
// Picks the armed tool is holding that are not yet an entity or a constraint: the Dimension
// tool's first anchor, an edit-op's or a transform's subjects, Constrain's picks. Together
// with pending_points() they are CadLevel::Gesture, so Esc drops them and keeps the tool.
bool has_pending_picks() const {
return m_dim_has0 || m_op_a >= 0 || !m_tf_targets.empty() || m_pick0 >= 0 || m_sel_a >= 0;
}
bool gesture_pending() const { return !m_points.empty() || has_pending_picks(); }
// Enter while an edit-op or a transform is ready: apply it, the same as its value field's
// Enter. False when nothing is pending, so the key can fall through to the tab's ✓.
bool confirm_pending();
bool end_chain(); // Polyline / Spline: finish the open chain as drawn
void emit_step_hint(); // fires on_step_changed when the step actually moved
// Is an in-canvas value field open? While one is, the canvas is frozen and every letter is
// swallowed — the single most common reason a driven gesture "does nothing".
@@ -203,9 +214,6 @@ public:
void clear_move_gizmo();
bool moving_body() const { return m_mv_active; }
int move_body_index() const { return m_mv_body; }
// F key forwarded from the canvas (Prepare's Place on Face): returns true if it acted.
bool request_place_on_face() { return on_place_on_face ? on_place_on_face() : false; }
std::function<bool()> on_place_on_face;
std::function<void(int body, const Transform3d& xform)> on_body_move_changed;
// Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id).
std::function<void(int level, int body, int face, int edge)> on_solid_selection_changed;
@@ -550,25 +558,15 @@ public:
reset_autoedit();
if (on_readout) on_readout(std::string()); // the HUD is not redrawn once the tool stops
}
// Ctrl+Z while sketching: drop the last drawn entity (reuses delete_selected's remap).
bool undo_last_entity() {
if (!m_active || m_entities.empty()) return false;
m_selection.assign(1, int(m_entities.size()) - 1);
delete_selected();
reset_autoedit();
return true;
}
// Delete while sketching: the selected entities, or the last drawn one if none is selected.
bool delete_selected_or_last() {
if (!m_active) return false;
if (m_selection.empty()) {
if (m_entities.empty()) return false;
m_selection.assign(1, int(m_entities.size()) - 1);
}
delete_selected();
reset_autoedit();
return true;
}
// Ctrl+Z while sketching: drop the last thing DRAWN — a whole rectangle, slot or polygon
// when the last entity belongs to one, since that was one gesture. Undoing one side of a
// rectangle left three lines and dissolved the shape.
bool undo_last_entity();
// Ctrl+Y / Ctrl+Shift+Z while sketching: bring back what undo_last_entity removed, as long
// as nothing was drawn or deleted since (then the old state is no longer "the next step").
bool redo_last_entity();
bool can_undo_entity() const { return m_active && !m_entities.empty(); }
bool can_redo_entity() const;
std::function<void(int count)> on_selection_changed;
// Dimension tool: infer a driving dimension from the current selection and set
@@ -612,6 +610,14 @@ public:
// characteristic dimensions). Empty string -> hide the HUD. The owner (DesignCanvas)
// shows it as a floating corner label over the GL canvas.
std::function<void(const std::string&)> on_readout;
// Why something did not happen, or what a gesture did as a side effect. NOT on_readout:
// that one is rewritten every frame from build_readout(), so a message sent through it was
// gone before anyone could read it. The host shows this on its persistent status line.
std::function<void(const std::string& msg, bool error)> on_notice;
void notify(const std::string& msg, bool error = true) { if (on_notice) on_notice(msg, error); }
// A typed value the tool cannot take: the open value field comes back with the reason, or,
// when no field is committing, the reason goes to the status line.
void show_refusal(const std::string& why);
// Driving dimension constraints accumulated during the session (the Dimension
// tool records a SketchEntityConstraintDef per applied dimension); committed
@@ -633,17 +639,11 @@ public:
// request_exit() declined to leave because the session holds geometry. The panel owns the
// status line, so the tool reports through this instead of writing text itself.
std::function<void()> on_exit_refused;
std::function<void()> on_move_exit; // right-click finished the move-body gizmo
// The two inner Esc levels, callable on their own so the panel can route one press to one
// level (see DesignInteraction.hpp). Each returns whether it had anything to unwind.
bool abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
bool disarm_tool(); // CadLevel::Tool — armed draw/edit tool falls back to Select
void request_exit();
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) while the Design canvas is focused: undo/redo the
// committed feature history. The tool just forwards to the host, which owns the
// CadDocument (the tool has no document of its own). redo == true requests redo.
std::function<void(bool /*redo*/)> on_undo_redo;
void request_undo_redo(bool redo);
private:
bool screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const;
@@ -660,10 +660,17 @@ private:
// chains join across entities (a line + an arc can close into one loop). Shift
// disables it. `snapped` reports whether a vertex was hit.
Vec2d snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw, bool& snapped) const;
bool click_snaps() const; // does the NEXT click of the armed draw tool land on a snap target?
// --- P1 inference / auto-constraint engine ---------------------------------
// Plane-units tolerance equivalent to ~`px` screen pixels at the cursor.
double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = 8.0) const;
// The sketcher's pick budgets, in screen pixels, converted to plane units by screen_tol().
// One set for every tool, so the same thing is equally easy to hit whichever tool is armed.
static constexpr double kPickPx = 8.0; // grab a point, a handle or an edge; hover uses the same
static constexpr double kToolPickPx = 24.0; // pick an entity for a tool to act on (trim, edit-ops,
// transforms, constrain)
static constexpr double kLabelPx = 24.0; // click or double-click a value label
double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = kPickPx) const;
// Run kernel inference at the cursor, cache the target for the hint renderer.
InferenceSnap infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw) const;
// True if m_constraints already holds an equivalent Coincident between the two refs.
@@ -1050,6 +1057,16 @@ private:
Vec2d m_live_slot_angle_label{0,0}; // straight-slot centreline angle label
int m_live_slot_fi{-1}; // the straight-slot Feature (rebuild edits)
std::vector<Feature> m_features; // parametric groups over m_entities
// Sketch-local redo: the state each undo_last_entity() replaced, and the entity/constraint
// counts it left behind (a mismatch means the sketch moved on and the redo is stale).
struct SketchSnap {
std::vector<SketchEntity> entities; std::vector<SketchEntityConstraintDef> constraints;
std::vector<Feature> features; std::vector<DimAnnot> dimensions;
size_t after_entities{0}; size_t after_constraints{0};
};
std::vector<SketchSnap> m_sketch_redo;
int m_skipped_last{0}; // constraints the last solve could not apply (reported on change)
double m_chain_dup_tol{1e-9}; // plane units ~3 px at the last chain click (double-click repeat)
int m_open_feature{-1}; // index of the Feature being built, or -1
// In-canvas edit-op gizmo state (Fillet/Chamfer/Offset/Mirror). GUI-only, reset by
+64 -5
View File
@@ -17,6 +17,8 @@
#include <cmath>
#include <algorithm>
#include <utility>
#include <atomic>
#include <set>
#include <nlohmann/json.hpp>
#include <boost/log/trivial.hpp>
@@ -1995,6 +1997,22 @@ std::string handle_on_main(const std::string& method, const json& params, const
DesignPanel* panel = DesignPanel::ensure();
if (!panel)
return rpc_error(id, -32001, "Design panel not ready");
// A project opened without ever showing the Design tab has its recipe only in the Model;
// load it, as showing the tab would, before anything reads or writes the document.
panel->hydrate_from_model();
// Methods that only LOOK. Everything else changes the document or the live sketch, and
// must not do it under a GUI editor, a sketch session it does not own, or a rebuild in
// progress (the GUI's worker thread holds the document then).
static const std::set<std::string> kReadOnly = {
"describe_tools", "describe_scene", "query_topology", "measure", "mass_properties",
"slice_body", "validate_against", "list_verbs", "sketch_describe", "sketch_validate",
"check_interference" };
if (kReadOnly.count(method) == 0) {
std::string why;
if (panel->mcp_busy(method.rfind("sketch_", 0) == 0, why))
return rpc_error(id, -32002, "Design tab busy: " + why);
}
// Stale-id guard, checked here rather than in each handler.
//
@@ -2109,11 +2127,16 @@ std::string dispatch_request(const std::string& line)
auto prom = std::make_shared<std::promise<std::string>>();
auto fut = prom->get_future();
// 0 = queued, 1 = running, 2 = abandoned. A request that timed out while still QUEUED must
// never run: the client has been told it failed, and a retry would otherwise apply it twice.
auto state = std::make_shared<std::atomic<int>>(0);
// Nothing may escape this lambda. It is invoked by the wx event loop, which has no
// handler of its own, so an escaping exception is std::terminate — the socket would
// become a way for any client to kill the application. handle_on_main() catches what
// it knows about; this catches what it does not, and still answers the caller.
wxGetApp().CallAfter([prom, method, params, id]() {
wxGetApp().CallAfter([prom, state, method, params, id]() {
int queued = 0;
if (!state->compare_exchange_strong(queued, 1)) return; // abandoned by a timeout
try {
prom->set_value(handle_on_main(method, params, id));
} catch (const std::exception& ex) {
@@ -2122,20 +2145,33 @@ std::string dispatch_request(const std::string& line)
prom->set_value(rpc_error(id, -32000, "internal error: unknown exception"));
}
});
if (fut.wait_for(std::chrono::seconds(15)) != std::future_status::ready)
return rpc_error(id, -32000, "main-thread timeout");
if (fut.wait_for(std::chrono::seconds(15)) != std::future_status::ready) {
int queued = 0;
if (state->compare_exchange_strong(queued, 2))
return rpc_error(id, -32000, "main-thread timeout: the command was NOT run");
// Already running: it will finish, so wait for its real answer rather than report a
// failure for a command that is in fact being applied.
fut.wait();
}
return fut.get();
}
// Read newline-delimited requests off one client connection until EOF.
// Read newline-delimited requests off one client connection until EOF. A line may not grow
// past kMaxLine: a client that never sends a newline would otherwise grow this buffer until the
// process runs out of memory.
void serve_client(int cfd)
{
constexpr size_t kMaxLine = 16u << 20; // 16 MB — far above any real request
std::string buf;
char chunk[4096];
for (;;) {
ssize_t n = ::read(cfd, chunk, sizeof(chunk));
if (n <= 0) break;
buf.append(chunk, size_t(n));
if (buf.size() > kMaxLine && buf.find('\n') == std::string::npos) {
BOOST_LOG_TRIVIAL(error) << "MCP: request line over " << kMaxLine << " bytes; closing the connection";
return;
}
size_t nl;
while ((nl = buf.find('\n')) != std::string::npos) {
std::string line = buf.substr(0, nl);
@@ -2155,9 +2191,30 @@ void serve_client(int cfd)
}
}
// Where the socket lives, for the exit handler.
std::string g_sock_path;
void remove_socket_at_exit()
{
struct stat st{};
if (!g_sock_path.empty() && ::lstat(g_sock_path.c_str(), &st) == 0 && S_ISSOCK(st.st_mode))
::unlink(g_sock_path.c_str());
}
void server_thread(std::string sock_path)
{
::unlink(sock_path.c_str());
// Clear a stale socket from an earlier run — and ONLY a socket. ORCA_CAD_MCP names a path,
// and unlinking it unconditionally deleted whatever file that path happened to be.
{
struct stat st{};
if (::lstat(sock_path.c_str(), &st) == 0) {
if (!S_ISSOCK(st.st_mode)) {
BOOST_LOG_TRIVIAL(error) << "MCP: " << sock_path << " exists and is not a socket; refusing to replace it";
return;
}
::unlink(sock_path.c_str());
}
}
int sfd = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd < 0) { BOOST_LOG_TRIVIAL(error) << "MCP: socket() failed"; return; }
@@ -2182,6 +2239,8 @@ void server_thread(std::string sock_path)
::close(sfd); ::unlink(sock_path.c_str()); return;
}
if (::listen(sfd, 1) < 0) { BOOST_LOG_TRIVIAL(error) << "MCP: listen() failed"; ::close(sfd); return; }
g_sock_path = sock_path;
std::atexit(remove_socket_at_exit); // do not leave the socket file behind
BOOST_LOG_TRIVIAL(info) << "MCP control listening on " << sock_path;
for (;;) {
+11 -9
View File
@@ -1,22 +1,24 @@
#ifndef slic3r_GUI_McpControl_hpp_
#define slic3r_GUI_McpControl_hpp_
// MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a
// Unix domain socket, that lets an external MCP bridge drive and perceive the Design
// tab. Off unless the env var ORCA_CAD_MCP is set:
// MCP control surface: a local JSON-RPC 2.0 server, line-delimited over a Unix domain
// socket, that lets an external MCP bridge drive and perceive the Design tab. Off unless the
// CAD feature is enabled in Preferences AND the env var ORCA_CAD_MCP is set:
// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock
// ORCA_CAD_MCP=/path/to.sock -> socket at that path
// All CAD work is marshalled onto the wx main thread and runs through the SAME
// CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods:
// describe_tools, describe_scene, extrude.
// ORCA_CAD_MCP=/path/to.sock -> socket at that path (an existing non-socket file there is
// left alone and the server does not start)
// The socket is created 0600 and removed at exit. All CAD work is marshalled onto the wx main
// thread and runs through the SAME CadDocument kernel the GUI uses (no parallel engine);
// describe_tools lists the methods. A method that changes the document is refused (-32002)
// while the Design tab is busy with it — a rebuild, an open feature card, a sketch session.
//
// ponytail: Unix-socket only (POSIX). Windows compiles this to a no-op; add a named
// pipe transport when a Windows agent actually needs it.
namespace Slic3r { namespace GUI {
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the
// MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows.
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once the MainFrame exists —
// the Design panel is built on the first request. No-op when the env var is unset or on Windows.
void start_mcp_control_if_enabled();
}} // namespace Slic3r::GUI
+48 -6
View File
@@ -22,7 +22,8 @@ namespace {
// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel,
// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error.
// Parsing accepts either separator because a keyboard's numeric pad may only offer one.
std::string fmt_value(double v, int digits = 2)
// Trailing zeros are dropped, so a count opens as "3" and a length as "37.457", not "37.46".
std::string fmt_value(double v, int digits = 3)
{
char fmt[16];
std::snprintf(fmt, sizeof(fmt), "%%.%df", digits);
@@ -30,7 +31,13 @@ std::string fmt_value(double v, int digits = 2)
std::snprintf(buf, sizeof(buf), fmt, v);
for (char* c = buf; *c; ++c)
if (*c == ',') *c = '.';
return std::string(buf);
std::string s(buf);
if (s.find('.') != std::string::npos) {
while (!s.empty() && s.back() == '0') s.pop_back();
if (!s.empty() && s.back() == '.') s.pop_back();
}
if (s == "-0") s = "0";
return s;
}
bool parse_value(const char* text, double& out)
@@ -80,6 +87,9 @@ void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std:
m_cancel = std::move(on_cancel);
const std::string v = fmt_value(value);
std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str());
m_prefill_value = value;
m_prefill_text = v;
m_active = false;
m_open = true;
// ImGui takes keyboard focus for one frame on request; asking on the frame the field first
// appears is what makes typing land without a click. There is no window manager to consult.
@@ -91,6 +101,7 @@ void SketchInlineEditor::close()
{
m_open = false;
m_focus_pending = false;
m_active = false;
m_commit = nullptr;
m_cancel = nullptr;
m_err.clear();
@@ -114,10 +125,32 @@ void SketchInlineEditor::do_cancel()
if (cb) cb();
}
bool SketchInlineEditor::refuse(const std::string& why)
{
// Only meaningful while a commit callback runs and before it opened another field.
if (!m_in_commit || m_open) return false;
m_anchor = m_last.anchor;
m_title = m_last.title;
m_commit = m_last.commit;
m_cancel = m_last.cancel;
m_prefill_value = m_last.prefill_value;
m_prefill_text = m_last.prefill_text;
std::snprintf(m_buf, sizeof(m_buf), "%s", m_last.buf.c_str());
m_err = why;
m_open = true;
m_focus_pending = true;
ux_trace("refused", m_title, "reason=" + why);
return true;
}
void SketchInlineEditor::do_commit()
{
double v = 0.0;
if (!parse_value(m_buf, v)) {
// An untouched field commits the value it opened with, not its rounded display: Enter on a
// 37.4567 line must not turn it into a 37.457 driving dimension.
if (m_prefill_text == m_buf)
v = m_prefill_value;
else if (!parse_value(m_buf, v)) {
// Refusing input in silence is indistinguishable from the app having frozen: the field
// just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing.
@@ -128,11 +161,15 @@ void SketchInlineEditor::do_commit()
}
ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4));
auto cb = m_commit;
m_last = Closed{ m_anchor, m_title, std::string(m_buf), m_commit, m_cancel, m_prefill_value, m_prefill_text };
close();
// AFTER close(): the callback may open the next queued dimension (a rectangle queues Width
// then Height), and doing that into a field that still believes it is open would drop the
// second one's prefill on the floor.
m_in_commit = true;
if (cb) cb(v);
m_in_commit = false;
m_last = Closed{};
}
bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
@@ -167,10 +204,15 @@ bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
// EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is
// replaced by the first digit typed, which is what "pre-selected" meant when this was a
// wxTextCtrl and is what makes typing a value a single gesture.
// No CharsDecimal: it lets only the LOCALE's decimal separator through, so in a C locale a
// comma could not be typed at all, and it admits '*' and '/' that the parser then refuses.
// parse_value() is the one judge of what is a number.
const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf),
ImGuiInputTextFlags_EnterReturnsTrue
| ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_CharsDecimal);
| ImGuiInputTextFlags_AutoSelectAll);
m_active = ImGui::IsItemActive();
// Tab accepts the value like Enter does (and moves on to the next queued field, if any).
const bool tabbed = m_active && ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Tab));
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
@@ -194,7 +236,7 @@ bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
// must not happen inside this frame's window.
if (entered)
if (entered || tabbed)
do_commit();
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
do_cancel();
+18 -4
View File
@@ -48,6 +48,10 @@ public:
void close(); // drop it with neither callback
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
// Called from inside a commit callback that cannot accept the value (a negative length, a
// count of 1): the field comes back with the typed text and `why` in its title line, instead
// of closing as if the value had been taken.
bool refuse(const std::string& why); // true if the field came back
bool is_open() const { return m_open; }
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
@@ -69,11 +73,10 @@ public:
// breaks the circle.
std::function<void()> request_frame;
// Kept because callers ask them, but there is no longer any difference to report: with no
// window there is no state where the field is on screen but logically closed, and no state
// where it is open but somebody else holds the keyboard.
bool is_mapped() const { return m_open; }
bool has_focus() const { return m_open; }
// Does the ImGui text widget own the keyboard? False once a click elsewhere deactivated it;
// the panel then forwards Enter/Tab to commit() itself, since ImGui no longer sees them.
bool has_focus() const { return m_open && (m_active || m_focus_pending); }
void dismiss() { close(); }
private:
@@ -88,6 +91,17 @@ private:
std::string m_title;
std::string m_err; // why the last value was refused, shown in the title line
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
double m_prefill_value{0.0}; // the exact value the field opened with...
std::string m_prefill_text; // ...and how it was shown: an untouched field commits the former
bool m_active{false}; // the InputText was the active item last frame
// What the last commit closed, so refuse() can bring the same field back.
struct Closed {
wxPoint anchor; std::string title; std::string buf;
std::function<void(double)> commit; std::function<void()> cancel;
double prefill_value{0.0}; std::string prefill_text;
};
Closed m_last;
bool m_in_commit{false};
};
}} // namespace Slic3r::GUI
+4 -75
View File
@@ -3415,68 +3415,10 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
return;
}
#ifdef SLIC3R_CAD
const int ctrlMask = wxMOD_CONTROL;
const int shiftMask = wxMOD_SHIFT;
#endif
// Design tab: Delete/Backspace removes the selected sketch entities while a
// sketch tool is active and the canvas has focus (dialog text fields are separate
// wx controls, so this never eats their editing keys).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& (keyCode == WXK_DELETE || keyCode == WXK_BACK)
&& !m_design_sketch_tool->selection().empty()) {
m_design_sketch_tool->delete_selected();
m_dirty = true;
render();
return;
}
#endif
// Esc exits the active sketch tool (Onshape-like, layered: abort in-progress entity ->
// drop to Select -> exit the session back to Feature mode).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& keyCode == WXK_ESCAPE) {
m_design_sketch_tool->request_exit();
m_dirty = true;
render();
return;
}
#endif
// Design tab: Ctrl+Z / Ctrl+Shift+Z (and Ctrl+Y) undo/redo the Design feature
// history. Scoped by m_design_sketch_tool — only the Design canvas owns one — so the
// main 3D editor's undo/redo (the CanvasView3D-gated cases further below) is untouched.
// Handled here, before the generic Ctrl block, so it takes precedence and early-returns.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && (evt.GetModifiers() & ctrlMask) != 0) {
const bool is_z = (keyCode == 'z' || keyCode == 'Z' || keyCode == WXK_CONTROL_Z);
const bool is_y = (keyCode == 'y' || keyCode == 'Y' || keyCode == WXK_CONTROL_Y);
if (is_z || is_y) {
const bool redo = is_y || ((evt.GetModifiers() & shiftMask) != 0);
m_design_sketch_tool->request_undo_redo(redo);
m_dirty = true;
render();
return;
}
}
#endif
// Design tab: F = Place on Face (Prepare's lay-flat), when the Design viewport is up
// and a body face is selected. The tool forwards to DesignPanel::place_on_face; it returns
// false (no face picked) so F falls through to the default handler below.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()
&& (keyCode == 'f' || keyCode == 'F') && (evt.GetModifiers() & ctrlMask) == 0) {
if (m_design_sketch_tool->request_place_on_face()) {
m_dirty = true;
render();
return;
}
}
#endif
// Design tab: Delete, Esc, Ctrl+Z/Y and F are NOT handled here. DesignPanel's CHAR_HOOK owns
// them (one route per key, whatever holds focus) and only lets a key through to this canvas
// when an in-canvas value field is open -- which is exactly when Backspace must edit the
// number rather than delete the geometry it measures.
//BBS: add orient deactivate logic
if (keyCode == WXK_ESCAPE
@@ -3754,19 +3696,6 @@ static void key_released(int key) { s_keys_down.erase(key); }
void GLCanvas3D::on_key(wxKeyEvent& evt)
{
// Design tab: Delete/Backspace removes selected sketch entities. GTK delivers
// these as KEY_DOWN rather than CHAR, so handle it here too.
#ifdef SLIC3R_CAD
if (evt.GetEventType() == wxEVT_KEY_DOWN
&& m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& (evt.GetKeyCode() == WXK_DELETE || evt.GetKeyCode() == WXK_BACK)
&& !m_design_sketch_tool->selection().empty()) {
m_design_sketch_tool->delete_selected();
m_dirty = true;
render();
return;
}
#endif
const int keyCode = evt.GetKeyCode();
if (evt.GetEventType() == wxEVT_KEY_DOWN)
+47 -8
View File
@@ -1320,6 +1320,14 @@ void MainFrame::show_option(bool show)
}
}
#ifdef SLIC3R_CAD
DesignPanel* MainFrame::shown_design_panel() const
{
DesignPanel* design = DesignPanel::if_built();
return (design != nullptr && m_design_page != nullptr && m_design_page->IsShownOnScreen()) ? design : nullptr;
}
#endif
void MainFrame::init_tabpanel() {
// wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on
// Windows 10 with multiple high resolution displays connected.
@@ -1411,8 +1419,9 @@ void MainFrame::init_tabpanel() {
#ifdef SLIC3R_CAD
// The experimental feature is off by default, and when it is off the page is never
// created, so the tab does not appear at all (the preference takes effect on the next
// start, like the other feature toggles).
if (wxGetApp().is_enable_cad_feature()) {
// start, like the other feature toggles). Nor in the G-code viewer, which has no Design tab to put
// it in — and no business opening a control socket onto one.
if (wxGetApp().is_enable_cad_feature() && wxGetApp().is_editor()) {
// Experimental and heavy enough that building it unasked would cost more than it saves.
m_design_page = new LazyPage<DesignPanel>(this, TAB_ID_DESIGN, -1);
m_lazy_pages.push_back(m_design_page);
@@ -3001,12 +3010,28 @@ void MainFrame::init_menubar_as_editor()
#ifndef __APPLE__
// BBS undo
append_shortcut_item(editMenu, Shortcut::Undo, true, _L("Undo"),
_L("Undo"), [this](wxCommandEvent&) { m_plater->undo(); },
"menu_undo", nullptr, [this](){return m_plater->can_undo(); }, this);
_L("Undo"), [this](wxCommandEvent&) {
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(false); return; }
#endif
m_plater->undo(); },
"menu_undo", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(false);
#endif
return m_plater->can_undo(); }, this);
// BBS redo
append_shortcut_item(editMenu, Shortcut::Redo, true, _L("Redo"),
_L("Redo"), [this](wxCommandEvent&) { m_plater->redo(); },
"menu_redo", nullptr, [this](){return m_plater->can_redo(); }, this);
_L("Redo"), [this](wxCommandEvent&) {
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(true); return; }
#endif
m_plater->redo(); },
"menu_redo", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(true);
#endif
return m_plater->can_redo(); }, this);
editMenu->AppendSeparator();
// BBS Cut TODO
append_shortcut_item(editMenu, Shortcut::Cut, true, _L("Cut"),
@@ -3053,8 +3078,15 @@ void MainFrame::init_menubar_as_editor()
if (handle_key_event(e)) {
return;
}
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(false); return; }
#endif
m_plater->undo(); },
"", nullptr, [this](){return m_plater->can_undo(); }, this);
"", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(false);
#endif
return m_plater->can_undo(); }, this);
// BBS redo
append_shortcut_item(editMenu, Shortcut::Redo, false, _L("Redo"),
_L("Redo"), [this, handle_key_event](wxCommandEvent&) {
@@ -3065,8 +3097,15 @@ void MainFrame::init_menubar_as_editor()
if (handle_key_event(e)) {
return;
}
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) { dp->menu_undo_redo(true); return; }
#endif
m_plater->redo(); },
"", nullptr, [this](){return m_plater->can_redo(); }, this);
"", nullptr, [this](){
#ifdef SLIC3R_CAD
if (DesignPanel* dp = shown_design_panel()) return dp->menu_can_undo_redo(true);
#endif
return m_plater->can_redo(); }, this);
editMenu->AppendSeparator();
// BBS Cut TODO
append_shortcut_item(editMenu, Shortcut::Cut, false, _L("Cut"),
+3
View File
@@ -495,6 +495,9 @@ public:
// through LazyInstance's statics, and show_device() only moves pages in and out of the book.
#ifdef SLIC3R_CAD
LazyPage<DesignPanel>* m_design_page { nullptr };
// The Design panel when its tab is the one on screen, else null. Edit > Undo/Redo act on
// the tab that is shown: its own history when that is Design, the plater's otherwise.
DesignPanel* shown_design_panel() const;
#endif
//BBS: GUI refactor
LazyPage<MonitorPanel>* m_monitor_page{ nullptr };