254 lines
14 KiB
Rust
254 lines
14 KiB
Rust
use serde_json::{Value, json};
|
||
|
||
use crate::Session;
|
||
|
||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||
match s.execute(id, &p) {
|
||
Ok(v) => v,
|
||
Err(e) => panic!("{id} {p}: {e}"),
|
||
}
|
||
}
|
||
|
||
fn cube(s: &mut Session, name: &str, size: f64, corner: [f64; 3]) {
|
||
run(s, "solid.box", json!({"length": size, "width": size, "height": size, "corner": corner, "operation": "new", "body_name": name}));
|
||
}
|
||
|
||
fn f(v: &Value) -> f64 {
|
||
v.as_f64().unwrap_or(f64::NAN)
|
||
}
|
||
|
||
/// The pair `{a, b}` of a list entry, sorted.
|
||
fn pair(v: &Value) -> [String; 2] {
|
||
let mut p = [v["a"].as_str().unwrap_or("").to_string(), v["b"].as_str().unwrap_or("").to_string()];
|
||
p.sort();
|
||
p
|
||
}
|
||
|
||
/// Two 10 mm cubes sharing a 1 mm corner cube overlap by 1 mm³; a third, far off, is pruned.
|
||
#[test]
|
||
fn an_overlap_of_one_cubic_millimetre_is_reported() {
|
||
let mut s = Session::default();
|
||
cube(&mut s, "A", 10.0, [0.0, 0.0, 0.0]);
|
||
cube(&mut s, "B", 10.0, [9.0, 9.0, 9.0]);
|
||
cube(&mut s, "Far", 10.0, [100.0, 0.0, 0.0]);
|
||
let r = run(&mut s, "inspect.clearance", json!({}));
|
||
let overlaps = r["overlaps"].as_array().cloned().unwrap_or_default();
|
||
assert_eq!(overlaps.len(), 1, "{r}");
|
||
assert_eq!(pair(&overlaps[0]), ["A", "B"]);
|
||
assert!((f(&overlaps[0]["volume_mm3"]) - 1.0).abs() < 1e-6, "{r}");
|
||
// Three faces of 1 x 1 from each cube: 6 mm².
|
||
assert!((f(&overlaps[0]["area_mm2"]) - 6.0).abs() < 1e-6, "{r}");
|
||
assert!((f(&overlaps[0]["center"][0]) - 9.5).abs() < 1e-6, "{r}");
|
||
assert_eq!(r["pairs_checked"], 1, "{r}");
|
||
assert_eq!(r["pairs_pruned"], 2, "{r}");
|
||
assert_eq!(r["unchecked"].as_array().map(Vec::len), Some(0), "{r}");
|
||
// An overlap under min_volume is contact, not an overlap: with a clearance it is near, at 0.
|
||
let r = run(&mut s, "inspect.clearance", json!({"min_volume": 2, "clearance": 0.1}));
|
||
assert_eq!(r["overlaps"].as_array().map(Vec::len), Some(0), "{r}");
|
||
assert_eq!(f(&r["near"][0]["distance_mm"]), 0.0, "{r}");
|
||
assert!((f(&r["near"][0]["overlap_mm3"]) - 1.0).abs() < 1e-6, "{r}");
|
||
}
|
||
|
||
/// Two cubes 0.5 mm apart are near with a clearance of 1 mm; 2 mm apart they are not.
|
||
#[test]
|
||
fn pairs_closer_than_the_clearance_are_near() {
|
||
let mut s = Session::default();
|
||
cube(&mut s, "A", 10.0, [0.0, 0.0, 0.0]);
|
||
cube(&mut s, "B", 10.0, [10.5, 2.0, 3.0]);
|
||
cube(&mut s, "C", 10.0, [-12.0, 0.0, 0.0]);
|
||
let r = run(&mut s, "inspect.clearance", json!({"clearance": 1}));
|
||
let near = r["near"].as_array().cloned().unwrap_or_default();
|
||
assert_eq!(near.len(), 1, "{r}");
|
||
assert_eq!(pair(&near[0]), ["A", "B"]);
|
||
assert!((f(&near[0]["distance_mm"]) - 0.5).abs() < 1e-9, "{r}");
|
||
assert!(near[0].get("exact").is_none(), "measured on the exact faces: {r}");
|
||
// The points lie on the facing faces x = 10 and x = 10.5.
|
||
let xs = [f(&near[0]["from"][0]), f(&near[0]["to"][0])];
|
||
assert!(xs.iter().any(|x| (x - 10.0).abs() < 1e-9) && xs.iter().any(|x| (x - 10.5).abs() < 1e-9), "{r}");
|
||
assert_eq!(r["overlaps"].as_array().map(Vec::len), Some(0), "{r}");
|
||
// With no clearance nothing is near; with 2.5 mm C (2 mm from A) is near too.
|
||
let r = run(&mut s, "inspect.clearance", json!({}));
|
||
assert_eq!(r["near"].as_array().map(Vec::len), Some(0), "{r}");
|
||
let r = run(&mut s, "inspect.clearance", json!({"clearance": 2.5}));
|
||
let near = r["near"].as_array().cloned().unwrap_or_default();
|
||
assert_eq!(near.len(), 2, "{r}");
|
||
assert_eq!(pair(&near[1]), ["A", "C"]);
|
||
assert!((f(&near[1]["distance_mm"]) - 2.0).abs() < 1e-9, "{r}");
|
||
}
|
||
|
||
/// Distances where the nearest points are on edges, and on curved faces whose meshes are far
|
||
/// coarser than the answer: cube edge to cube edge, a cylinder's side to a cube's face, and a
|
||
/// large sphere (mesh chord about 1 mm) to a cube.
|
||
#[test]
|
||
fn distances_are_exact_on_edges_and_curved_faces() {
|
||
let mut s = Session::default();
|
||
cube(&mut s, "A", 10.0, [0.0, 0.0, 0.0]);
|
||
// Its vertical edge at (12, 13) faces A's at (10, 10): √(2² + 3²).
|
||
cube(&mut s, "B", 10.0, [12.0, 13.0, 0.0]);
|
||
run(&mut s, "solid.cylinder", json!({"base": [-30, 5, 0], "radius": 5, "height": 10, "operation": "new", "body_name": "Pin"}));
|
||
run(&mut s, "solid.sphere", json!({"radius": 500, "center": [5, 5, 510.7], "operation": "new", "body_name": "Ball"}));
|
||
let r = run(&mut s, "inspect.clearance", json!({"clearance": 50, "against": ["A"], "bodies": ["B", "Pin", "Ball"]}));
|
||
let near = r["near"].as_array().cloned().unwrap_or_default();
|
||
let d = |other: &str| near.iter().find(|n| pair(n).contains(&other.to_string())).map(|n| (f(&n["distance_mm"]), n.get("exact").is_none()));
|
||
assert_eq!(near.len(), 3, "{r}");
|
||
let (edge, exact) = d("B").unwrap_or((f64::NAN, false));
|
||
assert!((edge - 13f64.sqrt()).abs() < 1e-6 && exact, "{r}");
|
||
// The pin's side (x = -25) is 25 from A's face x = 0.
|
||
let (side, exact) = d("Pin").unwrap_or((f64::NAN, false));
|
||
assert!((side - 25.0).abs() < 1e-6 && exact, "{r}");
|
||
// The ball's lowest point, (5, 5, 10.7), is 0.7 above A's top.
|
||
let (ball, exact) = d("Ball").unwrap_or((f64::NAN, false));
|
||
assert!((ball - 0.7).abs() < 1e-6 && exact, "{r}");
|
||
// `against` leaves out the pairs among `bodies`, which are by default every other body.
|
||
assert!(near.iter().all(|n| pair(n).contains(&"A".to_string())), "{r}");
|
||
let all = run(&mut s, "inspect.clearance", json!({"clearance": 50, "against": ["A"]}));
|
||
assert_eq!(all["near"].as_array().map(Vec::len), Some(3), "{all}");
|
||
}
|
||
|
||
/// A pin in a hole 0.2 mm wider all round, in a plate whose mesh is coarser than that gap: the
|
||
/// plate's flat faces reach over the hole on their surfaces but not on the faces, so the
|
||
/// distance is the gap to the hole's wall.
|
||
#[test]
|
||
fn a_pin_in_a_hole_is_as_far_as_the_holes_wall() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "solid.box", json!({"length": 100, "width": 60, "height": 2, "operation": "new", "body_name": "Plate"}));
|
||
run(&mut s, "solid.cylinder", json!({"radius": 3.2, "height": 4, "base": [10, 10, -1], "operation": "cut", "targets": ["Plate"]}));
|
||
run(&mut s, "solid.cylinder", json!({"radius": 3, "height": 12, "base": [10, 10, -5], "operation": "new", "body_name": "Pin"}));
|
||
let r = run(&mut s, "inspect.clearance", json!({"clearance": 1}));
|
||
assert_eq!(r["overlaps"].as_array().map(Vec::len), Some(0), "{r}");
|
||
assert!((f(&r["near"][0]["distance_mm"]) - 0.2).abs() < 1e-6, "{r}");
|
||
let r = run(&mut s, "inspect.clearance", json!({"clearance": 0.15}));
|
||
assert_eq!(r["near"].as_array().map(Vec::len), Some(0), "{r}");
|
||
}
|
||
|
||
/// Three spheres' nearest neighbours against their distances worked out by hand.
|
||
#[test]
|
||
fn nearest_distances_of_three_spheres() {
|
||
let mut s = Session::default();
|
||
for (name, r, c) in [("S1", 5.0, [0.0, 0.0, 0.0]), ("S2", 3.0, [12.0, 0.0, 0.0]), ("S3", 2.0, [3.0, 20.0, 4.0])] {
|
||
run(&mut s, "solid.sphere", json!({"radius": r, "center": c, "operation": "new", "body_name": name}));
|
||
}
|
||
let r = run(&mut s, "inspect.clearance", json!({"nearest": 2}));
|
||
let d12 = 12.0 - 8.0;
|
||
let d13 = (9.0f64 + 400.0 + 16.0).sqrt() - 7.0;
|
||
let d23 = (81.0f64 + 400.0 + 16.0).sqrt() - 5.0;
|
||
let want = [("S1", [("S2", d12), ("S3", d13)]), ("S2", [("S1", d12), ("S3", d23)]), ("S3", [("S1", d13), ("S2", d23)])];
|
||
for (body, list) in want {
|
||
let got = r["nearest"][body].as_array().cloned().unwrap_or_default();
|
||
assert_eq!(got.len(), 2, "{body}: {r}");
|
||
for (g, (other, d)) in got.iter().zip(list) {
|
||
assert_eq!(g["body"], other, "{body}: {r}");
|
||
assert!((f(&g["distance_mm"]) - d).abs() < 1e-6, "{body} to {other}: {} against {d}", g["distance_mm"]);
|
||
}
|
||
}
|
||
// One neighbour each.
|
||
let r = run(&mut s, "inspect.clearance", json!({"nearest": 1, "bodies": ["S3"]}));
|
||
assert_eq!(r["nearest"]["S3"].as_array().map(Vec::len), Some(0), "only S3 is in `bodies`, so it has no partner: {r}");
|
||
let r = run(&mut s, "inspect.clearance", json!({"nearest": 1, "bodies": ["S3"], "against": ["S1", "S2"]}));
|
||
assert_eq!(r["nearest"]["S3"][0]["body"], "S1", "{r}");
|
||
}
|
||
|
||
/// `skip_within` leaves out the pairs inside one component (two bodies made to touch, as the
|
||
/// parts of a bought component do) and keeps those between components.
|
||
#[test]
|
||
fn skip_within_leaves_out_a_components_own_pairs() {
|
||
let mut s = Session::default();
|
||
run(&mut s, "component.create", json!({"name": "Bought"}));
|
||
cube(&mut s, "Housing", 10.0, [0.0, 0.0, 0.0]);
|
||
cube(&mut s, "Insert", 4.0, [8.0, 3.0, 3.0]);
|
||
run(&mut s, "component.activate", json!({"component": "root"}));
|
||
run(&mut s, "component.create", json!({"name": "Bracket"}));
|
||
cube(&mut s, "Plate", 10.0, [-9.5, 0.0, 0.0]);
|
||
let all = run(&mut s, "inspect.clearance", json!({}));
|
||
let pairs: Vec<[String; 2]> = all["overlaps"].as_array().into_iter().flatten().map(pair).collect();
|
||
assert!(pairs.contains(&["Housing".into(), "Insert".into()]) && pairs.contains(&["Housing".into(), "Plate".into()]), "{all}");
|
||
for mode in ["component", "occurrence"] {
|
||
let r = run(&mut s, "inspect.clearance", json!({"skip_within": mode}));
|
||
let pairs: Vec<[String; 2]> = r["overlaps"].as_array().into_iter().flatten().map(pair).collect();
|
||
assert_eq!(pairs, vec![["Housing".to_string(), "Plate".to_string()]], "{mode}: {r}");
|
||
assert!((f(&r["overlaps"][0]["volume_mm3"]) - 50.0).abs() < 1e-6, "{r}");
|
||
}
|
||
}
|
||
|
||
/// Parameters out of range are refused, not guessed at.
|
||
#[test]
|
||
fn bad_parameters_are_refused() {
|
||
let mut s = Session::default();
|
||
cube(&mut s, "A", 10.0, [0.0, 0.0, 0.0]);
|
||
for p in [
|
||
json!({"clearance": -1}),
|
||
json!({"clearance": "wide"}),
|
||
json!({"min_volume": -0.1}),
|
||
json!({"nearest": 1e9}),
|
||
json!({"tolerance": 5}),
|
||
json!({"skip_within": "body"}),
|
||
json!({"bodies": ["Nope"]}),
|
||
json!({"against": ["Nope"]}),
|
||
] {
|
||
assert!(s.execute("inspect.clearance", &p).is_err(), "{p}");
|
||
}
|
||
let r = run(&mut s, "inspect.clearance", json!({"nearest": 3, "skip_within": null}));
|
||
assert_eq!(r["pairs_checked"], 0, "{r}");
|
||
assert_eq!(r["nearest"]["A"].as_array().map(Vec::len), Some(0), "{r}");
|
||
}
|
||
|
||
/// A pin sunk 0.01 mm into a plate 1.7 m long: the shared ring holds π (4.43² − 2.5²) × 0.01 mm³
|
||
/// wherever the plate stands, as on a plate of 100 mm (the boolean of the whole plate gave 0.31).
|
||
#[test]
|
||
fn a_thin_overlap_with_a_large_plate_is_measured_in_full() {
|
||
let ring = std::f64::consts::PI * (4.43f64.powi(2) - 2.5f64.powi(2)) * 0.01;
|
||
for (length, far) in [(100.0, false), (1700.0, false), (1700.0, true)] {
|
||
let mut s = Session::default();
|
||
let corner = [-length / 2.0, -length / 4.0, 0.0];
|
||
run(&mut s, "solid.box", json!({"length": length, "width": length / 2.0, "height": 5, "corner": corner, "body_name": "Plate"}));
|
||
run(&mut s, "solid.cylinder", json!({"radius": 2.5, "height": 9, "base": [0, 0, -2], "operation": "cut", "targets": ["Plate"]}));
|
||
run(&mut s, "solid.cylinder", json!({"radius": 2.4, "height": 7, "base": [0, 0, -1.99], "operation": "new", "body_name": "Pin"}));
|
||
run(&mut s, "solid.cylinder", json!({"radius": 4.43, "height": 2, "base": [0, 0, 4.99], "operation": "join", "targets": ["Pin"]}));
|
||
let shift = if far { [-326.3, 13.5, 1278.3] } else { [0.0; 3] };
|
||
if far {
|
||
run(&mut s, "solid.move", json!({"bodies": ["Plate", "Pin"], "translate": shift}));
|
||
}
|
||
let r = run(&mut s, "inspect.clearance", json!({"min_volume": 1e-4}));
|
||
let o = &r["overlaps"][0];
|
||
assert!((f(&o["volume_mm3"]) - ring).abs() < 1e-3 * ring, "{length} {far}: {r}");
|
||
// The ring's centre is on the axis, 0.005 below the plate's face.
|
||
assert!((f(&o["center"][2]) - (4.995 + shift[2])).abs() < 1e-4, "{length} {far}: {r}");
|
||
assert!((f(&o["center"][0]) - shift[0]).abs() < 1e-4, "{length} {far}: {r}");
|
||
}
|
||
}
|
||
|
||
/// A shared part whose centre lies outside its own box, or which holds more than its box, is
|
||
/// malformed: the pair is reported unchecked rather than with that volume.
|
||
#[test]
|
||
fn a_malformed_shared_part_is_not_measured() {
|
||
use solidcraft_geom::{Aabb3, Vec3};
|
||
let bbox = Aabb3 { min: Vec3::new(0.0, 0.0, 0.0), max: Vec3::new(10.0, 10.0, 1.0) };
|
||
assert!(super::plausible(50.0, Vec3::new(5.0, 5.0, 0.5), bbox));
|
||
assert!(!super::plausible(50.0, Vec3::new(5.0, 5.0, 3.0), bbox));
|
||
assert!(!super::plausible(1239.0, Vec3::new(5.0, 5.0, 0.5), bbox));
|
||
assert!(!super::plausible(f64::NAN, Vec3::new(5.0, 5.0, 0.5), bbox));
|
||
}
|
||
|
||
/// Two plates 0.2 mm apart, the upper one set 10 mm in from the lower one's corner, and two set
|
||
/// side by side with a 0.2 mm gap: 0.2, not 0 (a point past a corner, on the line of one of its
|
||
/// edges, was taken for a point of the face). Gaps under twice the meshes' chord went wrong.
|
||
#[test]
|
||
fn a_small_gap_past_a_corner_is_not_contact() {
|
||
let mut s = Session::default();
|
||
let plate = |s: &mut Session, name: &str, size: [f64; 3], corner: [f64; 3]| {
|
||
run(s, "solid.box", json!({"length": size[0], "width": size[1], "height": size[2], "corner": corner, "operation": "new", "body_name": name}));
|
||
};
|
||
plate(&mut s, "A", [100.0, 100.0, 2.0], [0.0, 0.0, 0.0]);
|
||
plate(&mut s, "B", [100.0, 100.0, 2.0], [10.0, 10.0, 2.2]);
|
||
plate(&mut s, "C", [100.0, 100.0, 2.0], [0.0, 0.0, 10.0]);
|
||
plate(&mut s, "D", [100.0, 100.0, 2.0], [100.2, 0.0, 10.0]);
|
||
// 3 mm apart in x and 0.2 in z, along their long sides: √(3² + 0.2²).
|
||
plate(&mut s, "E", [1000.0, 100.0, 2.0], [0.0, 300.0, 0.0]);
|
||
plate(&mut s, "F", [1000.0, 100.0, 2.0], [1003.0, 300.0, 2.2]);
|
||
for (a, b, want) in [("A", "B", 0.2), ("C", "D", 0.2), ("E", "F", (9.0f64 + 0.04).sqrt())] {
|
||
let r = run(&mut s, "inspect.clearance", json!({"bodies": [a], "against": [b], "clearance": 5, "nearest": 4}));
|
||
assert!((f(&r["near"][0]["distance_mm"]) - want).abs() < 1e-9, "{a} {b}: {r}");
|
||
assert!((f(&r["nearest"][a][0]["distance_mm"]) - want).abs() < 1e-9, "{a} {b}: {r}");
|
||
}
|
||
}
|