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