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