Files
solidcraft-upstream/crates/engine/src/tests/mesh_convert.rs
T
Mateusz Sych 9cabc3532d STL export welds shared vertices like the 3MF (#816)
STL (binary and ASCII) now welds each body's export mesh with the same tolerance as 3MF, so filleted and drilled bodies are closed. The weld grid's neighbour lookup no longer overflows for bodies far from the origin.

Fixes #784
2026-10-13 14:39:20 -04:00

130 lines
6.4 KiB
Rust

use super::*;
use solidcraft_doc::{Document, FeatureKind, MeshData};
use solidcraft_geom::Vec3;
fn imported_box() -> Session {
let body = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 20., 30.)).unwrap();
let mesh = body.tessellate(0.01).unwrap();
let mut doc = Document::default();
doc.add_feature(
FeatureKind::MeshImport {
file: "public-safe-box.stl".into(),
meshes: vec![MeshData {
name: "Mesh".into(),
positions: mesh.positions.iter().flat_map(|p| [p.x, p.y, p.z]).collect(),
triangles: mesh.triangles.iter().flatten().copied().collect(),
color: Some([0.2, 0.5, 0.8]),
}],
},
Some("Imported mesh"),
)
.unwrap();
Session::new(doc)
}
#[test]
fn mesh_conversion_is_parametric_undoable_and_editable_after_cold_reopen() {
let mut s = imported_box();
run(&mut s, "parameters.add", json!({"name":"size","expression":"1","unit":""}));
run(&mut s, "solid.scale", json!({"bodies":["Mesh"],"factor":"size","name":"Mesh scale"}));
run(&mut s, "mesh.convert", json!({"body":"Mesh","tolerance":"0.01 mm","name":"Reconstructed prism"}));
let check = |s: &Session| {
let state = s.model.state();
let body = &state.body("Mesh").unwrap().body;
assert!(!body.is_mesh());
assert_eq!(body.faces(0.01).unwrap().len(), 6);
assert_eq!(body.color(), Some([0.2, 0.5, 0.8]));
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
};
check(&s);
assert!((volume(&mut s) - 6000.).abs() < 1e-6);
run(&mut s, "edit.undo", json!({}));
assert!(s.model.state().body("Mesh").unwrap().body.is_mesh());
run(&mut s, "edit.redo", json!({}));
check(&s);
run(&mut s, "parameters.change", json!({"name":"size","expression":"2"}));
check(&s);
assert!((volume(&mut s) - 48000.).abs() < 1e-5);
let mut cold = Session::new(Document::from_json(&s.doc.to_json()).unwrap());
check(&cold);
assert!((volume(&mut cold) - 48000.).abs() < 1e-5);
run(&mut cold, "solid.fillet", json!({"body":"Mesh","edges":[[0,20,60]],"radius":2}));
assert!(volume(&mut cold) < 48000.);
assert!(cold.model.results.iter().all(|r| r.error.is_none()));
}
#[test]
fn failed_mesh_conversions_leave_the_design_unchanged() {
let mut s = imported_box();
let before = s.doc.to_json();
for params in [
json!({}),
json!({"body":"Missing"}),
json!({"body":"Mesh","tolerance":0}),
json!({"body":"Mesh","tolerance":"unknown"}),
json!({"body":"Mesh","tolerance":"2 mm"}),
] {
assert!(s.execute("mesh.convert", &params).is_err());
assert_eq!(s.doc.to_json(), before);
assert!(s.model.state().body("Mesh").unwrap().body.is_mesh());
}
}
#[test]
fn exported_binary_stl_with_a_bore_reconstructs_analytic_faces() {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane":"XY","name":"Section"}));
run(&mut s, "sketch.line", json!({"points":[[0,0],[40,0],[40,20],[20,20],[20,30],[0,30]],"closed":true}));
run(&mut s, "sketch.circle.center", json!({"center":[10,10],"radius":4}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"sketch":"Section","profiles":[{"point":[30,10]}],"distance":15,"body_name":"Bracket"}));
let path = std::env::temp_dir().join(format!("solidcraft-mesh-convert-{}.stl", std::process::id()));
run(&mut s, "file.save_mesh", json!({"path":path}));
run(&mut s, "doc.open", json!({"path":path}));
let name = s.model.state().bodies[0].name.clone();
run(&mut s, "mesh.convert", json!({"body":name,"tolerance":"0.05 mm"}));
let state = s.model.state();
let body = &state.bodies[0].body;
assert!(!body.is_mesh());
assert!(body.validity().is_empty());
assert_eq!(solidcraft_kernel::measure(body).unwrap().merged.face_types.get("cylinder"), Some(&1));
assert_eq!(body.faces(0.01).unwrap().len(), 10);
std::fs::remove_file(path).unwrap();
}
/// #784: STL is closed as written where faces meet (a fillet, a bored hole): every edge of the
/// file, keyed by its exact f32 ends, is shared by exactly two triangles, as in 3MF.
#[test]
fn stl_export_is_closed_where_faces_meet() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 30, "width": 20, "height": 10, "operation": "new", "body_name": "BoxFillet"}));
run(&mut s, "solid.fillet", json!({"body": "BoxFillet", "edges": [[15, 0, 10]], "radius": 2}));
run(&mut s, "solid.cylinder", json!({"diameter": 30, "height": 12, "base": [60, 0, 0], "operation": "new", "body_name": "CylHole"}));
run(&mut s, "solid.hole", json!({"position": [60, 0, 12], "diameter": 6}));
let state = s.model.state();
for name in ["BoxFillet", "CylHole"] {
let body = &state.bodies.iter().find(|b| b.name == name).unwrap().body;
let expected = solidcraft_kernel::measure(body).unwrap().volume;
for format in [solidcraft_io::Format::StlBinary, solidcraft_io::Format::StlAscii] {
let bytes = solidcraft_io::export(&state, &[name.to_string()], format, name).unwrap();
let tris = solidcraft_io::read_stl(&bytes).unwrap();
let key = |p: [f32; 3]| p.map(f32::to_bits);
let mut edges = std::collections::HashMap::new();
for t in &tris {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
let (a, b) = (key(a), key(b));
*edges.entry(if a < b { (a, b) } else { (b, a) }).or_insert(0) += 1;
}
}
let open = edges.values().filter(|n| **n != 2).count();
assert_eq!(open, 0, "{name} {format:?}: {open} edges not shared by exactly two triangles");
let p = |v: [f32; 3]| Vec3::new(v[0] as f64, v[1] as f64, v[2] as f64);
let v: f64 = tris.iter().map(|t| p(t[0]).dot(p(t[1]).cross(p(t[2]))) / 6.0).sum();
assert!((v - expected).abs() <= expected * 1e-3, "{name} {format:?}: STL volume {v}, body {expected}");
let mf = solidcraft_io::export(&state, &[name.to_string()], solidcraft_io::Format::ThreeMf, name).unwrap();
let (objs, _) = solidcraft_io::read_3mf(&mf).unwrap();
assert_eq!(tris.len(), objs.iter().map(|o| o.triangles.len()).sum::<usize>(), "{name} {format:?}: STL and 3MF triangles");
}
}
}