STEP export: pieces of a plane or extruded cylinder written as one face

Fusion round trip WARNs on face counts:
- 48: the pulley's annular end faces arrived as 240° and 120° sectors.
- 54: the side faces and holes of a body glued to its mirror image came
  in halves.

Changes:
- Planes merge: PLANE faces and flat B-spline sheets, as well as revolved
  planes. A plane keeps several loops (an annulus, a face with holes), no
  seam. Flat B-spline sheets are written as PLANE.
- Pieces of an extruded cylinder (B-spline sheets) merge when their edges
  are circles square to the axis. The merged face is written as a
  CYLINDRICAL_SURFACE, with its parameter seam on the face's seam edge.
- A full turn's loop is built explicitly: ring, seam, other ring (or the
  apex on the axis), seam back. Before, it depended on edge order, and
  after 81c85b5 the pulley's last pieces no longer merged.
- The read-back check moved to the whole file. If the merged file reads
  with warnings and the unmerged one does not, the unmerged one is
  written.

Results: 48 has 12 faces (Fusion 12), 54 has 12 (Fusion 12). Oracle
results are unchanged (73 PASS) and the STEP corpus passes 76/76. New
test: a plate with a hole glued to its mirror exports 8 faces, all
planes and cylinders, and round-trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 13:16:39 +00:00
1 parent c009bb2c2a
commit 0a1b6f0fff
6 files changed
+396 -163

No files matched your search

+1 -1
View File
@@ -692,7 +692,7 @@ fn revolved_oriented(curve: mt::Curve, o: mt::Point3, axis: mt::Vector3, u: f64,
revolved(curve, o, -axis)
}
fn bspline_surface(cx: &Ctx, e: &Entity) -> R<mt::Surface> {
pub(crate) fn bspline_surface(cx: &Ctx, e: &Entity) -> R<mt::Surface> {
let (base, knots, kind): (Vec<Param>, Option<[Option<&Param>; 4]>, &str) = if e.is_complex() {
let b = e.record("B_SPLINE_SURFACE").ok_or("complex surface without B_SPLINE_SURFACE")?;
let mut base = vec![Param::Str(String::new())];
+21 -1
View File
@@ -1,7 +1,7 @@
use solvecraft_geom::{Loop2, Plane, Region2, Vec2, Vec3};
use super::*;
use crate::{BoolOp, boolean, box_solid, cylinder, fillet, measure, revolve, sphere, step_export, torus};
use crate::{BoolOp, boolean, box_solid, cylinder, extrude, fillet, measure, revolve, sphere, step_export, torus};
fn rel(a: f64, b: f64) -> f64 {
(a - b).abs() / b.abs().max(1e-9)
@@ -502,3 +502,23 @@ fn tee_fillet_exports_byte_identical() {
assert!(export() == first, "export differs between runs");
}
}
/// A plate with a hole glued to its mirror image: the faces split at the mirror plane and the
/// hole's B-spline halves are written as one plane or one cylinder each, as other systems
/// count them.
#[test]
fn glued_halves_export_as_whole_faces() {
let half = Region2 {
outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(30.0, 0.0), Vec2::new(30.0, 40.0), Vec2::new(0.0, 40.0)]),
holes: vec![Loop2::circle(Vec2::new(15.0, 20.0), 4.0).reversed()],
};
let right = extrude(&Plane::XY, &[half], 0.0, 10.0).unwrap().pop().unwrap();
let left = crate::transform_matrix(&right, [[-1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]).unwrap();
let b = boolean(&right, &left, BoolOp::Union).unwrap().unwrap();
let text = step_export(&[&b], "t").unwrap();
assert!(!text.contains("B_SPLINE_SURFACE"), "{text}");
assert_eq!(text.matches("CYLINDRICAL_SURFACE(").count(), 2);
let imp = round_trip(&b);
// Box sides and caps (6) and two holes.
assert_eq!(imp.bodies[0].file_faces, 8);
}