78 lines
4.0 KiB
Rust
78 lines
4.0 KiB
Rust
//! Mesh export regressions: a closed body exports as a closed mesh.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use serde_json::{Value, json};
|
|
use solidcraft_engine::Session;
|
|
use solidcraft_io::{Format, export, read_3mf, read_stl};
|
|
|
|
fn run(s: &mut Session, script: Value) {
|
|
for c in script.as_array().into_iter().flatten() {
|
|
let (id, p) = (c["command"].as_str().unwrap_or_default(), &c["params"]);
|
|
if let Err(e) = s.execute(id, p) {
|
|
panic!("{id} {p}: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The edges (by vertex index) that are not used by exactly two triangles.
|
|
fn open_edges(triangles: &[[u32; 3]]) -> Vec<((u32, u32), u32)> {
|
|
let mut uses: HashMap<(u32, u32), u32> = HashMap::new();
|
|
for t in triangles {
|
|
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
|
|
*uses.entry((a.min(b), a.max(b))).or_default() += 1;
|
|
}
|
|
}
|
|
uses.into_iter().filter(|(_, n)| *n != 2).collect()
|
|
}
|
|
|
|
/// A bowl section (lines and arcs) revolved about Z, its rim cut by a large cylinder across it.
|
|
/// The edge where the cylinder meets the inner cone is an intersection curve; one of its samples
|
|
/// lay a few ten-thousandths of a millimetre off both surfaces, each face put the point on its
|
|
/// own surface, and the exported mesh had a crack there (four edges used once).
|
|
#[test]
|
|
fn a_rim_cut_by_a_cylinder_exports_closed() {
|
|
let mut s = Session::default();
|
|
run(
|
|
&mut s,
|
|
json!([
|
|
{"command": "sketch.create", "params": {"plane": "XZ", "name": "Section"}},
|
|
{"command": "sketch.line", "params": {"points": [[0, 0], [48.8, 0], [50.0240096077, -1.2]]}},
|
|
{"command": "sketch.arc.three_point", "params": {"start": [50.0240096077, -1.2], "end": [58.7867965644, -21.2132034356], "through": [52.5188160894, -12.0326443841]}},
|
|
{"command": "sketch.line", "params": {"points": [[58.7867965644, -21.2132034356], [85.5735931288, -48], [82.1794805791, -48], [57.0897402896, -22.9102597104]]}},
|
|
{"command": "sketch.arc.three_point", "params": {"start": [57.0897402896, -22.9102597104], "end": [48.3846976331, -7.0875], "through": [51.6125175266, -15.6176451113]}},
|
|
{"command": "sketch.arc.three_point", "params": {"start": [48.3846976331, -7.0875], "end": [42.5300120096, -2.4], "through": [46.2800120096, -3.7162515012]}},
|
|
{"command": "sketch.line", "params": {"points": [[42.5300120096, -2.4], [0, -2.4], [0, 0]]}},
|
|
{"command": "sketch.finish", "params": {}},
|
|
{"command": "solid.revolve", "params": {"sketch": "Section", "axis": "Z"}},
|
|
{"command": "sketch.create", "params": {"plane": "YZ", "name": "RimCutter"}},
|
|
{"command": "sketch.arc.three_point", "params": {"start": [-100, 11.6204810674], "end": [100, 46.4045454912], "through": [-52, 8]}},
|
|
{"command": "sketch.line", "params": {"points": [[100, 46.4045454912], [100, 70], [-100, 70], [-100, 11.6204810674]]}},
|
|
{"command": "sketch.finish", "params": {}},
|
|
{"command": "solid.extrude", "params": {"sketch": "RimCutter", "distance": 200, "direction": "symmetric", "operation": "cut"}}
|
|
]),
|
|
);
|
|
let st = s.model.state();
|
|
assert_eq!(st.bodies.len(), 1);
|
|
|
|
let (objects, _) = read_3mf(&export(&st, &[], Format::ThreeMf, "cove").unwrap()).unwrap();
|
|
assert_eq!(objects.len(), 1);
|
|
let open = open_edges(&objects[0].triangles);
|
|
assert!(open.is_empty(), "3MF: {} edges not used twice, e.g. {:?}", open.len(), open.first());
|
|
|
|
// STL has no indices: vertices are the same where their coordinates are.
|
|
let mut index: HashMap<[u32; 3], u32> = HashMap::new();
|
|
let triangles: Vec<[u32; 3]> = read_stl(&export(&st, &[], Format::StlBinary, "cove").unwrap())
|
|
.unwrap()
|
|
.iter()
|
|
.map(|t| {
|
|
t.map(|p| {
|
|
let n = index.len() as u32;
|
|
*index.entry(p.map(f32::to_bits)).or_insert(n)
|
|
})
|
|
})
|
|
.collect();
|
|
let open = open_edges(&triangles);
|
|
assert!(open.is_empty(), "STL: {} edges not used twice, e.g. {:?}", open.len(), open.first());
|
|
}
|