1use crate::analytic_surface::{circumcenter, AnalyticSurface};
2use crate::topology::{BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, VertexRecord};
3use crate::{make_arc, KernelTolerances, NurbsCurve, NurbsSurface, Vec3};
4use rustc_hash::FxHashMap as HashMap;
5
6#[path = "step/pcurve.rs"]
7mod pcurve;
8use pcurve::{build_pcurve, EmittedCurve, EmittedFrame, EmittedSurface, Pcurve2d};
9#[path = "step/pmi.rs"]
10pub(crate) mod pmi;
11pub use pmi::StepPmi;
12#[path = "step/assembly.rs"]
13pub mod assembly;
14pub use assembly::{
15 export_step_assembly, export_step_assembly_report, StepAssemblyExport, StepExportOccurrence,
16 StepExportProduct,
17};
18#[path = "step/export_tree.rs"]
19mod export_tree;
20pub use export_tree::assembly_export_tree;
21
22pub use crate::step_matrix::Mat4;
23pub(crate) use crate::step_matrix::{mat4_mul, MAT4_IDENTITY};
24
25pub(crate) fn transform_point(matrix: &Mat4, point: Vec3) -> Vec3 {
26 crate::AffineTransform {
27 elements: *matrix,
28 }
29 .point(point)
30}
31
32pub(crate) fn step_string(value: &str) -> String {
33 value.replace('\'', "''")
34}
35
36pub(crate) fn real(value: f64) -> Result<String, String> {
37 if !value.is_finite() {
38 return Err(format!("export_step: non-finite number {value}"));
39 }
40 if value == 0.0 {
43 return Ok("0.".into());
44 }
45 if value.fract() == 0.0 && value.abs() < 1e15 {
46 return Ok(format!("{value:.0}."));
47 }
48 let mut output = format!("{value:.15}");
49 while output.ends_with('0') {
50 output.pop();
51 }
52 if output.ends_with('.') {
53 output.push('0');
54 }
55 if output == "-0.0" {
56 output = "0.0".into();
57 }
58 Ok(output)
59}
60
61fn knot_runs(knots: &[f64]) -> (Vec<f64>, Vec<usize>) {
62 let mut values = Vec::new();
63 let mut multiplicities = Vec::new();
64 for &knot in knots {
65 if values
66 .last()
67 .is_some_and(|previous: &f64| (*previous - knot).abs() <= 1e-12)
68 {
69 *multiplicities.last_mut().unwrap() += 1;
70 } else {
71 values.push(knot);
72 multiplicities.push(1);
73 }
74 }
75 (values, multiplicities)
76}
77
78pub(crate) fn edge_subcurve(edge: &EdgeRecord) -> Result<NurbsCurve, String> {
79 let [start, end] = edge.curve.domain()?;
80 let epsilon = (1e-9 * (end - start)).max(2e-9);
81 let mut curve = edge.curve.clone();
82 if edge.t0 > start + epsilon && edge.t0 < end - epsilon {
83 curve = curve.split(edge.t0)?.1;
84 }
85 let domain = curve.domain()?;
86 if edge.t1 < domain[1] - epsilon && edge.t1 > domain[0] + epsilon {
87 curve = curve.split(edge.t1)?.0;
88 }
89 Ok(curve)
90}
91
92#[derive(Default)]
93pub(crate) struct StepWriter {
94 lines: Vec<String>,
95}
96
97impl StepWriter {
98 pub(crate) fn add(&mut self, body: impl Into<String>) -> usize {
99 let id = self.lines.len() + 1;
100 self.lines.push(format!("#{id}={};", body.into()));
101 id
102 }
103
104 fn data(&self) -> String {
105 self.lines.join("\n")
106 }
107}
108
109pub(crate) fn write_point(writer: &mut StepWriter, point: Vec3) -> Result<usize, String> {
110 Ok(writer.add(format!(
111 "CARTESIAN_POINT('',({},{},{}))",
112 real(point.x)?,
113 real(point.y)?,
114 real(point.z)?
115 )))
116}
117
118pub(crate) fn id_list(ids: &[usize]) -> String {
119 format!(
120 "({})",
121 ids.iter()
122 .map(|id| format!("#{id}"))
123 .collect::<Vec<_>>()
124 .join(",")
125 )
126}
127
128pub(crate) fn write_direction(writer: &mut StepWriter, direction: Vec3) -> Result<usize, String> {
129 Ok(writer.add(format!(
130 "DIRECTION('',({},{},{}))",
131 real(direction.x)?,
132 real(direction.y)?,
133 real(direction.z)?
134 )))
135}
136
137pub(crate) fn write_placement(
138 writer: &mut StepWriter,
139 origin: Vec3,
140 axis: Vec3,
141 ref_direction: Vec3,
142) -> Result<usize, String> {
143 let origin = write_point(writer, origin)?;
144 let axis = write_direction(writer, axis)?;
145 let ref_direction = write_direction(writer, ref_direction)?;
146 Ok(writer.add(format!(
147 "AXIS2_PLACEMENT_3D('',#{origin},#{axis},#{ref_direction})"
148 )))
149}
150
151fn write_analytic_surface(
160 writer: &mut StepWriter,
161 surface: &NurbsSurface,
162) -> Result<Option<(usize, bool, EmittedSurface)>, String> {
163 let Some(analytic) = surface.analytic() else {
164 return Ok(None);
165 };
166 match analytic {
167 AnalyticSurface::Plane {
168 origin,
169 u_dir,
170 v_dir,
171 ..
172 } => {
173 let (Ok(normal), Ok(x_axis)) = (u_dir.cross(*v_dir).normalized(), u_dir.normalized())
176 else {
177 return Ok(None);
178 };
179 let placement = write_placement(writer, *origin, normal, x_axis)?;
180 Ok(Some((
185 writer.add(format!("PLANE('',#{placement})")),
186 false,
187 EmittedSurface::Plane {
188 origin: *origin,
189 x_axis,
190 y_axis: normal.cross(x_axis),
191 },
192 )))
193 }
194 AnalyticSurface::RuledRevolution {
195 frame,
196 rho0,
197 rho1,
198 height,
199 } => {
200 let flipped = *height < 0.0;
204 let radius_scale = 1.0 + rho0.abs().max(rho1.abs());
205 if (rho1 - rho0).abs() <= 1e-9 * radius_scale {
206 if *rho0 <= 0.0 {
207 return Ok(None);
208 }
209 let placement = write_placement(writer, frame.origin, frame.axis, frame.x_axis)?;
210 return Ok(Some((
211 writer.add(format!(
212 "CYLINDRICAL_SURFACE('',#{placement},{})",
213 real(*rho0)?
214 )),
215 flipped,
216 EmittedSurface::Cylinder {
217 frame: EmittedFrame {
218 origin: frame.origin,
219 x_axis: frame.x_axis,
220 y_axis: frame.y_axis,
221 axis: frame.axis,
222 azimuth_sign: 1.0,
223 },
224 radius: *rho0,
225 },
226 )));
227 }
228 let slope = (rho1 - rho0) / height;
234 let apex_margin = 2.0 * (1e-4 * height.abs().max(1.0) + 1e-9) * slope.abs();
235 if rho0.min(*rho1) <= apex_margin {
236 return Ok(None);
237 }
238 let axis = if slope >= 0.0 {
242 frame.axis
243 } else {
244 frame.axis.scale(-1.0)
245 };
246 let placement = write_placement(writer, frame.origin, axis, frame.x_axis)?;
247 Ok(Some((
248 writer.add(format!(
249 "CONICAL_SURFACE('',#{placement},{},{})",
250 real(*rho0)?,
251 real(slope.abs().atan())?
252 )),
253 flipped,
254 EmittedSurface::Cone {
255 frame: EmittedFrame {
256 origin: frame.origin,
257 x_axis: frame.x_axis,
258 y_axis: axis.cross(frame.x_axis),
262 axis,
263 azimuth_sign: if slope >= 0.0 { 1.0 } else { -1.0 },
264 },
265 radius: *rho0,
266 semi_angle: slope.abs().atan(),
267 },
268 )))
269 }
270 AnalyticSurface::Sphere { frame, radius } => {
271 let placement = write_placement(writer, frame.origin, frame.axis, frame.x_axis)?;
274 Ok(Some((
275 writer.add(format!(
276 "SPHERICAL_SURFACE('',#{placement},{})",
277 real(*radius)?
278 )),
279 false,
280 EmittedSurface::Sphere {
281 frame: EmittedFrame {
282 origin: frame.origin,
283 x_axis: frame.x_axis,
284 y_axis: frame.y_axis,
285 axis: frame.axis,
286 azimuth_sign: 1.0,
287 },
288 radius: *radius,
289 },
290 )))
291 }
292 AnalyticSurface::Torus {
293 frame,
294 major_radius,
295 minor_radius,
296 } => {
297 let placement = write_placement(writer, frame.origin, frame.axis, frame.x_axis)?;
298 Ok(Some((
299 writer.add(format!(
300 "TOROIDAL_SURFACE('',#{placement},{},{})",
301 real(*major_radius)?,
302 real(*minor_radius)?
303 )),
304 false,
305 EmittedSurface::Torus {
306 frame: EmittedFrame {
307 origin: frame.origin,
308 x_axis: frame.x_axis,
309 y_axis: frame.y_axis,
310 axis: frame.axis,
311 azimuth_sign: 1.0,
312 },
313 major_radius: *major_radius,
314 minor_radius: *minor_radius,
315 },
316 )))
317 }
318 AnalyticSurface::Revolution { .. } => Ok(None),
321 }
322}
323
324struct CircularArc {
329 center: Vec3,
330 axis: Vec3,
331 x_axis: Vec3,
332 y_axis: Vec3,
335 radius: f64,
336 sweep: f64,
338 spans: usize,
342}
343
344fn curve_scale(curve: &NurbsCurve) -> f64 {
345 curve
346 .control_points
347 .iter()
348 .map(|p| (p.x.abs() / p.w).max(p.y.abs() / p.w).max(p.z.abs() / p.w))
349 .fold(0.0, f64::max)
350}
351
352fn curves_match(a: &NurbsCurve, b: &NurbsCurve, scale: f64) -> bool {
356 if a.degree != b.degree
357 || a.knots.len() != b.knots.len()
358 || a.control_points.len() != b.control_points.len()
359 {
360 return false;
361 }
362 if a.knots
363 .iter()
364 .zip(&b.knots)
365 .any(|(x, y)| (x - y).abs() > 1e-12)
366 {
367 return false;
368 }
369 let tolerance = 1e-9 * scale.max(1.0);
370 a.control_points
371 .iter()
372 .zip(&b.control_points)
373 .all(|(p, q)| {
374 (p.x - q.x).abs() <= tolerance
375 && (p.y - q.y).abs() <= tolerance
376 && (p.z - q.z).abs() <= tolerance
377 && (p.w - q.w).abs() <= 1e-9
378 })
379}
380
381fn recognize_circular_arc(curve: &NurbsCurve) -> Option<CircularArc> {
386 if curve.degree != 2
387 || curve.control_points.len() < 3
388 || curve.control_points.len() % 2 == 0
389 || (curve.control_points.len() - 1) / 2 > 4
390 {
391 return None;
392 }
393 let [t0, t1] = curve.domain().ok()?;
394 let at = |fraction: f64| curve.evaluate(t0 + (t1 - t0) * fraction);
395 let p0 = at(0.0).ok()?;
398 let pa = at(0.35).ok()?;
399 let pb = at(0.7).ok()?;
400 let center = circumcenter(p0, pa, pb)?;
401 let radial = p0.sub(center);
402 let radius = radial.length();
403 let scale = curve_scale(curve);
404 if radius <= 1e-9 * scale.max(1.0) {
405 return None;
406 }
407 let x_axis = radial.scale(1.0 / radius);
408 let axis = radial.cross(pa.sub(center)).normalized().ok()?;
409 let y_axis = axis.cross(x_axis);
410 let p_end = at(1.0).ok()?;
411 let sweep = if p_end.sub(p0).length() <= 1e-9 * (1.0 + radius) {
412 std::f64::consts::TAU
413 } else {
414 let closing = p_end.sub(center);
415 let mut angle = closing.dot(y_axis).atan2(closing.dot(x_axis));
416 if angle < 0.0 {
417 angle += std::f64::consts::TAU;
418 }
419 angle
420 };
421 let rebuilt = make_arc(center, x_axis, y_axis, radius, 0.0, sweep).ok()?;
422 curves_match(curve, &rebuilt, scale).then_some(CircularArc {
423 center,
424 axis,
425 x_axis,
426 y_axis,
427 radius,
428 sweep,
429 spans: (curve.control_points.len() - 1) / 2,
430 })
431}
432
433fn write_analytic_curve(
440 writer: &mut StepWriter,
441 curve: &NurbsCurve,
442) -> Result<Option<(usize, EmittedCurve)>, String> {
443 if curve.degree == 1
444 && curve.control_points.len() == 2
445 && curve
446 .control_points
447 .iter()
448 .all(|control| (control.w - 1.0).abs() <= 1e-12)
449 {
450 let start = curve.control_points[0].point()?;
451 let end = curve.control_points[1].point()?;
452 let Ok(direction) = end.sub(start).normalized() else {
453 return Ok(None);
454 };
455 let point = write_point(writer, start)?;
456 let step_direction = write_direction(writer, direction)?;
457 let vector = writer.add(format!(
458 "VECTOR('',#{step_direction},{})",
459 real(end.sub(start).length())?
460 ));
461 return Ok(Some((
462 writer.add(format!("LINE('',#{point},#{vector})")),
463 EmittedCurve::Line { start, end },
467 )));
468 }
469 if let Some(arc) = recognize_circular_arc(curve) {
470 let placement = write_placement(writer, arc.center, arc.axis, arc.x_axis)?;
474 return Ok(Some((
475 writer.add(format!("CIRCLE('',#{placement},{})", real(arc.radius)?)),
476 EmittedCurve::Circle {
477 center: arc.center,
478 x_axis: arc.x_axis,
479 y_axis: arc.y_axis,
480 radius: arc.radius,
481 sweep: arc.sweep,
482 spans: arc.spans,
483 },
484 )));
485 }
486 Ok(None)
487}
488
489fn write_curve(writer: &mut StepWriter, curve: &NurbsCurve) -> Result<usize, String> {
490 let points = curve
491 .control_points
492 .iter()
493 .map(|control| write_point(writer, control.point()?))
494 .collect::<Result<Vec<_>, _>>()?;
495 write_bspline_curve(writer, curve, &points)
496}
497
498fn write_bspline_curve(
503 writer: &mut StepWriter,
504 curve: &NurbsCurve,
505 points: &[usize],
506) -> Result<usize, String> {
507 let (knot_values, multiplicities) = knot_runs(&curve.knots);
508 let multiplicities = format!(
509 "({})",
510 multiplicities
511 .iter()
512 .map(usize::to_string)
513 .collect::<Vec<_>>()
514 .join(",")
515 );
516 let knots = format!(
517 "({})",
518 knot_values
519 .iter()
520 .map(|value| real(*value))
521 .collect::<Result<Vec<_>, _>>()?
522 .join(",")
523 );
524 let rational = curve
525 .control_points
526 .iter()
527 .any(|control| (control.w - 1.0).abs() > 1e-12);
528 if !rational {
529 return Ok(writer.add(format!(
530 "B_SPLINE_CURVE_WITH_KNOTS('',{},{},.UNSPECIFIED.,.F.,.F.,{multiplicities},{knots},.UNSPECIFIED.)",
531 curve.degree,
532 id_list(points),
533 )));
534 }
535 let weights = format!(
536 "({})",
537 curve
538 .control_points
539 .iter()
540 .map(|control| real(control.w))
541 .collect::<Result<Vec<_>, _>>()?
542 .join(",")
543 );
544 Ok(writer.add(format!(
545 "(BOUNDED_CURVE()B_SPLINE_CURVE({},{},.UNSPECIFIED.,.F.,.F.)\
546 B_SPLINE_CURVE_WITH_KNOTS({multiplicities},{knots},.UNSPECIFIED.)\
547 CURVE()GEOMETRIC_REPRESENTATION_ITEM()RATIONAL_B_SPLINE_CURVE({weights})\
548 REPRESENTATION_ITEM(''))",
549 curve.degree,
550 id_list(points),
551 )))
552}
553
554fn write_point_2d(writer: &mut StepWriter, point: [f64; 2]) -> Result<usize, String> {
555 Ok(writer.add(format!(
556 "CARTESIAN_POINT('',({},{}))",
557 real(point[0])?,
558 real(point[1])?
559 )))
560}
561
562fn write_direction_2d(writer: &mut StepWriter, direction: [f64; 2]) -> Result<usize, String> {
563 Ok(writer.add(format!(
564 "DIRECTION('',({},{}))",
565 real(direction[0])?,
566 real(direction[1])?
567 )))
568}
569
570fn write_pcurve_geometry(writer: &mut StepWriter, curve: &Pcurve2d) -> Result<usize, String> {
580 match curve {
581 Pcurve2d::Line { point, vector } => {
582 let magnitude = vector[0].hypot(vector[1]);
583 if magnitude <= 0.0 {
584 return Err("export_step: degenerate 2D line pcurve".into());
585 }
586 let point_id = write_point_2d(writer, *point)?;
587 let direction =
588 write_direction_2d(writer, [vector[0] / magnitude, vector[1] / magnitude])?;
589 let vector_id = writer.add(format!(
590 "VECTOR('',#{direction},{})",
591 real(magnitude)?
592 ));
593 Ok(writer.add(format!("LINE('',#{point_id},#{vector_id})")))
594 }
595 Pcurve2d::Circle {
596 center,
597 ref_direction,
598 radius,
599 } => {
600 let center_id = write_point_2d(writer, *center)?;
601 let direction = write_direction_2d(writer, *ref_direction)?;
602 let placement = writer.add(format!(
603 "AXIS2_PLACEMENT_2D('',#{center_id},#{direction})"
604 ));
605 Ok(writer.add(format!("CIRCLE('',#{placement},{})", real(*radius)?)))
606 }
607 Pcurve2d::Spline(spline) => {
608 let points = spline
609 .control_points
610 .iter()
611 .map(|control| {
612 let point = control.point()?;
613 write_point_2d(writer, [point.x, point.y])
614 })
615 .collect::<Result<Vec<_>, String>>()?;
616 write_bspline_curve(writer, spline, &points)
617 }
618 }
619}
620
621fn write_pcurve_entity(
624 writer: &mut StepWriter,
625 surface_id: usize,
626 context_2d: usize,
627 curve: &Pcurve2d,
628) -> Result<usize, String> {
629 let geometry = write_pcurve_geometry(writer, curve)?;
630 let representation = writer.add(format!(
631 "DEFINITIONAL_REPRESENTATION('',(#{geometry}),#{context_2d})"
632 ));
633 Ok(writer.add(format!("PCURVE('',#{surface_id},#{representation})")))
634}
635
636fn write_surface(writer: &mut StepWriter, surface: &NurbsSurface) -> Result<usize, String> {
637 let rows = surface
638 .control_points
639 .iter()
640 .map(|row| {
641 row.iter()
642 .map(|control| write_point(writer, control.point()?))
643 .collect::<Result<Vec<_>, _>>()
644 .map(|ids| id_list(&ids))
645 })
646 .collect::<Result<Vec<_>, _>>()?;
647 let grid = format!("({})", rows.join(","));
648 let (u_values, u_multiplicities) = knot_runs(&surface.knots_u);
649 let (v_values, v_multiplicities) = knot_runs(&surface.knots_v);
650 let multiplicities = |values: &[usize]| {
651 format!(
652 "({})",
653 values
654 .iter()
655 .map(usize::to_string)
656 .collect::<Vec<_>>()
657 .join(",")
658 )
659 };
660 let knots = |values: &[f64]| -> Result<String, String> {
661 Ok(format!(
662 "({})",
663 values
664 .iter()
665 .map(|value| real(*value))
666 .collect::<Result<Vec<_>, _>>()?
667 .join(",")
668 ))
669 };
670 let u_mults = multiplicities(&u_multiplicities);
671 let v_mults = multiplicities(&v_multiplicities);
672 let u_knots = knots(&u_values)?;
673 let v_knots = knots(&v_values)?;
674 let rational = surface
675 .control_points
676 .iter()
677 .flatten()
678 .any(|control| (control.w - 1.0).abs() > 1e-12);
679 if !rational {
680 return Ok(writer.add(format!(
681 "B_SPLINE_SURFACE_WITH_KNOTS('',{},{},{grid},.UNSPECIFIED.,.F.,.F.,.F.,\
682 {u_mults},{v_mults},{u_knots},{v_knots},.UNSPECIFIED.)",
683 surface.degree_u, surface.degree_v,
684 )));
685 }
686 let weights = format!(
687 "({})",
688 surface
689 .control_points
690 .iter()
691 .map(|row| {
692 row.iter()
693 .map(|control| real(control.w))
694 .collect::<Result<Vec<_>, _>>()
695 .map(|values| format!("({})", values.join(",")))
696 })
697 .collect::<Result<Vec<_>, _>>()?
698 .join(",")
699 );
700 Ok(writer.add(format!(
701 "(BOUNDED_SURFACE()B_SPLINE_SURFACE({},{},{grid},.UNSPECIFIED.,.F.,.F.,.F.)\
702 B_SPLINE_SURFACE_WITH_KNOTS({u_mults},{v_mults},{u_knots},{v_knots},.UNSPECIFIED.)\
703 GEOMETRIC_REPRESENTATION_ITEM()RATIONAL_B_SPLINE_SURFACE({weights})\
704 REPRESENTATION_ITEM('')SURFACE())",
705 surface.degree_u, surface.degree_v,
706 )))
707}
708
709fn write_length_unit(writer: &mut StepWriter, unit: &str) -> Result<usize, String> {
710 let normalized = unit.to_lowercase();
711 if normalized == "meter" || normalized == "metre" {
712 return Ok(writer.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT($,.METRE.))"));
713 }
714 if normalized == "centimeter" || normalized == "centimetre" {
715 return Ok(writer.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.CENTI.,.METRE.))"));
716 }
717 if matches!(normalized.as_str(), "micron" | "micrometer" | "micrometre") {
718 return Ok(writer.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MICRO.,.METRE.))"));
719 }
720 if normalized == "inch" || normalized == "foot" {
721 let metre = writer.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT($,.METRE.))");
722 let (factor, name) = if normalized == "inch" {
723 (0.0254, "INCH")
724 } else {
725 (0.3048, "FOOT")
726 };
727 let measure = writer.add(format!(
728 "LENGTH_MEASURE_WITH_UNIT(LENGTH_MEASURE({}),#{metre})",
729 real(factor)?
730 ));
731 return Ok(writer.add(format!(
732 "(CONVERSION_BASED_UNIT('{name}',#{measure})LENGTH_UNIT()NAMED_UNIT(*))"
733 )));
734 }
735 Ok(writer.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))"))
736}
737
738fn vertex_for(solid: &BrepSolid, id: u64) -> Result<&VertexRecord, String> {
739 solid
740 .vertices
741 .iter()
742 .find(|vertex| vertex.id == id)
743 .ok_or_else(|| format!("export_step: missing vertex {id}"))
744}
745
746fn edge_for(solid: &BrepSolid, id: u64) -> Result<&EdgeRecord, String> {
747 solid
748 .edges
749 .iter()
750 .find(|edge| edge.id == id)
751 .ok_or_else(|| format!("export_step: missing edge {id}"))
752}
753
754fn surface_key(face: &FaceRecord) -> usize {
755 face as *const FaceRecord as usize
756}
757
758struct CoedgeUse<'a> {
762 surface_key: usize,
763 face: &'a FaceRecord,
764 coedge: &'a CoedgeRecord,
765}
766
767#[derive(Clone, Debug, Default)]
776pub struct StepExportReport {
777 pub text: String,
779 pub pcurves_written: usize,
782 pub pcurves_omitted: usize,
785 pub surface_curves: usize,
787 pub seam_curves: usize,
790 pub bare_curves: usize,
793 pub vertex_loops: usize,
796 pub max_pcurve_deviation: f64,
799 pub worst_omitted_deviation: f64,
810 pub pmi_unresolved_references: usize,
817 pub products: usize,
820 pub occurrences: usize,
823}
824
825pub fn export_step(
827 solids: &[BrepSolid],
828 name: &str,
829 unit: &str,
830 timestamp: &str,
831) -> Result<String, String> {
832 export_step_report(solids, name, unit, timestamp).map(|report| report.text)
833}
834
835pub fn export_step_report(
838 solids: &[BrepSolid],
839 name: &str,
840 unit: &str,
841 timestamp: &str,
842) -> Result<StepExportReport, String> {
843 let named: Vec<(String, &BrepSolid)> = solids
844 .iter()
845 .map(|solid| (name.to_string(), solid))
846 .collect();
847 export_step_report_named(&named, name, unit, timestamp, None)
848}
849
850#[derive(Clone, Copy)]
859pub(crate) struct StepItemOwner {
860 pub product_shape: usize,
861 pub representation: usize,
862}
863
864#[derive(Default)]
868pub(crate) struct ProductGeometry {
869 pub solids: Vec<usize>,
871 pub faces: Vec<(String, usize)>,
873 pub edges: Vec<(String, usize)>,
875 pub vertices: Vec<(String, Vec<(Vec3, usize)>)>,
877}
878
879#[derive(Default)]
884pub(crate) struct StepNameMaps {
885 pub faces: HashMap<String, (usize, StepItemOwner)>,
886 pub edges: HashMap<String, (usize, StepItemOwner)>,
887 pub vertices: HashMap<String, (StepItemOwner, Vec<(Vec3, usize)>)>,
890}
891
892impl StepNameMaps {
893 pub(crate) fn register(
897 &mut self,
898 geometry: &ProductGeometry,
899 owner: StepItemOwner,
900 prefix: &str,
901 world: &Mat4,
902 ) {
903 for (name, id) in &geometry.faces {
904 self.faces
905 .entry(format!("{prefix}{name}"))
906 .or_insert((*id, owner));
907 }
908 for (name, id) in &geometry.edges {
909 self.edges
910 .entry(format!("{prefix}{name}"))
911 .or_insert((*id, owner));
912 }
913 for (name, points) in &geometry.vertices {
914 let placed = points
915 .iter()
916 .map(|(point, id)| (transform_point(world, *point), *id))
917 .collect();
918 self.vertices
919 .entry(format!("{prefix}{name}"))
920 .or_insert((owner, placed));
921 }
922 }
923}
924
925pub(crate) struct StepFileContexts {
928 pub product_context: usize,
929 pub definition_context: usize,
930 pub geometry_context: usize,
931 pub parametric_context: usize,
932 pub length_unit: usize,
933 pub angle_unit: usize,
934 pub axis: usize,
938}
939
940pub(crate) fn write_file_contexts(
942 writer: &mut StepWriter,
943 unit: &str,
944) -> Result<StepFileContexts, String> {
945 let application = writer.add("APPLICATION_CONTEXT('managed model based 3d engineering')");
946 writer.add(format!(
947 "APPLICATION_PROTOCOL_DEFINITION('international standard','ap242_managed_model_based_3d_engineering',2014,#{application})"
948 ));
949 let product_context = writer.add(format!("PRODUCT_CONTEXT('',#{application},'mechanical')"));
950 let definition_context = writer.add(format!(
951 "PRODUCT_DEFINITION_CONTEXT('part definition',#{application},'design')"
952 ));
953 let length_unit = write_length_unit(writer, unit)?;
954 let angle_unit = writer.add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))");
955 let solid_angle_unit = writer.add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())");
956 let uncertainty = writer.add(format!(
957 "UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-6),#{length_unit},'distance_accuracy_value','')"
958 ));
959 let geometry_context = writer.add(format!(
960 "(GEOMETRIC_REPRESENTATION_CONTEXT(3)\
961 GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{uncertainty}))\
962 GLOBAL_UNIT_ASSIGNED_CONTEXT((#{length_unit},#{angle_unit},#{solid_angle_unit}))\
963 REPRESENTATION_CONTEXT('',''))"
964 ));
965 let parametric_context = writer.add(
970 "(GEOMETRIC_REPRESENTATION_CONTEXT(2)\
971 PARAMETRIC_REPRESENTATION_CONTEXT()\
972 REPRESENTATION_CONTEXT('2D SPACE',''))",
973 );
974 let origin = write_point(writer, Vec3::default())?;
975 let direction_z = writer.add("DIRECTION('',(0.,0.,1.))");
976 let direction_x = writer.add("DIRECTION('',(1.,0.,0.))");
977 let axis = writer.add(format!(
978 "AXIS2_PLACEMENT_3D('',#{origin},#{direction_z},#{direction_x})"
979 ));
980 Ok(StepFileContexts {
981 product_context,
982 definition_context,
983 geometry_context,
984 parametric_context,
985 length_unit,
986 angle_unit,
987 axis,
988 })
989}
990
991pub(crate) struct ProductIds {
995 pub definition: usize,
996 pub product_shape: usize,
997}
998
999pub(crate) fn write_product(
1005 writer: &mut StepWriter,
1006 contexts: &StepFileContexts,
1007 name: &str,
1008 id: &str,
1009 category: &str,
1010) -> ProductIds {
1011 let safe_name = step_string(name);
1012 let safe_id = step_string(if id.is_empty() { name } else { id });
1013 let product_context = contexts.product_context;
1014 let definition_context = contexts.definition_context;
1015 let product = writer.add(format!(
1016 "PRODUCT('{safe_id}','{safe_name}','',(#{product_context}))"
1017 ));
1018 writer.add(format!(
1019 "PRODUCT_RELATED_PRODUCT_CATEGORY('{}','',(#{product}))",
1020 step_string(category)
1021 ));
1022 let formation = writer.add(format!("PRODUCT_DEFINITION_FORMATION('','',#{product})"));
1023 let definition = writer.add(format!(
1024 "PRODUCT_DEFINITION('design','',#{formation},#{definition_context})"
1025 ));
1026 let product_shape = writer.add(format!("PRODUCT_DEFINITION_SHAPE('','',#{definition})"));
1027 ProductIds {
1028 definition,
1029 product_shape,
1030 }
1031}
1032
1033pub(crate) fn write_product_geometry(
1041 writer: &mut StepWriter,
1042 contexts: &StepFileContexts,
1043 bodies: &[(String, &BrepSolid)],
1044 report: &mut StepExportReport,
1045) -> Result<ProductGeometry, String> {
1046 for (_, solid) in bodies {
1047 let policy = KernelTolerances::for_solid(solid, 1e-7);
1048 let issues = solid.validate_with_tolerances(&KernelTolerances {
1049 pcurve_consistency: policy.export_knit,
1050 ..policy
1051 });
1052 if !issues.is_empty() {
1053 return Err(format!("export_step: invalid solid: {issues:?}"));
1054 }
1055 }
1056 let parametric_context = contexts.parametric_context;
1057 let mut written = ProductGeometry::default();
1058 for (solid_name, solid) in bodies {
1059 let solid = *solid;
1060 let solid_name = solid_name.as_str();
1061 let band = KernelTolerances::for_solid(solid, 1e-7).export_knit;
1062 let mut vertex_ids = HashMap::<u64, usize>::default();
1063 let mut edge_ids = HashMap::<u64, usize>::default();
1064 let mut surfaces = HashMap::<usize, (usize, bool, EmittedSurface)>::default();
1065 for shell in &solid.shells {
1066 for face in &shell.faces {
1071 let key = surface_key(face);
1072 if surfaces.contains_key(&key) {
1073 continue;
1074 }
1075 let entry = match write_analytic_surface(writer, &face.surface)? {
1076 Some(triple) => triple,
1077 None => (
1078 write_surface(writer, &face.surface)?,
1079 false,
1080 EmittedSurface::Spline,
1081 ),
1082 };
1083 surfaces.insert(key, entry);
1084 }
1085
1086 let mut edge_uses = HashMap::<u64, Vec<CoedgeUse>>::default();
1089 let mut edge_order: Vec<u64> = Vec::new();
1090 for face in &shell.faces {
1091 for loop_record in &face.loops {
1092 for coedge in &loop_record.coedges {
1093 let edge = edge_for(solid, coedge.edge_id)?;
1094 if edge.degenerate {
1095 continue;
1096 }
1097 let uses = edge_uses.entry(edge.id).or_default();
1098 if uses.is_empty() {
1099 edge_order.push(edge.id);
1100 }
1101 uses.push(CoedgeUse {
1102 surface_key: surface_key(face),
1103 face,
1104 coedge,
1105 });
1106 }
1107 }
1108 }
1109
1110 for edge_id in &edge_order {
1112 if edge_ids.contains_key(edge_id) {
1113 continue;
1114 }
1115 let edge = edge_for(solid, *edge_id)?;
1116 let subcurve = edge_subcurve(edge)?;
1117 let (curve, emitted_curve) = match write_analytic_curve(writer, &subcurve)? {
1118 Some(pair) => pair,
1119 None => (
1120 write_curve(writer, &subcurve)?,
1121 EmittedCurve::Spline { curve: subcurve },
1122 ),
1123 };
1124 let uses = &edge_uses[edge_id];
1125 let seam = uses.len() == 2 && uses[0].surface_key == uses[1].surface_key;
1129 let mut pcurves: Vec<(usize, Pcurve2d)> = Vec::new();
1130 let mut omitted = 0usize;
1131 if uses.len() == 2 {
1132 let mut ordered: Vec<&CoedgeUse> = uses.iter().collect();
1136 if seam && !ordered[0].coedge.forward {
1137 ordered.swap(0, 1);
1138 }
1139 for coedge_use in ordered {
1140 let oriented = if coedge_use.coedge.forward {
1145 coedge_use.coedge.pcurve.clone()
1146 } else {
1147 coedge_use.coedge.pcurve.reversed()?
1148 };
1149 let (surface_id, _, emitted_surface) = &surfaces[&coedge_use.surface_key];
1150 let outcome = build_pcurve(
1151 &coedge_use.face.surface,
1152 emitted_surface,
1153 &emitted_curve,
1154 &oriented,
1155 band,
1156 )?;
1157 match outcome.curve {
1158 Some(curve_2d) => {
1159 report.max_pcurve_deviation =
1160 report.max_pcurve_deviation.max(outcome.deviation);
1161 pcurves.push((*surface_id, curve_2d));
1162 }
1163 None => {
1164 omitted += 1;
1165 if outcome.deviation.is_finite() {
1166 report.worst_omitted_deviation =
1167 report.worst_omitted_deviation.max(outcome.deviation);
1168 } else {
1169 report.worst_omitted_deviation = f64::INFINITY;
1170 }
1171 }
1172 }
1173 }
1174 }
1175 if seam && pcurves.len() != 2 {
1179 omitted += pcurves.len();
1180 pcurves.clear();
1181 }
1182 report.pcurves_omitted += omitted;
1183 report.pcurves_written += pcurves.len();
1184 let geometry = if pcurves.is_empty() {
1185 report.bare_curves += 1;
1186 curve
1187 } else {
1188 let ids = pcurves
1189 .iter()
1190 .map(|(surface_id, curve_2d)| {
1191 write_pcurve_entity(
1192 writer,
1193 *surface_id,
1194 parametric_context,
1195 curve_2d,
1196 )
1197 })
1198 .collect::<Result<Vec<_>, String>>()?;
1199 let keyword = if seam {
1200 report.seam_curves += 1;
1201 "SEAM_CURVE"
1202 } else {
1203 report.surface_curves += 1;
1204 "SURFACE_CURVE"
1205 };
1206 writer.add(format!(
1212 "{keyword}('',#{curve},{},.CURVE_3D.)",
1213 id_list(&ids)
1214 ))
1215 };
1216 let start = vertex_step_id(writer, &mut vertex_ids, solid, edge.start_vertex_id)?;
1217 let end = vertex_step_id(writer, &mut vertex_ids, solid, edge.end_vertex_id)?;
1218 let step_id =
1219 writer.add(format!("EDGE_CURVE('',#{start},#{end},#{geometry},.T.)"));
1220 edge_ids.insert(edge.id, step_id);
1221 if let Some(edge_name) = edge.name.as_deref() {
1222 written.edges.push((edge_name.to_string(), step_id));
1223 }
1224 }
1225
1226 let mut face_ids = Vec::new();
1228 for face in &shell.faces {
1229 let mut bound_ids = Vec::new();
1230 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1231 let mut oriented_edges = Vec::new();
1232 for coedge in &loop_record.coedges {
1233 let edge = edge_for(solid, coedge.edge_id)?;
1234 if edge.degenerate {
1235 continue;
1236 }
1237 let edge_id = *edge_ids
1238 .get(&edge.id)
1239 .ok_or_else(|| format!("export_step: unwritten edge {}", edge.id))?;
1240 let orientation = if coedge.forward { ".T." } else { ".F." };
1241 oriented_edges.push(
1242 writer.add(format!("ORIENTED_EDGE('',*,*,#{edge_id},{orientation})")),
1243 );
1244 }
1245 let kind = if loop_index == 0 {
1246 "FACE_OUTER_BOUND"
1247 } else {
1248 "FACE_BOUND"
1249 };
1250 if oriented_edges.is_empty() {
1251 let Some(coedge) = loop_record.coedges.first() else {
1264 continue;
1265 };
1266 let collapsed = edge_for(solid, coedge.edge_id)?;
1267 let vertex = vertex_step_id(
1268 writer,
1269 &mut vertex_ids,
1270 solid,
1271 collapsed.start_vertex_id,
1272 )?;
1273 let vertex_loop = writer.add(format!("VERTEX_LOOP('',#{vertex})"));
1274 report.vertex_loops += 1;
1275 bound_ids.push(writer.add(format!("{kind}('',#{vertex_loop},.T.)")));
1276 continue;
1277 }
1278 let edge_loop =
1279 writer.add(format!("EDGE_LOOP('',{})", id_list(&oriented_edges)));
1280 bound_ids.push(writer.add(format!("{kind}('',#{edge_loop},.T.)")));
1281 }
1282 let (surface, flipped, _) = surfaces[&surface_key(face)];
1283 let sense = if face.same_sense != flipped {
1287 ".T."
1288 } else {
1289 ".F."
1290 };
1291 let face_step_id = writer.add(format!(
1292 "ADVANCED_FACE('',{},#{surface},{sense})",
1293 id_list(&bound_ids)
1294 ));
1295 if let Some(face_name) = face.name.as_deref() {
1296 written.faces.push((face_name.to_string(), face_step_id));
1297 }
1298 face_ids.push(face_step_id);
1299 }
1300 let closed_shell = writer.add(format!("CLOSED_SHELL('',{})", id_list(&face_ids)));
1301 written.solids.push(writer.add(format!(
1302 "MANIFOLD_SOLID_BREP('{}',#{closed_shell})",
1303 step_string(solid_name)
1304 )));
1305 }
1306 let mut points: Vec<(Vec3, usize)> = Vec::with_capacity(vertex_ids.len());
1308 for (vertex_id, step_id) in &vertex_ids {
1309 points.push((vertex_for(solid, *vertex_id)?.point, *step_id));
1310 }
1311 written.vertices.push((solid_name.to_string(), points));
1312 }
1313 Ok(written)
1314}
1315
1316pub(crate) fn finish_step_file(
1319 writer: StepWriter,
1320 name: &str,
1321 timestamp: &str,
1322 report: &mut StepExportReport,
1323) -> Result<(), String> {
1324 let safe_name = step_string(name);
1325 let safe_timestamp = step_string(timestamp);
1326 let output = [
1327 "ISO-10303-21;".to_string(),
1328 "HEADER;".to_string(),
1329 "FILE_DESCRIPTION((''),'2;1');".to_string(),
1330 format!(
1331 "FILE_NAME('{safe_name}.step','{safe_timestamp}',(''),(''),'brep-kernel-rs','brep-kernel-rs','');"
1332 ),
1333 "FILE_SCHEMA(('AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 1 1 4 }'));".to_string(),
1334 "ENDSEC;".to_string(),
1335 "DATA;".to_string(),
1336 writer.data(),
1337 "ENDSEC;".to_string(),
1338 "END-ISO-10303-21;".to_string(),
1339 String::new(),
1340 ]
1341 .join("\n");
1342 let manifold_issues = audit_step_manifold(&output);
1343 if !manifold_issues.is_empty() {
1344 return Err(format!(
1345 "export_step: emitted AP242 manifold audit failed: {}",
1346 manifold_issues.join("; ")
1347 ));
1348 }
1349 let pcurve_issues = audit_step_pcurves(&output);
1350 if !pcurve_issues.is_empty() {
1351 return Err(format!(
1352 "export_step: emitted AP242 pcurve audit failed: {}",
1353 pcurve_issues.join("; ")
1354 ));
1355 }
1356 report.text = output;
1357 Ok(())
1358}
1359
1360pub fn export_step_report_named(
1369 solids: &[(String, &BrepSolid)],
1370 name: &str,
1371 unit: &str,
1372 timestamp: &str,
1373 pmi: Option<&StepPmi<'_>>,
1374) -> Result<StepExportReport, String> {
1375 if solids.is_empty() {
1376 return Err("export_step: at least one solid is required".into());
1377 }
1378 let mut report = StepExportReport {
1379 products: 1,
1380 ..StepExportReport::default()
1381 };
1382 let mut writer = StepWriter::default();
1383 let contexts = write_file_contexts(&mut writer, unit)?;
1384 let geometry = write_product_geometry(&mut writer, &contexts, solids, &mut report)?;
1385 let mut items = vec![contexts.axis];
1386 items.extend(&geometry.solids);
1387 let geometry_context = contexts.geometry_context;
1388 let representation = writer.add(format!(
1389 "ADVANCED_BREP_SHAPE_REPRESENTATION('',{},#{geometry_context})",
1390 id_list(&items)
1391 ));
1392 let product = write_product(&mut writer, &contexts, name, "", "part");
1393 let product_shape = product.product_shape;
1394 writer.add(format!(
1395 "SHAPE_DEFINITION_REPRESENTATION(#{product_shape},#{representation})"
1396 ));
1397 if let Some(pmi) = pmi {
1398 let mut names = StepNameMaps::default();
1399 names.register(
1400 &geometry,
1401 StepItemOwner {
1402 product_shape,
1403 representation,
1404 },
1405 "",
1406 &MAT4_IDENTITY,
1407 );
1408 let context = pmi::StepContext {
1409 product_shape,
1410 representation,
1411 geometry_context,
1412 length_unit: contexts.length_unit,
1413 angle_unit: contexts.angle_unit,
1414 faces: &names.faces,
1415 edges: &names.edges,
1416 vertices: &names.vertices,
1417 };
1418 report.pmi_unresolved_references = pmi::write_pmi(&mut writer, &context, pmi)?;
1419 }
1420 finish_step_file(writer, name, timestamp, &mut report)?;
1421 Ok(report)
1422}
1423
1424fn vertex_step_id(
1426 writer: &mut StepWriter,
1427 vertex_ids: &mut HashMap<u64, usize>,
1428 solid: &BrepSolid,
1429 id: u64,
1430) -> Result<usize, String> {
1431 if let Some(step_id) = vertex_ids.get(&id) {
1432 return Ok(*step_id);
1433 }
1434 let point = write_point(writer, vertex_for(solid, id)?.point)?;
1435 let step_id = writer.add(format!("VERTEX_POINT('',#{point})"));
1436 vertex_ids.insert(id, step_id);
1437 Ok(step_id)
1438}
1439
1440fn step_entity_bodies(step: &str) -> HashMap<u64, &str> {
1443 step.lines()
1444 .filter_map(|line| {
1445 let rest = line.strip_prefix('#')?;
1446 let (digits, body) = rest.split_once('=')?;
1447 Some((
1448 digits.parse::<u64>().ok()?,
1449 body.trim_end().trim_end_matches(';'),
1450 ))
1451 })
1452 .collect()
1453}
1454
1455fn step_entity_refs(body: &str) -> Vec<u64> {
1459 let mut refs = Vec::new();
1460 let bytes = body.as_bytes();
1461 let mut index = 0;
1462 while index < bytes.len() {
1463 if bytes[index] == b'#' {
1464 let start = index + 1;
1465 let mut end = start;
1466 while end < bytes.len() && bytes[end].is_ascii_digit() {
1467 end += 1;
1468 }
1469 if end > start {
1470 if let Ok(id) = body[start..end].parse::<u64>() {
1471 refs.push(id);
1472 }
1473 }
1474 index = end;
1475 } else {
1476 index += 1;
1477 }
1478 }
1479 refs
1480}
1481
1482pub fn audit_step_pcurves(step: &str) -> Vec<String> {
1495 let bodies = step_entity_bodies(step);
1496 let mut issues = Vec::new();
1497 for (id, body) in &bodies {
1498 if !body.starts_with("EDGE_CURVE(") {
1499 continue;
1500 }
1501 let refs = step_entity_refs(body);
1502 let Some(geometry) = refs.get(2) else {
1503 issues.push(format!("EDGE_CURVE #{id} has no edge_geometry"));
1504 continue;
1505 };
1506 let Some(wrapper) = bodies.get(geometry) else {
1507 issues.push(format!("EDGE_CURVE #{id} references missing #{geometry}"));
1508 continue;
1509 };
1510 let seam = wrapper.starts_with("SEAM_CURVE(");
1511 if !seam && !wrapper.starts_with("SURFACE_CURVE(") {
1512 continue;
1513 }
1514 let wrapper_refs = step_entity_refs(wrapper);
1515 let pcurves = &wrapper_refs[wrapper_refs.len().min(1)..];
1516 if pcurves.is_empty() || pcurves.len() > 2 || (seam && pcurves.len() != 2) {
1517 issues.push(format!(
1518 "#{geometry} carries {} associated geometries",
1519 pcurves.len()
1520 ));
1521 continue;
1522 }
1523 let mut surfaces = Vec::new();
1524 for pcurve in pcurves {
1525 let Some(pcurve_body) = bodies.get(pcurve) else {
1526 issues.push(format!("#{geometry} references missing #{pcurve}"));
1527 continue;
1528 };
1529 if !pcurve_body.starts_with("PCURVE(") {
1530 issues.push(format!("#{geometry} associate #{pcurve} is not a PCURVE"));
1531 continue;
1532 }
1533 let pcurve_refs = step_entity_refs(pcurve_body);
1534 let representation = pcurve_refs.get(1).and_then(|id| bodies.get(id));
1535 if !representation.is_some_and(|body| body.starts_with("DEFINITIONAL_REPRESENTATION("))
1536 {
1537 issues.push(format!(
1538 "PCURVE #{pcurve} has no DEFINITIONAL_REPRESENTATION"
1539 ));
1540 }
1541 if let Some(surface) = pcurve_refs.first() {
1542 surfaces.push(*surface);
1543 }
1544 }
1545 if surfaces.len() == 2 && (surfaces[0] == surfaces[1]) != seam {
1546 issues.push(format!(
1547 "#{geometry} pcurves name {} surface(s) but it is a {}",
1548 if surfaces[0] == surfaces[1] { 1 } else { 2 },
1549 if seam { "SEAM_CURVE" } else { "SURFACE_CURVE" }
1550 ));
1551 }
1552 }
1553 issues.sort();
1554 issues
1555}
1556
1557pub fn audit_step_manifold(step: &str) -> Vec<String> {
1562 let marker = "ORIENTED_EDGE('',*,*,#";
1563 let mut uses = HashMap::<u64, Vec<bool>>::default();
1564 for line in step.lines() {
1565 let Some(offset) = line.find(marker) else {
1566 continue;
1567 };
1568 let rest = &line[offset + marker.len()..];
1569 let digits = rest
1570 .chars()
1571 .take_while(|character| character.is_ascii_digit())
1572 .collect::<String>();
1573 let Ok(edge_id) = digits.parse::<u64>() else {
1574 continue;
1575 };
1576 let suffix = &rest[digits.len()..];
1577 let sense = suffix.starts_with(",.T.");
1578 uses.entry(edge_id).or_default().push(sense);
1579 }
1580 let mut issues = uses
1581 .into_iter()
1582 .filter_map(|(edge, senses)| {
1583 (senses.len() != 2 || senses[0] == senses[1]).then(|| {
1584 format!(
1585 "EDGE_CURVE #{edge} has {} uses with senses {:?}",
1586 senses.len(),
1587 senses
1588 )
1589 })
1590 })
1591 .collect::<Vec<_>>();
1592 issues.sort();
1593 issues
1594}
1595
1596