diff --git a/docs/ux/interaction-model.md b/docs/CAD/ux/interaction-model.md similarity index 73% rename from docs/ux/interaction-model.md rename to docs/CAD/ux/interaction-model.md index e6d5428253..2041978ea0 100644 --- a/docs/ux/interaction-model.md +++ b/docs/CAD/ux/interaction-model.md @@ -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. diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index 1d0ea603c6..e538495caa 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -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; diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index b6ceca2390..c6b7a3d2aa 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -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 cb); void set_on_sketch_entities_commit( diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index c1ff2df5c3..c35f773330 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -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() diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index fe6d7d30a9..60003ff50c 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -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};