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

+32
View File
@@ -576,6 +576,38 @@ impl<'a> Reader<'a> {
}
}
/// Check a STEP file's structure: it parses, no entity id is defined twice, every reference
/// names a defined entity, and ids are dense (`#1`…`#n`). Returns the entity count.
pub fn step_validate(text: &str) -> std::result::Result<usize, String> {
let ex = p21::parse(text)?;
if let Some(d) = ex.duplicates.first() {
return Err(format!("{} duplicate entity ids (first #{d})", ex.duplicates.len()));
}
fn refs(p: &Param, out: &mut Vec<u64>) {
match p {
Param::Ref(r) => out.push(*r),
Param::List(v) | Param::Typed(_, v) => v.iter().for_each(|x| refs(x, out)),
_ => {}
}
}
let mut ids: Vec<&u64> = ex.entities.keys().collect();
ids.sort();
for id in &ids {
let mut out = Vec::new();
if let Some(e) = ex.get(**id) {
e.records.iter().flat_map(|r| &r.params).for_each(|p| refs(p, &mut out));
}
if let Some(r) = out.iter().find(|r| ex.get(**r).is_none()) {
return Err(format!("#{id} refers to undefined #{r}"));
}
}
let n = ex.entities.len();
if ids.last().is_some_and(|m| **m != n as u64) || ids.first().is_some_and(|m| **m != 1) {
return Err(format!("entity ids are not dense: {n} entities, #{}…#{}", ids.first().map_or(0, |x| **x), ids.last().map_or(0, |x| **x)));
}
Ok(n)
}
/// Recent imports, so a file read for a command and then evaluated in the timeline is only
/// read once.
static RECENT: std::sync::Mutex<Vec<(u64, usize, std::sync::Arc<StepImport>)>> = std::sync::Mutex::new(Vec::new());