1use crate::curve::curve_to_plane_parameters;
34use crate::step::edge_subcurve;
35use crate::step::pcurve::{build_pcurve, EmittedCurve, EmittedFrame, EmittedSurface, Pcurve2d};
36use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord};
37use crate::{AnalyticSurface, KernelTolerances, NurbsCurve, NurbsSurface, Vec3, Vec4};
38use rustc_hash::FxHashMap as HashMap;
39
40use super::entities::{curve_to_126, surface_to_128};
41use super::writer::{
42 hollerith, real_field, GlobalParams, IgesEntity, IgesWriter, STATUS_DEFINITION, STATUS_DEPENDENT,
43 STATUS_INDEPENDENT,
44};
45use crate::{StepAssemblyExport, StepColors};
46
47pub struct IgesBody<'a> {
49 pub name: String,
50 pub solid: &'a BrepSolid,
51 pub color: Option<[u8; 3]>,
52}
53
54pub fn export_iges(
61 solids: &[BrepSolid],
62 name: &str,
63 unit: &str,
64 timestamp: &str,
65) -> Result<String, String> {
66 let bodies: Vec<IgesBody<'_>> = solids
67 .iter()
68 .map(|solid| IgesBody { name: name.to_string(), solid, color: None })
69 .collect();
70 export_iges_bodies(&bodies, name, unit, timestamp)
71}
72
73pub fn export_iges_bodies(
75 bodies: &[IgesBody<'_>],
76 name: &str,
77 unit: &str,
78 timestamp: &str,
79) -> Result<String, String> {
80 if bodies.is_empty() {
81 return Err("iges_export: no solids to export".into());
82 }
83 let mut writer = IgesWriter::new();
84 writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
85 for body in bodies {
86 write_solid(&mut writer, body, STATUS_INDEPENDENT)?;
87 }
88 Ok(finish(writer, name, unit, timestamp))
89}
90
91pub fn export_iges_assembly(
98 assembly: &StepAssemblyExport,
99 colors: &StepColors,
100 name: &str,
101 unit: &str,
102 timestamp: &str,
103) -> Result<String, String> {
104 let root = assembly.root()?;
105 let mut writer = IgesWriter::new();
106 writer.add_start_line(&format!("IGES export of '{name}' from the BREP kernel."));
107 let mut subfigures: Vec<Option<(i64, usize)>> = vec![None; assembly.products.len()];
108 let prefixes = assembly.occurrence_prefixes(root);
111 let context = AssemblyContext { assembly, colors, root, prefixes: &prefixes };
112 write_product(&mut writer, &context, root, &mut subfigures)?;
113 Ok(finish(writer, name, unit, timestamp))
114}
115
116struct AssemblyContext<'a> {
119 assembly: &'a StepAssemblyExport,
120 colors: &'a StepColors,
121 root: usize,
122 prefixes: &'a [Vec<String>],
123}
124
125fn write_product(
126 writer: &mut IgesWriter,
127 context: &AssemblyContext<'_>,
128 index: usize,
129 subfigures: &mut Vec<Option<(i64, usize)>>,
130) -> Result<(), String> {
131 let (assembly, root) = (context.assembly, context.root);
132 if subfigures[index].is_some() {
133 return Ok(());
134 }
135 let is_root = index == root;
136 let status = if is_root { STATUS_INDEPENDENT } else { STATUS_DEPENDENT };
137 let mut members = Vec::new();
138 let mut depth = 1;
139 let product = &assembly.products[index];
140 for (body_name, solid) in &product.bodies {
141 let color = context.colors.resolve(&context.prefixes[index], body_name);
142 let body = IgesBody { name: body_name.clone(), solid, color };
143 members.push(write_solid(writer, &body, status)?);
144 }
145 for occurrence in assembly.occurrences.iter().filter(|occurrence| occurrence.parent == index) {
146 write_product(writer, context, occurrence.child, subfigures)?;
147 let (definition, child_depth) =
148 subfigures[occurrence.child].ok_or("iges_export: an occurrence places the root product")?;
149 depth = depth.max(child_depth + 1);
150 let mut matrix = IgesEntity::new(
151 124,
152 occurrence.placement[..12].iter().map(|&value| real_field(value)).collect(),
154 );
155 matrix.status = STATUS_DEPENDENT;
156 let matrix = writer.add_entity(matrix);
157 let mut params = vec![definition.to_string(), real_field(0.0), real_field(0.0), real_field(0.0), real_field(1.0)];
158 if !occurrence.designator.is_empty() {
159 let mut label = IgesEntity::new(406, vec!["1".into(), hollerith(&occurrence.designator)]);
160 label.form = 15;
161 let property = writer.add_entity(label);
162 params.extend(["0".into(), "1".into(), property.to_string()]);
163 }
164 let mut instance = IgesEntity::new(408, params);
165 instance.transform = matrix;
166 instance.status = status;
167 instance.label = occurrence.designator.chars().filter(|c| !c.is_whitespace()).take(8).collect();
168 members.push(writer.add_entity(instance));
169 }
170 if is_root {
171 subfigures[index] = Some((0, 0));
173 return Ok(());
174 }
175 let mut params = vec![depth.to_string(), hollerith(&product.name), members.len().to_string()];
176 params.extend(members.iter().map(i64::to_string));
177 let mut definition = IgesEntity::new(308, params);
178 definition.status = STATUS_DEFINITION;
179 definition.label = product.name.chars().filter(|c| !c.is_whitespace()).take(8).collect();
180 subfigures[index] = Some((writer.add_entity(definition), depth));
181 Ok(())
182}
183
184fn finish(writer: IgesWriter, name: &str, unit: &str, timestamp: &str) -> String {
185 let (units_flag, units_name) = units_for(unit);
186 let global = GlobalParams {
187 product_id: if name.is_empty() { "PART".into() } else { name.to_string() },
188 file_name: format!("{}.igs", if name.is_empty() { "part" } else { name }),
189 units_flag,
190 units_name,
191 timestamp: normalize_timestamp(timestamp),
192 min_resolution: 1e-7,
193 };
194 writer.finish(global)
195}
196
197struct Lists {
199 vertex_list: i64,
200 edge_list: i64,
201 vertex_index: HashMap<u64, usize>,
202 edge_index: HashMap<u64, usize>,
203 band: f64,
205}
206
207pub(super) fn write_solid(writer: &mut IgesWriter, body: &IgesBody<'_>, status: &'static str) -> Result<i64, String> {
210 let solid = body.solid;
211 if solid.shells.is_empty() {
212 return Err(format!("iges_export: solid '{}' has no shells", body.name));
213 }
214 let shells = write_shells(writer, solid)?;
215 let mut params = vec![shells[0].to_string(), "1".into(), (shells.len() - 1).to_string()];
217 for void in &shells[1..] {
218 params.push(void.to_string());
219 params.push("1".into());
220 }
221 let label: String = body.name.chars().filter(|c| !c.is_whitespace()).take(8).collect();
223 let property = (!body.name.is_empty()).then(|| {
224 let mut name = IgesEntity::new(406, vec!["1".into(), hollerith(&body.name)]);
225 name.form = 15;
226 writer.add_entity(name)
227 });
228 let color = body.color.map(|[r, g, b]| {
229 let percent = |c: u8| real_field(f64::from(c) * 100.0 / 255.0);
230 writer.add_entity(IgesEntity::new(
231 314,
232 vec![percent(r), percent(g), percent(b), hollerith(&body.name)],
233 ))
234 });
235 if let Some(property) = property {
236 params.extend(["0".into(), "1".into(), property.to_string()]);
237 }
238 let mut entity = IgesEntity::new(186, params);
239 entity.label = label;
240 entity.color = color.map_or(0, |pointer| -pointer);
241 entity.status = status;
242 Ok(writer.add_entity(entity))
243}
244
245pub(super) fn write_shells(writer: &mut IgesWriter, solid: &BrepSolid) -> Result<Vec<i64>, String> {
248 let lists = write_lists(writer, solid)?;
249 let mut shells = Vec::with_capacity(solid.shells.len());
250 for shell in &solid.shells {
251 let mut faces = Vec::with_capacity(shell.faces.len() * 2);
252 for face in &shell.faces {
253 let (pointer, agrees) = write_face(writer, solid, &lists, face)?;
254 faces.push(pointer.to_string());
255 faces.push(flag(agrees));
256 }
257 let mut params = vec![shell.faces.len().to_string()];
258 params.extend(faces);
259 let mut entity = IgesEntity::new(514, params);
260 entity.form = 1;
261 shells.push(writer.add_entity(entity));
262 }
263 Ok(shells)
264}
265
266fn flag(value: bool) -> String {
267 if value { "1" } else { "0" }.into()
268}
269
270fn write_lists(writer: &mut IgesWriter, solid: &BrepSolid) -> Result<Lists, String> {
271 let mut vertex_index = HashMap::default();
272 let mut vertex_params = vec![solid.vertices.len().to_string()];
273 for (index, vertex) in solid.vertices.iter().enumerate() {
274 vertex_index.insert(vertex.id, index + 1);
275 vertex_params.extend([real_field(vertex.point.x), real_field(vertex.point.y), real_field(vertex.point.z)]);
276 }
277 let mut vertex_entity = IgesEntity::new(502, vertex_params);
278 vertex_entity.form = 1;
279 let vertex_list = writer.add_entity(vertex_entity);
280
281 let mut edge_index = HashMap::default();
282 let edges: Vec<&EdgeRecord> = solid.edges.iter().filter(|edge| !edge.degenerate).collect();
283 let mut edge_params = vec![edges.len().to_string()];
284 for (index, edge) in edges.iter().enumerate() {
285 edge_index.insert(edge.id, index + 1);
286 let curve = writer.add_entity(curve_to_126(&edge_subcurve(edge)?)?);
287 let start = vertex_of(&vertex_index, edge.start_vertex_id)?;
288 let end = vertex_of(&vertex_index, edge.end_vertex_id)?;
289 edge_params.extend([
290 curve.to_string(),
291 vertex_list.to_string(),
292 start.to_string(),
293 vertex_list.to_string(),
294 end.to_string(),
295 ]);
296 }
297 let mut edge_entity = IgesEntity::new(504, edge_params);
298 edge_entity.form = 1;
299 let edge_list = writer.add_entity(edge_entity);
300 let band = KernelTolerances::for_solid(solid, 1e-7).export_knit;
301 Ok(Lists { vertex_list, edge_list, vertex_index, edge_index, band })
302}
303
304fn vertex_of(index: &HashMap<u64, usize>, id: u64) -> Result<usize, String> {
305 index.get(&id).copied().ok_or_else(|| format!("iges_export: dangling vertex id {id}"))
306}
307
308fn write_face(
311 writer: &mut IgesWriter,
312 solid: &BrepSolid,
313 lists: &Lists,
314 face: &FaceRecord,
315) -> Result<(i64, bool), String> {
316 if face.loops.is_empty() {
317 return Err(format!("iges_export: face {} has no loops", face.id));
318 }
319 let (surface, reversed, carrier) = write_surface(writer, &face.surface)?;
320 let agrees = face.same_sense != reversed;
321 let mut loops = Vec::with_capacity(face.loops.len());
322 for loop_record in &face.loops {
323 loops.push(write_loop(writer, solid, lists, face, loop_record, agrees, &carrier)?);
324 }
325 let mut params = vec![surface.to_string(), face.loops.len().to_string(), "1".into()];
327 params.extend(loops.iter().map(i64::to_string));
328 let mut entity = IgesEntity::new(510, params);
329 entity.form = 1;
330 Ok((writer.add_entity(entity), agrees))
331}
332
333fn write_loop(
334 writer: &mut IgesWriter,
335 solid: &BrepSolid,
336 lists: &Lists,
337 face: &FaceRecord,
338 loop_record: &LoopRecord,
339 agrees: bool,
340 carrier: &Carrier,
341) -> Result<i64, String> {
342 let mut uses: Vec<(&CoedgeRecord, bool)> = loop_record.coedges.iter().map(|c| (c, c.forward)).collect();
346 if !agrees {
347 uses.reverse();
348 for (_, forward) in &mut uses {
349 *forward = !*forward;
350 }
351 }
352 let mut entries = Vec::with_capacity(uses.len());
353 for (coedge, forward) in uses {
354 let edge_id = coedge.edge_id;
355 let edge = solid
356 .edges
357 .iter()
358 .find(|edge| edge.id == edge_id)
359 .ok_or_else(|| format!("iges_export: dangling edge id {edge_id}"))?;
360 let entry: Vec<String> = if edge.degenerate {
361 let vertex = vertex_of(&lists.vertex_index, edge.start_vertex_id)?;
363 vec!["1".into(), lists.vertex_list.to_string(), vertex.to_string(), "1".into(), "0".into()]
367 } else {
368 let index = lists.edge_index.get(&edge_id).copied().ok_or("iges_export: edge missing from its list")?;
369 let mut entry = vec!["0".into(), lists.edge_list.to_string(), index.to_string(), flag(forward)];
370 match parameter_curve(carrier, &face.surface, lists.band, edge, coedge)? {
371 Some(curve) => {
372 let curve = if forward { curve } else { curve.reversed()? };
376 let pointer = writer.add_entity(curve_to_126(&curve)?);
377 entry.extend(["1".into(), "0".into(), pointer.to_string()]);
379 }
380 None => entry.push("0".into()),
381 }
382 entry
383 };
384 entries.push(entry);
385 }
386 let mut params = vec![entries.len().to_string()];
387 for entry in entries {
388 params.extend(entry);
389 }
390 let mut entity = IgesEntity::new(508, params);
391 entity.form = 1;
392 Ok(writer.add_entity(entity))
393}
394
395enum Carrier {
397 Plane { origin: Vec3, x_axis: Vec3, y_axis: Vec3 },
399 Spline,
401 Quadric { surface: EmittedSurface, angular_v: bool },
404 Unparameterized,
407}
408
409const DEGREES: f64 = 180.0 / std::f64::consts::PI;
411
412fn parameter_curve(
416 carrier: &Carrier,
417 stored: &NurbsSurface,
418 band: f64,
419 edge: &EdgeRecord,
420 coedge: &CoedgeRecord,
421) -> Result<Option<NurbsCurve>, String> {
422 match carrier {
423 Carrier::Unparameterized => Ok(None),
424 Carrier::Quadric { surface, angular_v } => {
425 let curve = edge_subcurve(edge)?;
426 let [t0, t1] = curve.domain()?;
427 let oriented = if coedge.forward { coedge.pcurve.clone() } else { coedge.pcurve.reversed()? };
428 let outcome = build_pcurve(stored, surface, &EmittedCurve::Spline { curve }, &oriented, band)?;
429 let v_scale = if *angular_v { DEGREES } else { 1.0 };
430 let scaled = |c: Vec4| Vec4 { x: c.x * DEGREES, y: c.y * v_scale, z: 0.0, w: c.w };
432 Ok(match outcome.curve {
433 Some(Pcurve2d::Line { point, vector }) => {
434 let at = |s: f64| Vec4 { x: point[0] + s * vector[0], y: point[1] + s * vector[1], z: 0.0, w: 1.0 };
435 Some(NurbsCurve::new(1, vec![t0, t0, t1, t1], vec![scaled(at(t0)), scaled(at(t1))])?)
436 }
437 Some(Pcurve2d::Spline(curve)) => {
438 let points = curve.control_points.iter().map(|&c| scaled(c)).collect();
439 Some(NurbsCurve::new(curve.degree, curve.knots.clone(), points)?)
440 }
441 Some(Pcurve2d::Circle { .. }) | None => None,
444 })
445 }
446 Carrier::Plane { origin, x_axis, y_axis } => {
449 Ok(Some(curve_to_plane_parameters(&edge_subcurve(edge)?, *origin, *x_axis, *y_axis)?))
450 }
451 Carrier::Spline => {
452 let mut curve = if coedge.forward { coedge.pcurve.clone() } else { coedge.pcurve.reversed()? };
455 let [q0, q1] = curve.domain()?;
456 let [t0, t1] = edge_subcurve(edge)?.domain()?;
457 if q1 <= q0 || t1 <= t0 {
458 return Ok(None);
459 }
460 let scale = (t1 - t0) / (q1 - q0);
461 for knot in &mut curve.knots {
462 *knot = t0 + (*knot - q0) * scale;
463 }
464 Ok(Some(curve))
465 }
466 }
467}
468
469fn write_surface(writer: &mut IgesWriter, surface: &NurbsSurface) -> Result<(i64, bool, Carrier), String> {
472 if let Some(analytic) = surface.analytic() {
473 if let Some((pointer, reversed)) = write_analytic(writer, surface, analytic)? {
474 let emitted = |frame: &crate::analytic_surface::RevolutionFrame| EmittedFrame {
475 origin: frame.origin,
476 x_axis: frame.x_axis,
477 y_axis: frame.y_axis,
478 axis: frame.axis,
479 azimuth_sign: 1.0,
480 };
481 let carrier = match analytic {
482 AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => {
483 let normal = u_dir.cross(*v_dir).normalized()?;
484 let x_axis = u_dir.normalized()?;
485 Carrier::Plane { origin: *origin, x_axis, y_axis: normal.cross(x_axis) }
486 }
487 AnalyticSurface::RuledRevolution { frame, rho0, rho1, .. }
489 if (rho1 - rho0).abs() <= 1e-9 * (1.0 + rho0.abs().max(rho1.abs())) =>
490 {
491 Carrier::Quadric {
492 surface: EmittedSurface::Cylinder { frame: emitted(frame), radius: *rho0 },
493 angular_v: false,
494 }
495 }
496 AnalyticSurface::RuledRevolution { .. } => Carrier::Unparameterized,
497 AnalyticSurface::Sphere { frame, radius } => Carrier::Quadric {
498 surface: EmittedSurface::Sphere { frame: emitted(frame), radius: *radius },
499 angular_v: true,
500 },
501 AnalyticSurface::Torus { major_radius, minor_radius, .. }
507 if ((major_radius - minor_radius) - minor_radius).abs() <= 1e-6 * major_radius.abs().max(1.0) =>
508 {
509 Carrier::Unparameterized
510 }
511 AnalyticSurface::Torus { frame, major_radius, minor_radius } => Carrier::Quadric {
512 surface: EmittedSurface::Torus {
513 frame: emitted(frame),
514 major_radius: *major_radius,
515 minor_radius: *minor_radius,
516 },
517 angular_v: true,
518 },
519 AnalyticSurface::Revolution { .. } => Carrier::Unparameterized,
520 };
521 return Ok((pointer, reversed, carrier));
522 }
523 }
524 Ok((writer.add_entity(surface_to_128(surface)?), false, Carrier::Spline))
525}
526
527fn point_116(writer: &mut IgesWriter, p: Vec3) -> i64 {
528 writer.add_entity(IgesEntity::new(116, vec![real_field(p.x), real_field(p.y), real_field(p.z)]))
529}
530
531fn direction_123(writer: &mut IgesWriter, d: Vec3) -> i64 {
532 writer.add_entity(IgesEntity::new(123, vec![real_field(d.x), real_field(d.y), real_field(d.z)]))
533}
534
535fn sampled_normal(surface: &NurbsSurface) -> Result<(Vec3, Vec3), String> {
537 let [u0, u1] = surface.domain_u()?;
538 let [v0, v1] = surface.domain_v()?;
539 let (u, v) = (0.5 * (u0 + u1), 0.5 * (v0 + v1));
540 Ok((surface.evaluate(u, v)?, surface.normal(u, v)?))
541}
542
543fn write_analytic(
544 writer: &mut IgesWriter,
545 surface: &NurbsSurface,
546 analytic: &AnalyticSurface,
547) -> Result<Option<(i64, bool)>, String> {
548 let (point, normal) = sampled_normal(surface)?;
549 match analytic {
550 AnalyticSurface::Plane { origin, u_dir, v_dir, .. } => {
551 let (Ok(n), Ok(x)) = (u_dir.cross(*v_dir).normalized(), u_dir.normalized()) else {
552 return Ok(None);
553 };
554 let location = point_116(writer, *origin);
556 let normal = direction_123(writer, n);
557 let reference = direction_123(writer, x);
558 let mut entity = IgesEntity::new(190, vec![location.to_string(), normal.to_string(), reference.to_string()]);
559 entity.form = 1;
560 Ok(Some((writer.add_entity(entity), false)))
561 }
562 AnalyticSurface::RuledRevolution { frame, rho0, rho1, height } => {
563 let outward = {
564 let d = point.sub(frame.origin);
565 d.sub(frame.axis.scale(d.dot(frame.axis)))
566 };
567 let reversed = normal.dot(outward) < 0.0;
568 let radius_scale = 1.0 + rho0.abs().max(rho1.abs());
569 if (rho1 - rho0).abs() <= 1e-9 * radius_scale {
570 if *rho0 <= 0.0 {
571 return Ok(None);
572 }
573 let location = point_116(writer, frame.origin);
574 let axis = direction_123(writer, frame.axis);
575 let reference = direction_123(writer, frame.x_axis);
576 let mut entity = IgesEntity::new(
577 192,
578 vec![location.to_string(), axis.to_string(), real_field(*rho0), reference.to_string()],
579 );
580 entity.form = 1;
581 return Ok(Some((writer.add_entity(entity), reversed)));
582 }
583 let slope = (rho1 - rho0) / height;
586 let apex_margin = 2.0 * (1e-4 * height.abs().max(1.0) + 1e-9) * slope.abs();
587 if rho0.min(*rho1) <= apex_margin {
588 return Ok(None);
589 }
590 let direction = if slope >= 0.0 { frame.axis } else { frame.axis.scale(-1.0) };
591 let location = point_116(writer, frame.origin);
592 let axis = direction_123(writer, direction);
593 let reference = direction_123(writer, frame.x_axis);
594 let mut entity = IgesEntity::new(
595 194,
596 vec![
597 location.to_string(),
598 axis.to_string(),
599 real_field(*rho0),
600 real_field(slope.abs().atan().to_degrees()),
601 reference.to_string(),
602 ],
603 );
604 entity.form = 1;
605 Ok(Some((writer.add_entity(entity), reversed)))
606 }
607 AnalyticSurface::Sphere { frame, radius } => {
608 let reversed = normal.dot(point.sub(frame.origin)) < 0.0;
609 let location = point_116(writer, frame.origin);
610 let axis = direction_123(writer, frame.axis);
611 let reference = direction_123(writer, frame.x_axis);
612 let mut entity = IgesEntity::new(
613 196,
614 vec![location.to_string(), real_field(*radius), axis.to_string(), reference.to_string()],
615 );
616 entity.form = 1;
617 Ok(Some((writer.add_entity(entity), reversed)))
618 }
619 AnalyticSurface::Torus { frame, major_radius, minor_radius } => {
620 let tube_centre = {
621 let d = point.sub(frame.origin);
622 let radial = d.sub(frame.axis.scale(d.dot(frame.axis)));
623 let Ok(direction) = radial.normalized() else {
624 return Ok(None);
625 };
626 frame.origin.add(direction.scale(*major_radius))
627 };
628 let reversed = normal.dot(point.sub(tube_centre)) < 0.0;
629 let location = point_116(writer, frame.origin);
630 let axis = direction_123(writer, frame.axis);
631 let reference = direction_123(writer, frame.x_axis);
632 let mut entity = IgesEntity::new(
633 198,
634 vec![
635 location.to_string(),
636 axis.to_string(),
637 real_field(*major_radius),
638 real_field(*minor_radius),
639 reference.to_string(),
640 ],
641 );
642 entity.form = 1;
643 Ok(Some((writer.add_entity(entity), reversed)))
644 }
645 AnalyticSurface::Revolution { .. } => Ok(None),
646 }
647}
648
649fn units_for(unit: &str) -> (i64, String) {
650 match unit.trim().to_ascii_lowercase().as_str() {
651 "inch" | "inches" | "in" => (1, "IN".into()),
652 "foot" | "feet" | "ft" => (4, "FT".into()),
653 "meter" | "metre" | "m" => (6, "M".into()),
654 "centimeter" | "centimetre" | "cm" => (10, "CM".into()),
655 _ => (2, "MM".into()),
656 }
657}
658
659fn normalize_timestamp(timestamp: &str) -> String {
660 if timestamp.trim().is_empty() {
661 "00000000.000000".into()
662 } else {
663 timestamp.to_string()
664 }
665}