Fix Design tab getting stuck unable to start a new sketch

After undo, redo, New Design, a project load, a delete or a reorder, a sketch
profile picked beforehand stayed selected even though it was gone. The
right-click menu then offered "Sketch profile" over an empty design with Create
greyed out. Those operations now clear the selection.

The Confirm button no longer stays greyed in a sketch session after a card whose
preview was invalid has been closed.

Delete and reorder now wait while a sketch or constrain session, the Text dialog,
an Insert placement or the move gizmo is open, instead of editing whichever
feature took its place in the list.
This commit is contained in:
SoftFever
2026-10-04 00:01:05 +08:00
parent 25f755c434
commit 1d06b8576b
5 changed files with 120 additions and 64 deletions
+20 -3
View File
@@ -679,12 +679,27 @@ void DesignCanvas::clear_loop_pick()
m_sketch_tool.clear_display_pick();
}
void DesignCanvas::clear_solid_pick()
{
m_sketch_tool.clear_solid_selection();
}
void DesignCanvas::set_loop_pick(int feature, int region)
{
m_sketch_tool.set_display_pick(feature, region);
request_repaint();
}
int DesignCanvas::loop_pick_feature() const
{
return m_sketch_tool.display_pick();
}
int DesignCanvas::loop_pick_region() const
{
return m_sketch_tool.display_pick_region();
}
void DesignCanvas::set_escalate_on_repick(bool on)
{
m_sketch_tool.set_escalate_on_repick(on);
@@ -1472,18 +1487,20 @@ bool DesignCanvas::drawing_in_progress() const
bool DesignCanvas::has_any_selection() const
{
return m_sketch_tool.has_solid_selection() || m_sketch_tool.sketch_has_selection();
return m_sketch_tool.has_solid_selection() || m_sketch_tool.sketch_has_selection()
|| m_sketch_tool.display_pick() >= 0;
}
bool DesignCanvas::clear_any_selection()
{
if (!has_any_selection()) return false;
// Both, unconditionally: which of the two is live depends on the mode, and Esc at idle means
// "nothing is picked" in either of them. clear_selection() reports through the tool's own
// All three, unconditionally: which is live depends on the mode, and Esc at idle means
// "nothing is picked" in any of them. clear_selection() reports through the tool's own
// on_selection_changed; the solid side has no such notification, so the panel refreshes what
// depends on it (see DesignPanel::escape).
m_sketch_tool.clear_selection();
m_sketch_tool.clear_solid_selection();
m_sketch_tool.clear_display_pick();
// clear_solid_selection() is silent by design (recomputes call it while ids are invalid), but
// the panel mirrors the pick to aim Extrude and the dress-up tools. An Esc that cleared the
// highlight without telling the panel would leave those aimed at a body nothing points to.
+7 -2
View File
@@ -124,7 +124,12 @@ public:
// consumed loop must be compared against.
std::vector<std::vector<int>> region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const;
void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude)
void clear_solid_pick(); // drop the solid pick and its highlight; no callback, no repaint
void set_loop_pick(int feature, int region); // adopt a loop pick made before the commit
// The picked committed sketch (feature index, -1 = none) and its closed region (-1 also when
// the click hit a stroke on no closed loop). The panel reads the pick here and keeps no copy.
int loop_pick_feature() const;
int loop_pick_region() const;
void set_escalate_on_repick(bool on); // off while a card has armed a face/edge pick
// Solid whole/face/edge selection: point the tool at the bodies + concatenated
// tessellation (with per-triangle face & body ids), and a callback fired on each
@@ -312,8 +317,8 @@ public:
bool sketch_disarm_tool(); // CadLevel::Tool — armed sketch tool falls back to Select
bool sketch_confirm_pending(); // Enter — apply a ready edit-op or transform
bool drawing_in_progress() const;// clicks or picks are down but nothing is committed yet
bool has_any_selection() const; // model pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop both; true if anything was dropped
bool has_any_selection() const; // model pick, committed-loop pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop all three; true if anything was dropped
bool sketch_first_selected_type(SketchEntity::Type& out) const;
// Live sketch session (Fase 4.2 live constraint path): the panel reads the in-session
// selection and entities, and commits a planned constraint through the tool's
+84 -54
View File
@@ -707,7 +707,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Onshape push/pull: an explicitly picked solid face (Face-level cycle, no loop
// selected) is extruded as the profile — this takes priority over re-extruding an
// already-consumed sketch (resolve_extrude_sketch always returns the last Sketch).
if (m_sel_solid_face >= 0 && !m_doc.body.IsNull() && m_sel_sketch_region < 0) {
if (m_sel_solid_face >= 0 && !m_doc.body.IsNull()
&& (m_viewport == nullptr || m_viewport->loop_pick_region() < 0)) {
m_extrude_face_src = m_sel_solid_face;
m_extrude_sketch_ref = -1;
open_tool(Tool::Extrude);
@@ -3634,8 +3635,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
open_tool(Tool::Extrude);
// AFTER open_tool, not before: opening the tool re-derives the selection state, so a
// region recorded ahead of it is wiped before Extrude ever reads it.
m_sel_sketch_feat = m_extrude_sketch_ref;
m_sel_sketch_region = region;
m_sel_solid_face = m_sel_solid_edge = -1;
m_pick_face = m_pick_face_body = -1;
m_viewport->set_loop_pick(m_extrude_sketch_ref, region);
@@ -3644,11 +3643,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Clicking a committed sketch loop on the plate (no live session) selects THAT loop:
// the viewport highlights only it (cyan) and its Sketch feature's tree row is selected.
// The (feature, region) pair is remembered so Extrude builds just that one loop.
// The viewport keeps the (feature, region) pair so Extrude builds just that one loop.
m_viewport->set_on_display_sketch_selected([this](int feat, int region, int entity) {
if (feat < 0 || feat >= int(m_doc.features.size())) return;
m_sel_sketch_feat = feat;
m_sel_sketch_region = region;
// Last pick wins (symmetric with the solid-pick handler): selecting a sketch loop drops
// any stale solid face/edge pick so Extrude treats this loop as the profile.
m_sel_solid_face = m_sel_solid_edge = -1;
@@ -3734,12 +3731,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// but the user pointed AT a face and a sketch should be able to use it. 3a2.
m_pick_face_body = (level >= 1) ? body : -1;
m_pick_face = (level >= 1) ? face : -1;
// Last pick wins: selecting a solid drops any stale committed-sketch loop selection.
// Otherwise a leftover loop keeps `m_sel_sketch_region >= 0`, which blocks the face
// push/pull branch in Extrude (`m_sel_solid_face >= 0 && m_sel_sketch_region < 0`) and
// makes Extrude build a DETACHED new body from the last sketch instead of push/pulling
// the face the user just clicked.
if (level >= 1) { m_sel_sketch_region = -1; m_sel_sketch_feat = -1; }
// Last pick wins: a leftover loop pick would block Extrude's face push/pull branch, so
// Extrude would extrude a sketch instead of push/pulling the clicked face.
if (level >= 1) m_viewport->clear_loop_pick();
// 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
@@ -4471,6 +4465,10 @@ void DesignPanel::set_ui_mode(UiMode m)
{
m_ui_mode = m;
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
// for editing makes one with its first click.
if (m == UiMode::Sketch && m_viewport != nullptr) m_viewport->clear_loop_pick();
// The DoF readout describes a SKETCH's constraint state, so it means nothing back in Feature
// mode — where it nonetheless stayed on screen after every Confirm, Cancel and Escape
// (752). Cleared here rather than at those three exits because this is the one place
@@ -5207,9 +5205,9 @@ void DesignPanel::on_add_sketch()
bool DesignPanel::extrude_uses_loop() const
{
return m_viewport != nullptr
&& m_sel_sketch_region >= 0
&& m_viewport->loop_pick_region() >= 0
&& m_extrude_sketch_ref >= 0
&& m_extrude_sketch_ref == m_sel_sketch_feat
&& m_extrude_sketch_ref == m_viewport->loop_pick_feature()
&& m_extrude_sketch_ref < int(m_doc.features.size())
&& !m_viewport->selected_loop_entities().empty();
}
@@ -5236,13 +5234,12 @@ void DesignPanel::on_add_extrude()
// other loops intact and still selectable.
if (::getenv("ORCA_CAD_PICK_TRACE"))
std::fprintf(stderr, "[pick] on_add_extrude: feat=%d reg=%d ents=%zu\n",
m_extrude_sketch_ref, m_sel_sketch_region,
m_extrude_sketch_ref, m_viewport->loop_pick_region(),
m_viewport->selected_loop_entities().size());
idx = m_doc.add_extrude_entities(m_viewport->selected_loop_entities(),
m_doc.features[m_extrude_sketch_ref].plane,
m_distance->GetValue(), false, mode, name);
m_sel_sketch_region = -1; // consume the loop selection
m_viewport->clear_loop_pick(); // drop the now-stale loop highlight
m_viewport->clear_loop_pick(); // consume the loop selection
} else {
idx = m_doc.add_extrude(m_extrude_sketch_ref, m_distance->GetValue(), false, mode, name);
}
@@ -6245,7 +6242,12 @@ int DesignPanel::offer_selection_kind() const
m_sel_solid_face);
return int(GeometryEngine::cylinder_of_face(f).ok ? OfferSel::FaceCyl : OfferSel::FaceOther);
}
if (m_sel_sketch_region >= 0)
// Validated like the body index above: a loop pick that names no sketch, or no closed region
// of one, must not title the menu "Sketch profile" and grey Create, which is what a stale one
// did over an empty document.
if (const int lf = m_viewport ? m_viewport->loop_pick_feature() : -1;
lf >= 0 && lf < int(m_doc.features.size()) && m_doc.features[lf].type == CadFeatureType::Sketch
&& m_viewport->loop_pick_region() >= 0)
return int(OfferSel::SkLoop);
if (m_sel_solid_body >= 0 && m_sel_solid_body < nb)
return int(CadDocument::is_sheet_shape(m_doc.bodies[m_sel_solid_body].shape)
@@ -7447,6 +7449,7 @@ void DesignPanel::load_recipe(const std::string& blob)
return;
}
m_feature_counter = int(m_doc.features.size());
drop_selection();
feed_bodies(); // push the restored bodies into the viewport
refresh_tree(); // rebuild the feature tree from the restored recipe
set_status_ok();
@@ -7834,14 +7837,52 @@ void DesignPanel::set_tree_selection(int row)
m_tree->SelectItem(m_tree_items[row]);
}
// The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and
// features by index, so replacing or renumbering the feature list (undo/redo, New Design, load,
// delete, reorder) drops it, the viewport's highlights with it. The solid highlight too: a rebuild
// that leaves no body never reaches set_solid_pick. The callers repaint.
void DesignPanel::drop_selection()
{
m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1;
m_sel_solid_edges.clear();
m_sel_solid_vertex = false;
m_pick_face = m_pick_face_body = -1;
if (m_viewport != nullptr) {
m_viewport->clear_loop_pick();
m_viewport->clear_solid_pick();
}
}
// The shared front of delete and reorder, which renumber the feature list. A sketch or constrain
// session, the Text dialog, an Insert placement and the standalone move gizmo can each hold a
// feature or body index (m_edit_index, m_constrain_feat, m_text_feat, m_insert_feat, m_move_body)
// that a renumber would point at something else, so they are refused until Finish or Cancel; Undo
// and mcp_busy() refuse most of the same states. An open feature card is closed instead,
// discarding its candidate, rather than linger out of step with the tree.
bool DesignPanel::begin_renumber()
{
if (m_ui_mode != UiMode::Feature || m_text_dlg != nullptr || m_active == Tool::Insert
|| (m_active == Tool::None && m_viewport != nullptr && m_viewport->moving_body())) {
set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
return false;
}
if (m_active != Tool::None || m_edit_index >= 0) cancel_tool();
m_doc.checkpoint(); // undo boundary
drop_selection();
return true;
}
void DesignPanel::after_tree_edit(bool ok)
{
update_undo_redo_buttons();
refresh_tree();
refresh_variables();
if (!ok) {
// The edit was rolled back (recompute failed); the body is unchanged.
// The edit was refused or rolled back; the body is unchanged. Repaint anyway: picks the
// caller dropped before trying it are still drawn, and set_status() repaints only when
// its text changes.
set_status(StatusKind::Error, wxString::Format(_L("Edit rejected: %s"), kernel_error_text(m_doc.error)));
if (m_viewport != nullptr) m_viewport->request_repaint();
return;
}
sync_recipe_to_model(); // deletes, reorders and suppressions change the document too
@@ -7887,6 +7928,7 @@ void DesignPanel::clear_document()
m_doc.clear(); // features + bodies + meshes + history
m_doc.auto_close_loops = wxGetApp().is_auto_close_sketch_loops(); // a new design: today's preference
Slic3r::set_sketch_auto_close(m_doc.auto_close_loops);
drop_selection();
m_edit_index = -1;
m_move_body = -1;
show_move_card(false);
@@ -7920,15 +7962,7 @@ void DesignPanel::on_delete_body()
const std::string& raw = m_doc.features[src].name;
const wxString fname = raw.empty() ? wxString::Format(_L("feature %d"), src + 1)
: wxString::FromUTF8(raw);
// A card left open over a feature that is about to vanish goes stale — same reason
// on_delete_feature() closes it.
if (m_active != Tool::None || m_edit_index >= 0) {
reset_edit_state();
close_tool();
}
m_doc.checkpoint(); // undo boundary: deleting a body's feature
m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1; // the selection is about to
m_sel_solid_vertex = false; // name a body that is gone
if (!begin_renumber()) return;
const bool ok = m_doc.remove_feature(src);
after_tree_edit(ok);
if (ok)
@@ -7948,14 +7982,7 @@ void DesignPanel::on_delete_feature()
set_status(StatusKind::Info, _L("Select a feature in the tree first"));
return;
}
// If a feature dialog is open (e.g. the feature is being edited), dismiss it first —
// otherwise the deleted feature's settings card lingers in the left panel, out of sync
// with the tree. reset_edit_state() drops the stale m_edit_index; close_tool() hides the card.
if (m_active != Tool::None || m_edit_index >= 0) {
reset_edit_state();
close_tool();
}
m_doc.checkpoint(); // undo boundary: deleting a feature
if (!begin_renumber()) return;
after_tree_edit(m_doc.remove_feature(sel));
}
@@ -8022,7 +8049,7 @@ void DesignPanel::on_move_feature(int delta)
int target = sel + delta;
if (target < 0 || target >= int(m_doc.features.size()))
return; // already at the end
m_doc.checkpoint(); // undo boundary: reordering a feature
if (!begin_renumber()) return;
if (m_doc.move_feature(sel, delta)) {
after_tree_edit(true);
set_tree_selection(target); // keep the moved feature selected
@@ -8058,9 +8085,8 @@ void DesignPanel::on_begin_constrain(int sel_override)
// this verb from a SkLoop selection (a region clicked on screen), which carries no tree
// selection — without this, choosing "Constrain sketch" from the offer would answer
// "Select a sketch in the tree first" about a sketch the user has visibly selected.
if ((sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) && m_sel_sketch_feat >= 0
&& m_sel_sketch_feat < int(m_doc.features.size())) {
sel = m_sel_sketch_feat;
if (sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) {
sel = m_viewport ? m_viewport->loop_pick_feature() : wxNOT_FOUND; // range-checked below
set_tree_selection(sel); // keep the tree in step with what the viewport says
}
if (sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) {
@@ -10991,7 +11017,8 @@ void DesignPanel::refresh_preview()
default: ready = _L("Sketch ready"); break;
}
set_status(StatusKind::Ok, ready);
for (::Button* b : m_confirm_btns) if (b) b->Enable(true);
m_candidate_ok = true;
update_confirm_button();
m_status->Refresh();
update_datum_gizmo(); // Plane card: show/refresh the in-canvas resize handles
update_helix_gizmo(); // Helix card: draw the live curve + drag handles (no solid ghost)
@@ -11034,7 +11061,8 @@ void DesignPanel::refresh_preview()
set_status(StatusKind::Error, wxString::Format(_L("Invalid: %s"), kernel_error_text(err)));
}
}
for (::Button* b : m_confirm_btns) if (b) b->Enable(ok);
m_candidate_ok = ok;
update_confirm_button();
m_status->Refresh();
return;
}
@@ -11080,8 +11108,8 @@ void DesignPanel::refresh_preview()
}
// Onshape parity: a broken candidate cannot be committed. Grey the active dialog's
// Confirm so the user sees the gate before clicking; the red status says why.
for (::Button* b : m_confirm_btns)
if (b != nullptr) b->Enable(ok);
m_candidate_ok = ok;
update_confirm_button();
// Fillet/Chamfer/Draft: once the target edge/face yields a valid result, show ONLY the
// preview (hide the base bodies) so the user sees the finished shape, not the old solid
// doubled with the ghost. Before a valid pick the body stays visible so it can be picked.
@@ -11178,6 +11206,7 @@ void DesignPanel::push_polygon_params()
void DesignPanel::open_tool(Tool t)
{
m_active = t;
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
// here so the body is fully visible for picking the edge/face.
@@ -11680,11 +11709,14 @@ bool DesignPanel::confirm_enabled() const
return (m_viewport && m_viewport->moving_body())
|| m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain;
if (m_active == Tool::Insert) return true;
return m_candidate_ok;
}
void DesignPanel::update_confirm_button()
{
const bool ok = confirm_enabled();
for (::Button* b : m_confirm_btns)
if (b != nullptr && b->IsShownOnScreen()) return b->IsEnabled();
for (::Button* b : m_confirm_btns)
if (b != nullptr) return b->IsEnabled();
return true;
if (b != nullptr) b->Enable(ok);
}
// Which level of the interaction stack one Esc press belongs to. The rule itself lives in
@@ -11752,8 +11784,6 @@ void DesignPanel::escape()
// both of which say which one they are, and never through a key pressed on the way out of
// something else.
if (m_viewport && m_viewport->clear_any_selection()) {
m_sel_sketch_region = -1;
m_sel_sketch_feat = -1;
set_status(StatusKind::Info, wxString());
return;
}
@@ -11790,6 +11820,7 @@ void DesignPanel::update_undo_redo_buttons()
void DesignPanel::update_action_bar()
{
update_undo_redo_buttons(); // mode/tool changes flip the do_undo_redo gate -> refresh greying
update_confirm_button(); // ...and the ✓'s greying
if (m_tb_action == nullptr || m_toolbar == nullptr) return;
wxSizer* s = m_toolbar->GetSizer();
if (s == nullptr) return;
@@ -11841,10 +11872,9 @@ void DesignPanel::do_undo_redo(bool redo)
set_status(StatusKind::Info, redo ? _L("Nothing to redo") : _L("Nothing to undo"));
return;
}
// The solid whole/face/edge pick and any in-place edit reference ids that recompute()
// invalidates — drop them before refreshing from the restored document.
m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1;
m_pick_face = m_pick_face_body = -1; // recompute() invalidated the face ids too
// The picks and any in-place edit reference ids that recompute() invalidates — drop them
// before refreshing from the restored document.
drop_selection();
reset_edit_state();
after_tree_edit(true); // refresh tree + viewport meshes + status from the restored doc
set_status(StatusKind::Info, wxString::Format(redo ? _L("Redo (%zu more)") : _L("Undo (%zu more)"),
+7 -5
View File
@@ -140,9 +140,10 @@ private:
// used to be handled in four places that could not see each other, and that is how two
// presses in a row reached past a tool and discarded the sketch under it.
CadLevel escape_level() const;
bool confirm_enabled() const; // would the ✓ act right now (and is it not greyed)?
bool confirm_enabled() const; // would the ✓ act right now? (its greying follows this)
void escape();
void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active
void update_confirm_button(); // grey the ✓ to confirm_enabled()
void on_shape_changed();
void on_add_sketch();
@@ -294,6 +295,8 @@ private:
void after_edit_op(); // shared edit-op refresh tail
void on_edit_feature(); // reopen the selected feature's dialog populated
void after_tree_edit(bool ok); // shared post-op refresh of tree/viewport/status
void drop_selection(); // forget the selection (solid, hit face, sketch loop): the feature list was replaced or renumbered
bool begin_renumber(); // before delete/reorder: refuse while an index is held, else close the card, checkpoint, drop picks
void load_feature_into_dialog(const CadFeature& f);
void reset_edit_state(); // back to add-mode (m_edit_index = -1)
@@ -760,10 +763,6 @@ private:
int m_pl_edgeA_body{-1}, m_pl_edgeA{-1};
int m_pl_edgeB_body{-1}, m_pl_edgeB{-1};
PlanePick m_plane_pick{PlanePick::None}; // which ref the next solid pick fills
// Plate loop selection (click a committed sketch loop): the Sketch feature + the
// clicked closed-region index, so Extrude builds just that one loop. -1 = none.
int m_sel_sketch_feat{-1};
int m_sel_sketch_region{-1};
// Click-selected solid topology (whole/face/edge cycle): face id for up-to-face / dress-up.
int m_sel_solid_body{-1}; // which body the face/edge selection is on
int m_sel_solid_face{-1};
@@ -976,6 +975,9 @@ private:
int m_feature_counter{0};
std::vector<::Button*> m_confirm_btns;
// refresh_preview's verdict on the open card's candidate (open_tool resets it). Kept here, not
// read back from the ✓, which every card and mode shares; the ✓ is greyed from confirm_enabled().
bool m_candidate_ok{true};
// Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit
// (Sketch or Extrude independently) uses only m_edit_index as the row to replace.
+2
View File
@@ -254,6 +254,8 @@ public:
// sketch there is nothing left that would set this — and selected_loop_entities(), which is
// what Extrude consumes, reads exactly these two fields.
void set_display_pick(int feature, int region) { m_display_pick = feature; m_display_pick_region = region; }
int display_pick() const { return m_display_pick; }
int display_pick_region() const { return m_display_pick_region; }
// Visual Extrude gizmo (C5b). The Extrude tool is a DesignPanel docked card, so the
// sketch tool is NOT active during it; the panel feeds the profile plane + a 2D centroid