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 <noreply@anthropic.com>
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

+22 -1
View File
@@ -209,7 +209,7 @@ impl Builder {
.flat_map(|e| {
let c = e.curve();
let (t0, t1) = c.range_tuple();
(0..8).map(move |i| c.subs(t0 + (t1 - t0) * i as f64 / 8.0)).collect::<Vec<_>>()
(0..=8).map(move |i| c.subs(t0 + (t1 - t0) * i as f64 / 8.0)).collect::<Vec<_>>()
})
.collect();
// Seam edges: used twice by the face's loops.
@@ -236,6 +236,27 @@ impl Builder {
guard("step surface", || geom::surface(cx, sid, &extent, &seam, sense, 0).map_err(crate::KernelError::Failed))
.map_err(|e| e.to_string())?
};
// Loops on a surface of revolution start away from the axis: a pole (cone apex, sphere
// pole) has no definite rotation parameter, and the mesher takes the first point's
// parameter as the anchor for the rest of the loop.
let wires: Vec<mt::Wire> = match &surface {
mt::Surface::RevolutedCurve(rc) => {
let (o, a) = (rc.entity().origin(), rc.entity().axis());
let dist = |e: &mt::Edge| {
let v = e.front().point() - o;
(v - a * v.dot(a)).magnitude()
};
wires
.into_iter()
.map(|w| {
let edges: Vec<mt::Edge> = w.edge_iter().cloned().collect();
let start = edges.iter().enumerate().max_by(|x, y| dist(x.1).total_cmp(&dist(y.1))).map(|(i, _)| i).unwrap_or(0);
edges.iter().skip(start).chain(edges.iter().take(start)).cloned().collect::<Vec<_>>().into()
})
.collect()
}
_ => wires,
};
match mt::Face::try_new(wires.clone(), surface.clone()) {
Ok(f) => Ok(f),
// Faces on periodic surfaces run along a seam edge twice: valid in STEP.