use super::{
dto::{MeshDto, SavedPlanDto},
fixtures::{expect_error, plain_block, sample_layout_in, write},
format::{LoadedPlan, parse_and_validate_plan, payload_version_of},
};
use crate::gui::rough_plan::obj_import::{C_SHAPE_OBJ, CUBE_OBJ, parse_obj_mesh};
use glam::DVec3;
use indicatrix_cut_core::rough_plan::{
MAX_MESH_TRIANGLES, RoughBase, RoughCut, RoughModel, import_hull, import_mesh,
shape::hull::{add_inclusion_points, remove_inclusion},
};
fn mesh_base(obj: &str) -> RoughBase {
let (points, triangles) = parse_obj_mesh(obj).expect("the fixture parses");
let (base, note) = import_mesh(&points, &triangles).expect("the fixture imports");
assert_eq!(note, None);
base
}
fn assert_same_rough(a: &RoughModel, b: &RoughModel) {
assert_eq!(a.cuts, b.cuts);
assert_eq!(a.base.bounding_box_extents(), b.base.bounding_box_extents());
let (va, vb) = (
a.measure().expect("measures").volume_mm3,
b.measure().expect("measures").volume_mm3,
);
assert!((va - vb).abs() <= 1e-9 * va.abs(), "{va} against {vb}");
match (a.mesh(), b.mesh()) {
(None, None) => {}
(Some(ma), Some(mb)) => {
assert_eq!(ma.triangles(), mb.triangles());
assert_eq!(ma.vertices().len(), mb.vertices().len());
for (p, q) in ma.vertices().iter().zip(mb.vertices()) {
assert!((*p - *q).length() < 1e-9, "{p} against {q}");
}
assert!((ma.volume() - mb.volume()).abs() <= 1e-9 * ma.volume());
}
_ => panic!("one rough has a mesh and the other has not"),
}
}
fn c_shape() -> RoughModel {
RoughModel::new(mesh_base(C_SHAPE_OBJ), Vec::new())
}
#[test]
fn mesh_plan_roundtrips_with_version_2() {
let cut = RoughCut::Face {
normal: [0.0, 1.0, 0.0],
depth_mm: 2.0,
};
for model in [
c_shape(),
RoughModel::new(mesh_base(C_SHAPE_OBJ), vec![cut]),
] {
assert!(model.mesh().is_some(), "the notch keeps the mesh");
let layouts = vec![sample_layout_in(&model)];
let text = write(&model, &layouts);
assert_eq!(payload_version_of(&text), Some(2), "{text}");
assert!(text.contains("version = 2"), "{text}");
assert!(text.contains("[rough.mesh]"), "{text}");
assert!(!text.contains("hull = "), "the hull is derived: {text}");
let loaded: LoadedPlan = parse_and_validate_plan(&text).expect("the plan loads");
assert_eq!(loaded.version, 2);
assert_eq!(loaded.layouts, layouts);
assert_same_rough(&loaded.model, &model);
assert_eq!(write(&loaded.model, &layouts), text);
}
}
#[test]
fn version_1_files_still_load() {
let text = write(&plain_block(), &[]);
assert!(text.contains("version = 1"), "{text}");
let loaded = parse_and_validate_plan(&text).expect("version 1 loads");
assert_eq!(loaded.version, 1);
assert_same_rough(&loaded.model, &plain_block());
let hull = import_hull(&parse_obj_mesh(CUBE_OBJ).expect("parses").0).expect("hull");
let hull_model = RoughModel::new(hull, Vec::new());
let text = write(&hull_model, &[]);
assert!(text.contains("version = 1"), "{text}");
assert_same_rough(
&parse_and_validate_plan(&text).expect("loads").model,
&hull_model,
);
let mesh_text = write(&c_shape(), &[]);
let downgraded = mesh_text.replace("version = 2", "version = 1");
let error = expect_error(&downgraded);
assert!(error.contains("rough.mesh"), "{error}");
}
const BLOCK_PLAN_V1: &str = "\
format = \"indicatrix-rough-plan\"
version = 1
name = \"Test plan\"
created_at = 1790000000
library_id = 77
designs = []
layouts = []
[rough]
base = \"block\"
x_mm = 20.0
y_mm = 12.0
z_mm = 10.0
material = \"Aquamarine\"
specific_gravity = 3.51
weighed_ct = 8.9
cuts = []
[settings]
count = 6
kerf_mm = 0.3
allowance_mm = 0.2
skin_mm = 0.0
min_width_mm = 1.0
candidate_source = \"library\"
";
#[test]
fn saved_plan_bytes_unchanged_for_convex() {
let block = write(&plain_block(), &[]);
assert_eq!(block, BLOCK_PLAN_V1);
let (points, _) = parse_obj_mesh(CUBE_OBJ).expect("parses");
let hull_model = RoughModel::new(import_hull(&points).expect("hull"), Vec::new());
let text = write(&hull_model, &[]);
assert!(text.contains("version = 1\n"), "{text}");
assert!(text.contains("base = \"hull\"\n"), "{text}");
assert!(text.contains("hull = [\n"), "{text}");
assert!(!text.contains("mesh"), "{text}");
let convex_mesh = RoughModel::new(mesh_base(CUBE_OBJ), Vec::new());
assert_eq!(write(&convex_mesh, &[]), text);
assert_same_rough(&convex_mesh, &hull_model);
let c_text = write(&c_shape(), &[]);
assert!(c_text.contains("version = 2\n"), "{c_text}");
assert!(!c_text.contains("inclusions"), "{c_text}");
let (inner, inner_triangles) = inner_cube(2.0, 6.0);
let with = add_inclusion_points(&c_shape().base, &inner, &inner_triangles, 0.3)
.expect("the inclusion fits the arm");
let back = remove_inclusion(&with, 0).expect("removes");
assert_eq!(write(&RoughModel::new(back, Vec::new()), &[]), c_text);
}
fn inner_cube(lo: f64, hi: f64) -> (Vec<DVec3>, Vec<[u32; 3]>) {
let (mut points, triangles) = parse_obj_mesh(CUBE_OBJ).expect("parses");
for p in &mut points {
*p = *p * ((hi - lo) / 20.0) + DVec3::splat(lo);
}
(points, triangles)
}
fn with_inclusions(base: RoughBase, spans: &[(f64, f64)]) -> RoughModel {
let mut base = base;
for &(lo, hi) in spans {
let (points, triangles) = inner_cube(lo, hi);
base = add_inclusion_points(&base, &points, &triangles, 0.3).expect("the inclusion fits");
}
RoughModel::new(base, Vec::new())
}
#[test]
fn a_plan_with_inclusions_roundtrips_with_version_3() {
let cube = import_hull(&parse_obj_mesh(CUBE_OBJ).expect("parses").0).expect("hull");
for base in [mesh_base(C_SHAPE_OBJ), cube] {
let model = with_inclusions(base, &[(2.0, 6.0), (8.0, 9.0)]);
let layouts = vec![sample_layout_in(&model)];
let text = write(&model, &layouts);
assert_eq!(payload_version_of(&text), Some(3), "{text}");
assert!(text.contains("version = 3"), "{text}");
assert!(text.contains("[rough.mesh]"), "{text}");
assert!(text.contains("[[rough.inclusions]]"), "{text}");
let loaded: LoadedPlan = parse_and_validate_plan(&text).expect("the plan loads");
assert_eq!(loaded.version, 3);
assert_eq!(loaded.layouts, layouts);
assert_same_rough(&loaded.model, &model);
let (a, b) = (
model.mesh().expect("mesh"),
loaded.model.mesh().expect("mesh"),
);
assert_eq!(a.inclusion_count(), 2);
assert_eq!(b.inclusion_count(), 2);
assert_eq!(a.inclusion_shells(), b.inclusion_shells());
let (ma, mb) = (
model.measure().expect("m"),
loaded.model.measure().expect("m"),
);
assert!((ma.volume_mm3 - mb.volume_mm3).abs() <= 1e-9 * ma.volume_mm3);
assert!((ma.inclusion_mm3 - mb.inclusion_mm3).abs() <= 1e-9 * ma.volume_mm3);
assert_eq!(write(&loaded.model, &layouts), text);
}
}
#[test]
fn inclusions_need_version_3_and_a_mesh_and_must_fit() {
let model = with_inclusions(mesh_base(C_SHAPE_OBJ), &[(2.0, 6.0)]);
let text = write(&model, &[]);
let downgraded = text.replace("version = 3", "version = 2");
let error = expect_error(&downgraded);
assert!(error.contains("rough.inclusions"), "{error}");
let mut doc: SavedPlanDto = toml::from_str(&text).expect("parses");
let mut outside = doc.rough.inclusions[0].clone();
for v in &mut outside.vertices {
v[0] += 100.0;
}
doc.rough.inclusions = vec![outside];
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("rough.inclusions"), "{error}");
let mut doc: SavedPlanDto = toml::from_str(&text).expect("parses");
doc.rough.mesh = None;
doc.rough.hull = vec![
[0.0, 0.0, 0.0],
[20.0, 0.0, 0.0],
[0.0, 20.0, 0.0],
[0.0, 0.0, 20.0],
];
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("rough.inclusions"), "{error}");
}
#[test]
fn a_bad_or_oversized_mesh_is_refused() {
let good = write(&c_shape(), &[]);
let mut doc: SavedPlanDto = toml::from_str(&good).expect("parses");
let mesh = doc.rough.mesh.clone().expect("a mesh");
let mut broken = mesh.clone();
broken.triangles[0][0] = 9_999;
doc.rough.mesh = Some(broken);
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("rough.mesh"), "{error}");
let mut open = mesh.clone();
open.triangles.truncate(open.triangles.len() / 2);
doc.rough.mesh = Some(open);
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("rough.mesh"), "{error}");
let many = MeshDto {
vertices: mesh.vertices.clone(),
triangles: vec![[0, 1, 2]; MAX_MESH_TRIANGLES + 1],
};
doc.rough.mesh = Some(many);
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("triangles"), "{error}");
let mut nan = mesh;
nan.vertices[0][0] = f64::NAN;
doc.rough.mesh = Some(nan);
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("rough.mesh"), "{error}");
}
#[test]
fn a_mesh_that_import_turns_into_a_plain_hull_is_refused() {
let good = write(&c_shape(), &[]);
let mut doc: SavedPlanDto = toml::from_str(&good).expect("parses");
let (vertices, triangles) = parse_obj_mesh(CUBE_OBJ).expect("parses");
doc.rough.mesh = Some(MeshDto {
vertices: vertices.iter().map(DVec3::to_array).collect(),
triangles,
});
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(
error.contains("rough.mesh") && error.contains("convex"),
"{error}"
);
doc.rough.mesh = Some(MeshDto {
vertices: vertices.iter().map(DVec3::to_array).collect(),
triangles: Vec::new(),
});
let error = expect_error(&toml::to_string_pretty(&doc).expect("writes"));
assert!(error.contains("rough.mesh"), "{error}");
}
#[test]
fn a_stray_vertex_in_the_obj_does_not_change_the_rough_after_a_save() {
let with_stray = format!("{C_SHAPE_OBJ}v 40 -9 55\n");
let model = RoughModel::new(mesh_base(&with_stray), Vec::new());
assert!(model.mesh().is_some());
let clean = c_shape();
assert_eq!(model.base, clean.base);
let text = write(&model, &[]);
let loaded = parse_and_validate_plan(&text).expect("the plan loads");
assert_same_rough(&loaded.model, &model);
assert_eq!(loaded.model.base, model.base);
assert_eq!(
loaded.model.halfspaces().expect("planes"),
model.halfspaces().expect("planes")
);
let (before, after) = (
model.mesh().expect("mesh"),
loaded.model.mesh().expect("mesh"),
);
assert_eq!(before.bounds(), after.bounds());
assert_eq!(before.volume(), after.volume());
for extent in model.base.bounding_box_extents() {
assert!((extent - 20.0).abs() < 1e-9, "{extent}");
}
}