A sketch should not look like plate preparation

Three cues, because one is missed. A teal banner across the top of the viewport names the session
("Editing: Sketch N") and where its exits are; the printer bed is muted for the duration, since a
plate grid and a sketch grid are the same visual language and reading one as the other is how a
sketch gets drawn against the wrong reference; and N looks straight down the plane normal at the
current zoom, with the plane's own y axis as up, because no hand-orbit lands exactly square and a
sketch read at an angle is one whose right angles do not look like right angles.

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
This commit is contained in:
Tommaso Bianchi
2026-09-05 11:45:29 +02:00
co-authored by Claude Opus 5
parent 324b558747
commit 3f52166e32
5 changed files with 103 additions and 0 deletions
@@ -109,3 +109,36 @@ also mean "open a menu".
Right-hold-and-drag is not in the table on purpose: past 3 px or 200 ms it is navigation, and
navigation does not transition the state machine.
## 4. Visual scaffolding
Entering a sketch changes three things at once, so the state is legible from across the room:
- **Banner.** A teal strip across the top of the viewport: `Editing: Sketch N · N = look normal to
the plane · Finish or Cancel in the toolbar`. Indicator only — Confirm and Cancel stay on the one
ribbon action bar, per the Design UX contract. It is a sibling above the canvas, not a floating
child over it: a child window over a `wxGLCanvas` is a native window on GTK and does not reliably
stack over GL, and this banner's job is to be unmissable rather than clever.
- **The printer bed is muted.** A plate grid and a sketch grid are the same visual language, and
reading one as the other is how a sketch gets drawn against the wrong reference. The view
checkbox remains the stored preference and is restored on the way out; ticking it mid-sketch
still shows the bed, because that is a deliberate act and this is only a default.
- **`N` looks normal to the plane**, keeping the current zoom, with the plane's own y axis as up.
Sketch key map only — in Feature mode the navigator orb owns orientation.
## 5. Context menu content
The offer is generated from `docs/CAD/ux/tool_atlas.json`; its 8-row shape and permanent row
indices are ratified and are not changed here. Checked against the per-context vocabularies asked
for in the 2026-09-05 interaction brief, the atlas already carries all of them except two, both on
a planar face:
| Asked for | Status |
|---|---|
| Revolve on a planar face | **not offered, and should not be**: `revolve` accepts `sk_loop` only, because the kernel takes a sketch profile — a face is not one |
| Offset Face | offered as **Thicken** (`thicken`, accepts `face_planar`); `surf_offset` is the sheet-body verb and accepts `body_sheet` |
View and document actions — Zoom to Fit, View Isometric, Clear Selection, Finish Sketch, Normal to
Sketch — stay in chrome by the atlas's own rule: the offer describes verbs that consume a
*selection*, and these act on the document or the camera. Esc covers Clear Selection, `N` covers
Normal to Sketch, and the ribbon covers Finish.
+15
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/OpenGLManager.hpp"
#include "slic3r/GUI/3DBed.hpp"
#include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/Model.hpp"
@@ -1486,6 +1487,20 @@ int DesignCanvas::sketch_selection_count() const
return int(m_sketch_tool.selection().size());
}
bool DesignCanvas::view_normal_to_sketch()
{
if (m_canvas == nullptr) return false;
const SketchPlane& pl = m_sketch_tool.plane();
Camera& cam = wxGetApp().plater()->get_camera();
// Keep the distance: this is an orientation change, not a zoom. The plane's own y axis is
// the up vector, so "up" on screen is up in sketch coordinates — which is what makes a
// dimension typed after pressing N land where the eye expects it.
const double dist = cam.get_distance();
cam.look_at(pl.origin + pl.normal * dist, pl.origin, pl.y_axis);
request_repaint();
return true;
}
bool DesignCanvas::sketch_abort_gesture()
{
if (!m_sketch_tool.abort_gesture()) return false;
+3
View File
@@ -80,6 +80,9 @@ public:
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void set_show_bed(bool b); // view option: draw the printer bed + plate grid, or not
// N: look straight down the sketch plane's normal, keeping the current zoom. A sketch drawn
// at an angle is a sketch drawn wrong, and no amount of orbiting by hand lands exactly square.
bool view_normal_to_sketch();
void cancel_sketch();
void set_on_sketch_commit(std::function<void(const SketchProfile&, const SketchPlane&)> cb);
void set_on_sketch_entities_commit(
+45
View File
@@ -441,6 +441,16 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(_L("Nothing here has a value to type — pick a line, an arc, a circle, or two entities"));
};
m_keys_sketch['K'] = [this] { enter_constrain_inline(); };
// N: square up to the plane. Not a view preference but part of drawing — a sketch read at an
// angle is a sketch whose right angles do not look like right angles, and no hand-orbit lands
// exactly normal. Sketch map only: in Feature mode the navigator orb owns orientation.
m_keys_sketch['N'] = [this] {
if (m_viewport && m_viewport->view_normal_to_sketch()) {
m_status->SetForegroundColour(wxNullColour);
set_status(_L("Normal to the sketch plane"));
m_status->Refresh();
}
};
m_keys_sketch['Q'] = [this] {
// With geometry selected, Q converts THAT geometry (snaporca-6zic) — the reading
// everyone arrives with from other sketchers. With nothing selected it keeps its
@@ -4316,6 +4326,23 @@ DesignPanel::DesignPanel(wxWindow* parent)
});
auto* vcol = new wxBoxSizer(wxVERTICAL);
// The sketch banner sits ABOVE the viewport rather than floating inside it: a child window
// over a wxGLCanvas is a platform argument (it is a native window on GTK and does not
// reliably stack over GL), and the banner's job is to be unmissable, not to be clever.
m_sketch_banner = new wxPanel(this, wxID_ANY);
m_sketch_banner->SetBackgroundColour(wxColour(0, 122, 116)); // Orca teal: not a plate colour
m_sketch_banner_txt = new wxStaticText(m_sketch_banner, wxID_ANY, wxString());
m_sketch_banner_txt->SetForegroundColour(*wxWHITE);
{
wxFont f = m_sketch_banner_txt->GetFont();
f.SetWeight(wxFONTWEIGHT_BOLD);
m_sketch_banner_txt->SetFont(f);
auto* bs = new wxBoxSizer(wxHORIZONTAL);
bs->Add(m_sketch_banner_txt, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(6));
m_sketch_banner->SetSizer(bs);
}
m_sketch_banner->Hide();
vcol->Add(m_sketch_banner, 0, wxEXPAND);
// The bottom 3D-navigator orb handles all view orientation, so no separate view buttons.
// Fit view is a double-click on the viewport (the tool intercepts it -> zoom_to_volumes).
vcol->Add(m_viewport, 1, wxEXPAND);
@@ -4455,6 +4482,24 @@ void DesignPanel::set_ui_mode(UiMode m)
// leaving it takes them back. Without this they would only refresh on the next tree
// rebuild, which is not an event that happens when you merely press Sketch.
update_reference_planes();
// Say where you are, in words, across the top of the viewport — and mute the printer bed
// while you are there. The plate grid and a sketch grid are the same visual language, and
// reading one as the other is how a sketch gets drawn against the wrong reference. The bed
// checkbox stays the stored preference and is restored on the way out; ticking it mid-sketch
// still shows the bed, because that is a deliberate act and this is only a default.
if (m_sketch_banner != nullptr) {
const bool sketching = (m == UiMode::Sketch);
if (sketching && m_sketch_banner_txt != nullptr)
m_sketch_banner_txt->SetLabel(
wxString::Format(_L("Editing: Sketch %d · N = look normal to the plane · "
"Finish or Cancel in the toolbar"),
m_feature_counter + 1));
m_sketch_banner->Show(sketching);
m_sketch_banner->GetParent()->Layout();
if (m_viewport != nullptr)
m_viewport->set_show_bed(!sketching && (m_show_bed == nullptr || m_show_bed->GetValue()));
}
}
void DesignPanel::on_shape_changed()
+7
View File
@@ -451,6 +451,13 @@ private:
// Top contextual toolbar (parented to the panel, above the form/viewport row).
UiMode m_ui_mode{UiMode::Feature};
// Sketch environment banner: a strip across the top of the viewport saying, in words, that
// this is a sketch and which one. The mode used to be legible only from the toolbar and the
// left card — both of which look like the rest of the app — so a sketch session and plate
// preparation were one glance apart. Indicator only: Finish/Cancel stay on the ONE ribbon
// action bar (the Design UX contract), and the banner never grows a second pair.
wxPanel* m_sketch_banner{nullptr};
wxStaticText* m_sketch_banner_txt{nullptr};
wxScrolledWindow* m_toolbar{nullptr}; // horizontally scrollable so the action bar stays reachable on narrow windows
wxSizer* m_tb_feature{nullptr};
wxSizer* m_tb_sketch{nullptr};