The nearest point on a triangle with a zero-length edge (a cone's tip) is no longer NaN, so an axis or tangent plane on a drill tip works again (#567).
This commit is contained in:
1 parent
0e500cacd3
commit
cb071bb93b
3 files changed
+75
-7
No files matched your search
@@ -1169,8 +1169,36 @@ fn a_shell_of_a_swept_bar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The construction a drill-tip script ends with.
|
||||||
|
struct TipConstruction(solidcraft_engine::doc::construct::ConstructGeom);
|
||||||
|
|
||||||
|
impl TipConstruction {
|
||||||
|
fn axis(&self) -> (Vec3, Vec3) {
|
||||||
|
match &self.0 {
|
||||||
|
solidcraft_engine::doc::construct::ConstructGeom::Axis { origin, dir } => (*origin, *dir),
|
||||||
|
other => panic!("expected an axis, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn plane(&self) -> solidcraft_engine::geom::Plane {
|
||||||
|
match &self.0 {
|
||||||
|
solidcraft_engine::doc::construct::ConstructGeom::Plane(p) => *p,
|
||||||
|
other => panic!("expected a plane, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replays the script and returns its last construction.
|
||||||
|
fn drill_tip_construction(commands: Vec<Value>) -> TipConstruction {
|
||||||
|
let mut s = Session::default();
|
||||||
|
for c in &commands {
|
||||||
|
let id = c["command"].as_str().unwrap();
|
||||||
|
s.execute(id, &c["params"]).unwrap_or_else(|e| panic!("{id}: {e}"));
|
||||||
|
}
|
||||||
|
let state = s.model.state();
|
||||||
|
TipConstruction(state.construct.last().map(|c| c.geom.clone()).expect("a construction"))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "fails until #567 is fixed"]
|
|
||||||
fn an_axis_through_a_drill_tip() {
|
fn an_axis_through_a_drill_tip() {
|
||||||
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
||||||
c.extend([
|
c.extend([
|
||||||
@@ -1178,7 +1206,30 @@ fn an_axis_through_a_drill_tip() {
|
|||||||
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
|
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
|
||||||
json!({"command": "construct.axis.through_cylinder", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
|
json!({"command": "construct.axis.through_cylinder", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
|
||||||
]);
|
]);
|
||||||
reproduces(None, Value::Array(c));
|
reproduces(None, Value::Array(c.clone()));
|
||||||
|
// Not just "no error": the axis runs along the drill, through the hole's centre line.
|
||||||
|
let (origin, dir) = drill_tip_construction(c).axis();
|
||||||
|
assert!((origin.x - 50.).abs() < 1e-6 && (origin.y - 20.).abs() < 1e-6, "{origin:?}");
|
||||||
|
assert!(dir.z.abs() > 1. - 1e-9, "{dir:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_tangent_plane_on_a_drill_tip() {
|
||||||
|
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
|
||||||
|
c.extend([
|
||||||
|
json!({"command": "solid.extrude", "params": {"distance": 15}}),
|
||||||
|
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
|
||||||
|
json!({"command": "construct.plane.tangent", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
|
||||||
|
]);
|
||||||
|
reproduces(None, Value::Array(c.clone()));
|
||||||
|
// The plane touches the cone at the picked point and leans like the cone does: a 118 degree tip
|
||||||
|
// puts the wall 59 degrees off the drill axis, so its normal is 31 degrees off it.
|
||||||
|
let plane = drill_tip_construction(c).plane();
|
||||||
|
let picked = Vec3::new(48.44468907075447, 21.22448979591836, 4.399139380972439);
|
||||||
|
let distance = (picked - plane.origin).dot(plane.normal());
|
||||||
|
// The pick lies on the tessellated cone and the plane is the exact one, so allow for chord error.
|
||||||
|
assert!(distance.abs() < 0.05, "the pick is {distance} mm off the plane");
|
||||||
|
assert!((plane.normal().z.abs() - 31f64.to_radians().cos()).abs() < 1e-2, "{:?}", plane.normal());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ fn nearest_on_face(m: &Mesh, face: usize, p: Vec3) -> Option<(f64, Vec3)> {
|
|||||||
let Some([a, b, c]) = m.tri(t) else { continue };
|
let Some([a, b, c]) = m.tri(t) else { continue };
|
||||||
let q = closest(p, a, b, c);
|
let q = closest(p, a, b, c);
|
||||||
let d = q.dist(p);
|
let d = q.dist(p);
|
||||||
if best.is_none_or(|x| d < x.0) {
|
// A non-finite candidate must never become the best: nothing compares less than NaN.
|
||||||
|
if d.is_finite() && best.is_none_or(|x| d < x.0) {
|
||||||
best = Some((d, q));
|
best = Some((d, q));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,7 +318,7 @@ fn legacy_locate<'a>(bodies: impl Iterator<Item = (usize, &'a ModelBody)>, name:
|
|||||||
let Some([a, bb, c]) = m.tri(t) else { continue };
|
let Some([a, bb, c]) = m.tri(t) else { continue };
|
||||||
let q = closest(p, a, bb, c);
|
let q = closest(p, a, bb, c);
|
||||||
let d = q.dist(p) - if exact { 1e-9 } else { 0.0 };
|
let d = q.dist(p) - if exact { 1e-9 } else { 0.0 };
|
||||||
if best.is_none_or(|x| d < x.0) {
|
if d.is_finite() && best.is_none_or(|x| d < x.0) {
|
||||||
best = Some((d, bi, *f as usize, q, !exact, tol));
|
best = Some((d, bi, *f as usize, q, !exact, tol));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ fn samples(m: &Mesh, nf: usize) -> Vec<Vec<Vec3>> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The point of triangle `abc` nearest `p`. A zero-length edge (a cone's tip repeats its apex
|
||||||
|
/// vertex) has no interior to project onto, so its branch is skipped rather than dividing 0 by 0.
|
||||||
pub(crate) fn closest(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
pub(crate) fn closest(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||||
let (ab, ac, ap) = (b - a, c - a, p - a);
|
let (ab, ac, ap) = (b - a, c - a, p - a);
|
||||||
let (d1, d2) = (ab.dot(ap), ac.dot(ap));
|
let (d1, d2) = (ab.dot(ap), ac.dot(ap));
|
||||||
@@ -126,7 +128,7 @@ pub(crate) fn closest(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
let vc = d1 * d4 - d3 * d2;
|
let vc = d1 * d4 - d3 * d2;
|
||||||
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
|
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 && d1 - d3 > 0.0 {
|
||||||
return a + ab * (d1 / (d1 - d3));
|
return a + ab * (d1 / (d1 - d3));
|
||||||
}
|
}
|
||||||
let cp = p - c;
|
let cp = p - c;
|
||||||
@@ -135,11 +137,11 @@ pub(crate) fn closest(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
let vb = d5 * d2 - d1 * d6;
|
let vb = d5 * d2 - d1 * d6;
|
||||||
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
|
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 && d2 - d6 > 0.0 {
|
||||||
return a + ac * (d2 / (d2 - d6));
|
return a + ac * (d2 / (d2 - d6));
|
||||||
}
|
}
|
||||||
let va = d3 * d6 - d5 * d4;
|
let va = d3 * d6 - d5 * d4;
|
||||||
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
|
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 && (d4 - d3) + (d5 - d6) > 0.0 {
|
||||||
return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6)));
|
return b + (c - b) * ((d4 - d3) / ((d4 - d3) + (d5 - d6)));
|
||||||
}
|
}
|
||||||
let den = va + vb + vc;
|
let den = va + vb + vc;
|
||||||
@@ -753,6 +755,20 @@ pub fn face_on_body(body: &ModelBody, name: &str, p: Vec3) -> Option<(usize, Vec
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// A cone's tip repeats its apex vertex, so a triangle can have a zero-length edge. The nearest
|
||||||
|
/// point on it must still be a real point (it used to be NaN, which broke picking the tip).
|
||||||
|
#[test]
|
||||||
|
fn closest_point_on_a_degenerate_triangle_is_finite() {
|
||||||
|
let (a, c) = (Vec3::new(0., 0., 0.), Vec3::new(2., 0., 0.));
|
||||||
|
for (b, c) in [(a, c), (c, c), (a, a)] {
|
||||||
|
for p in [Vec3::new(1., 1., 0.), Vec3::new(-1., 0.5, 0.), Vec3::new(3., -2., 1.), a] {
|
||||||
|
let q = closest(p, a, b, c);
|
||||||
|
assert!(q.x.is_finite() && q.y.is_finite() && q.z.is_finite(), "{a:?} {b:?} {c:?} {p:?} -> {q:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(closest(Vec3::new(1., 1., 0.), a, a, c), Vec3::new(1., 0., 0.));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merged_face_votes_collectively_for_pieces_of_one_source() {
|
fn merged_face_votes_collectively_for_pieces_of_one_source() {
|
||||||
let body = ModelBody::new("Input".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 1., 1.)).unwrap(), 1);
|
let body = ModelBody::new("Input".into(), solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(4., 1., 1.)).unwrap(), 1);
|
||||||
|
|||||||
Reference in new issue
Block a user