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