use super::*;
impl EngineState {
pub fn import_step_feature(&mut self, step_text: &str) -> Result<String, String> {
if !step_text.contains("ISO-10303-21") {
return Err("not a STEP file (missing the ISO-10303-21 header)".into());
}
let id = self.next_feature_id(&crate::features::feature_short_name("IMPORT3D"));
let feature = serde_json::json!({
"type": "IMPORT3D",
"inputParams": { "id": id, "stepText": step_text },
"persistentData": {},
});
self.add_feature(&feature.to_string())
}
pub fn export_step_text(&self) -> Result<String, String> {
let request: HistoryRequest = serde_json::from_value(self.history.prefix_request())
.map_err(|e| format!("export STEP: history request: {e}"))?;
let handles: Vec<u32> = crate::pipeline::resident_solid_handles(&request)
.into_iter()
.map(|(_, handle)| handle)
.collect();
if handles.is_empty() {
return Err("nothing to export: the model has no solids".into());
}
brep_kernel::export_step_handles(&handles, "Part", "MM", "")
}
pub fn export_stl_text(&self) -> Result<String, String> {
let mut out = String::from("solid brep\n");
let mut triangles = 0usize;
for solid in self.scene.solids() {
let positions = &solid.mesh.positions;
for tri in solid.mesh.indices.chunks_exact(3) {
let a = positions[tri[0] as usize];
let b = positions[tri[1] as usize];
let c = positions[tri[2] as usize];
let normal = triangle_normal(a, b, c);
out.push_str(&format!(
" facet normal {} {} {}\n outer loop\n",
normal[0], normal[1], normal[2]
));
for v in [a, b, c] {
out.push_str(&format!(" vertex {} {} {}\n", v[0], v[1], v[2]));
}
out.push_str(" endloop\n endfacet\n");
triangles += 1;
}
}
out.push_str("endsolid brep\n");
if triangles == 0 {
return Err("nothing to export: the scene has no triangles".into());
}
Ok(out)
}
}
fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len > 0.0 {
[n[0] / len, n[1] / len, n[2] / len]
} else {
[0.0, 0.0, 0.0]
}
}
#[cfg(test)]
mod io_tests {
use super::*;
fn cube_history(id: &str, size: f64) -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": id,
"sizeX": size, "sizeY": size, "sizeZ": size,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
fn box_step(sx: f64, sy: f64, sz: f64) -> String {
let solid =
brep_kernel::make_box_brep(brep_kernel::Vec3::new(0.0, 0.0, 0.0), sx, sy, sz)
.unwrap();
brep_kernel::export_step(&[solid], "part", "MM", "").unwrap()
}
fn imported_volume(step_text: &str) -> f64 {
let solids = brep_kernel::import_step(step_text).unwrap();
assert_eq!(solids.len(), 1, "STEP round-trips to one solid");
brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume
}
#[test]
fn import_step_feature_adds_the_body_to_the_model() {
let step = box_step(4.0, 3.0, 2.0);
let mut state = EngineState::new();
state.import_step_feature(&step).unwrap();
assert_eq!(state.scene.solids().len(), 1, "one imported body");
let size = state.scene.solids()[0].bbox.size();
assert!(
(size[0] - 4.0).abs() < 1e-4
&& (size[1] - 3.0).abs() < 1e-4
&& (size[2] - 2.0).abs() < 1e-4,
"imported bbox {size:?} != 4x3x2"
);
let mut empty = EngineState::new();
assert!(empty.import_step_feature("not a step file").is_err());
assert_eq!(empty.history_len(), 0, "a bad import adds no feature");
}
#[test]
fn export_step_text_round_trips_a_box_model() {
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box", 10.0)).unwrap();
let step = state.export_step_text().unwrap();
assert!(step.contains("ISO-10303-21"), "STEP header present");
let volume = imported_volume(&step);
assert!((volume - 1000.0).abs() < 1e-3, "exported volume {volume} != 1000");
let empty = EngineState::new();
assert!(empty.export_step_text().is_err(), "empty model errs on export");
}
#[test]
fn import_export_import_preserves_count_and_volume() {
let step_in = box_step(5.0, 4.0, 3.0); let mut state = EngineState::new();
state.import_step_feature(&step_in).unwrap();
assert_eq!(state.scene.solids().len(), 1);
let step_out = state.export_step_text().unwrap();
let solids = brep_kernel::import_step(&step_out).unwrap();
assert_eq!(solids.len(), 1, "solid count preserved");
let volume = brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume;
assert!((volume - 60.0).abs() < 1e-3, "round-trip volume {volume} != 60");
}
#[test]
fn export_stl_text_emits_ascii_facets() {
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box", 6.0)).unwrap();
let stl = state.export_stl_text().unwrap();
assert!(stl.starts_with("solid brep"), "STL opens with the solid header");
assert!(stl.trim_end().ends_with("endsolid brep"), "STL closes the solid");
assert_eq!(
stl.matches("facet normal").count(),
12,
"a box tessellates to 12 triangles"
);
assert_eq!(stl.matches("vertex").count(), 36, "3 vertices per triangle");
let empty = EngineState::new();
assert!(empty.export_stl_text().is_err(), "empty scene errs on STL export");
}
}