mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+445
-591
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
|
||||
@@ -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 (;;) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user