feat(ui): Select Other — pick what lies under something else (#503)
Hold the left button still (long-press on touch) or right-click › Select Other… to list everything under the cursor: sketch points and curves ("Line 3", "Point (arc 2 centre)"), vertices, edges, faces, bodies, profiles, planes and axes, filtered like a click and by the open dialog's active input. Hovering a row lights it up; clicking selects it (Shift/Ctrl add) or feeds the dialog. A repeat click on the same spot with nothing changed selects the next thing there; a double-click keeps its own actions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fd4a2f782
commit
4aaa5457b3
13 files changed
+952
-76
No files matched your search
+184
-74
@@ -82,6 +82,8 @@ pub struct ViewportState {
|
||||
pub build_ms: f64,
|
||||
/// The right-click menu, open at this screen position.
|
||||
pub context_menu: Option<Pos2>,
|
||||
/// Select Other: its list, a press being held, and click cycling.
|
||||
pub select_other: crate::select_other::State,
|
||||
/// A sketch point or curve being dragged (its id, the undo depth when the drag began: the
|
||||
/// whole drag is one undo step, and for a curve where it was last under the cursor).
|
||||
pub point_drag: Option<(String, usize, Option<Vec2>)>,
|
||||
@@ -720,7 +722,68 @@ pub fn pick_3d(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
pick_with_sketch_points(app, proj, pos, true)
|
||||
}
|
||||
|
||||
/// Everything under `pos` (Select Other): like [`pick`], but every sketch point, sketch curve,
|
||||
/// vertex, edge, axis and profile near the cursor, and the nearest face of every body along the
|
||||
/// ray, instead of the nearest of each kind. Each kind is nearest first.
|
||||
pub fn pick_all(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
pick_hits(app, proj, pos, false, true)
|
||||
}
|
||||
|
||||
fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool) -> Vec<Hit> {
|
||||
pick_hits(app, proj, pos, other_sketch_points, false)
|
||||
}
|
||||
|
||||
/// The same thing picked twice (one row per target in [`pick_all`]).
|
||||
pub fn same_target(a: &Hit, b: &Hit) -> bool {
|
||||
match (a, b) {
|
||||
(Hit::SketchPoint { sketch: s, id: i, .. }, Hit::SketchPoint { sketch: t, id: j, .. })
|
||||
| (Hit::SketchCurve { sketch: s, id: i, .. }, Hit::SketchCurve { sketch: t, id: j, .. }) => s == t && i == j,
|
||||
(Hit::Vertex { body: a, point: p }, Hit::Vertex { body: b, point: q }) => a == b && p.dist(*q) < 1e-9,
|
||||
(Hit::Edge { body: a, index: i, .. }, Hit::Edge { body: b, index: j, .. })
|
||||
| (Hit::Face { body: a, index: i, .. }, Hit::Face { body: b, index: j, .. }) => a == b && i == j,
|
||||
(Hit::Profile { sketch: s, index: i }, Hit::Profile { sketch: t, index: j }) => s == t && i == j,
|
||||
(Hit::Plane { name: a, .. }, Hit::Plane { name: b, .. }) | (Hit::Axis { name: a }, Hit::Axis { name: b }) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The candidates of one kind: the nearest only, or with `all` every target (nearest first).
|
||||
struct Near<T> {
|
||||
all: bool,
|
||||
v: Vec<(T, Hit)>,
|
||||
}
|
||||
|
||||
impl<T: PartialOrd + Copy> Near<T> {
|
||||
fn new(all: bool) -> Self {
|
||||
Near { all, v: Vec::new() }
|
||||
}
|
||||
/// Whether a candidate at `d` would be kept.
|
||||
fn wants(&self, d: T) -> bool {
|
||||
self.all || self.v.first().is_none_or(|(b, _)| d < *b)
|
||||
}
|
||||
fn offer(&mut self, d: T, h: Hit) {
|
||||
if !self.all {
|
||||
if self.wants(d) {
|
||||
self.v = vec![(d, h)];
|
||||
}
|
||||
return;
|
||||
}
|
||||
match self.v.iter_mut().find(|(_, x)| same_target(x, &h)) {
|
||||
Some(e) if d < e.0 => *e = (d, h),
|
||||
Some(_) => {}
|
||||
None => self.v.push((d, h)),
|
||||
}
|
||||
}
|
||||
fn sorted(mut self) -> Vec<(T, Hit)> {
|
||||
self.v.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||
self.v
|
||||
}
|
||||
fn hits(self) -> impl Iterator<Item = Hit> {
|
||||
self.sorted().into_iter().map(|x| x.1)
|
||||
}
|
||||
}
|
||||
|
||||
fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool, all: bool) -> Vec<Hit> {
|
||||
let s = &app.session;
|
||||
let st = s.world_state();
|
||||
let bounds = body_bounds(app, &st);
|
||||
@@ -733,23 +796,21 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
let external_points =
|
||||
wants_points || other_sketch_points || (app.dialog.is_none() && app.tool.as_ref().is_none_or(|tool| tool.cmd == "sketch.project"));
|
||||
let point_sketches: Vec<u64> = if external_points { visible_sketches(app) } else { s.active_sketch.into_iter().collect() };
|
||||
let mut best: Option<(f32, Hit)> = None;
|
||||
let mut best = Near::new(all);
|
||||
for ss in point_sketches.iter().filter_map(|sid| st.sketch(*sid)) {
|
||||
let sid = ss.feature;
|
||||
for p in &ss.sketch.points {
|
||||
if let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) {
|
||||
let dd = sp.distance(pos) - if s.active_sketch == Some(sid) { 2.0 } else { 0.0 };
|
||||
if dd < 8.0 && best.as_ref().is_none_or(|(b, _)| dd < *b) {
|
||||
best = Some((dd, Hit::SketchPoint { sketch: sid, id: p.id.clone(), at: p.pos }));
|
||||
if dd < 8.0 && best.wants(dd) {
|
||||
best.offer(dd, Hit::SketchPoint { sketch: sid, id: p.id.clone(), at: p.pos });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, h)) = best {
|
||||
hits.push(h);
|
||||
}
|
||||
hits.extend(best.hits());
|
||||
// Sketch curves (the active sketch wins ties).
|
||||
let mut bestc: Option<(f32, Hit)> = None;
|
||||
let mut bestc = Near::new(all);
|
||||
for sid in visible_sketches(app) {
|
||||
let Some(ss) = st.sketch(sid) else { continue };
|
||||
let bonus = if s.active_sketch == Some(sid) { 2.0 } else { 0.0 };
|
||||
@@ -762,8 +823,8 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
let pts: Vec<Pos2> = part.iter().filter_map(|q| proj.to_screen(*q)).collect();
|
||||
for w in pts.windows(2) {
|
||||
let dd = seg_dist(pos, w[0], w[1]) - bonus;
|
||||
if dd < 6.0 && bestc.as_ref().is_none_or(|(b, _)| dd < *b) {
|
||||
bestc = Some((dd, Hit::SketchCurve { sketch: sid, id: c.id.clone(), straight }));
|
||||
if dd < 6.0 && bestc.wants(dd) {
|
||||
bestc.offer(dd, Hit::SketchCurve { sketch: sid, id: c.id.clone(), straight });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -773,17 +834,16 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
let pts: Vec<Pos2> = w.pts.iter().filter_map(|q| proj.to_screen(*q)).collect();
|
||||
for seg in pts.windows(2) {
|
||||
let dd = seg_dist(pos, seg[0], seg[1]) - bonus;
|
||||
if dd < 6.0 && bestc.as_ref().is_none_or(|(b, _)| dd < *b) {
|
||||
bestc = Some((dd, Hit::SketchCurve { sketch: sid, id: w.id.clone(), straight: false }));
|
||||
if dd < 6.0 && bestc.wants(dd) {
|
||||
bestc.offer(dd, Hit::SketchCurve { sketch: sid, id: w.id.clone(), straight: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, h)) = bestc {
|
||||
hits.push(h);
|
||||
}
|
||||
// Nearest face along the ray (hides what is behind it).
|
||||
hits.extend(bestc.hits());
|
||||
// Nearest face along the ray (hides what is behind it); with `all`, every body's nearest.
|
||||
let mut bestf: Option<(f64, Hit)> = None;
|
||||
let mut faces = Near::new(all);
|
||||
for b in &st.bodies {
|
||||
if !app.session.body_visible(&b.name) {
|
||||
continue;
|
||||
@@ -793,23 +853,27 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
let pad = bb.diagonal() * 1e-6 + 1e-9;
|
||||
match ray_box(o, d, &bb, pad) {
|
||||
None => continue,
|
||||
Some(t0) if bestf.as_ref().is_some_and(|(bt, _)| t0 > *bt) => continue,
|
||||
Some(t0) if !all && bestf.as_ref().is_some_and(|(bt, _)| t0 > *bt) => continue,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let m = b.mesh();
|
||||
if let Some((t, ti)) = m.raycast(o, d)
|
||||
&& bestf.as_ref().is_none_or(|(bt, _)| t < *bt)
|
||||
{
|
||||
if let Some((t, ti)) = m.raycast(o, d) {
|
||||
let fi = m.tri_face.get(ti).copied().unwrap_or(0) as usize;
|
||||
bestf = Some((t, Hit::Face { body: b.name.clone(), index: fi, point: o + d * t }));
|
||||
let hit = Hit::Face { body: b.name.clone(), index: fi, point: o + d * t };
|
||||
if bestf.as_ref().is_none_or(|(bt, _)| t < *bt) {
|
||||
bestf = Some((t, hit.clone()));
|
||||
}
|
||||
if all {
|
||||
faces.offer(t, hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
let slack = scene_radius(app) * 0.02;
|
||||
let visible = |p: Vec3| bestf.as_ref().is_none_or(|(t, _)| (p - o).dot(d) <= *t + slack);
|
||||
// Vertices (edge end points) and edges.
|
||||
let mut bestv: Option<(f32, Hit)> = None;
|
||||
let mut beste: Option<(f32, Hit)> = None;
|
||||
let mut bestv = Near::new(all);
|
||||
let mut beste = Near::new(all);
|
||||
for b in &st.bodies {
|
||||
if !app.session.body_visible(&b.name) {
|
||||
continue;
|
||||
@@ -828,35 +892,35 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
for v in [e.first(), e.last()].into_iter().flatten() {
|
||||
if let Some(sp) = proj.to_screen(*v) {
|
||||
let dd = sp.distance(pos);
|
||||
if dd < 7.0 && visible(*v) && bestv.as_ref().is_none_or(|(bd, _)| dd < *bd) {
|
||||
bestv = Some((dd, Hit::Vertex { body: b.name.clone(), point: *v }));
|
||||
if dd < 7.0 && visible(*v) && bestv.wants(dd) {
|
||||
bestv.offer(dd, Hit::Vertex { body: b.name.clone(), point: *v });
|
||||
}
|
||||
}
|
||||
}
|
||||
let pts: Vec<Pos2> = e.iter().filter_map(|q| proj.to_screen(*q)).collect();
|
||||
for (k, w) in pts.windows(2).enumerate() {
|
||||
let dd = seg_dist(pos, w[0], w[1]);
|
||||
if dd < 6.0 && beste.as_ref().is_none_or(|(bd, _)| dd < *bd) {
|
||||
if dd < 6.0 && beste.wants(dd) {
|
||||
let mid = polyline_mid(e);
|
||||
// Visible where the cursor is (not just at the middle).
|
||||
let at = e.get(k).copied().unwrap_or(mid);
|
||||
if visible(at) || visible(mid) {
|
||||
beste = Some((dd, Hit::Edge { body: b.name.clone(), index: ei, mid }));
|
||||
beste.offer(dd, Hit::Edge { body: b.name.clone(), index: ei, mid });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hits.extend(bestv.map(|x| x.1));
|
||||
hits.extend(beste.map(|x| x.1));
|
||||
hits.extend(bestv.hits());
|
||||
hits.extend(beste.hits());
|
||||
// Origin axes.
|
||||
let size = origin_size(app.cam.half_height());
|
||||
let mut besta: Option<(f32, Hit)> = None;
|
||||
let mut besta = Near::new(all);
|
||||
for (name, dir) in origin_axes(app) {
|
||||
if let (Some(a), Some(b)) = (proj.to_screen(Vec3::ZERO), proj.to_screen(dir * (size * AXIS_LEN))) {
|
||||
let dd = seg_dist(pos, a, b);
|
||||
if dd < 5.0 && besta.as_ref().is_none_or(|(bd, _)| dd < *bd) {
|
||||
besta = Some((dd, Hit::Axis { name: name.into() }));
|
||||
if dd < 5.0 && besta.wants(dd) {
|
||||
besta.offer(dd, Hit::Axis { name: name.into() });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -864,15 +928,16 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
for (name, a, b) in construction_axis_segments(app) {
|
||||
if let (Some(a), Some(b)) = (proj.to_screen(a), proj.to_screen(b)) {
|
||||
let dd = seg_dist(pos, a, b);
|
||||
if dd < 5.0 && besta.as_ref().is_none_or(|(bd, _)| dd < *bd) {
|
||||
besta = Some((dd, Hit::Axis { name }));
|
||||
if dd < 5.0 && besta.wants(dd) {
|
||||
besta.offer(dd, Hit::Axis { name });
|
||||
}
|
||||
}
|
||||
}
|
||||
hits.extend(besta.map(|x| x.1));
|
||||
hits.extend(besta.hits());
|
||||
// Surfaces by depth: profiles (slightly preferred over a face they lie on), faces, planes.
|
||||
let mut surf: Vec<(f64, Hit)> = Vec::new();
|
||||
let mut bestp: Option<(f64, f64, Hit)> = None;
|
||||
let mut profiles_all: Vec<(f64, f64, Hit)> = Vec::new();
|
||||
for sid in visible_sketches(app) {
|
||||
let Some(ss) = st.sketch(sid) else { continue };
|
||||
let n = ss.plane.normal();
|
||||
@@ -888,15 +953,24 @@ fn pick_with_sketch_points(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_
|
||||
// Show Profile off: the sketch being edited offers no profiles.
|
||||
let profiles = if ss.sketch.view.hide_profile && s.active_sketch == Some(sid) { &[][..] } else { &ss.profiles[..] };
|
||||
for (pi, p) in profiles.iter().enumerate() {
|
||||
if p.region.contains(lp) && bestp.as_ref().is_none_or(|(_, a, _)| p.area < *a) {
|
||||
if !p.region.contains(lp) {
|
||||
continue;
|
||||
}
|
||||
if all {
|
||||
profiles_all.push((t, p.area, Hit::Profile { sketch: sid, index: pi }));
|
||||
} else if bestp.as_ref().is_none_or(|(_, a, _)| p.area < *a) {
|
||||
bestp = Some((t, p.area, Hit::Profile { sketch: sid, index: pi }));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((t, _, h)) = bestp {
|
||||
// Smallest profile first.
|
||||
profiles_all.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
for (t, _, h) in bestp.into_iter().chain(profiles_all) {
|
||||
surf.push((t - slack, h));
|
||||
}
|
||||
if let Some(f) = bestf {
|
||||
if all {
|
||||
surf.extend(faces.sorted());
|
||||
} else if let Some(f) = bestf {
|
||||
surf.push(f);
|
||||
}
|
||||
// Planes give way to a face or profile at the same depth (a floor on the XZ plane).
|
||||
@@ -1103,27 +1177,57 @@ pub fn candidate(app: &SolveApp, hits: &[Hit]) -> Option<(Hit, Sel)> {
|
||||
if let Some(d) = app.dialog.as_ref().filter(|d| d.wants_picks()) {
|
||||
return hits.iter().find_map(|h| d.candidate(&app.session, h).map(|s| (h.clone(), s)));
|
||||
}
|
||||
hits.iter().filter(|h| pick_allowed(app, h)).find_map(|h| hit_candidate(app, h))
|
||||
}
|
||||
|
||||
/// Every hit a click could pick, in order, each with its selection (Select Other).
|
||||
pub fn candidates(app: &SolveApp, hits: &[Hit]) -> Vec<(Hit, Sel)> {
|
||||
if let Some(d) = app.dialog.as_ref().filter(|d| d.wants_picks()) {
|
||||
return hits.iter().filter_map(|h| d.candidate(&app.session, h).map(|s| (h.clone(), s))).collect();
|
||||
}
|
||||
hits.iter().filter(|h| pick_allowed(app, h)).filter_map(|h| hit_candidate(app, h)).collect()
|
||||
}
|
||||
|
||||
/// What the selection filter lets a click pick with no dialog.
|
||||
fn pick_allowed(app: &SolveApp, h: &Hit) -> bool {
|
||||
let off = |k: &str| app.ui.pick_off.iter().any(|x| x == k);
|
||||
hits.iter()
|
||||
.filter(|h| match h {
|
||||
Hit::SketchCurve { .. } => !off("sketch") && !off("sketch_curves"),
|
||||
Hit::SketchPoint { .. } => !off("sketch") && !off("sketch_points"),
|
||||
Hit::Profile { .. } => !off("sketch"),
|
||||
Hit::Vertex { .. } => !off("vertices") && !app.ui.pick_bodies,
|
||||
Hit::Edge { .. } => !off("edges") && !app.ui.pick_bodies,
|
||||
Hit::Face { .. } => !off("faces") || app.ui.pick_bodies,
|
||||
Hit::Plane { .. } | Hit::Axis { .. } => true,
|
||||
})
|
||||
.find_map(|h| match h {
|
||||
Hit::SketchCurve { sketch, id, .. } if app.session.active_sketch != Some(*sketch) => {
|
||||
Some((h.clone(), Sel::ExternalSketchCurve { sketch: *sketch, id: id.clone() }))
|
||||
}
|
||||
Hit::SketchPoint { sketch, id, .. } if app.session.active_sketch != Some(*sketch) => {
|
||||
Some((h.clone(), Sel::ExternalSketchPoint { sketch: *sketch, id: id.clone() }))
|
||||
}
|
||||
Hit::Face { body, .. } if app.ui.pick_bodies => Some((h.clone(), Sel::Body { name: body.clone() })),
|
||||
_ => hit_sel(h).map(|s| (h.clone(), s)),
|
||||
})
|
||||
match h {
|
||||
Hit::SketchCurve { .. } => !off("sketch") && !off("sketch_curves"),
|
||||
Hit::SketchPoint { .. } => !off("sketch") && !off("sketch_points"),
|
||||
Hit::Profile { .. } => !off("sketch"),
|
||||
Hit::Vertex { .. } => !off("vertices") && !app.ui.pick_bodies,
|
||||
Hit::Edge { .. } => !off("edges") && !app.ui.pick_bodies,
|
||||
Hit::Face { .. } => !off("faces") || app.ui.pick_bodies,
|
||||
Hit::Plane { .. } | Hit::Axis { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection a click on `h` makes with no dialog (sketch entities only belong to the active sketch).
|
||||
fn hit_candidate(app: &SolveApp, h: &Hit) -> Option<(Hit, Sel)> {
|
||||
match h {
|
||||
Hit::SketchCurve { sketch, id, .. } if app.session.active_sketch != Some(*sketch) => {
|
||||
Some((h.clone(), Sel::ExternalSketchCurve { sketch: *sketch, id: id.clone() }))
|
||||
}
|
||||
Hit::SketchPoint { sketch, id, .. } if app.session.active_sketch != Some(*sketch) => {
|
||||
Some((h.clone(), Sel::ExternalSketchPoint { sketch: *sketch, id: id.clone() }))
|
||||
}
|
||||
Hit::Face { body, .. } if app.ui.pick_bodies => Some((h.clone(), Sel::Body { name: body.clone() })),
|
||||
_ => hit_sel(h).map(|s| (h.clone(), s)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A pick for the open dialog's active input (a click, or a Select Other row).
|
||||
pub(crate) fn dialog_pick(app: &mut SolveApp, hit: &Hit, sel: Sel) {
|
||||
let Some(mut d) = app.dialog.take() else { return };
|
||||
d.pick(&app.session, sel);
|
||||
if let crate::dialogs::Kind::PathPlane { sketch, .. } = &mut d.kind {
|
||||
*sketch = match hit {
|
||||
Hit::SketchCurve { sketch, .. } => Some(*sketch),
|
||||
_ => None,
|
||||
};
|
||||
d.extra.remove("path");
|
||||
}
|
||||
app.dialog = Some(d);
|
||||
}
|
||||
|
||||
/// The edge a selection refers to now: by index if it still passes through the stored point,
|
||||
@@ -1855,6 +1959,11 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let hits = hover.filter(|_| inside && app.viewport.boxsel.is_none() && !navigating).map(|p| pick_cached(app, &proj, p)).unwrap_or_default();
|
||||
let cand = if app.tool.is_some() { hits.first().cloned().map(|h| (h, None)) } else { candidate(app, &hits).map(|(h, s)| (h, Some(s))) };
|
||||
app.viewport.hover = cand.as_ref().map(|c| c.0.clone());
|
||||
// Select Other: a press held still opens the list of everything under it; while the list is
|
||||
// open its row under the pointer is what lights up.
|
||||
let held =
|
||||
crate::select_other::track_hold(app, ui.ctx(), &proj, inside, primary_pressed, primary_down, hover, primary_nav.is_some() || mode.is_some());
|
||||
crate::select_other::hover_override(app);
|
||||
let painter = ui.painter_at(rect);
|
||||
// Background gradient.
|
||||
let (top, bot) = (t.viewport_top, t.viewport_bottom);
|
||||
@@ -1930,22 +2039,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if app.tool.is_some() {
|
||||
crate::tools::on_hover(app, &proj, p);
|
||||
}
|
||||
if resp.clicked() {
|
||||
if resp.clicked() && !held {
|
||||
if app.tool.is_some() {
|
||||
crate::tools::on_click(app, &proj, p);
|
||||
} else if app.dialog.as_ref().is_some_and(|d| d.wants_picks()) {
|
||||
if let Some((hit, Some(sel))) = cand
|
||||
&& let Some(mut d) = app.dialog.take()
|
||||
{
|
||||
d.pick(&app.session, sel);
|
||||
if let crate::dialogs::Kind::PathPlane { sketch, .. } = &mut d.kind {
|
||||
*sketch = match hit {
|
||||
Hit::SketchCurve { sketch, .. } => Some(sketch),
|
||||
_ => None,
|
||||
};
|
||||
d.extra.remove("path");
|
||||
}
|
||||
app.dialog = Some(d);
|
||||
if let Some((hit, Some(sel))) = cand {
|
||||
dialog_pick(app, &hit, sel);
|
||||
}
|
||||
} else {
|
||||
// Dimensions and constraint glyphs sit on top of the geometry.
|
||||
@@ -1961,15 +2060,23 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if !crate::sketch3d::click(app, &proj, p) {
|
||||
match on_dim.or(on_glyph) {
|
||||
Some(id) => select(app, Some(Sel::SketchConstraint { id }), add),
|
||||
None => select(app, cand.and_then(|c| c.1), add),
|
||||
// Clicking again on the same spot selects the next thing under it (not
|
||||
// the second click of a double-click, which has its own actions).
|
||||
None if !add && !resp.double_clicked() && crate::select_other::cycle_click(app, &proj, p) => {}
|
||||
None => {
|
||||
let sel = cand.and_then(|c| c.1);
|
||||
select(app, sel.clone(), add);
|
||||
crate::select_other::note_click(app, p, if add { None } else { sel });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Double-click a dimension: edit its value in place.
|
||||
let dim_edit = resp.double_clicked() && app.tool.is_none() && app.dialog.is_none() && crate::dim_view::double_click(app, p);
|
||||
let dim_edit = resp.double_clicked() && !held && app.tool.is_none() && app.dialog.is_none() && crate::dim_view::double_click(app, p);
|
||||
// Double-click a sketch curve: select the chain joined to it end to end.
|
||||
if resp.double_clicked()
|
||||
&& !held
|
||||
&& !dim_edit
|
||||
&& app.tool.is_none()
|
||||
&& app.dialog.is_none()
|
||||
@@ -1983,6 +2090,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
// Double-click an edge: select the edges that continue it smoothly (the loop).
|
||||
if resp.double_clicked()
|
||||
&& !held
|
||||
&& !dim_edit
|
||||
&& app.tool.is_none()
|
||||
&& app.dialog.is_none()
|
||||
@@ -2006,7 +2114,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
// Dragging a sketch point of the active sketch moves it; the solver keeps the constraints.
|
||||
if app.tool.is_none() && app.dialog.is_none() && primary_nav.is_none() && mode.is_none() && app.session.active_sketch.is_some() {
|
||||
if app.tool.is_none() && app.dialog.is_none() && primary_nav.is_none() && mode.is_none() && app.session.active_sketch.is_some() && !held {
|
||||
let on_plane = |app: &SolveApp, p: Pos2| {
|
||||
let ss = app.session.active_sketch.and_then(|sid| app.session.world_state().sketch(sid).cloned())?;
|
||||
let (o, d) = proj.ray(p);
|
||||
@@ -2118,6 +2226,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
// Box selection: a primary drag on the model when no navigation mode is on.
|
||||
if app.tool.is_none()
|
||||
&& !held
|
||||
&& primary_nav.is_none()
|
||||
&& mode.is_none()
|
||||
&& app.viewport.point_drag.is_none()
|
||||
@@ -2173,6 +2282,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::canvas::show(app, ui, &painter, &proj);
|
||||
crate::sketch_tools::show(app, ui, &painter, &proj);
|
||||
crate::sketch_dims::show(app, ui, &proj);
|
||||
crate::select_other::show(app, ui.ctx());
|
||||
axis_view(app, ui, rect);
|
||||
nav_bar(app, ui, rect);
|
||||
// Status chip.
|
||||
@@ -2192,7 +2302,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
|
||||
/// A click without a command: select the candidate (replacing the selection, or toggling it
|
||||
/// in with Shift/Ctrl); a click on nothing clears.
|
||||
fn select(app: &mut SolveApp, sel: Option<Sel>, add: bool) {
|
||||
pub(crate) fn select(app: &mut SolveApp, sel: Option<Sel>, add: bool) {
|
||||
match sel {
|
||||
Some(x) => {
|
||||
let items = grouped_selection(&app.session, &x);
|
||||
|
||||
Reference in new issue
Block a user