use super::*;
use indicatrix_cut_core::rough_plan::{BoxFace, RoughCut};
fn evaluate(request: &ShapeRequest) -> ShapeResult {
evaluate_reusing(request, None).0
}
fn request(model: RoughModel) -> ShapeRequest {
ShapeRequest {
revision: 7,
model,
specific_gravity: 2.65,
material_name: "Quartz".to_string(),
weighed_ct: None,
selected_cut: None,
}
}
fn block() -> RoughModel {
RoughModel::new(
RoughBase::Block {
x_mm: 10.0,
y_mm: 8.0,
z_mm: 6.0,
},
Vec::new(),
)
}
fn cornered_block() -> RoughModel {
let mut model = block();
model.cuts.push(RoughCut::Corner {
faces: [BoxFace::Top, BoxFace::Front, BoxFace::Right],
setbacks_mm: [4.0, 6.0, 3.0],
});
model
}
#[test]
fn a_plain_block_has_its_box_volume_and_a_centred_mesh() {
let result = evaluate(&request(block()));
assert_eq!(result.revision, 7);
let output = result.outcome.expect("a plain block is valid");
assert!((output.measure.volume_mm3 - 480.0).abs() < 1e-6);
assert!(
(output.specific_gravity - 2.65).abs() < 1e-12,
"the gravity is carried for the removed-volume text"
);
assert!(
output.model_text.contains("480 mm³"),
"{}",
output.model_text
);
assert!(
output.model_text.contains("Quartz"),
"{}",
output.model_text
);
assert_eq!(output.check_level, 0);
let mut low = DVec3::splat(f64::INFINITY);
let mut high = DVec3::splat(f64::NEG_INFINITY);
for &p in &output.mesh.positions {
low = low.min(p);
high = high.max(p);
}
assert!((low + high).length() < 1e-9, "the box centre is the origin");
assert!((high - low - DVec3::new(10.0, 8.0, 6.0)).length() < 1e-9);
}
fn cut_c_shape() -> RoughModel {
RoughModel::new(
crate::gui::rough_plan::obj_import::mesh_base_of(
crate::gui::rough_plan::obj_import::C_SHAPE_OBJ,
),
vec![RoughCut::Face {
normal: [0.0, 1.0, 0.0],
depth_mm: 2.0,
}],
)
}
#[test]
fn a_mesh_rough_is_drawn_from_its_clipped_surface_with_cut_faces_by_cut() {
let model = cut_c_shape();
let output = evaluate(&request(model))
.outcome
.expect("a valid mesh rough");
assert!(
(output.measure.volume_mm3 - 5200.0).abs() < 1e-6,
"{}",
output.measure.volume_mm3
);
let mesh = &output.mesh;
let pieces = mesh.piece_normals.as_ref().expect("one normal per ring");
let visible = mesh.edge_visible.as_ref().expect("one flag list per ring");
assert_eq!(pieces.len(), mesh.rings.len());
assert_eq!(visible.len(), mesh.rings.len());
assert!(visible.iter().all(|flags| flags.len() == 3));
let cap: Vec<usize> = (0..mesh.rings.len())
.filter(|&i| mesh.rings[i].0 == 6)
.collect();
assert!(!cap.is_empty(), "the cut leaves a face");
assert!(cap.iter().all(|&i| (pieces[i] - DVec3::Y).length() < 1e-9));
assert!(
mesh.rings.iter().all(|(id, _)| *id == 0 || *id == 6),
"ids: {:?}",
mesh.rings
.iter()
.map(|(id, _)| *id)
.collect::<BTreeSet<_>>()
);
let drawn = visible.iter().flatten().filter(|&&d| d).count();
let hidden = visible.iter().flatten().filter(|&&d| !d).count();
assert!(drawn > 0 && hidden > 0, "drawn {drawn}, hidden {hidden}");
assert!(mesh.rings.iter().any(|(id, ring)| {
*id == 0
&& ring
.iter()
.all(|p| p.x.abs() < 1e-9 && p.y.abs() < 5.0 + 1e-9)
}));
}
#[test]
fn the_selected_cut_reports_the_volume_it_removes() {
let mut ask = request(cornered_block());
ask.selected_cut = Some(0);
let output = evaluate(&ask).outcome.expect("valid corner cut");
assert!((output.measure.volume_mm3 - 468.0).abs() < 1e-6);
let removed = output.removed_mm3.expect("the selected cut has a volume");
assert!((removed - 12.0).abs() < 1e-6, "removed {removed}");
assert_eq!(output.selected_cut, Some(0), "the result names its cut");
ask.selected_cut = Some(5);
assert!(
evaluate(&ask)
.outcome
.expect("still valid")
.removed_mm3
.is_none()
);
ask.selected_cut = None;
let output = evaluate(&ask).outcome.expect("still valid");
assert!(output.removed_mm3.is_none());
assert_eq!(output.selected_cut, None);
}
#[test]
fn a_request_that_only_changes_the_cut_reuses_the_mesh_and_measure() {
let first = request(cornered_block());
let (first_result, kept) = evaluate_reusing(&first, None);
let first_output = first_result.outcome.expect("valid");
assert!(first_output.removed_mm3.is_none());
let mut second = first;
second.revision = 8;
second.selected_cut = Some(0);
let (second_result, kept) = evaluate_reusing(&second, kept);
let second_output = second_result.outcome.expect("valid");
assert!(
Arc::ptr_eq(&first_output.mesh, &second_output.mesh),
"the same model keeps its mesh"
);
assert_eq!(first_output.measure, second_output.measure);
let removed = second_output.removed_mm3.expect("the cut has a volume");
assert!((removed - 12.0).abs() < 1e-6, "removed {removed}");
let kept = kept.expect("the solid is kept");
assert_eq!(kept.removed, Some((0, second_output.removed_mm3)));
let mut third = second;
third.model = block();
third.selected_cut = None;
let (third_result, _) = evaluate_reusing(&third, Some(kept));
let third_output = third_result.outcome.expect("valid");
assert!(!Arc::ptr_eq(&second_output.mesh, &third_output.mesh));
assert!((third_output.measure.volume_mm3 - 480.0).abs() < 1e-6);
}
#[test]
fn a_bad_cut_is_reported_with_its_row() {
let mut model = block();
model.cuts.push(RoughCut::Edge {
faces: [BoxFace::Top, BoxFace::Front],
setbacks_mm: [1.0, 1.0],
});
model.cuts.push(RoughCut::Edge {
faces: [BoxFace::Top, BoxFace::Front],
setbacks_mm: [-1.0, 1.0],
});
let (result, kept) = evaluate_reusing(&request(model), None);
let failure = result.outcome.expect_err("negative setback");
assert!(kept.is_none(), "an invalid model keeps nothing");
assert!(
matches!(
failure,
ShapeFailure::Invalid(ShapeError::BadSetback { index: 1, .. })
),
"{failure:?}"
);
assert_eq!(failure.row(), Some(1));
assert_eq!(error_row(&ShapeError::NothingLeft), None);
}
#[test]
fn an_internal_failure_names_no_row_and_tells_the_user_to_edit() {
let failure = ShapeFailure::Internal;
assert_eq!(failure.row(), None);
assert_eq!(
failure.message(),
"The model could not be evaluated (internal error); edit the model to retry."
);
assert_eq!(
ShapeFailure::Invalid(ShapeError::NothingLeft).message(),
ShapeError::NothingLeft.to_string()
);
}
#[test]
fn the_weight_check_uses_the_weighed_carat() {
let mut ask = request(block());
ask.weighed_ct = Some(6.4);
let output = evaluate(&ask).outcome.expect("valid");
assert_eq!(output.check_level, 1);
ask.weighed_ct = Some(3.0);
assert_eq!(evaluate(&ask).outcome.expect("valid").check_level, 3);
}
#[test]
fn without_a_material_the_model_is_not_accused_of_the_wrong_weight() {
let mut ask = request(block());
ask.specific_gravity = 0.0;
ask.material_name = String::new();
ask.weighed_ct = Some(6.4);
let output = evaluate(&ask).outcome.expect("valid");
assert_eq!(output.check_level, 0);
assert!(output.check_text.is_empty(), "{}", output.check_text);
assert_eq!(output.model_text, "Model 480 mm³");
}
#[test]
fn a_bad_weighed_field_replaces_only_the_weight_check_line() {
assert_eq!(
weight_check_lines("", "close".to_string(), 2),
("close".to_string(), 2)
);
assert_eq!(
weight_check_lines("8,9", "matches".to_string(), 1),
("matches".to_string(), 1)
);
for typed in ["abc", "0", "-1", "inf"] {
let (text, level) = weight_check_lines(typed, "matches".to_string(), 1);
assert_eq!(level, 3, "{typed}");
assert!(text.contains("Weighed carat"), "{typed}: {text}");
}
let output = evaluate(&request(block())).outcome.expect("valid block");
assert_eq!(output.check_level, 0);
assert!(output.measure.volume_mm3 > 0.0);
}
#[test]
fn requests_that_differ_only_in_the_revision_are_the_same_inputs() {
let a = request(block());
let mut b = a.clone();
b.revision += 1;
assert!(a.same_inputs(&b));
b.selected_cut = Some(0);
assert!(!a.same_inputs(&b));
let mut c = a.clone();
c.weighed_ct = Some(1.0);
assert!(!a.same_inputs(&c));
}
#[test]
fn the_removed_line_names_volume_and_carats() {
assert_eq!(removed_text(84.0, 2.6), "removes 84 mm³ (1.09 ct)");
}