Design: kill the pick cycle — one click selects what is under the cursor

Tommaso, correctly: fix selection before building on it. I had taken the
whole→face→edge click cycle as terrain and hung the tool offer off it, when §10
of the charter already listed that cycle as an L5 violation. An offer can only
ever be as truthful as the selection beneath it, so this is the foundation and
it should have come first.

NOW: one click selects the SMALLEST thing under the pointer — the edge if the
cursor is within tolerance of one, otherwise the face. No repeat clicks, no
state, no memory of what was picked before. Verified by sweeping a column of
single clicks down a plate on :11: y=800..915 all report "face 5 selected", and
y=925/935 — within a few pixels of the front edge — report "edge 3 selected".
One gesture, one deterministic result, which is what L5 asks for.

TOLERANCE IS IN SCREEN PIXELS. The old edge step compared a ray-to-segment
distance in millimetres, so the same gesture meant different things at
different zoom levels. The pointer is a screen object; its tolerance has to be
one too. 8 px, measured against the edge polyline projected through the camera.

WHAT IS NOT HERE, AND WHY IT IS NOT FAKED. Whole-body selection has no viewport
gesture in this commit. Double-click is ALREADY zoom-to-fit, bound earlier in
the same on_mouse, and this pick runs on LeftUp where LeftDClick() can never be
true — so a double-click branch here would have been dead code that reads like
a working feature. I wrote one, found it unreachable, and deleted it rather
than leave it. The body gesture is the rubber band, which is its own piece of
work; until it lands bodies are selected from the Bodies list, and the hole is
named in a comment at the site instead of being left for someone to trip over.

