From 65e2b6f6266f9b62468e32c2fb43c2403450d1eb Mon Sep 17 00:00:00 2001 From: Tommaso Bianchi Date: Sun, 23 Aug 2026 13:46:21 +0200 Subject: [PATCH] The sketch says what to do next, and construction geometry looks like it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report, 2026-08-23, after using the freshly deployed build: "selection and removal of existing elements of the 2d sketch is not intuitive, and the bottom ui text does not illustrate what the user has to do to properly use the selected tools. Mirror, for example, does not indicate: first select mirror line then entities to be selected, and there is no UI indication of what is being selected. normally, costruction lines are dotted." Four defects, all of them in the 2D vocabulary this fork's charter puts at the centre, and all four fixed here. snaporca-1c0c (P1) — the prompt was written ONCE, when the tool was armed. DesignPanel's select_tool lambda set a sentence and nothing ever revised it, so every step after the first was unguided: Mirror said "pick axis, then entities" and then never said which of the two you were on; Escape silently downgraded an armed tool to Select (request_exit is layered: anchors, then tool, then session) while the line still named the tool you had left; and nothing ever mentioned that Del removes a selection. The fix moves the line off the arm event and onto the tool's LIVE state. DesignSketchTool::emit_step_hint() reports (mode, step, picks) whenever that triple moves, from render() — the one place every state change in this tool passes through. Putting it there instead of in the thirty-odd branches of on_mouse is the whole point: a per-call-site notification is a thing the next tool forgets to add, and it costs three int comparisons a frame. DesignPanel owns the words, in ONE table (sketch_step_prompt), whose step numbers are the same ones render() previews and on_mouse consumes, so the description cannot drift from the code that reads the clicks. Every tool now names the gesture that ENDS it, because none of them was discoverable: an empty click applies an edit-op or a transform, right-click cancels it, Esc goes back to Select. snaporca-vd6v (P2) — Mirror mirrors its axis pick and its target picks into m_selection, so both painted white and the picture could not answer "what did I select as what". The edit-op's first pick — Mirror's axis, Fillet/Chamfer's first line — now paints violet. Violet and not cyan: cyan means SELECTED in this canvas and nothing else may wear it, a rule this file already carries in writing. snaporca-imlq (P2) — construction geometry drew as a solid grey line. Every CAD dashes it, and grey alone does not read as "reference" against the under-constrained orange. dash_polyline() chops the polyline before it reaches draw_quad_strip, with the dash and gap in world units scaled by units-per-pixel, so a dash keeps its size on screen instead of becoming a solid line when you zoom out and three dashes when you zoom in. snaporca-oql1 (P2) — Backspace now deletes as Del does. On every laptop this runs on, Del is a chord and Backspace is what a hand reaches for. The Select-mode prompt states the rest (Shift-click adds, double-click takes the loop, Del removes), and the first step of every armed tool names the Esc route back to Select, which was the invisible half of "selection is not intuitive". Also, on the same report: the sketch stroke half-width goes 0.6 -> 0.3 mm. At 1.2 mm wide the orange line swallowed a short segment and hid which of two near parallel lines the cursor was on. One constant, because all twenty call sites of draw_quad_strip are sketch strokes. Retired on the way: the on_sketch_selection_changed status writer. It said "N selected — Delete removes them" while an edit-op mirrored its picks into the selection, i.e. in the middle of a Mirror gesture, where Delete does nothing of the sort. on_sketch_step says the true thing for Select and says nothing false anywhere else. And the live length/angle readout is now APPENDED to the step guidance rather than replacing it: it fires on every mouse move, so it used to erase the instruction for the step in progress one move after the click that started it. Two false trails, recorded so the next session does not walk them again: - DesignPanel.hpp deliberately does not include DesignSketchTool.hpp, so the panel's handler takes the mode as an int and the .cpp casts it back. The first attempt put Mode in the header signature and the build said only "expected ',' or '...' before 'mode'". - The offer ladder failed six properties against a perfectly good binary because I had relaunched the rig myself without SNAPORCA_KEYTRACE=1, and its [OFFER] trace lines ARE its instrument. A ladder with no instrument reports "None", which reads exactly like a regression in the offer. ladder-all.sh launches it correctly; a hand relaunch must too. VERIFIED, not merely compiled. Driven on the headless rig with synthetic mouse and keyboard, and photographed at each step: "Mirror — first click the LINE to mirror about (a construction line works) · Esc goes back to Select" -> "Mirror — axis set · now click the entities to mirror · right-click cancels" -> "Mirror — axis set · 1 to mirror · click another to add or remove it · click empty space to apply", with the axis violet, its target white, and the construction lines dashed while real geometry stays solid. Full gate green afterwards (scripts/ladder-all.sh, ALL LADDERS HELD): offer table vs the atlas OK kernel suite 188 test cases, 2532 assertions engine ladder rungs 1-8, ALL RUNGS HELD corpus rung 977-sheet drawing corpus, every 20th -> 49 sampled, 39 gradeable, 39 clean corpus scale rung the 6 heaviest sheets, all clean gesture ladder 93/93 properties, real mouse and keyboard offer ladder 108/108 properties, through the right-click menu and the verbs behind it That harness is the reason a UX change of this size can be made in one pass and believed: 93 + 108 properties are driven the way a person drives the app, and the 977-sheet corpus keeps the engine underneath them honest against real drawings rather than against my own arithmetic. --- src/slic3r/GUI/CAD/DesignCanvas.cpp | 5 + src/slic3r/GUI/CAD/DesignCanvas.hpp | 2 + src/slic3r/GUI/CAD/DesignPanel.cpp | 203 ++++++++++++++++++++++-- src/slic3r/GUI/CAD/DesignPanel.hpp | 8 + src/slic3r/GUI/CAD/DesignSketchTool.cpp | 89 ++++++++++- src/slic3r/GUI/CAD/DesignSketchTool.hpp | 12 ++ 6 files changed, 306 insertions(+), 13 deletions(-) diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index 613e934dce..b967273444 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -573,6 +573,11 @@ void DesignCanvas::set_on_solve_state(std::function cb) m_sketch_tool.on_solve_state = std::move(cb); } +void DesignCanvas::set_on_sketch_step(std::function cb) +{ + m_sketch_tool.on_step_changed = std::move(cb); +} + void DesignCanvas::apply_segment_length(double len) { m_sketch_tool.apply_segment_length(len); diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index aa14b36b82..1eb70b9eaa 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -78,6 +78,8 @@ public: void set_on_segment_drawn(std::function cb); void set_on_cursor_metrics(std::function cb); void set_on_solve_state(std::function cb); // dof, ok, has_constraints + // Live per-step guidance from the armed sketch tool (mode, step, picks). snaporca-1c0c. + void set_on_sketch_step(std::function cb); void apply_segment_length(double len); // exact length, then commit & repaint void keep_segment_as_drawn(); // commit as-drawn & repaint diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index f200931076..94d522e6fc 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -3407,11 +3407,21 @@ DesignPanel::DesignPanel(wxWindow* parent) m_viewport->set_on_cursor_metrics([this](double len, double ang_deg, bool locked) { double a = ang_deg; if (a < 0.0) a += 360.0; // show bearing 0..360 m_status->SetForegroundColour(wxNullColour); - set_status(wxString::Format(L"L %.2f mm %.1f°%s", - len, a, locked ? L" (locked)" : L"")); + // APPENDED to the step guidance, never in place of it. This fires on every mouse move + // while a segment is being dragged, so replacing the line wiped the instruction for the + // step the user is in the middle of — one mouse move after the click that armed it. + const wxString metrics = wxString::Format(L"L %.2f mm %.1f°%s", + len, a, locked ? L" (locked)" : L""); + set_status(m_sketch_step.IsEmpty() ? metrics + : m_sketch_step + L" · " + metrics); m_status->Refresh(); }); + // Live per-step guidance: the armed tool says which step it is on, we write the sentence. + m_viewport->set_on_sketch_step([this](DesignSketchTool::Mode mode, int step, int picks) { + on_sketch_step(int(mode), step, picks); + }); + // DoF feedback (P3): after each live solve, report constraint state on its own // line. Green = fully constrained, red = conflicting, neutral = N remaining DoF. m_viewport->set_on_solve_state([this](int dof, bool ok, bool has_constraints) { @@ -3422,14 +3432,10 @@ DesignPanel::DesignPanel(wxWindow* parent) apply_dof_status(dof, ok, has_constraints); }); - // Selection (Select tool): reflect the count in the status line. - m_viewport->set_on_sketch_selection_changed([this](int count) { - m_status->SetForegroundColour(wxNullColour); - set_status(count > 0 - ? wxString::Format(_L("%d selected — Delete removes them"), count) - : _L("Click to select; click a filled face to extrude; Shift to add")); - m_status->Refresh(); - }); + // Selection no longer writes the status line: on_sketch_step owns it, says the same thing + // for Select mode and — unlike this callback, which also fired while an edit-op mirrored its + // picks into the selection — never claims "N selected, Delete removes them" in the middle of + // a Mirror gesture, where Delete does nothing of the sort. snaporca-1c0c. // Onshape flow: clicking inside a closed-loop face commits the sketch and opens // the Extrude dialog (with a ghost preview) targeting that sketch. @@ -4044,7 +4050,9 @@ DesignPanel::DesignPanel(wxWindow* parent) } // Delete — the selected sketch entities (or the last drawn one if none is selected), or the // selected feature in Feature mode. Focus-independent, same reason as undo above. - if (!in_text && key == WXK_DELETE) { + // WXK_BACK too: on a keyboard whose Del is a chord (every laptop this runs on), Del is + // the one destructive key nobody can reach, and Backspace is what users press. snaporca-oql1. + if (!in_text && (key == WXK_DELETE || (key == WXK_BACK && sketching))) { if (sketching) { m_viewport->delete_selected_or_last_sketch_entity(); return; } if (m_ui_mode == UiMode::Feature && m_active == Tool::None && tree_selection() != wxNOT_FOUND) { on_delete_feature(); return; } @@ -5956,6 +5964,179 @@ void DesignPanel::set_status(const wxString& text) } } + +// The sentence for the step the armed sketch tool is on (snaporca-1c0c). One table, so a tool's +// gesture is described in one place and the description cannot drift from the code that reads the +// clicks: the step counts here are the ones DesignSketchTool::render previews and on_mouse +// consumes. `step` = anchors already placed (edit-ops: 0 none, 1 first pick down, 2 ready to +// apply); `picks` = the size of the set the gesture accumulates. +// +// It is deliberately explicit about the gesture that ENDS each tool, because none of them is +// discoverable: a click on empty space applies an edit-op or a transform, right-click cancels it, +// and Esc downgrades an armed tool to Select before it ever exits the sketch. +static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks) +{ + using Mode = DesignSketchTool::Mode; + auto pick_more = [](const wxString& lead, int n) { + return n <= 0 ? lead + : lead + wxString::Format(_L(" · %d picked"), n); + }; + switch (m) { + case Mode::Select: + return picks > 0 + ? wxString::Format(_L("%d selected · Del removes them · Shift-click adds · " + "double-click takes the whole loop"), picks) + : _L("Select — click an entity to pick it · drag an endpoint or centre to move it · " + "Shift-click adds · Del removes"); + case Mode::Constrain: + return picks > 0 + ? wxString::Format(_L("%d picked · now choose a constraint (Horizontal, Parallel, " + "Tangent, Equal…)"), picks) + : _L("Constrain — click one or two entities, then choose a constraint"); + case Mode::Dimension: + return step == 0 ? _L("Dimension — click an entity, or the first of two points") + : _L("Dimension — click the second point"); + case Mode::Line: + return step == 0 ? _L("Line — click the start point") + : _L("Line — click the end point, or type the length"); + case Mode::Polyline: + return step == 0 ? _L("Polyline — click the first point") + : pick_more(_L("Polyline — click the next point · click the start point " + "to close it · right-click to end the chain"), step); + case Mode::CornerRect: + return step == 0 ? _L("Rectangle — click one corner") + : _L("Rectangle — click the opposite corner"); + case Mode::CenterRect: + return step == 0 ? _L("Centre rectangle — click the centre") + : _L("Centre rectangle — click a corner"); + case Mode::ObliqueRect: + return step == 0 ? _L("Oblique rectangle — click the start of the base edge") + : step == 1 ? _L("Oblique rectangle — click the end of the base edge (this sets the angle)") + : _L("Oblique rectangle — click to set the width"); + case Mode::RoundedRect: + return step == 0 ? _L("Rounded rectangle — click one corner") + : step == 1 ? _L("Rounded rectangle — click the opposite corner") + : _L("Rounded rectangle — click to set the corner radius"); + case Mode::CenterCircle: + return step == 0 ? _L("Circle — click the centre") + : _L("Circle — click to set the radius, or type it"); + case Mode::TwoPointCircle: + return step == 0 ? _L("Circle (2 points) — click one end of the diameter") + : _L("Circle (2 points) — click the other end of the diameter"); + case Mode::ThreePointCircle: + return step == 0 ? _L("Circle (3 points) — click the first point on the circle") + : step == 1 ? _L("Circle (3 points) — click the second point") + : _L("Circle (3 points) — click the third point"); + case Mode::ThreePointArc: + return step == 0 ? _L("Arc — click the start point") + : step == 1 ? _L("Arc — click the end point") + : _L("Arc — click a point the arc passes through"); + case Mode::TangentArc: + return step == 0 ? _L("Tangent arc — click the endpoint it leaves from") + : _L("Tangent arc — click its far end"); + case Mode::CenterArc: + return step == 0 ? _L("Centre arc — click the centre") + : step == 1 ? _L("Centre arc — click the start point (this sets the radius)") + : _L("Centre arc — click the end point"); + case Mode::Slot: + return step == 0 ? _L("Slot — click one end of the centreline") + : step == 1 ? _L("Slot — click the other end of the centreline") + : _L("Slot — click to set the width"); + case Mode::ArcSlot: + return step == 0 ? _L("Arc slot — click the centre the slot curves about") + : step == 1 ? _L("Arc slot — click the start of the centreline (this sets the radius)") + : step == 2 ? _L("Arc slot — click the end of the centreline") + : _L("Arc slot — click to set the width"); + case Mode::Polygon: + return step == 0 ? _L("Polygon — click the centre") + : _L("Polygon — click a vertex (this sets size and orientation)"); + case Mode::Ellipse: + return step == 0 ? _L("Ellipse — click the centre") + : step == 1 ? _L("Ellipse — click the end of the major axis") + : _L("Ellipse — click a point on the minor axis"); + case Mode::EllipseArc: + return step == 0 ? _L("Elliptical arc — click the centre") + : step == 1 ? _L("Elliptical arc — click the end of the major axis") + : step == 2 ? _L("Elliptical arc — click a point on the minor axis") + : step == 3 ? _L("Elliptical arc — click where the arc starts") + : _L("Elliptical arc — click where the arc ends"); + case Mode::BSpline: + return step == 0 ? _L("Spline — click the first control point") + : pick_more(_L("Spline — click the next control point · right-click to " + "finish the curve"), step); + case Mode::Point: + return _L("Point — click to place one; the tool stays armed for more"); + case Mode::Trim: + return _L("Trim — click a segment where it crosses another entity · right-click to exit"); + case Mode::Extend: + return _L("Extend — click a line or arc to grow it out to the nearest entity · " + "right-click to exit"); + case Mode::Fillet: + return step == 0 ? _L("Fillet — click the first of two lines that meet") + : step == 1 ? _L("Fillet — click the second line") + : _L("Fillet — drag the arrow, or click the number to type the radius · " + "click empty space to apply · right-click cancels"); + case Mode::Chamfer: + return step == 0 ? _L("Chamfer — click the first of two lines that meet") + : step == 1 ? _L("Chamfer — click the second line") + : _L("Chamfer — drag the arrow, or click the number to type the setback · " + "click empty space to apply · right-click cancels"); + case Mode::Offset: + return step == 0 ? _L("Offset — click the entity to offset") + : _L("Offset — drag the arrow to either side, or click the number to type " + "the distance · click empty space to apply · right-click cancels"); + case Mode::Mirror: + // The two-phase pick is the one gesture users reported as unguided: nothing said the + // AXIS comes first, and nothing said an empty click is what applies it. + return step == 0 + ? _L("Mirror — first click the LINE to mirror about (a construction line works)") + : picks == 0 + ? _L("Mirror — axis set · now click the entities to mirror · right-click cancels") + : wxString::Format(_L("Mirror — axis set · %d to mirror · click another to add or " + "remove it · click empty space to apply"), picks); + case Mode::Move: + return step == 0 ? _L("Move — click the entities to move") + : pick_more(_L("Move — drag the handle, or click the number to type the " + "distance · click empty space to apply"), picks); + case Mode::Rotate: + return step == 0 ? _L("Rotate — click the entities to rotate") + : pick_more(_L("Rotate — drag the handle, or click the number to type the " + "angle · click empty space to apply"), picks); + case Mode::Scale: + return step == 0 ? _L("Scale — click the entities to scale") + : pick_more(_L("Scale — drag the handle, or click the number to type the " + "factor · click empty space to apply"), picks); + case Mode::Array: + return step == 0 ? _L("Array — click the entities to repeat") + : pick_more(_L("Array — drag the handle to set the step, click the count to " + "type it · click empty space to apply"), picks); + case Mode::PolarArray: + return step == 0 ? _L("Polar array — click the entities to repeat") + : pick_more(_L("Polar array — drag the handle to set the sweep, click the " + "count to type it · click empty space to apply"), picks); + case Mode::TransformArt: + return _L("Drag a corner to scale, the centre to move · right-click when done"); + } + return wxString(); +} + +void DesignPanel::on_sketch_step(int mode, int step, int picks) +{ + wxString text = sketch_step_prompt(DesignSketchTool::Mode(mode), step, picks); + // Esc is layered (DesignSketchTool::request_exit): it drops the anchors down, then downgrades + // the armed tool to Select, and only then leaves the sketch. Nothing in the UI said so, so + // the route back to selecting existing geometry was invisible. Said once, on the step where + // the gesture has not started yet, so it does not crowd the instruction that matters. + if (step == 0 && picks == 0 && DesignSketchTool::Mode(mode) != DesignSketchTool::Mode::Select + && !text.IsEmpty()) + text += _L(" · Esc goes back to Select"); + m_sketch_step = text; + if (text.IsEmpty() || m_status == nullptr) return; + m_status->SetForegroundColour(wxNullColour); + set_status(text); + m_status->Refresh(); +} + wxMenuItem* DesignPanel::append_offer_item(wxMenu* menu, int id, const wxString& text, const OfferVerb& v) { diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index f2d4e80fb8..0e6113195f 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -857,6 +857,14 @@ private: // "no opinion" by setting wxNullColour, which restores exactly this — so it is the only // reliable way to tell a chosen colour (the error red) from the default. See set_status(). wxColour m_status_default_fg; + // The guidance sentence for the step the armed sketch tool is on, kept so a transient + // readout (the live length/angle while a segment is being dragged) can be appended to it + // instead of replacing it — the guidance used to vanish on the first mouse move after a + // click, which is precisely when it is needed. snaporca-1c0c. + wxString m_sketch_step; + // mode is a DesignSketchTool::Mode; passed as an int because this header deliberately does + // not include the tool's, and the .cpp (which does) casts it back. + void on_sketch_step(int mode, int step, int picks); wxStaticText* m_dof_status{nullptr}; // DoF / constraint-state readout (P3) // Last live-solve result, so entering Constrain can restore the readout without a solve. int m_dof_last{-1}; diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 6357e07e4b..9d6a3a223b 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -66,6 +66,7 @@ void DesignSketchTool::begin(const SketchPlane& plane, Mode mode) { m_plane = plane; m_mode = mode; + m_step_mode_last = -1; // a new session re-announces its step, even if it repeats the last m_points.clear(); m_entities.clear(); m_construction = false; @@ -297,6 +298,7 @@ void DesignSketchTool::cancel() { close_session_chrome(); // same orphaned-field freeze as finish() — see snaporca-yce m_active = false; + m_step_mode_last = -1; m_points.clear(); m_entities.clear(); m_construction = false; @@ -6414,12 +6416,53 @@ int DesignSketchTool::region_at(const Vec2d& p) const // ---- rendering -------------------------------------------------------------- +// Chop a polyline into dashes (snaporca-imlq). Construction geometry is dashed in every CAD; +// this one painted it solid grey, which against the under-constrained orange reads as "another +// line", not as "reference only". The dash and gap arrive in WORLD units — the caller scales them +// by units-per-pixel, so the dash keeps its size on screen at any zoom instead of turning into a +// solid line when you zoom out and into three dashes when you zoom in. +static std::vector> dash_polyline(const std::vector& pts, bool closed, + double dash, double gap) +{ + std::vector> out; + if (pts.size() < 2 || dash <= 0.0 || gap <= 0.0) return out; + const size_t segs = closed ? pts.size() : pts.size() - 1; + bool on = true; // start on a dash, so a short entity is still visible + double left = dash; // distance remaining in the current dash/gap + std::vector cur; + if (on) cur.push_back(pts[0]); + for (size_t i = 0; i < segs; ++i) { + const Vec2d a = pts[i], b = pts[(i + 1) % pts.size()]; + double seg = (b - a).norm(); + if (seg < 1e-12) continue; + const Vec2d dir = (b - a) / seg; + double t = 0.0; + while (seg - t > left) { + t += left; + const Vec2d p = a + dir * t; + if (on) { cur.push_back(p); out.push_back(cur); cur.clear(); } + else { cur.clear(); cur.push_back(p); } + on = !on; + left = on ? dash : gap; + } + left -= (seg - t); + if (on) cur.push_back(b); + } + if (on && cur.size() >= 2) out.push_back(cur); + return out; +} + void DesignSketchTool::draw_quad_strip(GLModel& model, const std::vector& pts, bool closed, const ColorRGBA& color) { if (pts.size() < 2) return; - const double hw = 0.6; + // Half-width in WORLD units, so a stroke is 2*hw mm wide on the plane. Halved from 0.6 on + // user report 2026-08-23: at 1.2 mm the orange under-constrained line was heavy enough to + // swallow a short segment and to hide which of two near-parallel lines the cursor was on. + // Every one of this function's call sites is a sketch stroke (entities, previews, rubber + // bands), which is why the constant is here and not a parameter at twenty call sites. + const double hw = 0.3; GLModel::Geometry g; g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 }; unsigned int base = 0; @@ -8156,10 +8199,37 @@ const ColorRGBA* DesignSketchTool::sketch_hl_color(int feature) const return nullptr; } +// Which step of the armed gesture is live, reported only when it moves (snaporca-1c0c). Called +// from render(), which is the one place EVERY state change passes through — a per-call-site +// notification would have to be added to each of the thirty-odd tool branches and would be +// forgotten by the next one. Cheap: three ints compared per frame. +void DesignSketchTool::emit_step_hint() +{ + if (!on_step_changed) return; + int step = 0, picks = 0; + if (is_edit_op_mode()) { + picks = (m_mode == Mode::Mirror) ? int(m_mirror_targets.size()) + : int(m_op_a >= 0) + int(m_op_b >= 0); + step = (m_op_a < 0) ? 0 : (op_ready() ? 2 : 1); + } else if (is_transform_mode()) { + picks = int(m_tf_targets.size()); + step = m_tf_targets.empty() ? 0 : 1; + } else if (m_mode == Mode::Select || m_mode == Mode::Constrain) { + picks = int(m_selection.size()); + } else { + step = int(m_points.size()); + } + if (int(m_mode) == m_step_mode_last && step == m_step_last && picks == m_step_picks_last) + return; + m_step_mode_last = int(m_mode); m_step_last = step; m_step_picks_last = picks; + on_step_changed(m_mode, step, picks); +} + void DesignSketchTool::render(GLCanvas3D& canvas) { m_dim_label_seq = 0; m_render_scale = canvas.get_scale(); + emit_step_hint(); // before the early returns: an armed tool on an empty sketch still guides (void)canvas; if (!has_display()) { if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD @@ -8389,6 +8459,8 @@ void DesignSketchTool::render(GLCanvas3D& canvas) const ColorRGBA white(1.0f, 1.0f, 1.0f, 1.0f); const ColorRGBA green(0.30f, 0.85f, 0.42f, 1.0f); const ColorRGBA conflict(1.0f, 0.22f, 0.22f, 1.0f); + const ColorRGBA opref(0.80f, 0.45f, 1.0f, 1.0f); // violet: the edit-op's reference pick + const double upp_dash = 1.0 / std::max(camera.get_zoom(), 1e-6); // world units per pixel const ColorRGBA editing(1.0f, 0.78f, 0.10f, 1.0f); // amber: entity whose dim is being typed const bool fully = (m_dof == 0 && m_solve_ok); // While an auto-edit value field is open, the active step names the entities its dimension @@ -8405,8 +8477,15 @@ void DesignSketchTool::render(GLCanvas3D& canvas) const bool editing_this = edit_hi && std::find(edit_hi->begin(), edit_hi->end(), int(i)) != edit_hi->end(); const bool bad = i < m_entity_conflict.size() && m_entity_conflict[i]; + // The first pick of an edit-op has a DIFFERENT ROLE from the rest of the selection — + // Mirror's is the axis, Fillet/Chamfer's is the first of the two lines — and until now + // every pick painted the same white, so the picture could not answer "what did I select + // as what". Violet, not cyan: cyan means SELECTED here and nothing else may wear it. + // snaporca-vd6v. + const bool op_ref = is_edit_op_mode() && int(i) == m_op_a; ColorRGBA col; if (editing_this) col = editing; + else if (op_ref) col = opref; else if (selected) col = white; else if (bad) col = conflict; else if (e.construction) col = grey; @@ -8417,7 +8496,13 @@ void DesignSketchTool::render(GLCanvas3D& canvas) } bool closed = false; std::vector poly = entity_polyline(e, closed); - draw_quad_strip((selected || editing_this) ? m_highlight_model : m_line_model, poly, closed, col); + GLModel& target = (selected || editing_this || op_ref) ? m_highlight_model : m_line_model; + if (e.construction) { + for (const std::vector& d : dash_polyline(poly, closed, 9.0 * upp_dash, 6.0 * upp_dash)) + draw_quad_strip(target, d, false, col); + } else { + draw_quad_strip(target, poly, closed, col); + } } if (!point_markers.empty()) draw_vertices(m_vertex_model, point_markers, yellow); diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index 8aa9809048..fd2f406705 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -67,6 +67,7 @@ public: // row off arms a NEIGHBOURING tool and then grades whatever that drew. snaporca-ekt9. Mode mode() const { return m_mode; } int pending_points() const { return int(m_points.size()); } + 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". bool value_field_open() const { return m_awaiting_length; } @@ -435,6 +436,13 @@ public: // Live readout while drawing a Line/Polyline segment (anchor->cursor metrics). std::function on_cursor_metrics; + // Live step guidance (snaporca-1c0c). The armed tool reports WHICH STEP of its gesture the + // user is on, every time that changes, so the status line can name the next click instead of + // repeating the one-shot sentence written when the tool was armed. step = anchors/picks + // already down (Mirror: 0 = no axis, 1 = axis down, 2 = ready to apply); picks = size of the + // set the gesture accumulates (mirror targets, transform targets, Select's selection). + std::function on_step_changed; + // DoF feedback (P3): solver state after each live solve. dof>0 = under-constrained, // dof==0 = fully constrained, ok==false = conflicting/inconsistent constraints. // has_constraints is false while the sketch carries no driving constraints yet. @@ -1005,6 +1013,10 @@ private: // In-canvas edit-op gizmo state (Fillet/Chamfer/Offset/Mirror). GUI-only, reset by // set_tool/cancel. Fillet/Chamfer: m_op_a,m_op_b = the two lines; Offset: m_op_a = src; // Mirror: m_op_a = axis line, m_mirror_targets = entities to mirror. + // Last (mode, step, picks) reported through on_step_changed; -1 mode = nothing reported yet. + int m_step_mode_last{-1}; + int m_step_last{-1}; + int m_step_picks_last{-1}; int m_op_a{-1}; int m_op_b{-1}; double m_op_value{0.0}; // radius / setback / signed offset distance