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