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"]
8pub(crate) mod 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 void_owner(
741 solid: &BrepSolid,
742 void: usize,
743 outer: &[usize],
744 shells: &[(usize, bool, f64)],
745) -> Result<usize, String> {
746 let probe = solid.shells[void]
747 .faces
748 .iter()
749 .flat_map(|face| &face.loops)
750 .flat_map(|lp| &lp.coedges)
751 .find_map(|coedge| {
752 let edge = solid.edges.iter().find(|edge| edge.id == coedge.edge_id)?;
753 (!edge.degenerate).then(|| edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).ok()).flatten()
754 })
755 .ok_or("export_step: a void shell with no edge to place it by")?;
756 let mut best: Option<usize> = None;
757 for &candidate in outer {
758 let alone = BrepSolid {
759 shells: vec![solid.shells[candidate].clone()],
760 ..solid.clone()
761 };
762 let class = crate::classify_point(probe, &alone, 1e-7)?.class;
763 if class == crate::PointClass::In && best.is_none_or(|b| shells[candidate].2 < shells[b].2) {
764 best = Some(candidate);
765 }
766 }
767 best.ok_or_else(|| "export_step: a void shell lies inside no outward shell".into())
768}
769
770fn vertex_for(solid: &BrepSolid, id: u64) -> Result<&VertexRecord, String> {
771 solid
772 .vertices
773 .iter()
774 .find(|vertex| vertex.id == id)
775 .ok_or_else(|| format!("export_step: missing vertex {id}"))
776}
777
778fn edge_for(solid: &BrepSolid, id: u64) -> Result<&EdgeRecord, String> {
779 solid
780 .edges
781 .iter()
782 .find(|edge| edge.id == id)
783 .ok_or_else(|| format!("export_step: missing edge {id}"))
784}
785
786fn surface_key(face: &FaceRecord) -> usize {
787 face as *const FaceRecord as usize
788}
789
790struct CoedgeUse<'a> {
794 surface_key: usize,
795 face: &'a FaceRecord,
796 coedge: &'a CoedgeRecord,
797}
798
799#[derive(Clone, Debug, Default)]
808pub struct StepExportReport {
809 pub text: String,
811 pub pcurves_written: usize,
814 pub pcurves_omitted: usize,
817 pub surface_curves: usize,
819 pub seam_curves: usize,
822 pub bare_curves: usize,
825 pub vertex_loops: usize,
828 pub max_pcurve_deviation: f64,
831 pub worst_omitted_deviation: f64,
842 pub pmi_unresolved_references: usize,
849 pub products: usize,
852 pub occurrences: usize,
855 pub styled_items: usize,
860}
861
862pub fn export_step(
871 solids: &[BrepSolid],
872 name: &str,
873 unit: &str,
874 timestamp: &str,
875) -> Result<String, String> {
876 export_step_report(solids, name, unit, timestamp).map(|report| report.text)
877}
878
879pub fn export_step_report(
882 solids: &[BrepSolid],
883 name: &str,
884 unit: &str,
885 timestamp: &str,
886) -> Result<StepExportReport, String> {
887 let named: Vec<(String, &BrepSolid)> = solids
888 .iter()
889 .map(|solid| (name.to_string(), solid))
890 .collect();
891 export_step_report_named(&named, name, unit, timestamp, None, None)
892}
893
894#[derive(Clone, Copy)]
903pub(crate) struct StepItemOwner {
904 pub product_shape: usize,
905 pub representation: usize,
906}
907
908#[derive(Default)]
912pub(crate) struct ProductGeometry {
913 pub solids: Vec<(String, usize)>,
918 pub faces: Vec<(String, usize)>,
920 pub edges: Vec<(String, usize)>,
922 pub vertices: Vec<(String, Vec<(Vec3, usize)>)>,
924}
925
926#[derive(Default)]
931pub(crate) struct StepNameMaps {
932 pub faces: HashMap<String, (usize, StepItemOwner)>,
933 pub edges: HashMap<String, (usize, StepItemOwner)>,
934 pub vertices: HashMap<String, (StepItemOwner, Vec<(Vec3, usize)>)>,
937}
938
939impl StepNameMaps {
940 pub(crate) fn register(
944 &mut self,
945 geometry: &ProductGeometry,
946 owner: StepItemOwner,
947 prefix: &str,
948 world: &Mat4,
949 ) {
950 for (name, id) in &geometry.faces {
951 self.faces
952 .entry(format!("{prefix}{name}"))
953 .or_insert((*id, owner));
954 }
955 for (name, id) in &geometry.edges {
956 self.edges
957 .entry(format!("{prefix}{name}"))
958 .or_insert((*id, owner));
959 }
960 for (name, points) in &geometry.vertices {
961 let placed = points
962 .iter()
963 .map(|(point, id)| (transform_point(world, *point), *id))
964 .collect();
965 self.vertices
966 .entry(format!("{prefix}{name}"))
967 .or_insert((owner, placed));
968 }
969 }
970}
971
972pub(crate) struct StepFileContexts {
975 pub product_context: usize,
976 pub definition_context: usize,
977 pub geometry_context: usize,
978 pub parametric_context: usize,
979 pub length_unit: usize,
980 pub angle_unit: usize,
981 pub axis: usize,
985}
986
987pub(crate) fn write_file_contexts(
989 writer: &mut StepWriter,
990 unit: &str,
991) -> Result<StepFileContexts, String> {
992 let application = writer.add("APPLICATION_CONTEXT('managed model based 3d engineering')");
993 writer.add(format!(
994 "APPLICATION_PROTOCOL_DEFINITION('international standard','ap242_managed_model_based_3d_engineering',2014,#{application})"
995 ));
996 let product_context = writer.add(format!("PRODUCT_CONTEXT('',#{application},'mechanical')"));
997 let definition_context = writer.add(format!(
998 "PRODUCT_DEFINITION_CONTEXT('part definition',#{application},'design')"
999 ));
1000 let length_unit = write_length_unit(writer, unit)?;
1001 let angle_unit = writer.add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))");
1002 let solid_angle_unit = writer.add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())");
1003 let uncertainty = writer.add(format!(
1004 "UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-6),#{length_unit},'distance_accuracy_value','')"
1005 ));
1006 let geometry_context = writer.add(format!(
1007 "(GEOMETRIC_REPRESENTATION_CONTEXT(3)\
1008 GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{uncertainty}))\
1009 GLOBAL_UNIT_ASSIGNED_CONTEXT((#{length_unit},#{angle_unit},#{solid_angle_unit}))\
1010 REPRESENTATION_CONTEXT('',''))"
1011 ));
1012 let parametric_context = writer.add(
1017 "(GEOMETRIC_REPRESENTATION_CONTEXT(2)\
1018 PARAMETRIC_REPRESENTATION_CONTEXT()\
1019 REPRESENTATION_CONTEXT('2D SPACE',''))",
1020 );
1021 let origin = write_point(writer, Vec3::default())?;
1022 let direction_z = writer.add("DIRECTION('',(0.,0.,1.))");
1023 let direction_x = writer.add("DIRECTION('',(1.,0.,0.))");
1024 let axis = writer.add(format!(
1025 "AXIS2_PLACEMENT_3D('',#{origin},#{direction_z},#{direction_x})"
1026 ));
1027 Ok(StepFileContexts {
1028 product_context,
1029 definition_context,
1030 geometry_context,
1031 parametric_context,
1032 length_unit,
1033 angle_unit,
1034 axis,
1035 })
1036}
1037
1038pub(crate) struct ProductIds {
1042 pub definition: usize,
1043 pub product_shape: usize,
1044}
1045
1046pub(crate) fn write_product(
1052 writer: &mut StepWriter,
1053 contexts: &StepFileContexts,
1054 name: &str,
1055 id: &str,
1056 category: &str,
1057) -> ProductIds {
1058 let safe_name = step_string(name);
1059 let safe_id = step_string(if id.is_empty() { name } else { id });
1060 let product_context = contexts.product_context;
1061 let definition_context = contexts.definition_context;
1062 let product = writer.add(format!(
1063 "PRODUCT('{safe_id}','{safe_name}','',(#{product_context}))"
1064 ));
1065 writer.add(format!(
1066 "PRODUCT_RELATED_PRODUCT_CATEGORY('{}','',(#{product}))",
1067 step_string(category)
1068 ));
1069 let formation = writer.add(format!("PRODUCT_DEFINITION_FORMATION('','',#{product})"));
1070 let definition = writer.add(format!(
1071 "PRODUCT_DEFINITION('design','',#{formation},#{definition_context})"
1072 ));
1073 let product_shape = writer.add(format!("PRODUCT_DEFINITION_SHAPE('','',#{definition})"));
1074 ProductIds {
1075 definition,
1076 product_shape,
1077 }
1078}
1079
1080pub(crate) fn write_product_geometry(
1088 writer: &mut StepWriter,
1089 contexts: &StepFileContexts,
1090 bodies: &[(String, &BrepSolid)],
1091 report: &mut StepExportReport,
1092) -> Result<ProductGeometry, String> {
1093 for (_, solid) in bodies {
1094 let policy = KernelTolerances::for_solid(solid, 1e-7);
1095 let issues = solid.validate_with_tolerances(&KernelTolerances {
1096 pcurve_consistency: policy.export_knit,
1097 ..policy
1098 });
1099 if !issues.is_empty() {
1100 return Err(format!("export_step: invalid solid: {issues:?}"));
1101 }
1102 }
1103 let parametric_context = contexts.parametric_context;
1104 let mut written = ProductGeometry::default();
1105 for (solid_name, solid) in bodies {
1106 let solid = *solid;
1107 let solid_name = solid_name.as_str();
1108 let band = KernelTolerances::for_solid(solid, 1e-7).export_knit;
1109 let mut vertex_ids = HashMap::<u64, usize>::default();
1110 let mut edge_ids = HashMap::<u64, usize>::default();
1111 let mut surfaces = HashMap::<usize, (usize, bool, EmittedSurface)>::default();
1112 let mut emitted_shells = Vec::new();
1113 for shell in &solid.shells {
1114 for face in &shell.faces {
1119 let key = surface_key(face);
1120 if surfaces.contains_key(&key) {
1121 continue;
1122 }
1123 let entry = match write_analytic_surface(writer, &face.surface)? {
1124 Some(triple) => triple,
1125 None => (
1126 write_surface(writer, &face.surface)?,
1127 false,
1128 EmittedSurface::Spline,
1129 ),
1130 };
1131 surfaces.insert(key, entry);
1132 }
1133
1134 let mut edge_uses = HashMap::<u64, Vec<CoedgeUse>>::default();
1137 let mut edge_order: Vec<u64> = Vec::new();
1138 for face in &shell.faces {
1139 for loop_record in &face.loops {
1140 for coedge in &loop_record.coedges {
1141 let edge = edge_for(solid, coedge.edge_id)?;
1142 if edge.degenerate {
1143 continue;
1144 }
1145 let uses = edge_uses.entry(edge.id).or_default();
1146 if uses.is_empty() {
1147 edge_order.push(edge.id);
1148 }
1149 uses.push(CoedgeUse {
1150 surface_key: surface_key(face),
1151 face,
1152 coedge,
1153 });
1154 }
1155 }
1156 }
1157
1158 for edge_id in &edge_order {
1160 if edge_ids.contains_key(edge_id) {
1161 continue;
1162 }
1163 let edge = edge_for(solid, *edge_id)?;
1164 let subcurve = edge_subcurve(edge)?;
1165 let (curve, emitted_curve) = match write_analytic_curve(writer, &subcurve)? {
1166 Some(pair) => pair,
1167 None => (
1168 write_curve(writer, &subcurve)?,
1169 EmittedCurve::Spline { curve: subcurve },
1170 ),
1171 };
1172 let uses = &edge_uses[edge_id];
1173 let seam = uses.len() == 2 && uses[0].surface_key == uses[1].surface_key;
1177 let mut pcurves: Vec<(usize, Pcurve2d)> = Vec::new();
1178 let mut omitted = 0usize;
1179 if uses.len() == 2 {
1180 let mut ordered: Vec<&CoedgeUse> = uses.iter().collect();
1184 if seam && !ordered[0].coedge.forward {
1185 ordered.swap(0, 1);
1186 }
1187 for coedge_use in ordered {
1188 let oriented = if coedge_use.coedge.forward {
1193 coedge_use.coedge.pcurve.clone()
1194 } else {
1195 coedge_use.coedge.pcurve.reversed()?
1196 };
1197 let (surface_id, _, emitted_surface) = &surfaces[&coedge_use.surface_key];
1198 let outcome = build_pcurve(
1199 &coedge_use.face.surface,
1200 emitted_surface,
1201 &emitted_curve,
1202 &oriented,
1203 band,
1204 )?;
1205 match outcome.curve {
1206 Some(curve_2d) => {
1207 report.max_pcurve_deviation =
1208 report.max_pcurve_deviation.max(outcome.deviation);
1209 pcurves.push((*surface_id, curve_2d));
1210 }
1211 None => {
1212 omitted += 1;
1213 if outcome.deviation.is_finite() {
1214 report.worst_omitted_deviation =
1215 report.worst_omitted_deviation.max(outcome.deviation);
1216 } else {
1217 report.worst_omitted_deviation = f64::INFINITY;
1218 }
1219 }
1220 }
1221 }
1222 }
1223 if seam && pcurves.len() != 2 {
1227 omitted += pcurves.len();
1228 pcurves.clear();
1229 }
1230 report.pcurves_omitted += omitted;
1231 report.pcurves_written += pcurves.len();
1232 let geometry = if pcurves.is_empty() {
1233 report.bare_curves += 1;
1234 curve
1235 } else {
1236 let ids = pcurves
1237 .iter()
1238 .map(|(surface_id, curve_2d)| {
1239 write_pcurve_entity(
1240 writer,
1241 *surface_id,
1242 parametric_context,
1243 curve_2d,
1244 )
1245 })
1246 .collect::<Result<Vec<_>, String>>()?;
1247 let keyword = if seam {
1248 report.seam_curves += 1;
1249 "SEAM_CURVE"
1250 } else {
1251 report.surface_curves += 1;
1252 "SURFACE_CURVE"
1253 };
1254 writer.add(format!(
1260 "{keyword}('',#{curve},{},.CURVE_3D.)",
1261 id_list(&ids)
1262 ))
1263 };
1264 let start = vertex_step_id(writer, &mut vertex_ids, solid, edge.start_vertex_id)?;
1265 let end = vertex_step_id(writer, &mut vertex_ids, solid, edge.end_vertex_id)?;
1266 let step_id =
1267 writer.add(format!("EDGE_CURVE('',#{start},#{end},#{geometry},.T.)"));
1268 edge_ids.insert(edge.id, step_id);
1269 if let Some(edge_name) = edge.name.as_deref() {
1270 written.edges.push((edge_name.to_string(), step_id));
1271 }
1272 }
1273
1274 let mut face_ids = Vec::new();
1276 for face in &shell.faces {
1277 let mut bound_ids = Vec::new();
1278 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1279 let mut oriented_edges = Vec::new();
1280 for coedge in &loop_record.coedges {
1281 let edge = edge_for(solid, coedge.edge_id)?;
1282 if edge.degenerate {
1283 continue;
1284 }
1285 let edge_id = *edge_ids
1286 .get(&edge.id)
1287 .ok_or_else(|| format!("export_step: unwritten edge {}", edge.id))?;
1288 let orientation = if coedge.forward { ".T." } else { ".F." };
1289 oriented_edges.push(
1290 writer.add(format!("ORIENTED_EDGE('',*,*,#{edge_id},{orientation})")),
1291 );
1292 }
1293 let kind = if loop_index == 0 {
1294 "FACE_OUTER_BOUND"
1295 } else {
1296 "FACE_BOUND"
1297 };
1298 if oriented_edges.is_empty() {
1299 let Some(coedge) = loop_record.coedges.first() else {
1312 continue;
1313 };
1314 let collapsed = edge_for(solid, coedge.edge_id)?;
1315 let vertex = vertex_step_id(
1316 writer,
1317 &mut vertex_ids,
1318 solid,
1319 collapsed.start_vertex_id,
1320 )?;
1321 let vertex_loop = writer.add(format!("VERTEX_LOOP('',#{vertex})"));
1322 report.vertex_loops += 1;
1323 bound_ids.push(writer.add(format!("{kind}('',#{vertex_loop},.T.)")));
1324 continue;
1325 }
1326 let edge_loop =
1327 writer.add(format!("EDGE_LOOP('',{})", id_list(&oriented_edges)));
1328 bound_ids.push(writer.add(format!("{kind}('',#{edge_loop},.T.)")));
1329 }
1330 let (surface, flipped, _) = surfaces[&surface_key(face)];
1331 let sense = if face.same_sense != flipped {
1335 ".T."
1336 } else {
1337 ".F."
1338 };
1339 let face_step_id = writer.add(format!(
1340 "ADVANCED_FACE('',{},#{surface},{sense})",
1341 id_list(&bound_ids)
1342 ));
1343 if let Some(face_name) = face.name.as_deref() {
1344 written.faces.push((face_name.to_string(), face_step_id));
1345 }
1346 face_ids.push(face_step_id);
1347 }
1348 let closed_shell = writer.add(format!("CLOSED_SHELL('',{})", id_list(&face_ids)));
1349 let signed_volume = if solid.shells.len() > 1 {
1350 crate::mass_properties::shell_signed_volume(shell)?
1351 } else {
1352 1.0
1353 };
1354 emitted_shells.push((closed_shell, signed_volume < 0.0, signed_volume.abs()));
1355 }
1356 if emitted_shells.iter().any(|&(_, inward, _)| inward) {
1357 let outer: Vec<usize> = (0..emitted_shells.len()).filter(|&i| !emitted_shells[i].1).collect();
1358 if outer.is_empty() {
1359 return Err("export_step: a solid with voids must have an outward shell".into());
1360 }
1361 let mut owned: Vec<Vec<usize>> = vec![Vec::new(); emitted_shells.len()];
1365 for void in (0..emitted_shells.len()).filter(|&i| emitted_shells[i].1) {
1366 let owner = if outer.len() == 1 {
1367 outer[0]
1368 } else {
1369 void_owner(solid, void, &outer, &emitted_shells)?
1370 };
1371 owned[owner].push(void);
1372 }
1373 for &shell in &outer {
1374 let closed_shell = emitted_shells[shell].0;
1375 let body_id = if owned[shell].is_empty() {
1376 writer.add(format!("MANIFOLD_SOLID_BREP('{}',#{closed_shell})", step_string(solid_name)))
1377 } else {
1378 let voids = owned[shell].iter()
1379 .map(|&void| writer.add(format!("ORIENTED_CLOSED_SHELL('',*,#{},.T.)", emitted_shells[void].0)))
1380 .collect::<Vec<_>>();
1381 writer.add(format!(
1382 "BREP_WITH_VOIDS('{}',#{closed_shell},{})", step_string(solid_name), id_list(&voids)
1383 ))
1384 };
1385 written.solids.push((solid_name.to_string(), body_id));
1386 }
1387 } else {
1388 for (closed_shell, _, _) in emitted_shells {
1389 let body_id = writer.add(format!(
1390 "MANIFOLD_SOLID_BREP('{}',#{closed_shell})", step_string(solid_name)
1391 ));
1392 written.solids.push((solid_name.to_string(), body_id));
1393 }
1394 }
1395 let mut points: Vec<(Vec3, usize)> = Vec::with_capacity(vertex_ids.len());
1397 for (vertex_id, step_id) in &vertex_ids {
1398 points.push((vertex_for(solid, *vertex_id)?.point, *step_id));
1399 }
1400 written.vertices.push((solid_name.to_string(), points));
1401 }
1402 Ok(written)
1403}
1404
1405pub(crate) fn finish_step_file(
1408 writer: StepWriter,
1409 name: &str,
1410 timestamp: &str,
1411 report: &mut StepExportReport,
1412) -> Result<(), String> {
1413 let safe_name = step_string(name);
1414 let safe_timestamp = step_string(timestamp);
1415 let output = [
1416 "ISO-10303-21;".to_string(),
1417 "HEADER;".to_string(),
1418 "FILE_DESCRIPTION((''),'2;1');".to_string(),
1419 format!(
1420 "FILE_NAME('{safe_name}.step','{safe_timestamp}',(''),(''),'brep-kernel-rs','brep-kernel-rs','');"
1421 ),
1422 "FILE_SCHEMA(('AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 1 1 4 }'));".to_string(),
1423 "ENDSEC;".to_string(),
1424 "DATA;".to_string(),
1425 writer.data(),
1426 "ENDSEC;".to_string(),
1427 "END-ISO-10303-21;".to_string(),
1428 String::new(),
1429 ]
1430 .join("\n");
1431 let manifold_issues = audit_step_manifold(&output);
1432 if !manifold_issues.is_empty() {
1433 return Err(format!(
1434 "export_step: emitted AP242 manifold audit failed: {}",
1435 manifold_issues.join("; ")
1436 ));
1437 }
1438 let pcurve_issues = audit_step_pcurves(&output);
1439 if !pcurve_issues.is_empty() {
1440 return Err(format!(
1441 "export_step: emitted AP242 pcurve audit failed: {}",
1442 pcurve_issues.join("; ")
1443 ));
1444 }
1445 report.text = output;
1446 Ok(())
1447}
1448
1449pub fn export_step_report_named(
1458 solids: &[(String, &BrepSolid)],
1459 name: &str,
1460 unit: &str,
1461 timestamp: &str,
1462 pmi: Option<&StepPmi<'_>>,
1463 colors: Option<&StepColors>,
1464) -> Result<StepExportReport, String> {
1465 if solids.is_empty() {
1466 return Err("export_step: at least one solid is required".into());
1467 }
1468 let mut report = StepExportReport {
1469 products: 1,
1470 ..StepExportReport::default()
1471 };
1472 let mut writer = StepWriter::default();
1473 let contexts = write_file_contexts(&mut writer, unit)?;
1474 let geometry = write_product_geometry(&mut writer, &contexts, solids, &mut report)?;
1475 let mut items = vec![contexts.axis];
1476 items.extend(geometry.solids.iter().map(|(_, id)| *id));
1477 let geometry_context = contexts.geometry_context;
1478 let representation = writer.add(format!(
1479 "ADVANCED_BREP_SHAPE_REPRESENTATION('',{},#{geometry_context})",
1480 id_list(&items)
1481 ));
1482 let product = write_product(&mut writer, &contexts, name, "", "part");
1483 let product_shape = product.product_shape;
1484 writer.add(format!(
1485 "SHAPE_DEFINITION_REPRESENTATION(#{product_shape},#{representation})"
1486 ));
1487 if let Some(colors) = colors.filter(|colors| !colors.is_empty()) {
1492 let items = styles::styled_items(&geometry, colors, &[String::new()]);
1493 report.styled_items = styles::write_geometry_styles(&mut writer, geometry_context, &items)?;
1494 }
1495 if let Some(pmi) = pmi {
1496 let mut names = StepNameMaps::default();
1497 names.register(
1498 &geometry,
1499 StepItemOwner {
1500 product_shape,
1501 representation,
1502 },
1503 "",
1504 &MAT4_IDENTITY,
1505 );
1506 let context = pmi::StepContext {
1507 product_shape,
1508 representation,
1509 geometry_context,
1510 length_unit: contexts.length_unit,
1511 angle_unit: contexts.angle_unit,
1512 faces: &names.faces,
1513 edges: &names.edges,
1514 vertices: &names.vertices,
1515 };
1516 report.pmi_unresolved_references = pmi::write_pmi(&mut writer, &context, pmi)?;
1517 }
1518 finish_step_file(writer, name, timestamp, &mut report)?;
1519 Ok(report)
1520}
1521
1522fn vertex_step_id(
1524 writer: &mut StepWriter,
1525 vertex_ids: &mut HashMap<u64, usize>,
1526 solid: &BrepSolid,
1527 id: u64,
1528) -> Result<usize, String> {
1529 if let Some(step_id) = vertex_ids.get(&id) {
1530 return Ok(*step_id);
1531 }
1532 let point = write_point(writer, vertex_for(solid, id)?.point)?;
1533 let step_id = writer.add(format!("VERTEX_POINT('',#{point})"));
1534 vertex_ids.insert(id, step_id);
1535 Ok(step_id)
1536}
1537
1538fn step_entity_bodies(step: &str) -> HashMap<u64, &str> {
1541 step.lines()
1542 .filter_map(|line| {
1543 let rest = line.strip_prefix('#')?;
1544 let (digits, body) = rest.split_once('=')?;
1545 Some((
1546 digits.parse::<u64>().ok()?,
1547 body.trim_end().trim_end_matches(';'),
1548 ))
1549 })
1550 .collect()
1551}
1552
1553fn step_entity_refs(body: &str) -> Vec<u64> {
1557 let mut refs = Vec::new();
1558 let bytes = body.as_bytes();
1559 let mut index = 0;
1560 while index < bytes.len() {
1561 if bytes[index] == b'#' {
1562 let start = index + 1;
1563 let mut end = start;
1564 while end < bytes.len() && bytes[end].is_ascii_digit() {
1565 end += 1;
1566 }
1567 if end > start {
1568 if let Ok(id) = body[start..end].parse::<u64>() {
1569 refs.push(id);
1570 }
1571 }
1572 index = end;
1573 } else {
1574 index += 1;
1575 }
1576 }
1577 refs
1578}
1579
1580pub fn audit_step_pcurves(step: &str) -> Vec<String> {
1593 let bodies = step_entity_bodies(step);
1594 let mut issues = Vec::new();
1595 for (id, body) in &bodies {
1596 if !body.starts_with("EDGE_CURVE(") {
1597 continue;
1598 }
1599 let refs = step_entity_refs(body);
1600 let Some(geometry) = refs.get(2) else {
1601 issues.push(format!("EDGE_CURVE #{id} has no edge_geometry"));
1602 continue;
1603 };
1604 let Some(wrapper) = bodies.get(geometry) else {
1605 issues.push(format!("EDGE_CURVE #{id} references missing #{geometry}"));
1606 continue;
1607 };
1608 let seam = wrapper.starts_with("SEAM_CURVE(");
1609 if !seam && !wrapper.starts_with("SURFACE_CURVE(") {
1610 continue;
1611 }
1612 let wrapper_refs = step_entity_refs(wrapper);
1613 let pcurves = &wrapper_refs[wrapper_refs.len().min(1)..];
1614 if pcurves.is_empty() || pcurves.len() > 2 || (seam && pcurves.len() != 2) {
1615 issues.push(format!(
1616 "#{geometry} carries {} associated geometries",
1617 pcurves.len()
1618 ));
1619 continue;
1620 }
1621 let mut surfaces = Vec::new();
1622 for pcurve in pcurves {
1623 let Some(pcurve_body) = bodies.get(pcurve) else {
1624 issues.push(format!("#{geometry} references missing #{pcurve}"));
1625 continue;
1626 };
1627 if !pcurve_body.starts_with("PCURVE(") {
1628 issues.push(format!("#{geometry} associate #{pcurve} is not a PCURVE"));
1629 continue;
1630 }
1631 let pcurve_refs = step_entity_refs(pcurve_body);
1632 let representation = pcurve_refs.get(1).and_then(|id| bodies.get(id));
1633 if !representation.is_some_and(|body| body.starts_with("DEFINITIONAL_REPRESENTATION("))
1634 {
1635 issues.push(format!(
1636 "PCURVE #{pcurve} has no DEFINITIONAL_REPRESENTATION"
1637 ));
1638 }
1639 if let Some(surface) = pcurve_refs.first() {
1640 surfaces.push(*surface);
1641 }
1642 }
1643 if surfaces.len() == 2 && (surfaces[0] == surfaces[1]) != seam {
1644 issues.push(format!(
1645 "#{geometry} pcurves name {} surface(s) but it is a {}",
1646 if surfaces[0] == surfaces[1] { 1 } else { 2 },
1647 if seam { "SEAM_CURVE" } else { "SURFACE_CURVE" }
1648 ));
1649 }
1650 }
1651 issues.sort();
1652 issues
1653}
1654
1655pub fn audit_step_manifold(step: &str) -> Vec<String> {
1660 let marker = "ORIENTED_EDGE('',*,*,#";
1661 let mut uses = HashMap::<u64, Vec<bool>>::default();
1662 for line in step.lines() {
1663 let Some(offset) = line.find(marker) else {
1664 continue;
1665 };
1666 let rest = &line[offset + marker.len()..];
1667 let digits = rest
1668 .chars()
1669 .take_while(|character| character.is_ascii_digit())
1670 .collect::<String>();
1671 let Ok(edge_id) = digits.parse::<u64>() else {
1672 continue;
1673 };
1674 let suffix = &rest[digits.len()..];
1675 let sense = suffix.starts_with(",.T.");
1676 uses.entry(edge_id).or_default().push(sense);
1677 }
1678 let mut issues = uses
1679 .into_iter()
1680 .filter_map(|(edge, senses)| {
1681 (senses.len() != 2 || senses[0] == senses[1]).then(|| {
1682 format!(
1683 "EDGE_CURVE #{edge} has {} uses with senses {:?}",
1684 senses.len(),
1685 senses
1686 )
1687 })
1688 })
1689 .collect::<Vec<_>>();
1690 issues.sort();
1691 issues
1692}
1693