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