brep_render/engine_state/
model_io.rs1use super::*;
2
3impl EngineState {
12 pub fn import_step_feature(&mut self, step_text: &str) -> Result<String, String> {
19 if !step_text.contains("ISO-10303-21") {
20 return Err("not a STEP file (missing the ISO-10303-21 header)".into());
21 }
22 let id = self.next_feature_id(&crate::features::feature_short_name("IMPORT3D"));
23 let feature = serde_json::json!({
24 "type": "IMPORT3D",
25 "inputParams": { "id": id, "stepText": step_text },
26 "persistentData": {},
27 });
28 self.add_feature(&feature.to_string())
29 }
30
31 pub fn export_step_text(&self) -> Result<String, String> {
38 let request: HistoryRequest = serde_json::from_value(self.history.prefix_request())
39 .map_err(|e| format!("export STEP: history request: {e}"))?;
40 let handles: Vec<u32> = crate::pipeline::resident_solid_handles(&request)
41 .into_iter()
42 .map(|(_, handle)| handle)
43 .collect();
44 if handles.is_empty() {
45 return Err("nothing to export: the model has no solids".into());
46 }
47 brep_kernel::export_step_handles(&handles, "Part", "MM", "")
48 }
49
50 pub fn export_stl_text(&self) -> Result<String, String> {
57 let mut out = String::from("solid brep\n");
58 let mut triangles = 0usize;
59 for solid in self.scene.solids() {
60 let positions = &solid.mesh.positions;
61 for tri in solid.mesh.indices.chunks_exact(3) {
62 let a = positions[tri[0] as usize];
63 let b = positions[tri[1] as usize];
64 let c = positions[tri[2] as usize];
65 let normal = triangle_normal(a, b, c);
66 out.push_str(&format!(
67 " facet normal {} {} {}\n outer loop\n",
68 normal[0], normal[1], normal[2]
69 ));
70 for v in [a, b, c] {
71 out.push_str(&format!(" vertex {} {} {}\n", v[0], v[1], v[2]));
72 }
73 out.push_str(" endloop\n endfacet\n");
74 triangles += 1;
75 }
76 }
77 out.push_str("endsolid brep\n");
78 if triangles == 0 {
79 return Err("nothing to export: the scene has no triangles".into());
80 }
81 Ok(out)
82 }
83}
84
85fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
88 let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
89 let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
90 let n = [
91 u[1] * v[2] - u[2] * v[1],
92 u[2] * v[0] - u[0] * v[2],
93 u[0] * v[1] - u[1] * v[0],
94 ];
95 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
96 if len > 0.0 {
97 [n[0] / len, n[1] / len, n[2] / len]
98 } else {
99 [0.0, 0.0, 0.0]
100 }
101}
102
103#[cfg(test)]
104mod io_tests {
105 use super::*;
106
107 fn cube_history(id: &str, size: f64) -> String {
110 serde_json::json!({
111 "expressions": "",
112 "configurator": {},
113 "features": [{
114 "type": "P.CU",
115 "inputParams": {
116 "id": id,
117 "sizeX": size, "sizeY": size, "sizeZ": size,
118 "transform": {
119 "position": [0.0, 0.0, 0.0],
120 "rotationEuler": [0.0, 0.0, 0.0],
121 "scale": [1.0, 1.0, 1.0]
122 },
123 "boolean": { "targets": [], "operation": "NONE" }
124 },
125 "persistentData": {}
126 }]
127 })
128 .to_string()
129 }
130
131 fn box_step(sx: f64, sy: f64, sz: f64) -> String {
133 let solid =
134 brep_kernel::make_box_brep(brep_kernel::Vec3::new(0.0, 0.0, 0.0), sx, sy, sz)
135 .unwrap();
136 brep_kernel::export_step(&[solid], "part", "MM", "").unwrap()
137 }
138
139 fn imported_volume(step_text: &str) -> f64 {
141 let solids = brep_kernel::import_step(step_text).unwrap();
142 assert_eq!(solids.len(), 1, "STEP round-trips to one solid");
143 brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume
144 }
145
146 #[test]
150 fn import_step_feature_adds_the_body_to_the_model() {
151 let step = box_step(4.0, 3.0, 2.0);
152 let mut state = EngineState::new();
153 state.import_step_feature(&step).unwrap();
154 assert_eq!(state.scene.solids().len(), 1, "one imported body");
155 let size = state.scene.solids()[0].bbox.size();
156 assert!(
157 (size[0] - 4.0).abs() < 1e-4
158 && (size[1] - 3.0).abs() < 1e-4
159 && (size[2] - 2.0).abs() < 1e-4,
160 "imported bbox {size:?} != 4x3x2"
161 );
162
163 let mut empty = EngineState::new();
164 assert!(empty.import_step_feature("not a step file").is_err());
165 assert_eq!(empty.history_len(), 0, "a bad import adds no feature");
166 }
167
168 #[test]
171 fn export_step_text_round_trips_a_box_model() {
172 let mut state = EngineState::new();
173 state.set_history_json(&cube_history("Box", 10.0)).unwrap();
174 let step = state.export_step_text().unwrap();
175 assert!(step.contains("ISO-10303-21"), "STEP header present");
176 let volume = imported_volume(&step);
177 assert!((volume - 1000.0).abs() < 1e-3, "exported volume {volume} != 1000");
178
179 let empty = EngineState::new();
180 assert!(empty.export_step_text().is_err(), "empty model errs on export");
181 }
182
183 #[test]
186 fn import_export_import_preserves_count_and_volume() {
187 let step_in = box_step(5.0, 4.0, 3.0); let mut state = EngineState::new();
189 state.import_step_feature(&step_in).unwrap();
190 assert_eq!(state.scene.solids().len(), 1);
191
192 let step_out = state.export_step_text().unwrap();
193 let solids = brep_kernel::import_step(&step_out).unwrap();
194 assert_eq!(solids.len(), 1, "solid count preserved");
195 let volume = brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume;
196 assert!((volume - 60.0).abs() < 1e-3, "round-trip volume {volume} != 60");
197 }
198
199 #[test]
202 fn export_stl_text_emits_ascii_facets() {
203 let mut state = EngineState::new();
204 state.set_history_json(&cube_history("Box", 6.0)).unwrap();
205 let stl = state.export_stl_text().unwrap();
206 assert!(stl.starts_with("solid brep"), "STL opens with the solid header");
207 assert!(stl.trim_end().ends_with("endsolid brep"), "STL closes the solid");
208 assert_eq!(
209 stl.matches("facet normal").count(),
210 12,
211 "a box tessellates to 12 triangles"
212 );
213 assert_eq!(stl.matches("vertex").count(), 36, "3 vertices per triangle");
214
215 let empty = EngineState::new();
216 assert!(empty.export_stl_text().is_err(), "empty scene errs on STL export");
217 }
218}
219
220