Let the camera pan and orbit freely while sketching in the Design tab

Panning or orbiting with a right-drag no longer ends the polyline chain or drops
the point already placed. Only a right-click that doesn't move does. In the
Touchpad camera style, Alt+move and Shift+move now orbit and pan even while a
draw tool is armed.
This commit is contained in:
SoftFever
2026-10-05 23:24:50 +08:00
parent b67269ccc4
commit cd30b196b2
6 changed files with 63 additions and 40 deletions
@@ -262,9 +262,9 @@ and a band is not a "gesture" in the `CadLevel::Gesture` sense because nothing h
|---|---|---|---|---|---|
| `Move` | — | `update_hover` / `update_solid_hover` (**non-consuming**, returns false) | — | passthrough | hover only asks for a repaint, `:10022-10025` |
| `LeftDown` | orbit may begin | latch press, **return false** | — | consume-or-orbit is the canvas's call | `:10058-10063`; consuming here killed orbit once already |
| `LeftDrag` > 8 px | — | start + drive band, **consume** | — | no longer orbits in this canvas | middle-drag orbits, right-drag pans (`:10013-10016`) |
| `LeftDrag` > 8 px | — | start + drive band, **consume**, when Shift is held or the left button has no camera action | — | otherwise the left button's drag action (Preferences > Control) | the band is Prepare's Shift+left-drag rectangle selection |
| `LeftUp` | — | commit pick **or** resolve band | — | — | `:10035-10041` |
| `MiddleDrag` / `RightDrag` | — | must not see it | — | orbit / pan | camera gestures never reach the FSM |
| `MiddleDrag` / `RightDrag` | — | must not see it | — | the button's drag action (Preferences > Control) | camera gestures never reach the FSM; a right press reaches the tool only once its release shows it was a click |
| `RightClick` | — | — | offer menu | — | `snaporca-xmh6` open: a right-click that only clears the sketch selection eats the offer |
| `Esc` | — | — | `escape()` ladder | — | one route whatever holds focus (`:4228-4231`) |
| `Del` / `Backspace` | — | `delete_selected_or_last_sketch_entity()` | char hook | — | `:4260` |
+10 -7
View File
@@ -154,15 +154,18 @@ rather than to its end points: a press that wandered past the budget at any mome
navigation, even if it comes back to where it started, which is what stops a slow, careful
orbit from ending in a menu. There is no time budget — a gesture that means something different
when it is slow is exactly what the interaction charter rules out. The raycast uses the press
position, not the release. An armed sketch tool that already consumed the right
button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag.
Past either budget the event is navigation, and navigation does not transition the state
machine.
position, not the release. The sketch tool sees a right press only once the release has shown it
was a click: the press itself goes to the camera, which may pan or orbit with that button, and the
canvas replays it to the tool on a stationary release. A tool that uses the click (to terminate a
chain, say) keeps the menu closed. Past either budget the event is navigation, and navigation
does not transition the state machine.
Navigation itself is Prepare's: the camera reads the drag actions set in Preferences > Control
for each button. The left button is shared with picking, so a whole body is swept with a
rectangle on plain left-drag only while no camera action is assigned to it, and with
Shift+left-drag otherwise — Prepare's own rectangle selection.
for each button, and in the Touchpad camera style a move with Alt held orbits and one with Shift
held pans, whatever tool is armed. The left button is shared with picking and drawing, so a tool
handle or a press that draws takes it first, as a gizmo does in Prepare; a whole body is swept
with a rectangle on plain left-drag only while no camera action is assigned to the left button,
and with Shift+left-drag otherwise — Prepare's own rectangle selection.
Entering a sketch changes three things at once so the mode is legible: a banner above the
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
+15 -11
View File
@@ -1051,15 +1051,18 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
return;
m_ctx_bound = true;
// Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event.
// It only consumes when it actually opens the offer; every other right-click still falls
// 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 (kCadRightClickDriftPx).
// It only consumes when it actually opens the offer.
// Right-drag may pan or orbit (Preferences > Control). Without remembering where the press
// landed, every such drag ended by popping the offer over wherever the camera stopped — the
// menu appearing as the reward for moving the view. A right-click is the release of a
// STATIONARY press (kCadRightClickDriftPx); only that reaches the sketch tool or the offer.
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition();
m_ctx_travelled = false;
e.Skip(); // the canvas still needs the press to seed the orbit
// Drop any press the tool still keeps (only a click's release takes it, so a pan's stays)
// before the canvas offers it this one, which ImGui may take instead.
m_sketch_tool.drop_right_click();
e.Skip(); // the canvas still needs the press to seed a pan or an orbit
});
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) {
@@ -1070,12 +1073,13 @@ void DesignCanvas::set_on_context_menu(std::function<void(const wxPoint&)> cb)
});
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press;
// 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? A press that travelled orbited; one that did not, did not.
// Click, or navigation? A press that travelled panned or 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) {
// Ending a chain or abandoning an anchor uses the click up.
const bool terminated = is_click && m_canvas && m_sketch_tool.take_right_click(*m_canvas);
if (terminated)
m_canvas->set_as_dirty(); // drawn by the canvas's own RightUp (e.Skip below) or at idle
else if (m_on_context_menu && !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
// two are the same pixel in practice; using the press is what makes that a
+25 -13
View File
@@ -10315,14 +10315,14 @@ std::vector<int> DesignSketchTool::connected_loop(int seed) const
// the offer was excluded in sketch mode wholesale so a right-click could end a polyline chain,
// abandon an anchor or exit a tool. That made every sketch row in the atlas unreachable.
// The honest test is not "which mode are we in" but "did the tool actually USE this right-click",
// and only the tool knows. Wrapping on_mouse records that once, for every terminator, instead of
// threading a flag through the twenty-odd sites that consume a RightDown.
// and only the tool knows: take_right_click returns it for every terminator, from the
// twenty-odd sites that consume a RightDown.
// Right-click abandons the anchor a draw tool has down. With NOTHING down there is nothing to
// abandon — and consuming the click anyway made the offer unreachable from every armed draw tool:
// on_mouse records the consumption in m_right_consumed and DesignCanvas's RIGHT_UP handler
// suppresses the menu whenever it is set, so right-click became a no-op that also hid the one door
// to half the vocabulary (47 of 86 verbs have no shortcut). Measured on the rig: with Line armed,
// two right-clicks in a row produced no menu and no tool change; only Escape freed it.
// DesignCanvas's RIGHT_UP handler suppresses the menu whenever the tool used the click, so
// right-click became a no-op that also hid the one door to half the vocabulary (47 of 86 verbs
// have no shortcut). Measured on the rig: with Line armed, two right-clicks in a row produced no
// menu and no tool change; only Escape freed it.
// Same rule as xmh6, which said it for the selection: clearing nothing is not a gesture
// terminator. ghcz.
bool DesignSketchTool::right_abandon()
@@ -10334,12 +10334,24 @@ bool DesignSketchTool::right_abandon()
return true;
}
// The camera follows Preferences > Control here as in Prepare, so no tool may take a gesture the
// camera owns. A right press may start whatever drag action the right button is given, and
// whether it did is known only at the release: ending a chain on the press made every pan or
// orbit started there end the chain too. The press goes to the camera and is kept;
// DesignCanvas's RIGHT_UP handler replays it through take_right_click when it was a click.
bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
{
const bool consumed = on_mouse_impl(evt, canvas);
if (evt.RightDown())
m_right_consumed = consumed;
return consumed;
if (evt.RightDown()) {
m_right_press = evt;
return false;
}
return on_mouse_impl(evt, canvas);
}
bool DesignSketchTool::take_right_click(GLCanvas3D& canvas)
{
auto press = std::exchange(m_right_press, {});
return press && on_mouse_impl(*press, canvas);
}
bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
@@ -11288,9 +11300,9 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
return true;
}
if (evt.RightDown()) {
// Hand the click back (return false) so the offer opens: the m_right_consumed flag
// this return value feeds means "the tool USED this right-click", and a plain
// right-click in Select mode is not a gesture terminator.
// Hand the click back (return false) so the offer opens: take_right_click returns
// this value as "the tool USED this right-click", and a plain right-click in Select
// mode is not a gesture terminator.
//
// But do NOT drop the selection on the way out. The offer menu describes WHAT IS
// SELECTED, so clearing first guaranteed it could only ever describe nothing: select
+7 -6
View File
@@ -9,14 +9,15 @@
#include "slic3r/GUI/GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies
#include <Eigen/Core>
#include <cstddef>
#include <wx/event.h>
#include <functional>
#include <optional>
#include "libslic3r/Color.hpp"
#include <math.h>
#include <vector>
#include <string>
#include <utility>
class wxMouseEvent;
class wxPoint;
namespace Slic3r {
@@ -149,10 +150,10 @@ public:
// Right-click on a draw tool: true when an in-progress anchor was abandoned, false when
// there was nothing to abandon — and false is what lets the offer menu open. ghcz.
bool right_abandon();
// True if the LAST right-press was consumed as a gesture terminator (end a polyline chain,
// abandon an anchor, exit a tool). Read-and-clear: the canvas asks on the matching release to
// decide whether that right-click was the user's, in which case it opens the offer.
bool take_right_consumed() { const bool b = m_right_consumed; m_right_consumed = false; return b; }
// Replays and clears the right press on_mouse kept for the camera: true if the tool used it as
// a gesture terminator (end a polyline chain, abandon an anchor), so no offer opens.
bool take_right_click(GLCanvas3D& canvas);
void drop_right_click() { m_right_press.reset(); }
void render(GLCanvas3D& canvas);
// The in-canvas value field, drawn by render() before any early return. Owned by
// DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp.
@@ -1274,7 +1275,7 @@ private:
void hit_display_sketch(const DisplaySketch& d, const Vec2d& p, double tol,
int& edge_feat, int& edge_reg, int& edge_ent,
double& edge_d, int& face_feat, int& face_reg) const;
bool m_right_consumed{false}; // last RightDown was a gesture terminator, not a menu
std::optional<wxMouseEvent> m_right_press; // right press not yet known to be a click
bool m_escalate_repick{true}; // re-picking the same sub-element takes the whole body
void render_solid_highlight();
// The above's edge and vertex highlight, from explicit arguments, so the committed selection
+4 -1
View File
@@ -4311,7 +4311,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
if (evt.LeftDown() && m_canvas != nullptr)
m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas
if (m_design_sketch_tool->on_mouse(evt, *this)) {
// A Touchpad-style orbit or pan (a plain move with Alt or Shift held) is the camera's,
// whatever the tool would make of the move. Only a drag consults the button mappings.
const bool camera_move = evt.Moving() && (is_camera_rotate(evt, {}) || is_camera_pan(evt, {}));
if (!camera_move && m_design_sketch_tool->on_mouse(evt, *this)) {
m_dirty = true;
render(); // force an immediate redraw so the sketch overlay updates live
return;