1 parent
2141a3802a
commit
d188eea1f4
1 file changed
+82
-11
@@ -1013,6 +1013,23 @@ fn ray_box(o: Vec3, d: Vec3, b: &solidcraft_engine::geom::Aabb3, pad: f64) -> Op
|
||||
(t0 <= t1).then_some(t0)
|
||||
}
|
||||
|
||||
/// The point of segment `a`–`b` nearest the ray from `o` along `d`.
|
||||
fn ray_segment_closest(o: Vec3, d: Vec3, a: Vec3, b: Vec3) -> Vec3 {
|
||||
let u = b - a;
|
||||
let w = a - o;
|
||||
let (dd, du, uu) = (d.dot(d), d.dot(u), u.dot(u));
|
||||
let den = dd * uu - du * du;
|
||||
// Parallel or degenerate: every point is as near, so the start stands for the segment.
|
||||
if !(den.is_finite() && den > 1e-12 * dd * uu) {
|
||||
return a;
|
||||
}
|
||||
let s = (du * d.dot(w) - dd * u.dot(w)) / den;
|
||||
if !s.is_finite() {
|
||||
return a;
|
||||
}
|
||||
a + u * s.clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// A box's outline on screen, grown by `px` (None: behind the eye).
|
||||
fn screen_box(proj: &Proj, b: &solidcraft_engine::geom::Aabb3, px: f32) -> Option<Rect> {
|
||||
let mut r = Rect::NOTHING;
|
||||
@@ -1227,7 +1244,9 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
|
||||
}
|
||||
}
|
||||
let slack = scene_radius(app) * 0.02;
|
||||
let visible = |p: Vec3| bestf.as_ref().is_none_or(|(t, _)| (p - o).dot(d) <= *t + slack);
|
||||
// Wireframe draws no faces, so nothing hides an edge or a vertex.
|
||||
let wireframe = app.ui.visual_style == 2;
|
||||
let visible = |p: Vec3| wireframe || bestf.as_ref().is_none_or(|(t, _)| (p - o).dot(d) <= *t + slack);
|
||||
// Vertices (edge end points) and edges.
|
||||
let mut bestv = Near::new(all);
|
||||
let mut beste = Near::new(all);
|
||||
@@ -1254,16 +1273,13 @@ fn pick_hits(app: &SolveApp, proj: &Proj, pos: Pos2, other_sketch_points: bool,
|
||||
}
|
||||
}
|
||||
}
|
||||
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.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.offer(dd, Hit::Edge { body: b.name.clone(), index: ei, mid });
|
||||
}
|
||||
for w in e.windows(2) {
|
||||
let (Some(a), Some(c)) = (proj.to_screen(w[0]), proj.to_screen(w[1])) else { continue };
|
||||
let dd = seg_dist(pos, a, c);
|
||||
// Visible where the cursor is: the edge's point under the cursor, not an end or
|
||||
// the middle, which can lie deeper than the face the cursor is on.
|
||||
if dd < 6.0 && beste.wants(dd) && visible(ray_segment_closest(o, d, w[0], w[1])) {
|
||||
beste.offer(dd, Hit::Edge { body: b.name.clone(), index: ei, mid: polyline_mid(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3784,6 +3800,61 @@ mod tests {
|
||||
assert!(bounds.min.x >= 29.99 && bounds.max.x <= 32.01 && bounds.max.y >= 41.99, "{bounds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ray_segment_closest_takes_the_point_under_the_ray() {
|
||||
let (o, d) = (Vec3::new(0.0, 0.0, 10.0), Vec3::new(0.0, 0.0, -1.0));
|
||||
let x = |a: Vec3, b: Vec3| ray_segment_closest(o, d, a, b);
|
||||
assert!(x(Vec3::new(-5.0, 0.0, 0.0), Vec3::new(5.0, 0.0, -4.0)).dist(Vec3::new(0.0, 0.0, -2.0)) < 1e-9);
|
||||
// Beyond an end: the end.
|
||||
assert!(x(Vec3::new(1.0, 0.0, 0.0), Vec3::new(3.0, 0.0, 0.0)).dist(Vec3::new(1.0, 0.0, 0.0)) < 1e-9);
|
||||
// Parallel, degenerate and non-finite input: the start, no panic.
|
||||
assert_eq!(x(Vec3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 5.0)), Vec3::new(1.0, 0.0, 0.0));
|
||||
assert_eq!(x(Vec3::new(2.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0)), Vec3::new(2.0, 0.0, 0.0));
|
||||
let nan = Vec3::new(f64::NAN, 0.0, 0.0);
|
||||
let _ = x(nan, Vec3::ZERO);
|
||||
}
|
||||
|
||||
/// Hovering anywhere along a visible edge between two faces picks that edge, not a face
|
||||
/// (the edge used to pick only where its start or middle was as near as the face).
|
||||
#[test]
|
||||
fn edge_between_two_faces_picks_along_its_whole_length() {
|
||||
for style in [0u8, 2] {
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.run("solid.box", json!({"length":200,"width":120,"height":160})).unwrap();
|
||||
app.ui.show_origin = false;
|
||||
app.ui.visual_style = style;
|
||||
app.cam.yaw = 0.7;
|
||||
app.cam.pitch = 0.5;
|
||||
app.cam.target = Vec3::new(100.0, 60.0, 80.0);
|
||||
app.cam.distance = 500.0;
|
||||
hover_frame(&mut app, &ctx, vec![]);
|
||||
let proj = projection(&app, app.viewport.rect.unwrap());
|
||||
let st = app.session.world_state();
|
||||
let m = st.body("Body1").unwrap().mesh();
|
||||
let mut checked = 0;
|
||||
for (ei, e) in m.edges.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (e.first().copied(), e.last().copied()) else { continue };
|
||||
for k in 1..10 {
|
||||
let p = a.lerp(b, k as f64 / 10.0);
|
||||
let Some(sp) = proj.to_screen(p) else { continue };
|
||||
let (o, d) = proj.ray(sp);
|
||||
// Only edges in front at that point (shaded); every edge in wireframe.
|
||||
let depth = (p - o).dot(d);
|
||||
if style != 2 && m.raycast(o, d).is_some_and(|(t, _)| t < depth - 1e-3) {
|
||||
continue;
|
||||
}
|
||||
let hits = pick(&app, &proj, sp);
|
||||
let first = candidate(&app, &hits).map(|x| x.1);
|
||||
assert!(matches!(first, Some(Sel::Edge { index, .. }) if index == ei), "style {style}: edge {ei} at {k}/10 picked {first:?}");
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
assert!(checked > 50, "style {style}: only {checked} samples");
|
||||
}
|
||||
}
|
||||
|
||||
fn hover_frame(app: &mut SolveApp, ctx: &egui::Context, events: Vec<egui::Event>) {
|
||||
let selected = app.highlighted();
|
||||
let mut output = ctx.run_ui(
|
||||
|
||||
Reference in new issue
Block a user