1use ogeom_core::FastMap;
32use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
33use ogeom_geom::Curve3d as _;
34use ogeom_geom::Reversible as _;
35use ogeom_geom::Surface as _;
36use ogeom_geom::Transformable as _;
37use ogeom_geom::{BSplineCurve, Curve, SurfaceGeometry};
38use ogeom_math::{Frame, Point, Transform};
39use ogeom_topo::{EdgeRepr, Filter, NodeData, Shape, ShapeType, explore};
40
41pub fn write_iges(document: &ogeom_doc::Document, tol: Tolerances) -> OgeomResult<String> {
57 write_iges_reported(document, tol).map(|written| written.text)
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct IgesExport {
63 pub text: String,
65 pub warnings: Vec<String>,
69}
70
71pub fn write_iges_reported(
92 document: &ogeom_doc::Document,
93 tol: Tolerances,
94) -> OgeomResult<IgesExport> {
95 let mut writer = Writer {
96 model: document.model(),
97 entities: Vec::new(),
98 vertices: FastMap::default(),
99 edges: FastMap::default(),
100 vertex_coords: Vec::new(),
101 edge_records: Vec::new(),
102 construction_from: usize::MAX,
103 tol,
104 };
105
106 let mut parts = Vec::new();
107 for (id, product) in document.products() {
108 let ogeom_doc::ProductKind::Part { shape } = &product.kind else {
109 continue;
110 };
111 let bodies = if document.construction_of(id).next().is_none() {
113 crate::bodies::bodies_of(writer.model, shape, "IGES", true)?
114 } else {
115 crate::bodies::bodies_within(writer.model, shape, "IGES", true)?
116 };
117 parts.push((product.name.clone(), bodies));
118 }
119 if parts.is_empty() && document.construction().is_empty() {
120 ogeom_bail!(Construction, "the document holds no part to write as IGES");
121 }
122 for (label, bodies) in parts {
123 for solid in &bodies.solids {
124 writer.solid(solid, &label)?;
125 }
126 for shell in &bodies.shells {
127 for face in writer.model.ordered_children_of(shell)? {
128 writer.trimmed_surface(&face, &label)?;
129 }
130 }
131 for face in &bodies.faces {
132 writer.trimmed_surface(face, &label)?;
133 }
134 for wire in &bodies.wires {
135 writer.wireframe(wire, &label)?;
136 }
137 for vertex in &bodies.vertices {
138 writer.free_point(vertex, &label)?;
139 }
140 }
141 let mut warnings = Vec::new();
142 writer.construction_from = writer.entities.len();
143 for item in document.construction() {
144 let mark = writer.entities.len();
147 match writer.construction(item) {
148 Ok(name_warning) => warnings.extend(name_warning),
149 Err(e) => {
150 writer.entities.truncate(mark);
151 warnings.push(format!(
152 "construction item '{}' has no IGES spelling here and is left out: {e}",
153 item.name
154 ));
155 }
156 }
157 }
158 Ok(IgesExport {
159 text: writer.serialize(),
160 warnings,
161 })
162}
163
164struct Pending {
166 kind: i64,
167 form: i64,
168 transform: Option<usize>,
170 independent: bool,
172 label: String,
173 params: String,
174}
175
176struct Writer<'a> {
177 model: &'a ogeom_topo::Model,
178 entities: Vec<Pending>,
179 vertices: FastMap<(ogeom_topo::TShapeId, [u64; 3]), usize>,
183 edges: FastMap<(ogeom_topo::TShapeId, [u64; 3]), usize>,
186 vertex_coords: Vec<Point>,
188 edge_records: Vec<(usize, usize, usize)>,
190 construction_from: usize,
193 tol: Tolerances,
194}
195
196impl Writer<'_> {
197 fn push(&mut self, p: Pending) -> usize {
198 self.entities.push(p);
199 self.entities.len() - 1
200 }
201
202 #[allow(clippy::cast_possible_wrap, reason = "entity counts are small")]
204 fn de(&self, index: usize) -> i64 {
205 2 * index as i64 + 1
206 }
207
208 fn solid(&mut self, solid: &Shape, label: &str) -> OgeomResult<()> {
209 self.vertices.clear();
213 self.edges.clear();
214 self.vertex_coords.clear();
215 self.edge_records.clear();
216
217 let shells = explore(self.model, solid, Filter::OfType(ShapeType::Shell))?;
222 if shells.is_empty() {
223 ogeom_bail!(Construction, "a solid with no shell cannot be written");
224 }
225 let mut shell_faces = Vec::with_capacity(shells.len());
226 for (k, shell) in shells.iter().enumerate() {
227 let mut face_entities = Vec::new();
228 for face in self.model.ordered_children_of(shell)? {
229 let face = if k == 0 { face } else { face.reversed() };
230 let (entity, turned) = self.face(&face)?;
231 let forward = face.orientation() != ogeom_topo::Orientation::Reversed;
232 face_entities.push((entity, forward != turned));
233 }
234 shell_faces.push(face_entities);
235 }
236
237 let vertex_list = {
239 let mut params = format!("{}", self.vertex_coords.len());
240 for p in &self.vertex_coords {
241 params.push_str(&format!(",{},{},{}", fmt(p.x), fmt(p.y), fmt(p.z)));
242 }
243 self.push(Pending {
244 kind: 502,
245 form: 1,
246 transform: None,
247 independent: false,
248 label: String::new(),
249 params,
250 })
251 };
252 let edge_list = {
253 let records = std::mem::take(&mut self.edge_records);
254 let mut params = format!("{}", records.len());
255 let mut curve_des = Vec::new();
256 for (curve_entity, sv, tv) in &records {
257 curve_des.push(self.de(*curve_entity));
258 params.push_str(&format!(",{},@V,{sv},@V,{tv}", self.de(*curve_entity)));
259 }
260 self.push(Pending {
261 kind: 504,
262 form: 1,
263 transform: None,
264 independent: false,
265 label: String::new(),
266 params,
267 })
268 };
269 let vlist_de = self.de(vertex_list);
272 let elist_de = self.de(edge_list);
273 for e in &mut self.entities {
274 if e.kind == 508 || e.kind == 504 {
275 e.params = e.params.replace("@E", &elist_de.to_string());
276 e.params = e.params.replace("@V", &vlist_de.to_string());
277 }
278 }
279
280 let mut shell_des = Vec::with_capacity(shell_faces.len());
281 for face_entities in &shell_faces {
282 let mut params = format!("{}", face_entities.len());
283 for (face, forward) in face_entities {
284 let flag = i32::from(*forward);
285 params.push_str(&format!(",{},{flag}", self.de(*face)));
286 }
287 let shell_entity = self.push(Pending {
288 kind: 514,
289 form: 1,
290 transform: None,
291 independent: false,
292 label: String::new(),
293 params,
294 });
295 shell_des.push(self.de(shell_entity));
296 }
297 let mut params = format!("{},1,{}", shell_des[0], shell_des.len() - 1);
298 for void in &shell_des[1..] {
299 params.push_str(&format!(",{void},0"));
300 }
301 if let Some(name) = self.name_property(label) {
305 params.push_str(&format!(",0,1,{}", self.de(name)));
306 }
307 self.push(Pending {
308 kind: 186,
309 form: 0,
310 transform: None,
311 independent: true,
312 label: label.chars().take(8).collect(),
313 params,
314 });
315 Ok(())
316 }
317
318 fn name_property(&mut self, name: &str) -> Option<usize> {
322 let plain = !name.is_empty()
323 && name
324 .chars()
325 .all(|c| (c.is_ascii_graphic() || c == ' ') && c != ',' && c != ';');
326 plain.then(|| {
327 self.push(Pending {
328 kind: 406,
329 form: 15,
330 transform: None,
331 independent: false,
332 label: String::new(),
333 params: format!("1,{}H{name}", name.len()),
334 })
335 })
336 }
337
338 fn face(&mut self, face: &Shape) -> OgeomResult<(usize, bool)> {
343 let placement = face.transform(self.model.datums())?;
344 let surface = {
345 let Some(node) = self.model.node(face) else {
346 ogeom_bail!(Dangling, "face is not in this model");
347 };
348 let NodeData::Face(data) = node.data() else {
349 ogeom_bail!(Construction, "face node holds no face data");
350 };
351 let Some(surface) = self.model.geometry().surface(data.surface) else {
352 ogeom_bail!(Dangling, "face refers to a surface not in this model");
353 };
354 surface.clone().transformed(&placement, self.tol)?
355 };
356 let restated = self.restates(&surface)?;
357 let turned = crate::bodies::turned_in_file(&placement, restated);
358 let surface_entity = self.surface(&surface)?;
359 let walked = if restated {
364 ogeom_topo::Orientation::Reversed
365 } else {
366 ogeom_topo::Orientation::Forward
367 };
368 let wires = self.model.children_of(&face.oriented(walked))?;
369 let mut loop_entities = Vec::new();
370 for wire in &wires {
371 if let Some(entity) = self.wire(wire)? {
372 loop_entities.push(entity);
373 }
374 }
375 if loop_entities.is_empty() {
376 ogeom_bail!(
377 Construction,
378 "a face whose every boundary is degenerate cannot be written \
379 as IGES; see docs/CAPABILITIES.md, io.iges"
380 );
381 }
382 let mut params = format!("{},{},1", self.de(surface_entity), loop_entities.len());
383 for entity in &loop_entities {
384 params.push_str(&format!(",{}", self.de(*entity)));
385 }
386 let entity = self.push(Pending {
387 kind: 510,
388 form: 1,
389 transform: None,
390 independent: false,
391 label: String::new(),
392 params,
393 });
394 Ok((entity, turned))
395 }
396
397 fn trimmed_surface(&mut self, face: &Shape, label: &str) -> OgeomResult<()> {
406 let placement = face.transform(self.model.datums())?;
407 let surface = {
408 let Some(node) = self.model.node(face) else {
409 ogeom_bail!(Dangling, "face is not in this model");
410 };
411 let NodeData::Face(data) = node.data() else {
412 ogeom_bail!(Construction, "face node holds no face data");
413 };
414 let Some(surface) = self.model.geometry().surface(data.surface) else {
415 ogeom_bail!(Dangling, "face refers to a surface not in this model");
416 };
417 surface.clone().transformed(&placement, self.tol)?
418 };
419 let surface_entity = if face.orientation() == ogeom_topo::Orientation::Reversed {
420 self.turned_surface(&surface)?
421 } else {
422 self.surface(&surface)?
423 };
424 let mut boundaries = Vec::new();
427 for wire in self.model.children_of(face)? {
428 if let Some(curve) = self.boundary_curve(&wire)? {
429 let params = format!("0,{},0,{},2", self.de(surface_entity), self.de(curve));
432 boundaries.push(self.push(Pending {
433 kind: 142,
434 form: 0,
435 transform: None,
436 independent: false,
437 label: String::new(),
438 params,
439 }));
440 }
441 }
442 if self.model.children_of(face)?.is_empty() {
445 self.push(Pending {
446 kind: 144,
447 form: 0,
448 transform: None,
449 independent: true,
450 label: label.chars().take(8).collect(),
451 params: format!("{},0,0,0", self.de(surface_entity)),
452 });
453 return Ok(());
454 }
455 let Some((outer, inner)) = boundaries.split_first() else {
456 ogeom_bail!(
457 Construction,
458 "a face whose every boundary is degenerate cannot be written \
459 as IGES; see docs/CAPABILITIES.md, io.iges"
460 );
461 };
462 let mut params = format!(
463 "{},1,{},{}",
464 self.de(surface_entity),
465 inner.len(),
466 self.de(*outer)
467 );
468 for boundary in inner {
469 params.push_str(&format!(",{}", self.de(*boundary)));
470 }
471 self.push(Pending {
472 kind: 144,
473 form: 0,
474 transform: None,
475 independent: true,
476 label: label.chars().take(8).collect(),
477 params,
478 });
479 Ok(())
480 }
481
482 fn boundary_curve(&mut self, wire: &Shape) -> OgeomResult<Option<usize>> {
489 let edges = self.model.ordered_children_of(wire)?;
490 self.chained_curve(&edges)
491 }
492
493 fn wireframe(&mut self, shape: &Shape, label: &str) -> OgeomResult<()> {
495 let edges = if self.model.kind_of(shape)? == ShapeType::Edge {
496 vec![shape.clone()]
497 } else {
498 self.model.ordered_children_of(shape)?
499 };
500 let Some(curve) = self.chained_curve(&edges)? else {
501 ogeom_bail!(
502 Construction,
503 "a free wire whose every edge is degenerate cannot be written as IGES"
504 );
505 };
506 let entity = &mut self.entities[curve];
507 entity.independent = true;
508 entity.label = label.chars().take(8).collect();
509 Ok(())
510 }
511
512 fn free_point(&mut self, vertex: &Shape, label: &str) -> OgeomResult<()> {
515 let Some(data) = self.model.node(vertex).and_then(|n| n.data().as_vertex()) else {
516 ogeom_bail!(Construction, "vertex node holds no vertex data");
517 };
518 let at = vertex.transform(self.model.datums())?.apply(data.point);
519 let point = self.point_entity(at);
520 let entity = &mut self.entities[point];
521 entity.independent = true;
522 entity.label = label.chars().take(8).collect();
523 Ok(())
524 }
525
526 fn construction(&mut self, item: &ogeom_doc::Construction) -> OgeomResult<Option<String>> {
531 use ogeom_doc::ConstructionGeometry as G;
532 let entity = match &item.geometry {
533 G::Point(p) => self.point_entity(*p),
534 G::Axis(frame) => {
535 let entity = self.transform_entity(frame);
536 if frame.handedness() == ogeom_math::Handedness::Left {
537 self.entities[entity].form = 1;
538 }
539 entity
540 }
541 G::Curve { curve, range } => self.construction_curve(curve, *range)?,
542 G::Surface { surface, u, v } => self.construction_surface(surface, *u, *v)?,
543 };
544 if self.entities[entity].kind == 126 {
547 self.entities[entity].params.push_str(",0.,0.,0.");
548 }
549 let name = self.name_property(&item.name);
550 if let Some(name) = name {
551 let pointer = self.de(name);
552 self.entities[entity]
553 .params
554 .push_str(&format!(",0,1,{pointer}"));
555 }
556 let label: String = item
557 .name
558 .chars()
559 .filter(|c| c.is_ascii_graphic() || *c == ' ')
560 .take(8)
561 .collect();
562 let written = &mut self.entities[entity];
563 written.independent = true;
564 written.label.clone_from(&label);
565 Ok(name.is_none().then(|| {
566 format!(
567 "construction item '{}' keeps only its label '{}': a Name property \
568 carries plain ASCII with no comma or semicolon",
569 item.name,
570 label.trim_end()
571 )
572 }))
573 }
574
575 fn construction_curve(&mut self, curve: &Curve, range: (f64, f64)) -> OgeomResult<usize> {
579 let walk = |frame: Frame, domain: (f64, f64), reversed: bool| {
583 let (s, e) = if reversed {
584 (domain.0 + domain.1 - range.0, domain.0 + domain.1 - range.1)
585 } else {
586 range
587 };
588 if s <= e {
589 (frame, s, e)
590 } else {
591 (frame.with_z_reversed(), -s, -e)
592 }
593 };
594 let (form, frame, coefficients, ends) = match curve {
595 Curve::Hyperbola(h) => {
596 let hyperbola = h.hyperbola();
597 let (a, b) = (hyperbola.major_radius(), hyperbola.minor_radius());
598 let (frame, s, e) = walk(
599 hyperbola.frame(),
600 ogeom_geom::Curve3d::domain(h),
601 h.is_reversed(),
602 );
603 let at = |t: f64| (a * t.cosh(), b * t.sinh());
604 (
606 2,
607 frame,
608 [1.0 / (a * a), 0.0, -1.0 / (b * b), 0.0, 0.0, -1.0],
609 [at(s), at(e)],
610 )
611 }
612 Curve::Parabola(p) => {
613 let parabola = p.parabola();
614 let f = parabola.focal();
615 let (frame, s, e) = walk(
616 parabola.frame(),
617 ogeom_geom::Curve3d::domain(p),
618 p.is_reversed(),
619 );
620 let at = |t: f64| (t * t / (4.0 * f), t);
621 (
623 3,
624 frame,
625 [0.0, 0.0, 1.0, -4.0 * f, 0.0, 0.0],
626 [at(s), at(e)],
627 )
628 }
629 _ => return self.curve(curve, range, true),
630 };
631 let transform = self.transform_entity(&frame);
632 let mut params = coefficients.map(fmt).join(",");
633 let [(sx, sy), (ex, ey)] = ends;
634 params.push_str(&format!(
635 ",0.,{},{},{},{}",
636 fmt(sx),
637 fmt(sy),
638 fmt(ex),
639 fmt(ey)
640 ));
641 Ok(self.push(Pending {
642 kind: 104,
643 form,
644 transform: Some(transform),
645 independent: false,
646 label: String::new(),
647 params,
648 }))
649 }
650
651 fn construction_surface(
658 &mut self,
659 surface: &SurfaceGeometry,
660 u: (f64, f64),
661 v: (f64, f64),
662 ) -> OgeomResult<usize> {
663 let patch = if let SurfaceGeometry::Plane(_) = surface {
664 let corner = |s: f64, t: f64| surface.point_at(s, t, self.tol);
665 let grid = ogeom_math::ControlGrid::new(
666 vec![
667 corner(u.0, v.0)?,
668 corner(u.0, v.1)?,
669 corner(u.1, v.0)?,
670 corner(u.1, v.1)?,
671 ],
672 2,
673 2,
674 )?;
675 ogeom_geom::BSplineSurface::new(
676 ogeom_math::KnotVector::new(vec![u.0, u.0, u.1, u.1], 1)?,
677 ogeom_math::KnotVector::new(vec![v.0, v.0, v.1, v.1], 1)?,
678 &grid,
679 self.tol,
680 )?
681 } else {
682 let window = ogeom_geom::TrimmedSurface::new(surface.clone(), u, v, self.tol)?;
683 SurfaceGeometry::Trimmed(Box::new(window)).to_bspline(self.tol)?
684 };
685 let basis = self.nurbs_surface(&patch, false);
686 let params = format!("{},0,0,0", self.de(basis));
687 Ok(self.push(Pending {
688 kind: 144,
689 form: 0,
690 transform: None,
691 independent: false,
692 label: String::new(),
693 params,
694 }))
695 }
696
697 fn chained_curve(&mut self, edges: &[Shape]) -> OgeomResult<Option<usize>> {
700 let mut pieces = Vec::new();
701 for edge in edges {
702 let forward = edge.orientation() != ogeom_topo::Orientation::Reversed;
703 let (curve, range) = {
704 let Some(data) = self.model.node(edge).and_then(|n| n.data().as_edge()) else {
705 ogeom_bail!(Construction, "edge node holds no edge data");
706 };
707 if data.degenerate {
708 continue;
709 }
710 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
711 ogeom_bail!(Construction, "an edge with no curve cannot be written");
712 };
713 let Some(geometry) = self.model.geometry().curve(*curve) else {
714 ogeom_bail!(Dangling, "curve is not in this model");
715 };
716 let placement = edge.transform(self.model.datums())?;
717 (geometry.clone().transformed(&placement, self.tol)?, *range)
718 };
719 pieces.push(self.curve(&curve, range, forward)?);
720 }
721 match pieces.as_slice() {
722 [] => Ok(None),
723 [only] => Ok(Some(*only)),
724 _ => {
725 let mut params = format!("{}", pieces.len());
726 for piece in &pieces {
727 params.push_str(&format!(",{}", self.de(*piece)));
728 }
729 Ok(Some(self.push(Pending {
730 kind: 102,
731 form: 0,
732 transform: None,
733 independent: false,
734 label: String::new(),
735 params,
736 })))
737 }
738 }
739 }
740
741 fn turned_surface(&mut self, surface: &SurfaceGeometry) -> OgeomResult<usize> {
743 match surface {
744 SurfaceGeometry::Plane(p) => {
745 let frame = p.plane().frame();
746 let point = self.point_entity(frame.origin());
747 let normal = self.direction_entity(-frame.z());
748 let params = format!("{},{}", self.de(point), self.de(normal));
749 Ok(self.push(Pending {
750 kind: 190,
751 form: 0,
752 transform: None,
753 independent: false,
754 label: String::new(),
755 params,
756 }))
757 }
758 SurfaceGeometry::BSpline(b) => Ok(self.nurbs_surface(b, true)),
759 other => {
760 if let Some(entity) = self.swept_surface(other, false)? {
761 return Ok(entity);
762 }
763 let bspline = other.to_bspline(self.tol)?;
764 Ok(self.nurbs_surface(&bspline, true))
765 }
766 }
767 }
768
769 fn restates(&self, surface: &SurfaceGeometry) -> OgeomResult<bool> {
774 if let SurfaceGeometry::Offset(o) = surface {
775 if let Some(analytic) = o.analytic(self.tol)? {
776 return Ok(crate::bodies::restates(&analytic, false));
777 }
778 if matches!(
779 o.basis(),
780 SurfaceGeometry::Plane(_) | SurfaceGeometry::BSpline(_)
781 ) {
782 return Ok(crate::bodies::restates(o.basis(), false));
783 }
784 }
785 Ok(crate::bodies::restates(surface, false))
786 }
787
788 fn swept_surface(
795 &mut self,
796 surface: &SurfaceGeometry,
797 forward: bool,
798 ) -> OgeomResult<Option<usize>> {
799 use SurfaceGeometry as S;
800 let (kind, params) = match surface {
801 S::Revolution(r) => {
804 let ((a0, a1), (c0, c1)) = r.domain();
805 let axis = r.axis();
806 let (from, to) = (axis.location, axis.location + axis.direction.vector());
807 let line = self.push(Pending {
808 kind: 110,
809 form: 0,
810 transform: None,
811 independent: false,
812 label: String::new(),
813 params: format!(
814 "{},{},{},{},{},{}",
815 fmt(from.x),
816 fmt(from.y),
817 fmt(from.z),
818 fmt(to.x),
819 fmt(to.y),
820 fmt(to.z)
821 ),
822 });
823 let generatrix = self.curve(r.curve(), (c0, c1), forward)?;
824 (
825 120,
826 format!(
827 "{},{},{},{}",
828 self.de(line),
829 self.de(generatrix),
830 fmt(a0),
831 fmt(a1)
832 ),
833 )
834 }
835 S::Extrusion(e) => {
838 let ((u0, u1), (v0, v1)) = e.domain();
839 let along = e.direction().vector();
840 let directrix = e
841 .curve()
842 .transformed(&Transform::translation(along * v0), self.tol)?;
843 let start = directrix.point_at(if forward { u0 } else { u1 }, self.tol)?;
844 let far = start + along * (v1 - v0);
845 let curve = self.curve(&directrix, (u0, u1), forward)?;
846 (
847 122,
848 format!(
849 "{},{},{},{}",
850 self.de(curve),
851 fmt(far.x),
852 fmt(far.y),
853 fmt(far.z)
854 ),
855 )
856 }
857 S::Offset(o) => {
858 if let Some(analytic) = o.analytic(self.tol)? {
859 let entity = if forward {
860 self.surface(&analytic)?
861 } else {
862 self.turned_surface(&analytic)?
863 };
864 return Ok(Some(entity));
865 }
866 let basis = o.basis();
867 if !matches!(basis, S::Plane(_) | S::BSpline(_)) {
868 return Ok(None);
869 }
870 let ((ua, ub), (va, vb)) = basis.domain();
875 let normal = basis
876 .normal_at(f64::midpoint(ua, ub), f64::midpoint(va, vb), self.tol)?
877 .vector();
878 let (written, normal, distance) = if forward {
879 (self.surface(basis)?, normal, o.distance())
880 } else {
881 (self.turned_surface(basis)?, -normal, -o.distance())
882 };
883 let indicator = if distance < 0.0 { -normal } else { normal };
884 (
885 140,
886 format!(
887 "{},{},{},{},{}",
888 fmt(indicator.x),
889 fmt(indicator.y),
890 fmt(indicator.z),
891 fmt(distance.abs()),
892 self.de(written)
893 ),
894 )
895 }
896 _ => return Ok(None),
897 };
898 Ok(Some(self.push(Pending {
899 kind,
900 form: 0,
901 transform: None,
902 independent: false,
903 label: String::new(),
904 params,
905 })))
906 }
907
908 fn wire(&mut self, wire: &Shape) -> OgeomResult<Option<usize>> {
912 let children = self.model.ordered_children_of(wire)?;
913 let degenerate = |edge: &Shape| {
914 self.model
915 .node(edge)
916 .and_then(|n| n.data().as_edge())
917 .is_some_and(|d| d.degenerate)
918 };
919 let live: Vec<&Shape> = children.iter().filter(|e| !degenerate(e)).collect();
920 if live.is_empty() {
921 return Ok(None);
922 }
923 let mut entries = Vec::new();
924 for edge in live {
925 let index = self.edge(edge)?;
926 let flag = i32::from(edge.orientation() != ogeom_topo::Orientation::Reversed);
927 entries.push(format!("0,@E,{index},{flag},0"));
928 }
929 let params = format!("{},{}", entries.len(), entries.join(","));
930 Ok(Some(self.push(Pending {
931 kind: 508,
932 form: 1,
933 transform: None,
934 independent: false,
935 label: String::new(),
936 params,
937 })))
938 }
939
940 fn edge(&mut self, edge: &Shape) -> OgeomResult<usize> {
943 let placement = edge.transform(self.model.datums())?;
944 let key = (edge.node(), transform_bits(&placement));
945 if let Some(&index) = self.edges.get(&key) {
946 return Ok(index);
947 }
948 let (curve, range) = {
949 let Some(data) = self.model.node(edge).and_then(|n| n.data().as_edge()) else {
950 ogeom_bail!(Construction, "edge node holds no edge data");
951 };
952 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
953 ogeom_bail!(Construction, "an edge with no curve cannot be written");
954 };
955 let Some(geometry) = self.model.geometry().curve(*curve) else {
956 ogeom_bail!(Dangling, "curve is not in this model");
957 };
958 (geometry.clone().transformed(&placement, self.tol)?, *range)
959 };
960 let curve_entity = self.curve(&curve, range, true)?;
961 let vertices = self.model.children_of(edge)?;
962 let (from, to) = match vertices.len() {
963 0 => ogeom_bail!(Construction, "an edge with no vertices cannot be written"),
964 1 => (vertices[0].clone(), vertices[0].clone()),
965 _ => (vertices[0].clone(), vertices[vertices.len() - 1].clone()),
966 };
967 let sv = {
972 let at = from.transform(self.model.datums())?;
973 self.vertex(&from, &at)?
974 };
975 let tv = {
976 let at = to.transform(self.model.datums())?;
977 self.vertex(&to, &at)?
978 };
979 self.edge_records.push((curve_entity, sv, tv));
980 let index = self.edge_records.len();
981 self.edges.insert(key, index);
982 Ok(index)
983 }
984
985 fn vertex(&mut self, vertex: &Shape, placement: &Transform) -> OgeomResult<usize> {
987 let Some(data) = self.model.node(vertex).and_then(|n| n.data().as_vertex()) else {
988 ogeom_bail!(Construction, "vertex node holds no vertex data");
989 };
990 let at = placement.apply(data.point);
991 let key = (
992 vertex.node(),
993 [at.x.to_bits(), at.y.to_bits(), at.z.to_bits()],
994 );
995 if let Some(&index) = self.vertices.get(&key) {
996 return Ok(index);
997 }
998 self.vertex_coords.push(at);
999 let index = self.vertex_coords.len();
1000 self.vertices.insert(key, index);
1001 Ok(index)
1002 }
1003
1004 fn curve(&mut self, curve: &Curve, range: (f64, f64), forward: bool) -> OgeomResult<usize> {
1007 let walk = |reversed: bool| {
1009 let angle = |u: f64| if reversed { -u } else { u };
1010 let (s, e) = if forward { range } else { (range.1, range.0) };
1011 let counter_clockwise = reversed != forward;
1015 let sign = if counter_clockwise { 1.0 } else { -1.0 };
1016 (sign * angle(s), sign * angle(e), counter_clockwise)
1017 };
1018 match curve {
1019 Curve::Line(line) => {
1020 let (from, to) = if forward { range } else { (range.1, range.0) };
1021 let a = line.point_at(from, self.tol)?;
1022 let b = line.point_at(to, self.tol)?;
1023 Ok(self.push(Pending {
1024 kind: 110,
1025 form: 0,
1026 transform: None,
1027 independent: false,
1028 label: String::new(),
1029 params: format!(
1030 "{},{},{},{},{},{}",
1031 fmt(a.x),
1032 fmt(a.y),
1033 fmt(a.z),
1034 fmt(b.x),
1035 fmt(b.y),
1036 fmt(b.z)
1037 ),
1038 }))
1039 }
1040 Curve::Circle(c) => {
1041 let circle = c.circle();
1042 let r = circle.radius();
1043 let (s, e, counter_clockwise) = walk(c.is_reversed());
1044 let frame = if counter_clockwise {
1045 circle.frame()
1046 } else {
1047 circle.frame().with_z_reversed()
1048 };
1049 let transform = self.transform_entity(&frame);
1050 let params = format!(
1051 "0.,0.,0.,{},{},{},{}",
1052 fmt(r * s.cos()),
1053 fmt(r * s.sin()),
1054 fmt(r * e.cos()),
1055 fmt(r * e.sin()),
1056 );
1057 Ok(self.push(Pending {
1058 kind: 100,
1059 form: 0,
1060 transform: Some(transform),
1061 independent: false,
1062 label: String::new(),
1063 params,
1064 }))
1065 }
1066 Curve::Ellipse(el) => {
1067 let ellipse = el.ellipse();
1068 let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
1069 let (s, e, counter_clockwise) = walk(el.is_reversed());
1070 let frame = if counter_clockwise {
1071 ellipse.frame()
1072 } else {
1073 ellipse.frame().with_z_reversed()
1074 };
1075 let transform = self.transform_entity(&frame);
1076 let at = |t: f64| (a * t.cos(), b * t.sin());
1077 let (sx, sy) = at(s);
1078 let (ex, ey) = at(e);
1079 let params = format!(
1081 "{},0.,{},0.,0.,-1.,0.,{},{},{},{}",
1082 fmt(1.0 / (a * a)),
1083 fmt(1.0 / (b * b)),
1084 fmt(sx),
1085 fmt(sy),
1086 fmt(ex),
1087 fmt(ey),
1088 );
1089 Ok(self.push(Pending {
1090 kind: 104,
1091 form: 1,
1092 transform: Some(transform),
1093 independent: false,
1094 label: String::new(),
1095 params,
1096 }))
1097 }
1098 Curve::Trimmed(t) if t.is_reversed() => {
1101 let (lo, hi) = ogeom_geom::Curve3d::domain(curve);
1102 self.curve(t.basis(), (lo + hi - range.1, lo + hi - range.0), !forward)
1103 }
1104 Curve::Trimmed(t) => self.curve(t.basis(), range, forward),
1105 Curve::BSpline(b) if forward => self.nurbs_curve(b, range),
1106 Curve::BSpline(b) => {
1107 let (lo, hi) = ogeom_geom::Curve3d::domain(b);
1109 let back = reversed_bspline(b)?;
1110 self.nurbs_curve(&back, (lo + hi - range.1, lo + hi - range.0))
1111 }
1112 other => {
1113 let mut bspline = other.to_bspline_over(range, self.tol)?;
1116 if !forward {
1117 bspline = reversed_bspline(&bspline)?;
1118 }
1119 self.nurbs_curve(&bspline, ogeom_geom::Curve3d::domain(&bspline))
1120 }
1121 }
1122 }
1123
1124 fn nurbs_curve(&mut self, curve: &BSplineCurve, range: (f64, f64)) -> OgeomResult<usize> {
1125 let knots = curve.knots();
1126 let control = curve.control_points();
1127 let degree = knots.degree();
1128 let k = control.len() - 1;
1129 let rational = curve.is_rational();
1130 let closed = {
1131 let (lo, hi) = ogeom_geom::Curve3d::domain(curve);
1132 let a = curve.point_at(lo, self.tol)?;
1133 let b = curve.point_at(hi, self.tol)?;
1134 i32::from(a.distance(b) < self.tol.confusion())
1135 };
1136 let mut params = format!("{k},{degree},0,{closed},{},0", i32::from(!rational));
1137 for t in knots.knots() {
1138 params.push_str(&format!(",{}", fmt(*t)));
1139 }
1140 for w in control {
1141 params.push_str(&format!(",{}", fmt(w.weight)));
1142 }
1143 for w in control {
1144 let p = (*w).point();
1145 params.push_str(&format!(",{},{},{}", fmt(p.x), fmt(p.y), fmt(p.z)));
1146 }
1147 params.push_str(&format!(",{},{}", fmt(range.0), fmt(range.1)));
1148 Ok(self.push(Pending {
1149 kind: 126,
1150 form: 0,
1151 transform: None,
1152 independent: false,
1153 label: String::new(),
1154 params,
1155 }))
1156 }
1157
1158 fn transform_entity(&mut self, frame: &Frame) -> usize {
1160 let (x, y, z) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
1161 let o = frame.origin();
1162 let params = format!(
1163 "{},{},{},{},{},{},{},{},{},{},{},{}",
1164 fmt(x.x),
1165 fmt(y.x),
1166 fmt(z.x),
1167 fmt(o.x),
1168 fmt(x.y),
1169 fmt(y.y),
1170 fmt(z.y),
1171 fmt(o.y),
1172 fmt(x.z),
1173 fmt(y.z),
1174 fmt(z.z),
1175 fmt(o.z),
1176 );
1177 self.push(Pending {
1178 kind: 124,
1179 form: 0,
1180 transform: None,
1181 independent: false,
1182 label: String::new(),
1183 params,
1184 })
1185 }
1186
1187 fn point_entity(&mut self, p: Point) -> usize {
1189 self.push(Pending {
1190 kind: 116,
1191 form: 0,
1192 transform: None,
1193 independent: false,
1194 label: String::new(),
1195 params: format!("{},{},{},0", fmt(p.x), fmt(p.y), fmt(p.z)),
1196 })
1197 }
1198
1199 fn direction_entity(&mut self, d: ogeom_math::Direction) -> usize {
1201 let v = d.vector();
1202 self.push(Pending {
1203 kind: 123,
1204 form: 0,
1205 transform: None,
1206 independent: false,
1207 label: String::new(),
1208 params: format!("{},{},{}", fmt(v.x), fmt(v.y), fmt(v.z)),
1209 })
1210 }
1211
1212 fn surface(&mut self, surface: &SurfaceGeometry) -> OgeomResult<usize> {
1214 use SurfaceGeometry as S;
1215 match surface {
1216 S::Plane(p) => {
1217 let frame = p.plane().frame();
1218 let point = self.point_entity(frame.origin());
1219 let normal = self.direction_entity(frame.z());
1220 let params = format!("{},{}", self.de(point), self.de(normal));
1221 Ok(self.push(Pending {
1222 kind: 190,
1223 form: 0,
1224 transform: None,
1225 independent: false,
1226 label: String::new(),
1227 params,
1228 }))
1229 }
1230 S::Cylinder(c) => {
1231 let cyl = c.cylinder();
1232 let frame = cyl.frame();
1233 let point = self.point_entity(frame.origin());
1234 let axis = self.direction_entity(frame.z());
1235 let params = format!("{},{},{}", self.de(point), self.de(axis), fmt(cyl.radius()));
1236 Ok(self.push(Pending {
1237 kind: 192,
1238 form: 0,
1239 transform: None,
1240 independent: false,
1241 label: String::new(),
1242 params,
1243 }))
1244 }
1245 S::Cone(c) => {
1246 let cone = c.cone();
1247 let frame = cone.frame();
1248 let point = self.point_entity(frame.origin());
1249 let axis = self.direction_entity(frame.z());
1250 let params = format!(
1251 "{},{},{},{}",
1252 self.de(point),
1253 self.de(axis),
1254 fmt(cone.reference_radius()),
1255 fmt(cone.half_angle().to_degrees()),
1256 );
1257 Ok(self.push(Pending {
1258 kind: 194,
1259 form: 0,
1260 transform: None,
1261 independent: false,
1262 label: String::new(),
1263 params,
1264 }))
1265 }
1266 S::Sphere(s) => {
1270 let sphere = s.sphere();
1271 let frame = sphere.frame();
1272 let point = self.point_entity(sphere.centre());
1273 let axis = self.direction_entity(frame.z());
1274 let reference = self.direction_entity(frame.x());
1275 let params = format!(
1276 "{},{},{},{}",
1277 self.de(point),
1278 fmt(sphere.radius()),
1279 self.de(axis),
1280 self.de(reference)
1281 );
1282 Ok(self.push(Pending {
1283 kind: 196,
1284 form: 1,
1285 transform: None,
1286 independent: false,
1287 label: String::new(),
1288 params,
1289 }))
1290 }
1291 S::Torus(t) => {
1292 let torus = t.torus();
1293 let frame = torus.frame();
1294 let point = self.point_entity(frame.origin());
1295 let axis = self.direction_entity(frame.z());
1296 let params = format!(
1297 "{},{},{},{}",
1298 self.de(point),
1299 self.de(axis),
1300 fmt(torus.major_radius()),
1301 fmt(torus.minor_radius()),
1302 );
1303 Ok(self.push(Pending {
1304 kind: 198,
1305 form: 0,
1306 transform: None,
1307 independent: false,
1308 label: String::new(),
1309 params,
1310 }))
1311 }
1312 S::Revolution(_) | S::Extrusion(_) | S::Offset(_) => {
1313 match self.swept_surface(surface, true)? {
1314 Some(entity) => Ok(entity),
1315 None => {
1316 let bspline = surface.to_bspline(self.tol)?;
1317 self.surface(&SurfaceGeometry::BSpline(bspline))
1318 }
1319 }
1320 }
1321 S::BSpline(b) => Ok(self.nurbs_surface(b, false)),
1322 other => {
1323 let bspline = other.to_bspline(self.tol)?;
1327 self.surface(&SurfaceGeometry::BSpline(bspline))
1328 }
1329 }
1330 }
1331
1332 fn nurbs_surface(&mut self, b: &ogeom_geom::BSplineSurface, exchanged: bool) -> usize {
1335 let (mut uk, mut vk) = (b.u_knots(), b.v_knots());
1336 let grid = b.grid();
1337 let (mut nu, mut nv) = (grid.u_count(), grid.v_count());
1338 let ((mut u0, mut u1), (mut v0, mut v1)) = b.domain();
1339 if exchanged {
1340 std::mem::swap(&mut uk, &mut vk);
1341 std::mem::swap(&mut nu, &mut nv);
1342 std::mem::swap(&mut u0, &mut v0);
1343 std::mem::swap(&mut u1, &mut v1);
1344 }
1345 let at = |u: usize, v: usize| {
1347 if exchanged {
1348 grid.get(v, u)
1349 } else {
1350 grid.get(u, v)
1351 }
1352 };
1353 let (k1, k2) = (nu - 1, nv - 1);
1354 let (m1, m2) = (uk.degree(), vk.degree());
1355 let mut params = format!(
1356 "{k1},{k2},{m1},{m2},0,0,{},0,0",
1357 i32::from(!b.is_rational())
1358 );
1359 for t in uk.knots() {
1360 params.push_str(&format!(",{}", fmt(*t)));
1361 }
1362 for t in vk.knots() {
1363 params.push_str(&format!(",{}", fmt(*t)));
1364 }
1365 for v in 0..nv {
1367 for u in 0..nu {
1368 let w = at(u, v).map_or(1.0, |w| w.weight);
1369 params.push_str(&format!(",{}", fmt(w)));
1370 }
1371 }
1372 for v in 0..nv {
1373 for u in 0..nu {
1374 let p = at(u, v).map_or(Point::ORIGIN, ogeom_math::Weighted::point);
1375 params.push_str(&format!(",{},{},{}", fmt(p.x), fmt(p.y), fmt(p.z)));
1376 }
1377 }
1378 params.push_str(&format!(",{},{},{},{}", fmt(u0), fmt(u1), fmt(v0), fmt(v1)));
1379 self.push(Pending {
1380 kind: 128,
1381 form: 0,
1382 transform: None,
1383 independent: false,
1384 label: String::new(),
1385 params,
1386 })
1387 }
1388
1389 fn serialize(&self) -> String {
1391 let mut s = String::new();
1392 fn push_record(s: &mut String, body: &str, section: char, seq: usize) {
1393 s.push_str(&format!("{body:<72}{section}{seq:>7}\n"));
1394 }
1395 push_record(&mut s, "ogeom IGES writer", 'S', 1);
1396
1397 let mut param_lines: Vec<Vec<String>> = Vec::with_capacity(self.entities.len());
1400 for e in &self.entities {
1401 let full = format!("{},{};", e.kind, e.params);
1402 param_lines.push(wrap_params(&full));
1403 }
1404 let mut param_starts = Vec::with_capacity(self.entities.len());
1405 let mut next_param = 1usize;
1406 for lines in ¶m_lines {
1407 param_starts.push(next_param);
1408 next_param += lines.len();
1409 }
1410
1411 let globals = [
1412 "1H,".to_string(),
1413 "1H;".to_string(),
1414 "5Hogeom".to_string(),
1415 "9Hmodel.igs".to_string(),
1416 "5Hogeom".to_string(),
1417 "5Hogeom".to_string(),
1418 "32".to_string(),
1419 "308".to_string(),
1420 "15".to_string(),
1421 "308".to_string(),
1422 "15".to_string(),
1423 "5Hogeom".to_string(),
1424 "1.".to_string(),
1425 "2".to_string(),
1427 "2HMM".to_string(),
1428 "1".to_string(),
1429 "0.01".to_string(),
1430 "15H20260807.000000".to_string(),
1431 fmt(1e-7),
1432 "0.".to_string(),
1433 "5Hogeom".to_string(),
1434 "5Hogeom".to_string(),
1435 "11".to_string(),
1436 "0".to_string(),
1437 "15H20260807.000000".to_string(),
1438 ];
1439 let global_text = globals.join(",") + ";";
1440 let global_lines = wrap_params(&global_text);
1441 for (i, line) in global_lines.iter().enumerate() {
1442 push_record(&mut s, line, 'G', i + 1);
1443 }
1444
1445 let mut d_seq = 1usize;
1446 for (i, e) in self.entities.iter().enumerate() {
1447 let transform_de = e.transform.map_or(0, |t| self.de(t));
1448 let subordinate = if e.independent { "00" } else { "01" };
1449 let use_flag = if i >= self.construction_from {
1450 "06"
1451 } else {
1452 "00"
1453 };
1454 let status = format!("00{subordinate}{use_flag}00");
1455 let line1 = format!(
1456 "{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{status}",
1457 e.kind, param_starts[i], 0, 0, 0, 0, transform_de, 0
1458 );
1459 push_record(&mut s, &line1, 'D', d_seq);
1460 let line2 = format!(
1461 "{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}",
1462 e.kind,
1463 0,
1464 0,
1465 param_lines[i].len(),
1466 e.form,
1467 "",
1468 "",
1469 e.label,
1470 0
1471 );
1472 push_record(&mut s, &line2, 'D', d_seq + 1);
1473 d_seq += 2;
1474 }
1475
1476 let mut p_seq = 1usize;
1477 for (i, lines) in param_lines.iter().enumerate() {
1478 let back = self.de(i);
1479 for line in lines {
1480 s.push_str(&format!("{line:<64}{back:>8}P{p_seq:>7}\n"));
1481 p_seq += 1;
1482 }
1483 }
1484 let tail = format!(
1485 "S{:>7}G{:>7}D{:>7}P{:>7}",
1486 1,
1487 global_lines.len(),
1488 d_seq - 1,
1489 p_seq - 1
1490 );
1491 push_record(&mut s, &tail, 'T', 1);
1492 s
1493 }
1494}
1495
1496fn reversed_bspline(b: &BSplineCurve) -> OgeomResult<BSplineCurve> {
1498 match Curve::BSpline(b.clone()).reversed() {
1499 Curve::BSpline(back) => Ok(back),
1500 _ => ogeom_bail!(Construction, "a reversed B-spline is a B-spline"),
1501 }
1502}
1503
1504fn transform_bits(t: &Transform) -> [u64; 3] {
1507 let p = t.apply(Point::new(0.123_456_789, 9.87, -3.21));
1508 [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
1509}
1510
1511fn fmt(v: f64) -> String {
1518 let positional = {
1519 let s = format!("{v}");
1520 if s.contains('.') { s } else { format!("{s}.") }
1521 };
1522 let exponent = {
1523 let s = format!("{v:E}");
1524 match s.split_once('E') {
1525 Some((mantissa, power)) if !mantissa.contains('.') => {
1526 format!("{mantissa}.E{power}")
1527 }
1528 _ => s,
1529 }
1530 };
1531 if exponent.len() < positional.len() {
1532 exponent
1533 } else {
1534 positional
1535 }
1536}
1537
1538fn wrap_params(text: &str) -> Vec<String> {
1548 const COLUMNS: usize = 64;
1549 let mut lines = Vec::new();
1550 let mut current = String::new();
1551 for piece in text.split_inclusive(',') {
1552 if current.len() + piece.len() > COLUMNS && piece.len() <= COLUMNS {
1553 lines.push(std::mem::take(&mut current));
1554 }
1555 let mut rest = piece;
1556 while current.len() + rest.len() > COLUMNS {
1557 let mut room = COLUMNS - current.len();
1558 while !rest.is_char_boundary(room) {
1559 room -= 1;
1560 }
1561 current.push_str(&rest[..room]);
1562 rest = &rest[room..];
1563 lines.push(std::mem::take(&mut current));
1564 }
1565 current.push_str(rest);
1566 }
1567 if !current.is_empty() {
1568 lines.push(current);
1569 }
1570 lines
1571}
1572
1573#[cfg(test)]
1574#[allow(clippy::unwrap_used, reason = "test code")]
1575mod tests {
1576 use super::{fmt, wrap_params};
1577
1578 #[test]
1579 fn a_real_takes_its_shorter_spelling_and_reads_back() {
1580 for v in [
1581 0.0,
1582 1.0,
1583 -2.5,
1584 1e-51,
1585 1.48776941870366e-51,
1586 2.76e-76,
1587 1e20,
1588 123_456.789,
1589 ] {
1590 let s = fmt(v);
1591 assert!(s.len() <= 24, "{v} spelt {s}");
1592 assert!(s.contains('.'), "{s} reads as a real");
1593 let back: f64 = s.replace('D', "E").parse().unwrap();
1594 assert_eq!(back, v, "{s}");
1595 }
1596 assert_eq!(fmt(1.0), "1.");
1597 assert_eq!(fmt(1e-51), "1.E-51");
1598 }
1599
1600 #[test]
1601 fn a_parameter_longer_than_a_record_fills_the_records_it_crosses() {
1602 let name = format!("100H{},", "x".repeat(100));
1603 let text = format!("128,{name}1.5,2.;");
1604 let lines = wrap_params(&text);
1605 assert!(lines.iter().all(|l| l.len() <= 64));
1606 let long: Vec<&String> = lines.iter().filter(|l| l.contains('x')).collect();
1609 for l in &long[..long.len() - 1] {
1610 assert_eq!(l.len(), 64);
1611 }
1612 assert_eq!(lines.concat(), text);
1613 }
1614}