diff --git a/docs/HLSD/design-tab.md b/docs/HLSD/design-tab.md index 45da2fa607..58bab4bfcb 100644 --- a/docs/HLSD/design-tab.md +++ b/docs/HLSD/design-tab.md @@ -135,7 +135,7 @@ contract between them is stated in code rather than spread across handlers. | --- | --- | --- | | `Transient` | a value field or a popup menu | closes it; the tool stays armed | | `Gesture` | an uncommitted delta — an entity being drawn, a body being dragged | reverts it; committed work is untouched | -| `Tool` | a feature card, an armed sketch tool, a constrain session | exits it; drawn entities survive | +| `Tool` | a feature card, Sketch waiting for its plane, an armed sketch tool, a constrain session | exits it; drawn entities survive | | `Idle` | nothing transient | clears the selection, a Feature tree or Bodies row included; leaves a sketch session only if it is empty | `cad_escape_level()` is a `constexpr` free function over a POD of four booleans rather than a @@ -181,10 +181,18 @@ canvas (a sibling of the canvas, not a child over it — on GTK a child window o a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that changes any of the three belongs with a change to this section. +Sketch mode is never entered without a plane under it, so the banner, the sketch keys and the +sketch offer always have a session to act on. Sketch on a picked flat face or reference plane opens +the session on it at once. With nothing picked it stays in Feature mode and waits for one — an +armed `Tool`, left with `Esc` or ✗, and ended by anything that starts another edit — and the +reference plane or flat face clicked next opens the session. A picked plane is a selection like a +face: the sketch on it uses it up, and `Esc` or a click on nothing lets go of it, so a plane that +can no longer be seen never decides where the next sketch goes. + The reference planes — XY, XZ and YZ through the modeling origin, with their half-axes — are drawn on demand, because three translucent squares over every model are noise once they are not -the thing being picked. Sketch mode brings them up, since choosing the sketch plane is exactly -when they are picked; a live session draws none, and Finish or Cancel takes them away. The +the thing being picked. Sketch brings them up while it waits for a plane, which is exactly when +they are picked, and the session the pick opens takes them away; a live session draws none. The Feature tree's Origin row, pinned above the features and never removable, keeps them up outside a sketch. Its state is a view preference in AppConfig rather than part of the recipe, so it costs the project format nothing. The Plane tool keeps its own rule: the planes and the datums as Offset diff --git a/scripts/CAD/check-gui-click-edit.py b/scripts/CAD/check-gui-click-edit.py index 9d202a35b0..b0ee63dee9 100755 --- a/scripts/CAD/check-gui-click-edit.py +++ b/scripts/CAD/check-gui-click-edit.py @@ -480,13 +480,14 @@ def type_into_open_field(value, mark): def enter_sketch(timeout=180): """Open a real sketch on a real plane, and PROVE it with the socket before drawing anything. - THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. The reference planes are hidden until Shift+S - enters sketch MODE, so the plane is clicked AFTER it. Entering with a plane an earlier rung - chose pops the plane offer at once, and a click outside a popup menu only closes it, so the - plane is clicked twice, apart: the second click always lands on the plane. No Escape — with - no offer up, Escape in an empty sketch leaves it. Then a tool letter starts the session. - check-gui-sketching.py does the same steps. This ladder once did two of them — Design tab, - then Shift+S — and went straight to the tool letters. + THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. The reference planes are hidden until Shift+S, + with nothing picked, brings them up to choose from, so the plane is clicked AFTER it. That + click opens the session on the plane and pops the tool offer, and a click outside a popup + menu only closes it, so the spot is clicked twice, apart: the second click closes the offer, + and inside the session it is empty canvas. No Escape — with no offer up, Escape in an empty + sketch leaves it. Then a tool letter arms the tool. check-gui-sketching.py does the same + steps. This ladder once did two of them — Design tab, then Shift+S — and went straight to + the tool letters. That intermediate state is the trap. `is_sketching` reads 1, every tool key is accepted and traced, and not one click draws anything, because there is no plane under them. The ladder @@ -502,10 +503,10 @@ def enter_sketch(timeout=180): click(132, 53) # Design tab time.sleep(2.0) dismiss_modals() - key("shift+s", 1.0) # sketch mode brings the reference planes up - click(*PLANE_PX, pause=0.8) # closes the offer if an earlier rung's plane popped it... - click(*PLANE_PX) # ...so this is the click that picks the plane IN THE VIEWPORT - key("p", 0.6) # any sketch tool starts the session on that plane + key("shift+s", 1.0) # nothing picked: the reference planes come up + click(*PLANE_PX, pause=0.8) # picks the plane IN THE VIEWPORT: session open, offer up... + click(*PLANE_PX) # ...and this closes the offer + key("p", 0.6) # arms Point in the session if try_call("sketch_describe") is not None: # NO Escape here. Every rung already opens with one to drop whatever tool the last # one left armed, and Escape in the Design tab walks a LIFO: first press drops the @@ -520,7 +521,7 @@ def enter_sketch(timeout=180): # tool key, the clicks that draw it, and one distinct value per queued field. The values are # deliberately nothing like the as-drawn size, so a committed prefill cannot coincide with them. -# Where the plane label sits in the viewport once sketch mode has brought the planes up. Same +# Where the plane label sits in the viewport once Shift+S has brought the planes up. Same # constant the gesture ladder uses; it is a label on the 3D view, not a widget, so it moves only # if the camera does. PLANE_PX = (913, 359) diff --git a/scripts/CAD/check-gui-sketching.py b/scripts/CAD/check-gui-sketching.py index 16a7713444..9d8729c49f 100644 --- a/scripts/CAD/check-gui-sketching.py +++ b/scripts/CAD/check-gui-sketching.py @@ -268,16 +268,17 @@ def reset_document(): def enter_sketch(tool_key, plane_px=(913, 359)): """Enter a sketch the way the design law says: pick the plane in the viewport, then the tool. - Shift+S enters sketch MODE, which brings the reference planes up; the click picks one; the - tool letter then starts the session on it. Entering with a plane an earlier rung chose pops - the offer at once, and a click outside a popup menu only closes it, so the plane is clicked - twice, apart. No Escape: with no offer up, Escape in an empty sketch leaves it. All of it is - real input — nothing here goes through the socket. + leave_sketch()'s Escapes let go of every pick, so Shift+S finds nothing to sketch on and only + brings the reference planes up; the click on one opens the session on it and pops the tool + offer. A click outside a popup menu only closes it, so the same spot is clicked again, apart: + inside the session it is empty canvas, so it selects nothing if the offer is slow to appear. + No Escape: with no offer up, Escape in an empty sketch leaves it. The tool letter then arms + the tool. All of it is real input — nothing here goes through the socket. """ leave_sketch() key("shift+s", 0.8) - click(*plane_px, pause=0.8) # closes the offer if an earlier rung's plane popped it... - click(*plane_px) # ...so this is the click that picks the plane + click(*plane_px, pause=0.8) # picks the plane: the session opens and the offer pops... + click(*plane_px) # ...and this closes the offer key("p", 0.6) if try_call("sketch_describe") is None: shot("/shots/gl-enter-failed.png") diff --git a/scripts/CAD/tool_atlas.json b/scripts/CAD/tool_atlas.json index 1e7f1bf968..ef106a02ee 100644 --- a/scripts/CAD/tool_atlas.json +++ b/scripts/CAD/tool_atlas.json @@ -229,11 +229,11 @@ "none" ], "needs": {}, - "refusal": "Click a face or a reference plane in the viewport, then a sketch tool", + "refusal": "Select a flat face or a reference plane to sketch on", "gui": true, "action": "key:S+S", "icon": "design_sketch", - "hint": "Click a face or a reference plane, then pick a drawing tool" + "hint": "Sketch on the selected flat face or plane, or click one next" }, { "id": "extrude", diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index 72e29ff562..a2ced05cc0 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -481,13 +481,6 @@ void DesignCanvas::begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode if (m_canvas_widget) m_canvas_widget->Refresh(); } -void DesignCanvas::set_sketch_plane(const SketchPlane& plane) -{ - m_sketch_tool.set_plane(plane); // keeps the 2D entities; only the carrier plane changes - if (m_canvas) m_canvas->set_as_dirty(); - if (m_canvas_widget) m_canvas_widget->Refresh(); -} - void DesignCanvas::edit_sketch(const std::vector& entities, const std::vector& constraints, const SketchPlane& plane) diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index dc6b4bf41e..62becddc10 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -68,7 +68,6 @@ public: const std::vector& constraints, const SketchPlane& plane); void set_sketch_tool(DesignSketchTool::Mode mode); - void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D void set_sketch_construction(bool c); // Flip the sketch selection between construction and real geometry; returns the // number of entities changed (0 = nothing selected, caller falls back to the mode). diff --git a/src/slic3r/GUI/CAD/DesignOffer.hpp b/src/slic3r/GUI/CAD/DesignOffer.hpp index 67133573a3..f58f9ab658 100644 --- a/src/slic3r/GUI/CAD/DesignOffer.hpp +++ b/src/slic3r/GUI/CAD/DesignOffer.hpp @@ -99,7 +99,7 @@ static const bool kOfferRowFlat[] = { }; static const OfferVerb kOfferVerbs[] = { - {"sketch", L("Sketch"), 0, "Shift+S", "key:S+S", L("Click a face or a reference plane in the viewport, then a sketch tool"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Click a face or a reference plane, then pick a drawing tool")}, + {"sketch", L("Sketch"), 0, "Shift+S", "key:S+S", L("Select a flat face or a reference plane to sketch on"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Sketch on the selected flat face or plane, or click one next")}, {"extrude", L("Extrude"), 1, "Shift+E", "key:S+E", L("Create a sketch, or pick a solid face, first"), 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", L("Extrude a sketch profile, or push/pull a picked face")}, {"revolve", L("Revolve"), 1, "Shift+R", "key:S+R", L("Create a sketch profile to revolve first"), 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", L("Revolve a profile about an axis")}, {"sweep", L("Sweep"), 1, "Shift+W", "key:S+W", L("Create a profile sketch to sweep first"), 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", L("Sweep a profile along a path")}, diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index f5910157b8..ac9b5d59d6 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -302,6 +302,12 @@ static wxString origin_toggle_text(bool shown) return shown ? _L("Hide reference planes and axes") : _L("Show reference planes and axes"); } +// What Sketch asks for while it waits for a plane (DesignPanel::start_sketch). +static wxString sketch_plane_prompt() +{ + return _L("Click a reference plane or a flat face to sketch on it — Esc cancels"); +} + // Prepare outlines every numeric field (rounded, #4A4A51 on dark). wxSpinCtrlDouble is a // native GTK control that cannot draw that, and Orca's own SpinInput is int-only — it would // silently truncate a 2.5 mm radius. So the double spin keeps its arrows and validation and @@ -490,22 +496,11 @@ DesignPanel::DesignPanel(wxWindow* parent) m_construction->SetForegroundColour(dp_ctl_text()); auto select_tool = [this](DesignSketchTool::Mode mode, const wxString& hint) { if (!m_viewport) return; - if (!m_viewport->is_sketching()) { - wxString on; - const SketchPlane plane = sketch_plane_from_selection(on); - m_viewport->begin_sketch(plane, mode); - m_construction->SetValue(false); // a fresh session starts non-construction - m_sketch_on = on; // shown with the tool hint, so the target is visible - // The face has been CONSUMED as the sketch plane, so drop the pick. Leaving it live - // meant the next Extrude saw a selected face and push/pulled it instead of extruding - // the sketch just drawn — the same trap the imported-art path already guards against. - if (m_sel_solid_face >= 0 || m_pick_face >= 0) { - m_sel_solid_face = m_sel_solid_edge = m_sel_solid_body = -1; - m_pick_face = m_pick_face_body = -1; - } - } else { - m_viewport->set_sketch_tool(mode); - } + // Sketch mode opens with its session (start_sketch_on_target); this covers a mode a + // failed Constrain left without one. + if (!m_viewport->is_sketching()) + start_sketch_on_target(false); + m_viewport->set_sketch_tool(mode); m_viewport->set_sketch_construction(m_construction->GetValue()); set_status(StatusKind::Info, m_sketch_on.IsEmpty() ? hint : wxString::Format(_L("%s · on %s"), hint, m_sketch_on)); @@ -730,32 +725,9 @@ DesignPanel::DesignPanel(wxWindow* parent) }; auto* b_sketch = icon_btn("design_sketch", _L("Sketch")); - // Sketch means a tool. Pressing it used to set the mode and then ask, unconditionally, - // for the very thing the user had just done — click XZ, read "XZ plane selected, press - // Sketch", press Sketch, and be told to click a reference plane. The plane was never - // lost (m_ref_plane holds it and begin_sketch captures it when the first tool is armed); - // the sentence was simply false, and with right-click excluded in sketch mode there was - // no door to the tools at all, so the only way on was the toolbar this tab is retiring. - std::function act_sketch = [this] { - set_ui_mode(UiMode::Sketch); - wxString where; - const bool have_plane = sketch_plane_target(where); - set_status(StatusKind::Info, have_plane - ? wxString::Format(_L("Sketching on %s — pick a tool"), where) - : _L("Click a face or a reference plane in the viewport, then a sketch tool")); - if (m_sketch_hint) { // the card must agree with the status line, not argue with it - m_sketch_hint->SetLabel(have_plane - ? wxString::Format(_L("Drawing on %s.\nPick a tool, or press Menu for the list."), where) - : _L("Click a face or a reference plane, then a sketch tool.")); - m_sketch_hint->Refresh(); - m_cards->Layout(); - } - // Hand over the tools rather than naming them in a status line. CallAfter so the - // mode change has settled before a modal menu takes the loop; the menu carries each - // tool's shortcut, so pressing the key instead of picking a row costs nothing. - if (have_plane) - CallAfter([this] { show_offer_menu(offer_anchor()); }); - }; + // Sketch opens a sketch on the face or plane already picked, or asks for one first + // (start_sketch): sketch mode is never entered without a plane under it. + std::function act_sketch = [this] { start_sketch(); }; b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); }); m_keys_feature[SHIFT('S')] = act_sketch; fadd("sketch", b_sketch); @@ -1406,12 +1378,11 @@ DesignPanel::DesignPanel(wxWindow* parent) // the sketch tools, not in the generic Features strip. Each places the art // in-canvas, then commits via the Insert card's Confirm. { - // In Sketch MODE the art must land in the sketch — but begin_sketch() does not run - // until the first tool is armed, so pressing Sketch and then Text would find no - // session and commit a separate feature. Arm Select first: it starts the session on - // the picked plane without drawing anything, so Text behaves the same whether or not - // you had already drawn a line. (MODE vs SESSION — the distinction that has bitten - // this panel before.) + // In Sketch MODE the art must land in the sketch. The mode opens with its session + // (start_sketch_on_target), but a failed Constrain can leave it without one, and Text + // would then find no session and commit a separate feature. Arm Select first: it + // starts the session without drawing anything. (MODE vs SESSION — the distinction + // that has bitten this panel before.) auto ensure_sketch = [this, select_tool] { if (m_ui_mode == UiMode::Sketch && m_viewport && !m_viewport->is_sketching()) select_tool(DesignSketchTool::Mode::Select, @@ -3125,11 +3096,9 @@ DesignPanel::DesignPanel(wxWindow* parent) // looking and pointing. A combo duplicated that decision somewhere the geometry could not // see it, and once a face could be picked it went further and displayed a stale row that // contradicted the real target. e1p. - // Kept as a member, not a local: the card has to be able to STOP saying this. It asked - // for a plane even when one had just been picked, directly contradicting the status line - // two inches below it, which by then read "Sketching on XZ". - m_sketch_hint = new wxStaticText(m_cards, wxID_ANY, - _L("Click a face or a reference plane, then a sketch tool.")); + // A member, not a local: it names the plane the session opened on, which is known only + // when it opens (start_sketch_on_target writes it), so the card and the status line agree. + m_sketch_hint = new wxStaticText(m_cards, wxID_ANY, _L("Pick a tool, or press Menu for the list.")); m_sketch_hint->SetForegroundColour(dp_sec_text()); m_box_sketch_session->Add(m_sketch_hint, 0, wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 12); } @@ -3748,6 +3717,22 @@ DesignPanel::DesignPanel(wxWindow* parent) m_viewport->clear_loop_pick(); m_plane_picked = false; } + // Sketch is waiting for a plane: a flat face is one. m_pick_face is the face the click + // landed on whether it took the face, one of its edges or a corner — the rule Sketch + // applies to a face picked beforehand. A curved face says why it was not taken; a swept + // body or a click that let go of the pick leaves the prompt up. + if (sketch_plane_pick_live()) { + wxString on; + if (level >= 1 && sketch_plane_target(on)) { + start_sketch_on_target(true); + return; + } + set_status(StatusKind::Info, level >= 1 && m_pick_face >= 0 + ? _L("That face is not flat — click a flat face or a reference plane") + : sketch_plane_prompt()); + m_status->Refresh(); + return; + } // Say what got picked. Without this the ONLY feedback is the viewport highlight, so a // pick that registers but draws faintly is indistinguishable from one that never // happened — which is precisely how this failure was reported and why it resisted @@ -3934,7 +3919,9 @@ DesignPanel::DesignPanel(wxWindow* parent) // A click on nothing drops a list row as it drops a pick. Not while a card is open: the // feature being edited keeps its row. m_viewport->set_on_empty_pick([this] { - if (m_active == Tool::None && deselect_rows()) + if (m_active != Tool::None) return; + const bool row = deselect_rows(); + if (drop_plane_pick() || row) set_status(StatusKind::Info, _L("Nothing selected")); }); @@ -4012,25 +3999,25 @@ DesignPanel::DesignPanel(wxWindow* parent) refresh_plane_labels(); refresh_preview(); // re-resolve the frame + move the gizmo/ghosts to the new base } else { - // Clicking a reference plane in 3D IS how a sketch plane is chosen now. Record it and, - // when a session is already live, re-plane it immediately: begin_sketch captures the - // plane at first-tool-pick, so without this the entities would stay on the old plane - // while the committed feature landed on the new one. + // Clicking a reference plane in 3D IS how a sketch plane is chosen. The planes are + // only pickable outside a session (DesignSketchTool hit-tests them while no session + // is live), and the canvas reports the click after clearing the solid selection on the + // same miss, so the face pick is already empty and the sketch falls back to this plane. if (base >= 0) { m_ref_plane = base; m_plane_picked = true; // chosen, not merely defaulted to m_pick_face = m_pick_face_body = -1; // last pick wins: a plane beats a stale face - if (m_viewport && m_viewport->is_sketching()) - m_viewport->set_sketch_plane(plane_from_choice(m_ref_plane)); + } + if (base >= 0 && sketch_plane_pick_live()) { + start_sketch_on_target(true); // Sketch was waiting for exactly this + return; } const char* nm = (base == 0) ? "XY" : (base == 1) ? "XZ" : (base == 2) ? "YZ" : "datum"; - // Both halves named the TOOLBAR, which no longer carries either button: the tools - // moved to the offer. Name the gesture that actually works in each mode, and say - // what a plain click does, since the two are easy to confuse on a plane. - set_status(StatusKind::Ok, m_ui_mode == UiMode::Sketch - ? wxString::Format(_L("%s plane selected — right-click for the drawing tools"), nm) - : wxString::Format(_L("%s plane selected — right-click to sketch on it, " - "or click an object to select it"), nm)); + // Name the gesture that works, and what a plain click does, since the two are easy to + // confuse on a plane. + set_status(StatusKind::Ok, + wxString::Format(_L("%s plane selected — right-click to sketch on it, " + "or click an object to select it"), nm)); m_status->Refresh(); } }); @@ -4176,13 +4163,8 @@ DesignPanel::DesignPanel(wxWindow* parent) const int key = e.GetKeyCode(); const bool ctrl = e.ControlDown() || e.CmdDown(); const bool sketching = (m_ui_mode == UiMode::Sketch) && m_viewport && m_viewport->is_sketching(); - // Which key MAP applies is a question about the MODE, not about whether a session is - // already running. Gating the sketch map on is_sketching() made it unreachable by - // keyboard: is_sketching() only turns true inside select_tool()'s begin_sketch(), and - // select_tool() is what the sketch keys call — so the first letter after entering - // sketch mode fell through to the feature map, matched nothing (feature keys are - // Shift+letter), and did nothing. The mouse worked only because the toolbar flyout - // reaches select_tool() directly. That is why all 17 keys read as dead. 0ud. + // Which key MAP applies is a question about the MODE, not about the canvas tool: + // is_sketching() is true in Constrain too, which keeps the feature map. 0ud. const bool sketch_mode = (m_ui_mode == UiMode::Sketch); // Never steal editing keys from a focused text field or an open in-canvas value field — // Delete/Ctrl+Z there must edit the text, not the model. @@ -4500,6 +4482,9 @@ void DesignPanel::set_ui_mode(UiMode m) // since the gizmo is not drawn in a sketch. if (m != UiMode::Feature) end_body_move(true); m_ui_mode = m; + // Any mode change ends a plane choice under way; the refreshes below put the planes and the + // ✓/✗ right for it. + m_choosing_sketch_plane = false; if (m != UiMode::Sketch) m_sketch_on.clear(); // no stale "on the picked face" on the next hint // A committed loop picked before the session means nothing to the sketch map, but it would // still count as a selection and swallow the first Esc. The double-click that opens a sketch @@ -4546,9 +4531,8 @@ void DesignPanel::set_ui_mode(UiMode m) m_form->FitInside(); } update_action_bar(); // Sketch/Constrain modes show the unified ✓/✗; Feature idle hides it - // The reference planes follow the mode (update_reference_planes). Without this they would - // only refresh on the next tree rebuild, which is not an event that happens when you merely - // press Sketch. + // The reference planes follow the plane choice the mode change just ended + // (update_reference_planes); without this they would wait for the next tree rebuild. update_reference_planes(); // Say where you are, in words, across the top of the viewport. @@ -4699,6 +4683,7 @@ void DesignPanel::open_text_dialog(int feat) { if (m_text_dlg != nullptr) { m_text_dlg->Raise(); return; } end_body_move(true); + end_sketch_plane_choice(); // the text goes on the plane picked before, not on the next click m_text_editing = feat >= 0 && feat < int(m_doc.features.size()) && m_doc.features[feat].is_text(); m_text_feat = m_text_editing ? feat : -1; m_text_face_body = -1; @@ -5158,6 +5143,7 @@ void DesignPanel::open_insert_card(const wxString& base_name) { end_body_move(true); m_active = Tool::Insert; + end_sketch_plane_choice(); if (m_hdr_insert) m_hdr_insert->SetLabel(base_name); wxSizer* s = m_cards->GetSizer(); s->Show(m_box_insert, true, true); @@ -5203,6 +5189,7 @@ void DesignPanel::on_transform_imported(int feat_idx) if (f.imported_regions.empty()) return; end_body_move(true); // the art's placement gizmo and ✓/✗ take over from a Move left open + end_sketch_plane_choice(); // ...and from a plane choice: Scale artwork reaches here directly // In-canvas bbox handles (replaces the Move/Scale dialog): drag a corner to scale, // the centre to move. Values stream back via set_on_imported_transform. m_viewport->begin_imported_transform(feat_idx, f.imported_regions, f.plane, @@ -7658,6 +7645,16 @@ bool DesignPanel::deselect_rows() return any; } +// A picked reference plane is a selection like a face: the next Sketch opens on it, so letting go +// of everything has to let go of it too. The viewport asks for it every frame (set_selected_base). +bool DesignPanel::drop_plane_pick() +{ + if (!m_plane_picked) return false; + m_plane_picked = false; + if (m_viewport) m_viewport->request_repaint(); + return true; +} + void DesignPanel::update_section_flip_btn() { if (m_section_flip_btn) m_section_flip_btn->Enable(m_section_on); @@ -7828,6 +7825,7 @@ void DesignPanel::on_move_body() set_status(StatusKind::Info, hint); return; } + end_sketch_plane_choice(); // the gizmo takes the clicks a plane choice was waiting for sync_body_xform(); // Delta gizmo: pivot at the body's CURRENT world centroid; the tool composes the drag deltas // onto its current pose, so move + rotate both work (incl. on an already place-on-face'd body). @@ -7945,6 +7943,7 @@ bool DesignPanel::place_on_face() } const TopoDS_Face face = GeometryEngine::face_by_index(m_doc.bodies[b].shape, m_sel_solid_face); if (face.IsNull()) return false; + end_sketch_plane_choice(); // the body moves; a sketch was not started sync_body_xform(); const Transform3d old_x = m_body_xform[b]; // Outward face normal in the body's CURRENT displayed orientation. @@ -8017,6 +8016,9 @@ bool DesignPanel::begin_renumber() void DesignPanel::after_tree_edit(bool ok) { + // Any edit of the history (undo, delete, show/hide, a socket call...) writes the status line + // over the plane prompt, so it ends the choice rather than leave it waiting unannounced. + end_sketch_plane_choice(); update_undo_redo_buttons(); refresh_tree(); refresh_variables(); @@ -10094,6 +10096,7 @@ void DesignPanel::on_commit() // is shown on screen, not the pre-feature solid. (confirm_tool() applies + closes.) if (m_active != Tool::None) confirm_tool(); + end_sketch_plane_choice(); // the bodies leave for the plate; a sketch was not started if (m_doc.display_mesh.its.indices.empty()) { set_status(_L("Nothing to commit — add a feature first")); @@ -11105,11 +11108,11 @@ void DesignPanel::update_reference_planes() } return; } - // Sketch mode is where a plane is the thing being picked (act_sketch asks for "a face or a - // reference plane"); a live session draws none (DesignSketchTool::draws_reference_axes). + // Up while Sketch waits for a plane to be picked (start_sketch), which the session it opens + // takes away; a live session would draw none anyway (DesignSketchTool::draws_reference_axes). // They never block selection: clicking existing geometry wins, and a base-plane pick only // fires on a click that hit nothing else (see on_mouse fall-through). - if (m_show_origin || m_ui_mode == UiMode::Sketch) + if (m_show_origin || m_choosing_sketch_plane) m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl)); else m_viewport->clear_base_pick(); @@ -11119,9 +11122,77 @@ void DesignPanel::toggle_origin() { m_show_origin = !m_show_origin; wxGetApp().app_config->set_bool("design_show_origin", m_show_origin); + // A plane picked while they were up is not a selection once nobody can see it. + if (!m_show_origin) drop_plane_pick(); + // Not an edit: a plane choice under way stays, and keeps its planes up. refresh_tree(); // the Origin row's eye, and update_reference_planes(), which repaints } +void DesignPanel::start_sketch() +{ + if (m_ui_mode != UiMode::Feature) return; // already sketching or constraining + if (m_active != Tool::None || m_text_dlg != nullptr || body_move_pending()) { + set_status(StatusKind::Info, _L("Finish or cancel what is open first")); + return; + } + wxString where; + if (sketch_plane_target(where)) { + start_sketch_on_target(true); + return; + } + // Nothing to sketch on yet: offer the planes and stay in Feature mode until one is picked + // (set_on_datum_base_picked, set_on_solid_selection_changed). + m_choosing_sketch_plane = true; + update_reference_planes(); + update_action_bar(); // ✗ leaves the choice + set_status(StatusKind::Info, sketch_plane_prompt()); +} + +void DesignPanel::start_sketch_on_target(bool offer_tools) +{ + if (!m_viewport) return; + wxString on; + const SketchPlane plane = sketch_plane_from_selection(on); + m_viewport->begin_sketch(plane, DesignSketchTool::Mode::Select); + // The face or plane has been CONSUMED as the sketch plane, so drop the pick. A face left live + // meant the next Extrude saw a selected face and push/pulled it instead of extruding the + // sketch just drawn — the same trap the imported-art path already guards against — and a + // plane left picked would open the next sketch on it, unseen once the planes are hidden. + // The canvas's half goes too: left lit there, the next click on that face read as a second + // click on it and took the whole body, which has no plane to sketch on. + drop_solid_pick(); + m_plane_picked = false; + m_construction->SetValue(false); // a fresh session starts non-construction + set_ui_mode(UiMode::Sketch); // ends the plane choice, so the planes go + m_sketch_on = on; // shown with the tool hint, so the target is visible + set_status(StatusKind::Info, wxString::Format(_L("Sketching on %s — pick a tool"), on)); + if (m_sketch_hint) { // the card must agree with the status line, not argue with it + m_sketch_hint->SetLabel(wxString::Format(_L("Drawing on %s.\nPick a tool, or press Menu for the list."), on)); + m_sketch_hint->Refresh(); + m_cards->Layout(); + } + // Hand over the tools rather than naming them in a status line: the sketch bar carries none. + // CallAfter so the mode change has settled, and the pick that got here has returned, before a + // modal menu takes the loop; the menu carries each tool's shortcut, so pressing the key + // instead of picking a row costs nothing. + if (offer_tools) + CallAfter([this] { show_offer_menu(offer_anchor()); }); +} + +void DesignPanel::end_sketch_plane_choice() +{ + if (!m_choosing_sketch_plane) return; + m_choosing_sketch_plane = false; + update_reference_planes(); + update_action_bar(); +} + +bool DesignPanel::sketch_plane_pick_live() const +{ + return m_choosing_sketch_plane && m_ui_mode == UiMode::Feature && m_active == Tool::None + && m_text_dlg == nullptr && !body_move_pending(); +} + TriangleMesh DesignPanel::ghost_from_bodies(const std::vector& per_body) const { TriangleMesh out; @@ -11378,6 +11449,7 @@ void DesignPanel::open_tool(Tool t) { end_body_move(true); // first: arm_transform_gizmo below takes over the same gizmo m_active = t; + end_sketch_plane_choice(); // after m_active: the Plane card keeps its planes m_candidate_ok = true; // a fresh card is confirmable until its preview says otherwise // Fillet/Chamfer/Draft no longer fade the body see-through; instead, once a valid target // is picked, refresh_preview hides the base bodies entirely (preview-only). Keep it opaque @@ -11834,6 +11906,11 @@ void DesignPanel::tool_cancel() } if (m_active == Tool::Insert) { cancel_insert(); return; } if (m_active != Tool::None) { cancel_tool(); return; } + if (m_choosing_sketch_plane) { // Sketch was waiting for a plane: no sketch, planes away + end_sketch_plane_choice(); + set_status(StatusKind::Info, wxString()); + return; + } if (m_ui_mode == UiMode::Sketch) { // THE explicit discard. Esc no longer arrives here at all (it routes through escape(), // which cannot destroy anything), so this button is now the only way a drawn sketch is @@ -11895,9 +11972,10 @@ CadLevel DesignPanel::escape_level() const // being moved by a gizmo that has not been confirmed. st.gesture_active = body_move_pending() || (m_viewport && m_viewport->drawing_in_progress()); - // Something is armed and waiting for input: a feature card, a sketch draw tool, Constrain. - // A sketch SESSION is deliberately not in this list — see escape(). + // Something is armed and waiting for input: a feature card, Sketch waiting for its plane, a + // sketch draw tool, Constrain. A sketch SESSION is deliberately not in this list — see escape(). st.tool_armed = (m_active != Tool::None) + || m_choosing_sketch_plane || m_ui_mode == UiMode::Constrain || (m_ui_mode == UiMode::Sketch && m_viewport && !m_viewport->sketch_is_selecting()); st.has_selection = m_viewport && m_viewport->has_any_selection(); @@ -11934,7 +12012,10 @@ void DesignPanel::escape() // CANDIDATE (never a committed feature — in edit mode reset_edit_state only forgets which // feature was being edited, the feature itself is untouched); an armed sketch tool falls // back to Select, leaving every entity already drawn exactly where it is. - if (m_active != Tool::None || m_ui_mode == UiMode::Constrain) { tool_cancel(); return; } + if (m_active != Tool::None || m_choosing_sketch_plane || m_ui_mode == UiMode::Constrain) { + tool_cancel(); + return; + } if (m_viewport && m_viewport->sketch_disarm_tool()) { set_status(StatusKind::Info, _L("Select")); } @@ -11945,7 +12026,8 @@ void DesignPanel::escape() // both of which say which one they are, and never through a key pressed on the way out of // something else. The Feature tree and Bodies rows count as selections too. const bool row = deselect_rows(); - if ((m_viewport && m_viewport->clear_any_selection()) || row) { + const bool plane = drop_plane_pick(); + if ((m_viewport && m_viewport->clear_any_selection()) || row || plane) { set_status(StatusKind::Info, wxString()); return; } @@ -11988,6 +12070,7 @@ void DesignPanel::update_action_bar() wxSizer* s = m_toolbar->GetSizer(); if (s == nullptr) return; const bool active = (m_active != Tool::None) + || m_choosing_sketch_plane // ✗ only: ✓ has nothing to confirm || m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain || body_move_pending(); diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index ed8ccaf9f2..5f31d0fbeb 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -383,8 +383,14 @@ private: void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card) void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport void refresh_mate_connectors(); // push connector frames so verse + polarity are visible - void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, Sketch mode, Plane tool + void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, plane choice, Plane tool void toggle_origin(); // the Origin row's eye: keep the reference planes up, or not + // Sketch: open a session on the picked face or plane, or with none picked, put the planes up + // and wait for one (m_choosing_sketch_plane). + void start_sketch(); + void start_sketch_on_target(bool offer_tools); // a session on sketch_plane_from_selection() + void end_sketch_plane_choice(); // leave the plane choice without a sketch; no-op outside it + bool sketch_plane_pick_live() const; // may a pick made now still open the sketch? CadDocument m_doc; @@ -572,8 +578,13 @@ private: // deliberately no dropdown for it. e1p. int m_ref_plane{0}; // m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY" - // from "nobody has chosen anything yet". This does. + // from "nobody has chosen anything yet". This does. A selection like a picked face: the sketch + // opened on it uses it up, and Esc or a click on nothing lets go of it. bool m_plane_picked{false}; + // Sketch was pressed with nothing to sketch on: the reference planes are up and the next + // reference plane or flat face clicked opens the sketch on it. Still Feature mode — sketch + // mode is entered only with the session (start_sketch_on_target). + bool m_choosing_sketch_plane{false}; // The Feature tree's Origin row: keeps the reference planes and their axes up // (update_reference_planes). A view preference (AppConfig "design_show_origin"), not part of // the recipe. @@ -933,6 +944,7 @@ private: void request_feature_highlight(); void update_feature_highlight(); bool deselect_rows(); // Esc / a click on nothing: drop the tree and Bodies rows + bool drop_plane_pick(); // ...and a picked reference plane; true if one was picked // Bodies list under the feature tree: one row per body (parallel to m_doc.bodies). Selecting // one highlights that body and makes it the target for the next op. DesignRowList* m_parts{nullptr}; diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 92c50eefcb..e2d3d04627 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -9486,7 +9486,10 @@ const ColorRGBA* DesignSketchTool::sketch_hl_color(int feature) const // forgotten by the next one. Cheap: three ints compared per frame. void DesignSketchTool::emit_step_hint() { - if (!on_step_changed) return; + // No session, no step. render() also runs while only the reference planes are up, and m_mode + // then holds the last session's tool (Polyline before the first), whose prompt used to replace + // the panel's own line — "click a reference plane" while Sketch waits for one. + if (!on_step_changed || !m_active) return; int step = 0, picks = 0; if (is_edit_op_mode()) { picks = (m_mode == Mode::Mirror) ? int(m_mirror_targets.size()) diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index 5619c0b784..ed8f8330d5 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -173,7 +173,6 @@ public: // Returns false when no session is live, so the caller can fall back to a new feature. bool add_imported_regions(const std::vector>>& regions); void set_tool(Mode mode); // switch tool, keep accumulated entities - void set_plane(const SketchPlane& plane) { m_plane = plane; } // re-plane a live sketch (a reference plane was clicked mid-session); entities are 2D, re-lifted through the new plane void set_construction(bool c) { m_construction = c; } void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); } void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }