A Single hole picked on or near a sketch point that lies on the face (a circle's centre) now goes to that point, as a Multiple pick does. Picks away from sketch points, or near one that isn't on the face under the cursor, stay where they were clicked. Fixes #657. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
11084a5963
commit
831fa34752
6 files changed
+313
-11
No files matched your search
@@ -250,6 +250,10 @@ pub enum Kind {
|
||||
TextMove(crate::sketch_text_move::TextMove),
|
||||
}
|
||||
|
||||
/// The Single hole placement's input: places on planar faces. A pick on or near a sketch point
|
||||
/// that lies on the face (a circle's centre) is taken at that point, as Multiple picks it.
|
||||
const HOLE_FACES: Accept = PLANAR_FACES | selection::POINTS;
|
||||
|
||||
/// The rest of the Hole dialog: placement, extents, tap type and drill point.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HoleOpts {
|
||||
@@ -461,7 +465,7 @@ impl Dialog {
|
||||
vec![SelInput::new("Profile", PROFILES, true), SelInput::new("Path", CURVES, true)],
|
||||
),
|
||||
"solid.move" => Dialog::new(crate::dialogs_move::kind(0), crate::dialogs_move::inputs(0)),
|
||||
"solid.hole" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", PLANAR_FACES, true)]),
|
||||
"solid.hole" => Dialog::new(hole_defaults(), vec![SelInput::new("Face", HOLE_FACES, true)]),
|
||||
"solid.box" => Dialog::new(
|
||||
Kind::Primitive {
|
||||
cmd: "solid.box",
|
||||
@@ -707,6 +711,13 @@ impl Dialog {
|
||||
|
||||
/// The selection a hit would add to the active input.
|
||||
pub fn candidate(&self, s: &Session, h: &Hit) -> Option<Sel> {
|
||||
self.candidate_on(s, h, None)
|
||||
}
|
||||
|
||||
/// The selection a hit would add to the active input, knowing the face under the cursor (the
|
||||
/// nearest face the pick ray meets). A face place picked on or near a sketch point that lies
|
||||
/// on that face (a circle's centre) is taken at the point, as a points input picks it.
|
||||
pub fn candidate_on(&self, s: &Session, h: &Hit, face: Option<&Hit>) -> Option<Sel> {
|
||||
let inp = self.active_input()?;
|
||||
// Features are also picked on the canvas: a face stands for the feature that made it.
|
||||
if inp.accept == selection::FEATURES {
|
||||
@@ -715,6 +726,15 @@ impl Dialog {
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
if inp.accept & selection::POINTS != 0
|
||||
&& inp.accept & PLANAR_FACES != 0
|
||||
&& let Hit::SketchPoint { sketch, at, .. } = h
|
||||
{
|
||||
return match face {
|
||||
Some(Hit::Face { body, index, .. }) => face_place_at_sketch_point(s, *sketch, *at, body, *index),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
inp.accepts(h, |body, face| planar_face(s, body, face).is_some())
|
||||
}
|
||||
|
||||
@@ -802,6 +822,22 @@ fn default_profiles(s: &Session) -> Vec<Sel> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A place on planar face `face` of `body` at a sketch point (a circle's centre or a point), when
|
||||
/// the point lies on that face (within a micrometre); `None` otherwise.
|
||||
fn face_place_at_sketch_point(s: &Session, sketch: u64, at: solidcraft_engine::geom::Vec2, body: &str, face: usize) -> Option<Sel> {
|
||||
let st = s.world_state();
|
||||
let p = st.sketch(sketch)?.plane.to_world(at);
|
||||
let m = st.body(body)?.mesh();
|
||||
// The same wherever the part is: a planar face's triangles lie in its plane.
|
||||
let tol = 1e-3;
|
||||
let on = m
|
||||
.triangles
|
||||
.iter()
|
||||
.zip(&m.tri_face)
|
||||
.any(|(t, f)| *f as usize == face && m.tri(t).is_some_and(|[a, b, c]| crate::preview::point_triangle_dist(p, a, b, c) < tol));
|
||||
(p.is_finite() && on && planar_face(s, body, face).is_some()).then(|| Sel::Face { body: body.to_string(), index: face, point: p })
|
||||
}
|
||||
|
||||
/// Does a feature build from this sketch's profiles (extrude, revolve, sweep, loft, emboss…)?
|
||||
fn profiles_used(s: &Session, sketch: u64) -> bool {
|
||||
s.doc.features.iter().any(|f| f.kind.profile_sels().iter().any(|(id, _)| *id == sketch))
|
||||
@@ -1126,7 +1162,7 @@ fn placement_row(d: &mut Dialog, ui: &mut egui::Ui) {
|
||||
ui.end_row();
|
||||
if (m == 1) != opts.multiple {
|
||||
opts.multiple = m == 1;
|
||||
d.inputs = vec![if opts.multiple { SelInput::new("Points", selection::POINTS, true) } else { SelInput::new("Face", PLANAR_FACES, true) }];
|
||||
d.inputs = vec![if opts.multiple { SelInput::new("Points", selection::POINTS, true) } else { SelInput::new("Face", HOLE_FACES, true) }];
|
||||
d.active = 0;
|
||||
d.extra.remove("direction");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! Hole placement on the canvas. A Single hole picked on or near a sketch point on the face (a
|
||||
//! circle's centre) goes to that point, as a Multiple pick of the same point does (#657); a pick
|
||||
//! away from sketch points, or near one off the face under the cursor, stays where it was clicked.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::FeatureKind;
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
use solidcraft_engine::sketch::CurveKind;
|
||||
|
||||
use crate::dialogs::Kind;
|
||||
use crate::scenario::Harness;
|
||||
use crate::viewport::Hit;
|
||||
|
||||
/// A 40 × 30 × 20 block, seen from the top, with a sketch on its top face: a circle of radius 4
|
||||
/// about (12, 9). Returns the circle centre in the world.
|
||||
fn block_with_circle() -> (Harness, Vec3) {
|
||||
let (h, centre) = block_and_circle_on(json!({"plane": {"face": [20, 15, 20]}}), [12.0, 9.0]);
|
||||
assert!((centre.z - 20.0).abs() < 1e-9, "the circle lies on the top face: {centre:?}");
|
||||
(h, centre)
|
||||
}
|
||||
|
||||
/// The block, seen from the top, and a circle of radius 4 about `at` (sketch coordinates) in the
|
||||
/// sketch `sketch` (`sketch.create`'s parameters) makes. Returns the circle centre in the world.
|
||||
fn block_and_circle_on(sketch: Value, at: [f64; 2]) -> (Harness, Vec3) {
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.show_origin = false;
|
||||
h.app.run("solid.box", json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
h.app.run("sketch.create", sketch).unwrap();
|
||||
h.app.run("sketch.circle.center", json!({"center": at, "radius": 4})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
h.step(&json!({"call": "ui.view", "params": {"view": "top"}})).unwrap();
|
||||
h.settle();
|
||||
let st = h.app.session.world_state();
|
||||
let ss = st.sketches.first().unwrap();
|
||||
let centre = ss
|
||||
.sketch
|
||||
.curves
|
||||
.iter()
|
||||
.find_map(|c| match c.kind {
|
||||
CurveKind::Circle { c, .. } => Some(ss.plane.to_world(ss.sketch.points[c].pos)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
(h, centre)
|
||||
}
|
||||
|
||||
/// Where a world point is on screen.
|
||||
fn screen(h: &mut Harness, p: Vec3) -> [f64; 2] {
|
||||
h.at(&json!({"world": [p.x, p.y, p.z]})).unwrap()
|
||||
}
|
||||
|
||||
/// Five pixels off where `p` is on screen: within reach of a sketch point pick, not on `p`.
|
||||
fn near(h: &mut Harness, p: Vec3) -> [f64; 2] {
|
||||
let c = screen(h, p);
|
||||
[c[0] + 4.0, c[1] + 3.0]
|
||||
}
|
||||
|
||||
/// Everything under a screen point, as a click there sees it.
|
||||
fn hits_at(h: &Harness, at: [f64; 2]) -> Vec<Hit> {
|
||||
let proj = crate::viewport::projection(&h.app, h.app.viewport.rect.unwrap());
|
||||
crate::viewport::pick(&h.app, &proj, egui::pos2(at[0] as f32, at[1] as f32))
|
||||
}
|
||||
|
||||
/// The face the cursor is over, where the ray meets it, after hovering `at`.
|
||||
fn hovered_face_point(h: &mut Harness, at: [f64; 2]) -> Vec3 {
|
||||
h.step(&json!({ "move": at })).unwrap();
|
||||
match &h.app.viewport.hover {
|
||||
Some(Hit::Face { point, .. }) => *point,
|
||||
other => panic!("the face lights up, not {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The last feature's hole placement: its id, its position and whether it is at sketch points.
|
||||
fn hole(h: &Harness) -> (u64, Vec3, bool) {
|
||||
let f = h.app.session.doc.features.last().unwrap();
|
||||
match &f.kind {
|
||||
FeatureKind::Hole { position, points, .. } => (f.id, *position, points.is_some()),
|
||||
other => panic!("the last feature is not a hole: {}", other.type_name()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The face places the open dialog's first input holds.
|
||||
fn face_places(h: &Harness) -> Vec<(String, Vec3)> {
|
||||
h.app.dialog.as_ref().unwrap().inputs[0]
|
||||
.items
|
||||
.iter()
|
||||
.map(|x| match x {
|
||||
Sel::Face { body, point, .. } => (body.clone(), *point),
|
||||
other => panic!("a face place, not {other:?}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether the model holds material at `p`.
|
||||
fn solid_at(h: &Harness, p: Vec3) -> bool {
|
||||
h.app.session.model.state().bodies.iter().any(|b| b.body.contains(p))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_hole_picked_near_a_circle_centre_goes_to_the_centre() {
|
||||
let (mut h, centre) = block_with_circle();
|
||||
let click = near(&mut h, centre);
|
||||
h.step(&json!({"start": "solid.hole"})).unwrap();
|
||||
let d = h.app.dialog.as_ref().unwrap();
|
||||
assert!(matches!(&d.kind, Kind::Hole { opts, .. } if !opts.multiple), "a new hole starts with Single placement");
|
||||
h.step(&json!({"move": click})).unwrap();
|
||||
assert!(
|
||||
matches!(&h.app.viewport.hover, Some(Hit::SketchPoint { .. })),
|
||||
"the centre lights up as what the pick snaps to: {:?}",
|
||||
h.app.viewport.hover
|
||||
);
|
||||
h.step(&json!({"click": click})).unwrap();
|
||||
let places = face_places(&h);
|
||||
assert!(matches!(places.as_slice(), [(_, p)] if p.dist(centre) < 1e-9), "the face is picked at the centre {centre:?}, not {places:?}");
|
||||
h.step(&json!({"key": "Enter"})).unwrap();
|
||||
assert!(h.app.dialog.is_none(), "OK closed the dialog: {:?}", h.app.dialog.as_ref().map(|d| &d.error));
|
||||
let (id, position, at_points) = hole(&h);
|
||||
assert!(!at_points, "a Single hole is placed on its face");
|
||||
assert!(position.dist(centre) < 1e-9, "the hole is at the circle centre {centre:?}, not at {position:?}");
|
||||
assert!(h.app.session.model.result(id).is_some_and(|r| r.error.is_none() && r.warning.is_none()));
|
||||
// The bore (5 mm across, 10 mm deep) is round about the centre: empty on its axis and up to
|
||||
// 2.4 mm from it, solid 2.6 mm from it in every direction.
|
||||
let bore = |h: &Harness| {
|
||||
for k in 0..8 {
|
||||
let a = std::f64::consts::FRAC_PI_4 * f64::from(k);
|
||||
let dir = Vec3::new(a.cos(), a.sin(), 0.0);
|
||||
let at = |r: f64| centre + dir * r - Vec3::new(0.0, 0.0, 5.0);
|
||||
assert!(!solid_at(h, at(0.0)) && !solid_at(h, at(2.4)), "the bore is empty {a} rad from +x");
|
||||
assert!(solid_at(h, at(2.6)), "the wall is solid {a} rad from +x");
|
||||
}
|
||||
};
|
||||
bore(&h);
|
||||
// Edit Feature shows the Single placement on the top face at the centre, and OK keeps it.
|
||||
h.step(&json!({"call": "ui.editFeature", "params": {"feature": id}})).unwrap();
|
||||
h.frames(4);
|
||||
let d = h.app.dialog.as_ref().expect("the hole dialog");
|
||||
assert!(matches!(&d.kind, Kind::Hole { opts, .. } if !opts.multiple), "{:?}", d.kind);
|
||||
let places = face_places(&h);
|
||||
assert!(matches!(places.as_slice(), [(body, p)] if body == "Body1" && p.dist(centre) < 1e-6), "{places:?}");
|
||||
h.step(&json!({"key": "Enter"})).unwrap();
|
||||
assert!(h.app.dialog.is_none(), "OK closed the dialog: {:?}", h.app.dialog.as_ref().map(|d| &d.error));
|
||||
let (again, position, at_points) = hole(&h);
|
||||
assert_eq!(again, id, "the same feature");
|
||||
assert!(!at_points && position.dist(centre) < 1e-9, "{position:?}");
|
||||
bore(&h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_hole_picked_away_from_sketch_points_stays_where_it_was_clicked() {
|
||||
let (mut h, centre) = block_with_circle();
|
||||
// Inside the circle, well away from its centre (and from every other sketch point).
|
||||
let away = centre + Vec3::new(3.0, 2.0, 0.0);
|
||||
let click = screen(&mut h, away);
|
||||
let c = screen(&mut h, centre);
|
||||
assert!(c.iter().zip(click).map(|(a, b)| (a - b).powi(2)).sum::<f64>().sqrt() > 16.0, "twice a point pick's reach from the centre");
|
||||
h.step(&json!({"start": "solid.hole"})).unwrap();
|
||||
let raw = hovered_face_point(&mut h, click);
|
||||
h.step(&json!({"click": click})).unwrap();
|
||||
h.step(&json!({"key": "Enter"})).unwrap();
|
||||
assert!(h.app.dialog.is_none());
|
||||
let (_, position, at_points) = hole(&h);
|
||||
assert!(!at_points);
|
||||
assert!(position.dist(raw) < 1e-9 && position.dist(away) < 1e-3, "the hole stays where it was clicked {raw:?}, not at {position:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_hole_picked_near_a_sketch_point_off_the_face_under_it_stays_where_it_was_clicked() {
|
||||
// The circle 10 mm above the top face (its centre lies on no face), then on the bottom face
|
||||
// (hidden behind the top face): either way its centre is under the pick on screen, but not on
|
||||
// the face picked.
|
||||
// (The bottom face's sketch looks up through the block: its y runs along the world's −y.)
|
||||
let cases = [(json!({"plane": "XY", "offset": 30}), [12.0, 9.0], 30.0), (json!({"plane": {"face": [20, 15, 0]}}), [12.0, -9.0], 0.0)];
|
||||
for (sketch, at, z) in cases {
|
||||
let (mut h, centre) = block_and_circle_on(sketch, at);
|
||||
assert!(centre.dist(Vec3::new(12.0, 9.0, z)) < 1e-9, "{centre:?}");
|
||||
let click = near(&mut h, centre);
|
||||
h.step(&json!({"start": "solid.hole"})).unwrap();
|
||||
let hits = hits_at(&h, click);
|
||||
assert!(matches!(hits.first(), Some(Hit::SketchPoint { .. })), "the click reaches the centre: {hits:?}");
|
||||
let raw = hovered_face_point(&mut h, click);
|
||||
h.step(&json!({"click": click})).unwrap();
|
||||
h.step(&json!({"key": "Enter"})).unwrap();
|
||||
assert!(h.app.dialog.is_none());
|
||||
let (_, position, at_points) = hole(&h);
|
||||
assert!(!at_points);
|
||||
assert!(position.dist(raw) < 1e-9, "the hole stays where it was clicked {raw:?}, not at {position:?}");
|
||||
assert!((position.z - 20.0).abs() < 1e-9 && position.dist(Vec3::new(centre.x, centre.y, 20.0)) > 0.1, "{position:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_other_offers_the_centre_and_the_face_as_picked() {
|
||||
let (mut h, centre) = block_with_circle();
|
||||
let click = near(&mut h, centre);
|
||||
h.step(&json!({"start": "solid.hole"})).unwrap();
|
||||
// Where the click's ray meets the top face.
|
||||
let ray = hits_at(&h, click);
|
||||
let Some(Hit::Face { point: raw, .. }) = ray.iter().find(|x| matches!(x, Hit::Face { .. })) else { panic!("{ray:?}") };
|
||||
let raw = *raw;
|
||||
assert!(raw.dist(centre) > 0.1);
|
||||
for (row, want) in [(0, centre), (1, raw)] {
|
||||
let list = h.call("ui.selectOther", json!({"x": click[0], "y": click[1]}))["result"].clone();
|
||||
let labels: Vec<&str> = list["rows"].as_array().into_iter().flatten().filter_map(|r| r["label"].as_str()).collect();
|
||||
assert!(labels.len() == 2 && labels[0].contains("centre") && labels[1].starts_with("Face"), "the snapped centre, then the face: {labels:?}");
|
||||
h.call("ui.selectOther", json!({ "pick": row }));
|
||||
let places = face_places(&h);
|
||||
assert!(matches!(places.as_slice(), [(_, p)] if p.dist(want) < 1e-9), "row {row} places at {want:?}: {places:?}");
|
||||
// Start over for the other row (a second pick on the same face would take it out).
|
||||
h.step(&json!({"key": "Escape"})).unwrap();
|
||||
h.step(&json!({"start": "solid.hole"})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_other_keeps_the_face_as_picked_beside_many_snapped_points() {
|
||||
// More points under the cursor than Select Other lists of one kind: the snapped places are
|
||||
// capped as points, and the face as picked keeps its row.
|
||||
let mut h = Harness::new();
|
||||
h.app.ui.show_origin = false;
|
||||
h.app.run("solid.box", json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
h.app.run("sketch.create", json!({"plane": {"face": [20, 15, 20]}})).unwrap();
|
||||
let many = crate::select_other::MAX_PER_KIND + 4;
|
||||
for i in 0..many {
|
||||
let a = i as f64 * 0.3;
|
||||
h.app.run("sketch.point", json!({"point": [12.0 + 0.1 * a.cos(), 9.0 + 0.1 * a.sin()]})).unwrap();
|
||||
}
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
h.step(&json!({"call": "ui.view", "params": {"view": "top"}})).unwrap();
|
||||
h.settle();
|
||||
let click = near(&mut h, Vec3::new(12.0, 9.0, 20.0));
|
||||
h.step(&json!({"start": "solid.hole"})).unwrap();
|
||||
let list = h.call("ui.selectOther", json!({"x": click[0], "y": click[1]}))["result"].clone();
|
||||
let rows = list["rows"].as_array().cloned().unwrap_or_default();
|
||||
let count = |prefix: &str| rows.iter().filter(|r| r["label"].as_str().is_some_and(|l| l.starts_with(prefix))).count();
|
||||
assert_eq!((count("Point"), count("Face")), (crate::select_other::MAX_PER_KIND, 1), "{list}");
|
||||
assert_eq!(list["total"], json!(many + 1), "{list}");
|
||||
}
|
||||
@@ -35,6 +35,8 @@ pub mod frame;
|
||||
pub mod gizmo;
|
||||
pub mod gpu;
|
||||
pub mod help;
|
||||
#[cfg(test)]
|
||||
mod hole_placement_ui_tests;
|
||||
pub mod home;
|
||||
pub mod i18n;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -34,7 +34,7 @@ const CYCLE_SLOP: f32 = 4.0;
|
||||
/// The most rows kept per kind of thing (vertices, edges, faces, …). The list exists to tell a
|
||||
/// handful of overlapping things apart; nearest first, farther ones of the same kind are dropped
|
||||
/// so it stays readable and bounded.
|
||||
const MAX_PER_KIND: usize = 20;
|
||||
pub(crate) const MAX_PER_KIND: usize = 20;
|
||||
|
||||
/// One row of the list.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -103,21 +103,24 @@ fn rows_capped(app: &SolveApp, proj: &Proj, pos: Pos2) -> (Vec<Row>, usize) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A face place a sketch point snapped (a Single hole's) is its own row: beside the face as
|
||||
// picked, and beside other points on that face.
|
||||
let snapped = |h: &Hit| matches!(h, Hit::SketchPoint { .. });
|
||||
let mut out: Vec<(Hit, Sel)> = Vec::new();
|
||||
for (h, s) in found {
|
||||
if !out.iter().any(|(_, x)| crate::selection::same_item(x, &s)) {
|
||||
if !out.iter().any(|(hx, x)| if snapped(hx) || snapped(&h) { *x == s } else { crate::selection::same_item(x, &s) }) {
|
||||
out.push((h, s));
|
||||
}
|
||||
}
|
||||
let sketching = app.session.active_sketch.is_some();
|
||||
out.sort_by_key(|(_, s)| rank(s, sketching));
|
||||
out.sort_by_key(|(h, s)| rank(&shown_as(h, s), sketching));
|
||||
let total = out.len();
|
||||
// Nearest first within a kind, so capping drops the farther ones.
|
||||
let mut per_kind: std::collections::HashMap<u8, usize> = std::collections::HashMap::new();
|
||||
let rows: Vec<Row> = out
|
||||
.into_iter()
|
||||
.filter(|(_, s)| {
|
||||
let n = per_kind.entry(rank(s, sketching).1).or_default();
|
||||
.filter(|(h, s)| {
|
||||
let n = per_kind.entry(rank(&shown_as(h, s), sketching).1).or_default();
|
||||
if *n >= MAX_PER_KIND {
|
||||
return false;
|
||||
}
|
||||
@@ -125,13 +128,23 @@ fn rows_capped(app: &SolveApp, proj: &Proj, pos: Pos2) -> (Vec<Row>, usize) {
|
||||
true
|
||||
})
|
||||
.map(|(hit, sel)| {
|
||||
let label = label(app, &sel);
|
||||
Row { icon: icon(app, &sel), hit, sel, label }
|
||||
let shown = shown_as(&hit, &sel);
|
||||
let label = label(app, &shown);
|
||||
Row { icon: icon(app, &shown), hit, sel, label }
|
||||
})
|
||||
.collect();
|
||||
(rows, total)
|
||||
}
|
||||
|
||||
/// What a row shows itself as: a face place a sketch point snapped is that point (ranked, capped
|
||||
/// and named with the points).
|
||||
fn shown_as(hit: &Hit, sel: &Sel) -> Sel {
|
||||
match (hit, sel) {
|
||||
(Hit::SketchPoint { sketch, id, .. }, Sel::Face { .. }) => Sel::ExternalSketchPoint { sketch: *sketch, id: id.clone() },
|
||||
_ => sel.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sort key: in a sketch its own points and curves come first; then by kind.
|
||||
fn rank(s: &Sel, sketching: bool) -> (u8, u8) {
|
||||
let kind = match s {
|
||||
|
||||
@@ -1215,7 +1215,8 @@ pub fn hit_sel(h: &Hit) -> Option<Sel> {
|
||||
/// dialog the first hit (sketch entities only belong to the active sketch).
|
||||
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)));
|
||||
let face = face_under(hits);
|
||||
return hits.iter().find_map(|h| d.candidate_on(&app.session, h, face).map(|s| (h.clone(), s)));
|
||||
}
|
||||
hits.iter().filter(|h| pick_allowed(app, h)).find_map(|h| hit_candidate(app, h))
|
||||
}
|
||||
@@ -1223,11 +1224,17 @@ pub fn candidate(app: &SolveApp, hits: &[Hit]) -> Option<(Hit, Sel)> {
|
||||
/// 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();
|
||||
let face = face_under(hits);
|
||||
return hits.iter().filter_map(|h| d.candidate_on(&app.session, h, face).map(|s| (h.clone(), s))).collect();
|
||||
}
|
||||
hits.iter().filter(|h| pick_allowed(app, h)).filter_map(|h| hit_candidate(app, h)).collect()
|
||||
}
|
||||
|
||||
/// The face under the cursor: the nearest face the pick ray meets (faces come nearest first).
|
||||
fn face_under(hits: &[Hit]) -> Option<&Hit> {
|
||||
hits.iter().find(|h| matches!(h, Hit::Face { .. }))
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
@@ -52,6 +52,12 @@ unsupported. A new Emboss without an explicit face infers a unique eligible supp
|
||||
select the face when the target is ambiguous. Older saved features without a face retain their
|
||||
planar placement.
|
||||
|
||||
A **Hole** with **Single** placement drills where you pick a planar face. A pick on or near a
|
||||
point of a shown sketch that lies on that face, such as a circle's centre, places the hole
|
||||
exactly at that point, as **Multiple** placement picks it; **Select Other** (press and hold) also
|
||||
offers the face where you clicked. The hole keeps that place on the face; to have holes follow
|
||||
later edits of the sketch, place them with **Multiple**.
|
||||
|
||||
Fillets and chamfers handle straight edges between planar faces, whole smooth loops, runs of
|
||||
neighbouring edges with mitred or spherical corners, and closed chains of curved edges (a rolling
|
||||
ball). See the roadmap's known limitations for the rest.
|
||||
|
||||
Reference in new issue
Block a user