1use crate::topology::{
17 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
18};
19use crate::{build_pcurve_on_surface_range, sew_solid, NurbsCurve, NurbsSurface, Vec3};
20
21use super::entities::{
22 composite_members, curve_from_126, curve_on_surface_from_142, surface_from_128,
23 trimmed_surface_from_144,
24};
25use super::reader::{parse_iges, IgesFile};
26
27pub fn import_iges(text: &str) -> Result<Vec<BrepSolid>, String> {
29 let file = parse_iges(text)?;
30 let scale = file.global.length_scale_mm()?;
31
32 let mut soup = SoupBuilder::new();
33 let mut faces: Vec<FaceRecord> = Vec::new();
34 for entity in file.entities_of_type(144) {
35 let ts = trimmed_surface_from_144(entity)?;
36 let face = soup.build_face(&file, &ts, scale)?;
37 faces.push(face);
38 }
39
40 if faces.is_empty() {
41 if file.entities_of_type(186).next().is_some() {
43 return Err(
44 "iges_import: file uses Manifold Solid B-Rep Object (entity 186), which is not yet supported"
45 .into(),
46 );
47 }
48 if file.entities_of_type(128).next().is_some() {
49 return Err(
50 "iges_import: file has untrimmed B-spline surfaces (entity 128) but no trimmed surfaces (entity 144) to form faces"
51 .into(),
52 );
53 }
54 return Err("iges_import: no trimmed NURBS surfaces (entity 144) found".into());
55 }
56
57 let diag = soup.bounding_diagonal();
59 let sew_tol = (1e-6 * diag).max(1e-9);
60
61 let mut next_id = soup.next_id;
67 let SoupBuilder { vertices, edges, .. } = soup;
68 let components = partition_faces_into_bodies(&faces, &edges);
69
70 let mut solids = Vec::with_capacity(components.len());
71 for face_indices in components {
72 let body_faces: Vec<FaceRecord> =
73 face_indices.iter().map(|&i| faces[i].clone()).collect();
74 let mut edge_ids: rustc_hash::FxHashSet<u64> = Default::default();
75 for face in &body_faces {
76 for loop_record in &face.loops {
77 for coedge in &loop_record.coedges {
78 edge_ids.insert(coedge.edge_id);
79 }
80 }
81 }
82 let body_edges: Vec<EdgeRecord> =
83 edges.iter().filter(|e| edge_ids.contains(&e.id)).cloned().collect();
84 let mut vertex_ids: rustc_hash::FxHashSet<u64> = Default::default();
85 for edge in &body_edges {
86 vertex_ids.insert(edge.start_vertex_id);
87 vertex_ids.insert(edge.end_vertex_id);
88 }
89 let body_vertices: Vec<VertexRecord> =
90 vertices.iter().filter(|v| vertex_ids.contains(&v.id)).cloned().collect();
91
92 let shell_id = next_id;
93 let solid_id = next_id + 1;
94 next_id += 2;
95 let solid = BrepSolid {
96 mass_properties_cache: Default::default(),
97 id: solid_id,
98 vertices: body_vertices,
99 edges: body_edges,
100 shells: vec![ShellRecord {
101 id: shell_id,
102 faces: body_faces,
103 }],
104 genus: 0,
105 };
106 let (sewn, _report) = sew_solid(&solid, sew_tol)?;
107 let issues = sewn.validate();
108 if !issues.is_empty() {
109 return Err(format!(
110 "iges_import: reconstructed solid failed validation: {issues:?}"
111 ));
112 }
113 solids.push(sewn);
114 }
115 Ok(solids)
116}
117
118fn partition_faces_into_bodies(faces: &[FaceRecord], edges: &[EdgeRecord]) -> Vec<Vec<usize>> {
121 use rustc_hash::FxHashMap as HashMap;
122
123 let edge_vertices: HashMap<u64, (u64, u64)> = edges
124 .iter()
125 .map(|e| (e.id, (e.start_vertex_id, e.end_vertex_id)))
126 .collect();
127
128 let mut parent: Vec<usize> = (0..faces.len()).collect();
129 fn find(parent: &mut Vec<usize>, mut x: usize) -> usize {
130 while parent[x] != x {
131 parent[x] = parent[parent[x]];
132 x = parent[x];
133 }
134 x
135 }
136
137 let mut vertex_owner: HashMap<u64, usize> = HashMap::default();
139 for (face_index, face) in faces.iter().enumerate() {
140 for loop_record in &face.loops {
141 for coedge in &loop_record.coedges {
142 if let Some(&(a, b)) = edge_vertices.get(&coedge.edge_id) {
143 for vertex in [a, b] {
144 match vertex_owner.get(&vertex) {
145 Some(&owner) => {
146 let ra = find(&mut parent, owner);
147 let rb = find(&mut parent, face_index);
148 parent[ra] = rb;
149 }
150 None => {
151 vertex_owner.insert(vertex, face_index);
152 }
153 }
154 }
155 }
156 }
157 }
158 }
159
160 let mut groups: HashMap<usize, Vec<usize>> = HashMap::default();
161 for face_index in 0..faces.len() {
162 let root = find(&mut parent, face_index);
163 groups.entry(root).or_default().push(face_index);
164 }
165 let mut components: Vec<Vec<usize>> = groups.into_values().collect();
166 components.sort_by_key(|c| c.iter().copied().min().unwrap_or(0));
168 components
169}
170
171struct SoupBuilder {
174 next_id: u64,
175 vertices: Vec<VertexRecord>,
176 edges: Vec<EdgeRecord>,
177}
178
179impl SoupBuilder {
180 fn new() -> Self {
181 Self {
182 next_id: 1,
183 vertices: Vec::new(),
184 edges: Vec::new(),
185 }
186 }
187
188 fn fresh(&mut self) -> u64 {
189 let id = self.next_id;
190 self.next_id += 1;
191 id
192 }
193
194 fn weld_vertex(&mut self, point: Vec3) -> u64 {
195 let tol = 1e-7 * (1.0 + point.length());
196 if let Some(existing) = self
197 .vertices
198 .iter()
199 .find(|v| v.point.sub(point).length() <= tol)
200 {
201 return existing.id;
202 }
203 let id = self.fresh();
204 self.vertices.push(VertexRecord { id, point });
205 id
206 }
207
208 fn bounding_diagonal(&self) -> f64 {
209 if self.vertices.is_empty() {
210 return 1.0;
211 }
212 let mut lo = self.vertices[0].point;
213 let mut hi = self.vertices[0].point;
214 for v in &self.vertices {
215 lo.x = lo.x.min(v.point.x);
216 lo.y = lo.y.min(v.point.y);
217 lo.z = lo.z.min(v.point.z);
218 hi.x = hi.x.max(v.point.x);
219 hi.y = hi.y.max(v.point.y);
220 hi.z = hi.z.max(v.point.z);
221 }
222 hi.sub(lo).length().max(1.0)
223 }
224
225 fn build_face(
226 &mut self,
227 file: &IgesFile,
228 ts: &super::entities::TrimmedSurface,
229 scale: f64,
230 ) -> Result<FaceRecord, String> {
231 let surface_entity = file.entity(ts.surface)?;
232 if surface_entity.de.entity_type != 128 {
233 return Err(format!(
234 "iges_import: trimmed surface points to entity type {} (expected 128)",
235 surface_entity.de.entity_type
236 ));
237 }
238 let surface = surface_from_128(surface_entity, scale)?;
239
240 let mut loops: Vec<LoopRecord> = Vec::new();
241 if ts.outer.is_none() {
246 return Err(
247 "iges_import: trimmed surface with an implicit (natural-domain) outer boundary (144 N1=0) is not supported"
248 .into(),
249 );
250 }
251 let mut boundary_ptrs: Vec<i64> = Vec::new();
252 boundary_ptrs.extend(ts.outer);
253 boundary_ptrs.extend(ts.inner.iter().copied());
254 for ptr in boundary_ptrs {
255 loops.push(self.build_loop(file, ptr, ts.surface, &surface, scale)?);
256 }
257
258 let same_sense = loops
265 .first()
266 .map(|outer| loop_param_signed_area(outer) >= 0.0)
267 .unwrap_or(true);
268
269 let id = self.fresh();
270 Ok(FaceRecord {
271 id,
272 surface,
273 same_sense,
274 loops,
275 name: None,
276 })
277 }
278
279 fn build_loop(
280 &mut self,
281 file: &IgesFile,
282 boundary_ptr: i64,
283 surface_ptr: i64,
284 surface: &NurbsSurface,
285 scale: f64,
286 ) -> Result<LoopRecord, String> {
287 let cos_entity = file.entity(boundary_ptr)?;
288 if cos_entity.de.entity_type != 142 {
289 return Err(format!(
290 "iges_import: boundary points to entity type {} (expected 142 curve-on-surface)",
291 cos_entity.de.entity_type
292 ));
293 }
294 let cos = curve_on_surface_from_142(cos_entity)?;
295 if cos.surface != surface_ptr {
296 return Err(format!(
297 "iges_import: curve-on-surface references surface DE {} but its trimmed surface is DE {surface_ptr}",
298 cos.surface
299 ));
300 }
301 let model_curves = resolve_curve_list(file, cos.model_curve, Some(scale))?;
302 let param_curves = resolve_curve_list(file, cos.param_curve, None)?;
303
304 let use_faithful =
305 !param_curves.is_empty() && param_curves.len() == model_curves.len() && {
306 faithful_pcurves_agree(surface, &model_curves[0], ¶m_curves[0])
308 };
309
310 let mut coedges: Vec<CoedgeRecord> = Vec::new();
311 for (index, model_curve) in model_curves.iter().enumerate() {
312 let pcurve = if use_faithful {
313 param_curves[index].clone()
314 } else {
315 derive_pcurve(surface, model_curve)?
316 };
317 coedges.push(self.build_coedge(model_curve, pcurve)?);
318 }
319 let id = self.fresh();
320 Ok(LoopRecord { id, coedges })
321 }
322
323 fn build_coedge(
324 &mut self,
325 model_curve: &NurbsCurve,
326 pcurve: NurbsCurve,
327 ) -> Result<CoedgeRecord, String> {
328 let [t0, t1] = model_curve.domain()?;
329 let p_start = model_curve.evaluate(t0)?;
330 let p_end = model_curve.evaluate(t1)?;
331 let start_vertex_id = self.weld_vertex(p_start);
332 let end_vertex_id = self.weld_vertex(p_end);
333 let degenerate = start_vertex_id == end_vertex_id && curve_collapsed(model_curve, p_start);
334 let edge_id = self.fresh();
335 self.edges.push(EdgeRecord {
336 id: edge_id,
337 curve: model_curve.clone(),
338 t0,
339 t1,
340 start_vertex_id,
341 end_vertex_id,
342 degenerate,
343 name: None,
344 });
345 let coedge_id = self.fresh();
346 Ok(CoedgeRecord {
347 id: coedge_id,
348 edge_id,
349 forward: true,
350 pcurve,
351 })
352 }
353}
354
355fn resolve_curve_list(
359 file: &IgesFile,
360 ptr: i64,
361 coord_scale: Option<f64>,
362) -> Result<Vec<NurbsCurve>, String> {
363 let entity = file.entity(ptr)?;
364 match entity.de.entity_type {
365 102 => {
366 let members = composite_members(entity)?;
367 let mut curves = Vec::with_capacity(members.len());
368 for m in members {
369 let member = file.entity(m)?;
370 if member.de.entity_type != 126 {
371 return Err(format!(
372 "iges_import: composite member is entity type {} (expected 126)",
373 member.de.entity_type
374 ));
375 }
376 curves.push(curve_from_126(member, coord_scale)?);
377 }
378 Ok(curves)
379 }
380 126 => Ok(vec![curve_from_126(entity, coord_scale)?]),
381 other => Err(format!(
382 "iges_import: boundary curve is entity type {other} (expected 102 or 126)"
383 )),
384 }
385}
386
387fn faithful_pcurves_agree(surface: &NurbsSurface, model: &NurbsCurve, param: &NurbsCurve) -> bool {
390 let evaluate = || -> Result<bool, String> {
391 let [pt0, pt1] = param.domain()?;
392 let mid = 0.5 * (pt0 + pt1);
393 let uv = param.evaluate(mid)?;
394 let on_surface = surface.evaluate(uv.x, uv.y)?;
395 let [mt0, mt1] = model.domain()?;
396 let model_mid = model.evaluate(0.5 * (mt0 + mt1))?;
397 let tol = 1e-4 * (1.0 + model_mid.length());
398 Ok(on_surface.sub(model_mid).length() <= tol)
399 };
400 evaluate().unwrap_or(false)
401}
402
403fn derive_pcurve(surface: &NurbsSurface, model: &NurbsCurve) -> Result<NurbsCurve, String> {
406 let [t0, t1] = model.domain()?;
407 let magnitude = model.evaluate(0.5 * (t0 + t1)).map(|p| p.length()).unwrap_or(1.0);
408 let tol = 1e-6 * (1.0 + magnitude);
409 build_pcurve_on_surface_range(surface, model, t0, t1, true, tol)
410}
411
412fn loop_param_signed_area(loop_record: &LoopRecord) -> f64 {
415 let mut pts: Vec<(f64, f64)> = Vec::new();
416 for coedge in &loop_record.coedges {
417 let curve = &coedge.pcurve;
418 let [t0, t1] = match curve.domain() {
419 Ok(d) => d,
420 Err(_) => continue,
421 };
422 const SAMPLES: usize = 8;
425 for i in 0..SAMPLES {
426 let t = t0 + (t1 - t0) * i as f64 / SAMPLES as f64;
427 if let Ok(p) = curve.evaluate(t) {
428 pts.push((p.x, p.y));
429 }
430 }
431 }
432 if pts.len() < 3 {
433 return 0.0;
434 }
435 let mut area = 0.0;
436 for i in 0..pts.len() {
437 let (x0, y0) = pts[i];
438 let (x1, y1) = pts[(i + 1) % pts.len()];
439 area += x0 * y1 - x1 * y0;
440 }
441 0.5 * area
442}
443
444fn curve_collapsed(curve: &NurbsCurve, at: Vec3) -> bool {
447 let tol = 1e-7 * (1.0 + at.length());
448 let [t0, t1] = match curve.domain() {
449 Ok(d) => d,
450 Err(_) => return false,
451 };
452 for index in 1..8 {
453 let t = t0 + (t1 - t0) * index as f64 / 8.0;
454 match curve.evaluate(t) {
455 Ok(p) => {
456 if p.sub(at).length() > tol {
457 return false;
458 }
459 }
460 Err(_) => return false,
461 }
462 }
463 true
464}