use super::*;
use indicatrix::geometry::stone_metrics::build_solid_mesh;
fn box_planes(half: DVec3) -> Vec<(DVec3, f64)> {
vec![
(DVec3::X, half.x),
(DVec3::NEG_X, half.x),
(DVec3::Y, half.y),
(DVec3::NEG_Y, half.y),
(DVec3::Z, half.z),
(DVec3::NEG_Z, half.z),
]
}
fn turned_box(half: DVec3, degrees: f64, shift: DVec3) -> Vec<(DVec3, f64)> {
let (sin, cos) = degrees.to_radians().sin_cos();
let turn = |v: DVec3| {
DVec3::new(
v.x.mul_add(cos, v.z * sin),
v.y,
(-v.x).mul_add(sin, v.z * cos),
)
};
box_planes(half)
.into_iter()
.map(|(n, m)| {
let turned = turn(n);
(turned, m + turned.dot(shift))
})
.collect()
}
fn bounds(mesh: &DesignMesh) -> (DVec3, DVec3) {
let (mut low, mut high) = (DVec3::splat(f64::INFINITY), DVec3::splat(f64::NEG_INFINITY));
for facet in &mesh.facets {
for &p in &facet.ring {
low = low.min(p);
high = high.max(p);
}
}
(low, high)
}
#[test]
fn a_turned_box_gets_its_width_on_x_and_its_length_on_z_and_is_centred() {
let planes = turned_box(DVec3::new(1.0, 1.5, 3.0), 30.0, DVec3::new(0.7, 0.2, -0.4));
let mesh = design_mesh_from_planes(&planes, &[]).expect("a box closes");
let (low, high) = bounds(&mesh);
assert!((low + high).length() < 1e-9, "bounding box is centred");
assert!((high - low - DVec3::new(2.0, 3.0, 6.0)).length() < 1e-5);
assert_eq!(mesh.facets.len(), 6);
}
#[test]
fn the_caliper_width_is_the_extent_along_x() {
let planes = turned_box(DVec3::new(1.0, 1.5, 3.0), 30.0, DVec3::ZERO);
let mesh = design_mesh_from_planes(&planes, &[]).expect("a box closes");
assert!((mesh.caliper_width() - 2.0).abs() < 1e-5);
assert!(DesignMesh { facets: Vec::new() }.caliper_width().abs() < f64::EPSILON);
}
#[test]
fn facet_normals_stay_unit_and_point_away_from_the_centre() {
let planes = turned_box(DVec3::new(1.0, 1.5, 3.0), 30.0, DVec3::ZERO);
let mesh = design_mesh_from_planes(&planes, &[]).expect("a box closes");
for facet in &mesh.facets {
assert!((facet.normal.length() - 1.0).abs() < 1e-9);
let mid = facet.ring.iter().copied().sum::<DVec3>() / facet.ring.len() as f64;
assert!(facet.normal.dot(mid) > 0.0, "outward normal");
}
}
#[test]
fn the_caliper_frame_agrees_with_the_hull_pipeline_on_a_turned_box() {
let planes = turned_box(DVec3::new(1.0, 1.5, 3.0), 30.0, DVec3::new(0.7, 0.2, -0.4));
let mesh = design_mesh_from_planes(&planes, &[]).expect("a box closes");
let SolidStatus::Closed(raw) = build_solid_mesh(&planes) else {
panic!("a box closes");
};
let corners: Vec<[f64; 3]> = raw
.rings
.iter()
.flat_map(|(_, r)| r.iter().map(|p| p.to_array().map(|c| f64::from(c as f32))))
.collect();
let hull = rotate_into_caliper_frame(&corners).expect("outline");
let hull_size = hull.size();
let (mesh_low, mesh_high) = bounds(&mesh);
let mesh_size = (mesh_high - mesh_low).to_array();
for axis in 0..3 {
assert!(
(hull_size[axis] - mesh_size[axis]).abs() < 1e-5,
"axis {axis}: hull {} vs mesh {}",
hull_size[axis],
mesh_size[axis]
);
}
let centre = DVec3::from_array(hull.centre());
let hull_corners: Vec<DVec3> = hull
.vertices
.iter()
.map(|&v| DVec3::from_array(v) - centre)
.collect();
for facet in &mesh.facets {
for &corner in &facet.ring {
assert!(
hull_corners.iter().any(|&h| (h - corner).length() < 1e-5),
"mesh corner {corner:?} is not a centred hull corner"
);
}
}
assert!((mesh_size[0] - 2.0).abs() < 1e-5 && (mesh_size[1] - 3.0).abs() < 1e-5);
assert!((mesh_size[2] - 6.0).abs() < 1e-5);
}
#[test]
fn a_tied_outline_is_turned_the_way_the_stored_f32_hull_is() {
let planes = turned_box(DVec3::new(1.5, 1.0, 1.5), 17.0, DVec3::new(9.3, 0.1, -4.7));
let mesh = design_mesh_from_planes(&planes, &[]).expect("a box closes");
let SolidStatus::Closed(raw) = build_solid_mesh(&planes) else {
panic!("a box closes");
};
let stored: Vec<[f64; 3]> = raw
.rings
.iter()
.flat_map(|(_, r)| r.iter().map(|p| p.to_array().map(|c| f64::from(c as f32))))
.collect();
let hull = rotate_into_caliper_frame(&stored).expect("outline");
let [wx, wz] = hull.width_dir;
let along_width = mesh
.facets
.iter()
.filter(|facet| facet.normal.x > 0.999)
.count();
assert_eq!(along_width, 1, "one facet faces +x of the caliper frame");
let facet = mesh
.facets
.iter()
.find(|facet| facet.normal.x > 0.999)
.expect("a +x facet");
let back = DVec3::new(
facet.normal.x.mul_add(wx, -facet.normal.z * wz),
facet.normal.y,
facet.normal.x.mul_add(wz, facet.normal.z * wx),
);
assert!(
(back - DVec3::new(wx, 0.0, wz)).length() < 1e-5,
"the mesh's width axis {back:?} is the hull's {wx}, {wz}"
);
}
#[test]
fn a_concave_design_is_drawn_with_its_tool_cuts_in_the_flat_stones_frame() {
let planes = turned_box(DVec3::new(1.0, 1.5, 3.0), 30.0, DVec3::new(0.7, 0.2, -0.4));
let top = planes[2];
let on_top = top.0 * top.1;
let tool = ToolPrimitive {
kind: 0,
sweep_kind: 0,
_pad: [0; 2],
origin: [on_top.x as f32, on_top.y as f32, on_top.z as f32, 0.5],
axis: [0.0, 1.0, 0.0, 0.0],
profile: [0.0; 4],
sweep_dir: [0.0; 4],
};
let flat = design_mesh_from_planes(&planes, &[]).expect("a box closes");
let carved = design_mesh_from_planes(&planes, &[tool]).expect("a carved box closes");
assert!(
carved.facets.len() > flat.facets.len(),
"the dimple adds pieces: {} vs {}",
carved.facets.len(),
flat.facets.len()
);
let ((flat_low, flat_high), (low, high)) = (bounds(&flat), bounds(&carved));
assert!(
(flat_low - low).length() < 1e-6 && (flat_high - high).length() < 1e-6,
"carved bounds {low:?}..{high:?} differ from flat {flat_low:?}..{flat_high:?}"
);
for facet in &carved.facets {
assert!((facet.normal.length() - 1.0).abs() < 1e-9);
let drawn = facet
.edge_drawn
.as_ref()
.expect("a concave mesh has edge flags");
assert_eq!(drawn.len(), facet.ring.len());
}
assert!(
flat.facets.iter().all(|f| f.edge_drawn.is_none()),
"a flat design draws every edge"
);
}
#[test]
fn a_ring_keeps_a_boundary_edge_when_its_collinear_seam_points_are_dropped() {
let ring = [
DVec3::new(0.0, 0.0, 0.0),
DVec3::new(0.5, 0.0, 0.0),
DVec3::new(1.0, 0.0, 0.0),
DVec3::new(1.0, 0.0, 1.0),
DVec3::new(0.0, 0.0, 1.0),
];
let corners = simplify_ring(&ring);
assert_eq!(corners.len(), 4);
let flags = simplified_flags(&ring, &corners, &[false, true, false, false, false]);
assert_eq!(flags, vec![true, false, false, false]);
}
#[test]
fn a_cached_mesh_is_loaded_once_and_dropped_by_remove() {
let cache = MeshCache::default();
let loads = std::sync::atomic::AtomicUsize::new(0);
let load = || -> Result<Option<DesignMesh>, ()> {
loads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Some(DesignMesh { facets: Vec::new() }))
};
let first = cache.get_or_load(5, load).expect("loaded");
let second = cache.get_or_load(5, load).expect("cached");
assert!(Arc::ptr_eq(&first, &second));
assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 1);
cache.get_or_load(6, load).expect("loaded");
assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 2);
cache.remove(5);
let third = cache.get_or_load(5, load).expect("reloaded");
assert!(!Arc::ptr_eq(&first, &third));
assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 3);
let mut ids = cache.ids();
ids.sort_unstable();
assert_eq!(ids, vec![5, 6]);
cache.get_or_load(6, load).expect("still cached");
assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 3);
}
#[test]
fn an_absent_design_is_remembered_as_gone_but_a_failed_read_is_not_and_reads_unreadable() {
let cache = MeshCache::default();
let calls = std::sync::atomic::AtomicUsize::new(0);
let absent = || -> Result<Option<DesignMesh>, ()> {
calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(None)
};
assert_eq!(cache.get_or_load(1, absent).err(), Some(MeshMiss::Gone));
assert_eq!(cache.get_or_load(1, absent).err(), Some(MeshMiss::Gone));
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
let failing = || -> Result<Option<DesignMesh>, ()> {
calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Err(())
};
assert_eq!(
cache.get_or_load(2, failing).err(),
Some(MeshMiss::Unreadable)
);
assert_eq!(
cache.get_or_load(2, failing).err(),
Some(MeshMiss::Unreadable)
);
assert_eq!(
calls.load(std::sync::atomic::Ordering::SeqCst),
3,
"the failed read was tried twice"
);
let ok = || -> Result<Option<DesignMesh>, ()> { Ok(Some(DesignMesh { facets: Vec::new() })) };
assert!(cache.get_or_load(2, ok).is_ok());
}
#[test]
fn two_threads_asking_for_one_design_load_it_once() {
let cache = MeshCache::default();
let loads = std::sync::atomic::AtomicUsize::new(0);
let meshes: Vec<Arc<DesignMesh>> = std::thread::scope(|scope| {
let handles: Vec<_> = (0..4)
.map(|_| {
scope.spawn(|| {
cache
.get_or_load(9, || -> Result<Option<DesignMesh>, ()> {
loads.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(20));
Ok(Some(DesignMesh { facets: Vec::new() }))
})
.expect("loaded")
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("no panic"))
.collect()
});
assert_eq!(loads.load(std::sync::atomic::Ordering::SeqCst), 1);
assert!(meshes.iter().all(|m| Arc::ptr_eq(m, &meshes[0])));
}
#[test]
fn a_load_under_way_during_a_remove_cannot_refill_the_cache() {
let cache = MeshCache::default();
let stale = cache
.get_or_load(3, || -> Result<Option<DesignMesh>, ()> {
cache.remove(3);
Ok(Some(DesignMesh { facets: Vec::new() }))
})
.expect("the old asker still gets its mesh");
let fresh = cache
.get_or_load(3, || -> Result<Option<DesignMesh>, ()> {
Ok(Some(DesignMesh { facets: Vec::new() }))
})
.expect("reloaded");
assert!(!Arc::ptr_eq(&stale, &fresh), "the stale mesh was not kept");
}
#[test]
fn open_planes_give_no_mesh() {
assert!(design_mesh_from_planes(&[(DVec3::X, 1.0), (DVec3::NEG_X, 1.0)], &[]).is_none());
assert!(design_mesh_from_planes(&[], &[]).is_none());
}
#[test]
fn simplifying_a_ring_drops_repeats_and_midpoints() {
let ring = [
DVec3::new(0.0, 0.0, 0.0),
DVec3::new(0.5, 0.0, 0.0),
DVec3::new(0.5, 0.0, 0.0),
DVec3::new(1.0, 0.0, 0.0),
DVec3::new(1.0, 0.0, 1.0),
DVec3::new(0.0, 0.0, 1.0),
];
let corners = simplify_ring(&ring);
assert_eq!(corners.len(), 4);
assert_eq!(simplify_ring(&[]), Vec::<DVec3>::new());
}