STEP export: unique ids, plain curves, right revolution senses; seams on split faces

Vendored truck-stepio (writer only): an intersection curve's second surface
was written at the first one's id (duplicate ids for every boolean edge),
and revolved surfaces lost their orientation (the modeling-surface sense was
always true; inverted or mirrored revolutions came out inside out). Export
now writes intersection edges as C1 cubic B-splines through exact points,
so files carry plain 3D curves and no redundant surfaces.

Vendored truck-meshalgo: hinted parameter searches that run off a B-spline's
domain are rejected or clamped (meshing their grid never finished).

STEP import: straight revolution profiles are bounded in the meridian plane
and stop at the axis; loops on revolutions start away from the axis; face
extents include edge end points. Edges between pieces of one split surface
draw as seams.

kernel::step_validate checks a STEP file (duplicate ids, dangling
references, dense numbering); the oracle replay now exports every rebuilt
part, validates it and re-imports it with no warnings and the same volume
(all 32 rebuildable cases pass).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 23:40:07 +00:00
1 parent 32e6b2a578
commit 08cf223ced
47 files changed
+10677 -39

No files matched your search

+60 -1
View File
@@ -1,8 +1,67 @@
use truck_modeling as mt;
use truck_stepio::out;
use crate::body::Body;
use crate::{KernelError, Result, guard};
/// Intersection-curve edges (from booleans) as cubic B-splines through the exact curve: STEP
/// readers get plain 3D geometry that lies on both faces, and the file does not repeat the two
/// surfaces for every such edge.
fn plain_curves(b: &Body) -> mt::Solid {
let s = b.deep_copy();
let size = b.size();
let mut seen = std::collections::HashSet::new();
for e in s.edge_iter() {
if !seen.insert(e.id()) {
continue;
}
if let mt::Curve::IntersectionCurve(ic) = e.curve()
&& let Some(c) = hermite_through(&ic, size * 1e-4)
{
e.set_curve(mt::Curve::BSplineCurve(c));
}
}
s
}
/// A C¹ cubic B-spline through exact points of `c` spaced so that chords deviate at most `tol`
/// (Catmull–Rom tangents over chord length; the cubic is far closer than the chords).
fn hermite_through<C>(c: &C, tol: f64) -> Option<mt::BSplineCurve<mt::Point3>>
where
C: mt::ParametricCurve<Point = mt::Point3, Vector = mt::Vector3> + mt::BoundedCurve + mt::ParameterDivision1D<Point = mt::Point3>,
{
use mt::{InnerSpace, MetricSpace};
let (_, pts) = c.parameter_division(c.range_tuple(), tol);
let pts: Vec<mt::Point3> = pts.into_iter().fold(Vec::new(), |mut v: Vec<mt::Point3>, p| {
if v.last().is_none_or(|q| q.distance(p) > tol * 1e-3) {
v.push(p);
}
v
});
let n = pts.len().checked_sub(1).filter(|n| *n >= 1 && *n < 1_000_000)?;
let mut u = vec![0.0; n + 1];
for i in 1..=n {
u[i] = u[i - 1] + pts[i].distance(pts[i - 1]);
}
let tangent = |i: usize| {
let (a, b) = (i.saturating_sub(1), (i + 1).min(n));
(pts[b] - pts[a]) / (u[b] - u[a])
};
let mut ctrl = vec![pts[0]];
let mut knots = vec![u[0]; 4];
for i in 0..n {
let h = u[i + 1] - u[i];
ctrl.push(pts[i] + tangent(i) * (h / 3.0));
ctrl.push(pts[i + 1] - tangent(i + 1) * (h / 3.0));
ctrl.push(pts[i + 1]);
knots.extend(std::iter::repeat_n(u[i + 1], if i + 1 == n { 4 } else { 3 }));
}
if ctrl.iter().any(|p| !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite())) || tangent(0).magnitude() == 0.0 {
return None;
}
mt::BSplineCurve::try_new(mt::KnotVec::from(knots), ctrl).ok()
}
/// STEP (ISO 10303-21) text for the given bodies.
pub fn step_export(bodies: &[&Body], system: &str) -> Result<String> {
if bodies.is_empty() {
@@ -12,7 +71,7 @@ pub fn step_export(bodies: &[&Body], system: &str) -> Result<String> {
b.require_brep("STEP export")?;
}
guard("step export", || {
let cs: Vec<_> = bodies.iter().map(|b| b.solid.compress()).collect();
let cs: Vec<_> = bodies.iter().map(|b| plain_curves(b).compress()).collect();
let models: out::StepModels<_, _, _> = cs.iter().collect();
let header = out::StepHeaderDescriptor { organization_system: system.to_owned(), ..Default::default() };
Ok(out::CompleteStepDisplay::new(models, header).to_string())