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 kind: crate::BodyKind::Solid,
106 };
107 let (sewn, _report) = sew_solid(&solid, sew_tol)?;
108 let issues = sewn.validate();
109 if !issues.is_empty() {
110 return Err(format!(
111 "iges_import: reconstructed solid failed validation: {issues:?}"
112 ));
113 }
114 solids.push(sewn);
115 }
116 Ok(solids)
117}
118
119fn partition_faces_into_bodies(faces: &[FaceRecord], edges: &[EdgeRecord]) -> Vec<Vec<usize>> {
122 use rustc_hash::FxHashMap as HashMap;
123
124 let edge_vertices: HashMap<u64, (u64, u64)> = edges
125 .iter()
126 .map(|e| (e.id, (e.start_vertex_id, e.end_vertex_id)))
127 .collect();
128
129 let mut parent: Vec<usize> = (0..faces.len()).collect();
130 fn find(parent: &mut Vec<usize>, mut x: usize) -> usize {
131 while parent[x] != x {
132 parent[x] = parent[parent[x]];
133 x = parent[x];
134 }
135 x
136 }
137
138 let mut vertex_owner: HashMap<u64, usize> = HashMap::default();
140 for (face_index, face) in faces.iter().enumerate() {
141 for loop_record in &face.loops {
142 for coedge in &loop_record.coedges {
143 if let Some(&(a, b)) = edge_vertices.get(&coedge.edge_id) {
144 for vertex in [a, b] {
145 match vertex_owner.get(&vertex) {
146 Some(&owner) => {
147 let ra = find(&mut parent, owner);
148 let rb = find(&mut parent, face_index);
149 parent[ra] = rb;
150 }
151 None => {
152 vertex_owner.insert(vertex, face_index);
153 }
154 }
155 }
156 }
157 }
158 }
159 }
160
161 let mut groups: HashMap<usize, Vec<usize>> = HashMap::default();
162 for face_index in 0..faces.len() {
163 let root = find(&mut parent, face_index);
164 groups.entry(root).or_default().push(face_index);
165 }
166 let mut components: Vec<Vec<usize>> = groups.into_values().collect();
167 components.sort_by_key(|c| c.iter().copied().min().unwrap_or(0));
169 components
170}
171
172struct SoupBuilder {
175 next_id: u64,
176 vertices: Vec<VertexRecord>,
177 edges: Vec<EdgeRecord>,
178}
179
180impl SoupBuilder {
181 fn new() -> Self {
182 Self {
183 next_id: 1,
184 vertices: Vec::new(),
185 edges: Vec::new(),
186 }
187 }
188
189 fn fresh(&mut self) -> u64 {
190 let id = self.next_id;
191 self.next_id += 1;
192 id
193 }
194
195 fn weld_vertex(&mut self, point: Vec3) -> u64 {
196 let tol = 1e-7 * (1.0 + point.length());
197 if let Some(existing) = self
198 .vertices
199 .iter()
200 .find(|v| v.point.sub(point).length() <= tol)
201 {
202 return existing.id;
203 }
204 let id = self.fresh();
205 self.vertices.push(VertexRecord { id, point });
206 id
207 }
208
209 fn bounding_diagonal(&self) -> f64 {
210 if self.vertices.is_empty() {
211 return 1.0;
212 }
213 let mut lo = self.vertices[0].point;
214 let mut hi = self.vertices[0].point;
215 for v in &self.vertices {
216 lo.x = lo.x.min(v.point.x);
217 lo.y = lo.y.min(v.point.y);
218 lo.z = lo.z.min(v.point.z);
219 hi.x = hi.x.max(v.point.x);
220 hi.y = hi.y.max(v.point.y);
221 hi.z = hi.z.max(v.point.z);
222 }
223 hi.sub(lo).length().max(1.0)
224 }
225
226 fn build_face(
227 &mut self,
228 file: &IgesFile,
229 ts: &super::entities::TrimmedSurface,
230 scale: f64,
231 ) -> Result<FaceRecord, String> {
232 let surface_entity = file.entity(ts.surface)?;
233 if surface_entity.de.entity_type != 128 {
234 return Err(format!(
235 "iges_import: trimmed surface points to entity type {} (expected 128)",
236 surface_entity.de.entity_type
237 ));
238 }
239 let surface = surface_from_128(surface_entity, scale)?;
240
241 let mut loops: Vec<LoopRecord> = Vec::new();
242 if ts.outer.is_none() {
247 return Err(
248 "iges_import: trimmed surface with an implicit (natural-domain) outer boundary (144 N1=0) is not supported"
249 .into(),
250 );
251 }
252 let mut boundary_ptrs: Vec<i64> = Vec::new();
253 boundary_ptrs.extend(ts.outer);
254 boundary_ptrs.extend(ts.inner.iter().copied());
255 for ptr in boundary_ptrs {
256 loops.push(self.build_loop(file, ptr, ts.surface, &surface, scale)?);
257 }
258
259 let same_sense = loops
266 .first()
267 .map(|outer| loop_param_signed_area(outer) >= 0.0)
268 .unwrap_or(true);
269
270 let id = self.fresh();
271 Ok(FaceRecord {
272 id,
273 surface,
274 same_sense,
275 loops,
276 name: None,
277 })
278 }
279
280 fn build_loop(
281 &mut self,
282 file: &IgesFile,
283 boundary_ptr: i64,
284 surface_ptr: i64,
285 surface: &NurbsSurface,
286 scale: f64,
287 ) -> Result<LoopRecord, String> {
288 let cos_entity = file.entity(boundary_ptr)?;
289 if cos_entity.de.entity_type != 142 {
290 return Err(format!(
291 "iges_import: boundary points to entity type {} (expected 142 curve-on-surface)",
292 cos_entity.de.entity_type
293 ));
294 }
295 let cos = curve_on_surface_from_142(cos_entity)?;
296 if cos.surface != surface_ptr {
297 return Err(format!(
298 "iges_import: curve-on-surface references surface DE {} but its trimmed surface is DE {surface_ptr}",
299 cos.surface
300 ));
301 }
302 let model_curves = resolve_curve_list(file, cos.model_curve, Some(scale))?;
303 let param_curves = resolve_curve_list(file, cos.param_curve, None)?;
304
305 let use_faithful =
306 !param_curves.is_empty() && param_curves.len() == model_curves.len() && {
307 faithful_pcurves_agree(surface, &model_curves[0], ¶m_curves[0])
309 };
310
311 let mut coedges: Vec<CoedgeRecord> = Vec::new();
312 for (index, model_curve) in model_curves.iter().enumerate() {
313 let pcurve = if use_faithful {
314 param_curves[index].clone()
315 } else {
316 derive_pcurve(surface, model_curve)?
317 };
318 coedges.push(self.build_coedge(model_curve, pcurve)?);
319 }
320 let id = self.fresh();
321 Ok(LoopRecord { id, coedges })
322 }
323
324 fn build_coedge(
325 &mut self,
326 model_curve: &NurbsCurve,
327 pcurve: NurbsCurve,
328 ) -> Result<CoedgeRecord, String> {
329 let [t0, t1] = model_curve.domain()?;
330 let p_start = model_curve.evaluate(t0)?;
331 let p_end = model_curve.evaluate(t1)?;
332 let start_vertex_id = self.weld_vertex(p_start);
333 let end_vertex_id = self.weld_vertex(p_end);
334 let degenerate = start_vertex_id == end_vertex_id && curve_collapsed(model_curve, p_start);
335 let edge_id = self.fresh();
336 self.edges.push(EdgeRecord {
337 id: edge_id,
338 curve: model_curve.clone(),
339 t0,
340 t1,
341 start_vertex_id,
342 end_vertex_id,
343 degenerate,
344 name: None,
345 });
346 let coedge_id = self.fresh();
347 Ok(CoedgeRecord {
348 id: coedge_id,
349 edge_id,
350 forward: true,
351 pcurve,
352 })
353 }
354}
355
356fn resolve_curve_list(
360 file: &IgesFile,
361 ptr: i64,
362 coord_scale: Option<f64>,
363) -> Result<Vec<NurbsCurve>, String> {
364 let entity = file.entity(ptr)?;
365 match entity.de.entity_type {
366 102 => {
367 let members = composite_members(entity)?;
368 let mut curves = Vec::with_capacity(members.len());
369 for m in members {
370 let member = file.entity(m)?;
371 if member.de.entity_type != 126 {
372 return Err(format!(
373 "iges_import: composite member is entity type {} (expected 126)",
374 member.de.entity_type
375 ));
376 }
377 curves.push(curve_from_126(member, coord_scale)?);
378 }
379 Ok(curves)
380 }
381 126 => Ok(vec![curve_from_126(entity, coord_scale)?]),
382 other => Err(format!(
383 "iges_import: boundary curve is entity type {other} (expected 102 or 126)"
384 )),
385 }
386}
387
388fn faithful_pcurves_agree(surface: &NurbsSurface, model: &NurbsCurve, param: &NurbsCurve) -> bool {
391 let evaluate = || -> Result<bool, String> {
392 let [pt0, pt1] = param.domain()?;
393 let mid = 0.5 * (pt0 + pt1);
394 let uv = param.evaluate(mid)?;
395 let on_surface = surface.evaluate(uv.x, uv.y)?;
396 let [mt0, mt1] = model.domain()?;
397 let model_mid = model.evaluate(0.5 * (mt0 + mt1))?;
398 let tol = 1e-4 * (1.0 + model_mid.length());
399 Ok(on_surface.sub(model_mid).length() <= tol)
400 };
401 evaluate().unwrap_or(false)
402}
403
404fn derive_pcurve(surface: &NurbsSurface, model: &NurbsCurve) -> Result<NurbsCurve, String> {
407 let [t0, t1] = model.domain()?;
408 let magnitude = model.evaluate(0.5 * (t0 + t1)).map(|p| p.length()).unwrap_or(1.0);
409 let tol = 1e-6 * (1.0 + magnitude);
410 build_pcurve_on_surface_range(surface, model, t0, t1, true, tol)
411}
412
413fn loop_param_signed_area(loop_record: &LoopRecord) -> f64 {
416 let mut pts: Vec<(f64, f64)> = Vec::new();
417 for coedge in &loop_record.coedges {
418 let curve = &coedge.pcurve;
419 let [t0, t1] = match curve.domain() {
420 Ok(d) => d,
421 Err(_) => continue,
422 };
423 const SAMPLES: usize = 8;
426 for i in 0..SAMPLES {
427 let t = t0 + (t1 - t0) * i as f64 / SAMPLES as f64;
428 if let Ok(p) = curve.evaluate(t) {
429 pts.push((p.x, p.y));
430 }
431 }
432 }
433 if pts.len() < 3 {
434 return 0.0;
435 }
436 let mut area = 0.0;
437 for i in 0..pts.len() {
438 let (x0, y0) = pts[i];
439 let (x1, y1) = pts[(i + 1) % pts.len()];
440 area += x0 * y1 - x1 * y0;
441 }
442 0.5 * area
443}
444
445fn curve_collapsed(curve: &NurbsCurve, at: Vec3) -> bool {
448 let tol = 1e-7 * (1.0 + at.length());
449 let [t0, t1] = match curve.domain() {
450 Ok(d) => d,
451 Err(_) => return false,
452 };
453 for index in 1..8 {
454 let t = t0 + (t1 - t0) * index as f64 / 8.0;
455 match curve.evaluate(t) {
456 Ok(p) => {
457 if p.sub(at).length() > tol {
458 return false;
459 }
460 }
461 Err(_) => return false,
462 }
463 }
464 true
465}