Six status strings that promised the cycle ("click again for a face", "click
again for an edge", "click again to reset") are gone — they described a
behaviour that no longer exists, and a hint that lies is worse than none.

Both forks build. Parity: DesignSketchTool.cpp byte-identical, DesignPanel.cpp
30 divergent lines — the invariant exactly.

snaporca-6vs.
This commit is contained in:
Tommaso Bianchi
2026-07-31 12:37:21 +02:00
parent 7114e316ea
commit b003d20e37
2 changed files with 65 additions and 28 deletions
+6 -6
View File
@@ -3138,9 +3138,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
if (level <= 0)
m_status->SetLabel(_L("Selection cleared"));
else if (level == 1)
m_status->SetLabel(wxString::Format(_L("Body %d selected — click again for a face"), body + 1));
m_status->SetLabel(wxString::Format(_L("Body %d selected"), body + 1));
else if (level == 2)
m_status->SetLabel(wxString::Format(_L("Body %d, face %d — click again for an edge"), body + 1, face));
m_status->SetLabel(wxString::Format(_L("Body %d, face %d"), body + 1, face));
else
m_status->SetLabel(wxString::Format(_L("Body %d, edge %d selected"), body + 1, edge));
m_status->Refresh();
@@ -3240,9 +3240,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_status->SetForegroundColour(wxNullColour);
const int nb = int(m_doc.bodies.size());
const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString();
m_status->SetLabel(level == 1 ? bodytag + _L("selected (whole) — click again for a face")
: level == 2 ? bodytag + wxString::Format(_L("face %d selected — Extrude to push/pull it, or click again for an edge"), face)
: level == 3 ? bodytag + wxString::Format(_L("edge %d selected — open Fillet/Chamfer to dress it, or click again to reset"), edge)
m_status->SetLabel(level == 1 ? bodytag + _L("selected (whole body)")
: level == 2 ? bodytag + wxString::Format(_L("face %d selected — Extrude to push/pull it"), face)
: level == 3 ? bodytag + wxString::Format(_L("edge %d selected — Fillet/Chamfer to dress it"), edge)
: _L("Nothing selected"));
m_status->Refresh();
});
@@ -5600,7 +5600,7 @@ bool DesignPanel::place_on_face()
if (b < 0 || b >= int(m_doc.bodies.size()) || m_sel_solid_face < 0
|| b >= int(m_doc.display_body_meshes.size())) {
m_status->SetForegroundColour(wxColour(235, 110, 110));
m_status->SetLabel(_L("Pick a body face first (click a solid, then click again to a face), then press F"));
m_status->SetLabel(_L("Click a face on the solid, then press F"));
m_status->Refresh();
return false;
}
+59 -22
View File
@@ -2820,38 +2820,75 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent
if (best_face < 0 || best_body < 0 || best_body >= int(m_solid_bodies->size()))
return false; // missed the solid
if (best_body != m_sel_body || best_face != m_sel_face) {
// First click on a (new) body/face selects the WHOLE solid; refine on repeat clicks.
m_sel_body = best_body; m_sel_face = best_face; m_sel_edge = -1; m_sel_edge_pts.clear();
m_solid_sel = SolidSel::Whole;
} else if (m_solid_sel == SolidSel::Whole) {
m_solid_sel = SolidSel::Face;
} else if (m_solid_sel == SolidSel::Face) {
// Advance to the face's edge nearest the click (deterministic cycle step).
// ---- one click, one deterministic result ---------------------------------------------
// NO CYCLE. A click selects the SMALLEST thing under the cursor: the edge if the pointer is
// within tolerance of one, otherwise the face. A DOUBLE-click takes the whole body.
//
// What this replaces: click 1 = whole body, click 2 = face, click 3 = nearest edge, click 4
// = back to whole. That made "click a face" a two-click gesture and "click an edge" a
// three-click one, neither discoverable — the L5 violation §10 of the charter already
// listed, and the real reason sketching on a face kept reading as broken however often the
// plane resolution was fixed. Selection is the foundation the tool offer stands on: the
// offer can only ever be as truthful as the selection beneath it.
//
// Tolerance is measured in SCREEN PIXELS. The old edge step compared a ray-to-segment
// distance in millimetres, so the same gesture meant different things at different zooms —
// the pointer is a screen object and its tolerance has to be one too.
const Camera& cam = wxGetApp().plater()->get_camera();
const wxPoint cursor(evt.GetX(), evt.GetY());
const double kEdgeTolPx = 8.0;
auto seg_px = [](const wxPoint& p, const wxPoint& a, const wxPoint& b) { // 2D point→segment, px
const double vx = b.x - a.x, vy = b.y - a.y;
const double wx = p.x - a.x, wy = p.y - a.y;
const double L2 = vx * vx + vy * vy;
double t = (L2 > 1e-12) ? (wx * vx + wy * vy) / L2 : 0.0;
t = std::max(0.0, std::min(1.0, t));
return std::hypot(wx - t * vx, wy - t * vy);
};
m_sel_body = best_body;
m_sel_face = best_face;
m_sel_edge = -1;
m_sel_edge_pts.clear();
// WHOLE-BODY selection is deliberately NOT bound here. Double-click is already zoom-to-fit
// (see the LeftDClick branch at the top of on_mouse) and this pick runs on LeftUp, where
// LeftDClick() is never true — a double-click branch here would be dead code that reads as
// a working feature. The body gesture is the rubber band, which is its own piece of work;
// until it lands, bodies are selected from the Bodies list. Written down rather than
// half-done, because a selection model with a silent hole in it is how we got the cycle.
{
const TopoDS_Shape& bshape = (*m_solid_bodies)[m_sel_body].shape;
const TopoDS_Face face = GeometryEngine::face_by_index(bshape, m_sel_face);
int eid = -1; double best_ed = 1e30; std::vector<Vec3d> best_pts; TopoDS_Edge best_edge;
const TopoDS_Face face = GeometryEngine::face_by_index(bshape, m_sel_face);
double best_ed = 1e30; std::vector<Vec3d> ed_pts; TopoDS_Edge ed_edge; bool have_edge = false;
if (!face.IsNull()) {
const std::vector<TopoDS_Edge> edges = GeometryEngine::edges_of_face(face);
for (int k = 0; k < int(edges.size()); ++k) {
std::vector<Vec3d> pts = GeometryEngine::sample_edge_world(edges[k]);
for (const TopoDS_Edge& e : GeometryEngine::edges_of_face(face)) {
std::vector<Vec3d> pts = GeometryEngine::sample_edge_world(e);
for (Vec3d& q : pts) q = body_xform_pt(m_sel_body, q); // follow a moved body
if (pts.size() < 2) continue;
double d = 1e30;
for (size_t s = 1; s < pts.size(); ++s)
d = std::min(d, ray_segment_dist3(ro, rd, pts[s - 1], pts[s]));
if (d < best_ed) { best_ed = d; eid = k; best_pts = pts; best_edge = edges[k]; }
for (size_t s = 1; s < pts.size(); ++s) {
const wxPoint a = world_to_screen_px(cam, pts[s - 1]);
const wxPoint b = world_to_screen_px(cam, pts[s]);
if (a.x < 0 || b.x < 0) continue; // behind the camera
d = std::min(d, seg_px(cursor, a, b));
}
if (d < best_ed) { best_ed = d; ed_pts = pts; ed_edge = e; have_edge = true; }
}
}
if (eid >= 0) {
if (have_edge && best_ed <= kEdgeTolPx) {
// Promote the face-relative pick to a STABLE GLOBAL edge id so dress-up ops
// (fillet/chamfer) can target this exact edge across recomputes.
m_sel_edge = GeometryEngine::edge_index_of(bshape, best_edge);
m_sel_edge_pts = std::move(best_pts);
m_sel_edge = GeometryEngine::edge_index_of(bshape, ed_edge);
m_sel_edge_pts = std::move(ed_pts);
m_solid_sel = SolidSel::Edge;
} else { m_solid_sel = SolidSel::Whole; m_sel_edge = -1; m_sel_edge_pts.clear(); }
} else { // Edge -> back to Whole
m_solid_sel = SolidSel::Whole; m_sel_edge = -1; m_sel_edge_pts.clear();
} else {
m_solid_sel = SolidSel::Face;
}
}
dp_pick_trace("pick -> sel=%d body=%d face=%d edge=%d",
int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
if (on_solid_selection_changed)
on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
return true;