IGES reader checked against Fusion's IGES exports: 75 of 76 reference parts read back exactly; cone tips read whole

`cargo xtask step-corpus --iges` (and `solvecraft-cli step-corpus --iges`) opens every oracle part.igs as File → Open does and compares body count, volume, area and face count with Fusion's measurements, writing docs/iges-import.md. The run found faces closing at an apex (a drill point's cone, a sphere's pole: the seam runs out to the apex and straight back) kept whole and meshed wrongly; they are now split along the iso-line opposite the seam like bands are (18-hole-features and 40-box-sphere-torus pass). The one failure left, 50-sweep-tight-bend, is written by Fusion as five loose, partly degenerate surfaces that Fusion cannot read back as a solid either; documented in the report. Test: a drilled blind hole round-trips through IGES.

Fixes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-10 05:56:53 +00:00
1 parent 72bc6fb010
commit a7fe0429f0
6 files changed
+214 -18

No files matched your search

+15
View File
@@ -141,3 +141,18 @@ fn trimmed_surfaces_sew_into_a_solid() {
let m = measure(&imp.bodies[0].body).unwrap();
assert!(rel(m.volume, 6000.0) < 1e-6 && rel(m.area, 2200.0) < 1e-6, "{} {}", m.volume, m.area);
}
#[test]
fn a_drill_point_round_trips_through_iges() {
// A blind hole with a cone tip: the tip's face closes at its apex (its seam runs out to the
// apex and straight back).
use solvecraft_geom::{Loop2, Plane, Region2, Vec2};
let plate = box_solid(Vec3::new(-20.0, -15.0, 0.0), Vec3::new(20.0, 15.0, 10.0)).unwrap();
let profile =
Region2 { outer: Loop2::polygon(&[Vec2::new(0.0, 2.0), Vec2::new(4.0, 5.0), Vec2::new(4.0, 11.0), Vec2::new(0.0, 11.0)]), holes: vec![] };
let hole = crate::revolve(&Plane::XZ, &[profile], Vec2::ZERO, Vec2::new(0.0, 1.0), std::f64::consts::TAU).unwrap().pop().unwrap();
let drilled = boolean(&plate, &hole, BoolOp::Cut).unwrap().unwrap();
let v = 12000.0 - std::f64::consts::PI * 16.0 * (5.0 + 1.0);
assert!(rel(measure(&drilled).unwrap().volume, v) < 1e-3);
round_trip(&drilled);
}
+70
View File
@@ -299,6 +299,10 @@ fn plan(face: &mt::Face) -> R<Option<Plan>> {
_ => chains.push((seam, vec![e])),
}
}
// A cone's tip: the seam runs from the ring to the apex and straight back.
if let [(true, sm), (false, ring)] = chains.as_slice() {
return apex_plan(face, &s, (d, mid, period), sm, ring, &wires, outer_idx).map(Some);
}
let [(true, s1), (false, r1), (true, s2), (false, r2)] = chains.as_slice() else {
return Err(format!("unsupported seam loop layout ({} runs)", chains.len()));
};
@@ -358,6 +362,72 @@ fn plan(face: &mt::Face) -> R<Option<Plan>> {
Ok(Some(Plan { cuts: c1.into_iter().chain(c2).collect(), faces: (fa, fb) }))
}
/// Split a face closing at an apex (loop: seam out to the apex, seam back, the ring) along the
/// iso-line opposite the seam, from the ring to the apex.
fn apex_plan(
face: &mt::Face,
s: &mt::Surface,
(d, mid, period): (Dir, f64, f64),
sm: &[mt::Edge],
ring: &[mt::Edge],
wires: &[mt::Wire],
outer_idx: usize,
) -> R<Plan> {
let h = sm.len() / 2;
if !sm.len().is_multiple_of(2) || h == 0 {
return Err("seam edges are not used back and forth".into());
}
let (out, back) = (sm.get(..h).ok_or("seam")?, sm.get(h..).ok_or("seam")?);
if out.iter().map(mt::Edge::id).collect::<Vec<_>>() != back.iter().rev().map(mt::Edge::id).collect::<Vec<_>>() {
return Err("seam edges are not used back and forth".into());
}
let apex = out.last().ok_or("seam")?.back().clone();
let (k, t, q) = crossing(s, d, ring, mid, period)?;
let seam_line: Vec<mt::Point3> = out.iter().flat_map(|e| samples(&e.oriented_curve(), 16).into_iter().map(|x| x.1)).collect();
let iso = iso_curve(s, d, mid, q, apex.point(), &seam_line)?;
let (ra, rb, c, vq) = split_chain(ring, k, t, q)?;
let e = mt::Edge::new_unchecked(&vq, &apex, iso);
// Face A: out to the apex, down the split line, the ring's second part; face B the rest.
let mut wa: Vec<mt::Edge> = out.to_vec();
wa.push(e.inverse());
wa.extend(rb.iter().cloned());
let mut wb: Vec<mt::Edge> = vec![e];
wb.extend(back.iter().cloned());
wb.extend(ra);
let (wire_a, wire_b): (mt::Wire, mt::Wire) = (wa.into(), wb.into());
if !wire_a.is_closed() || !wire_b.is_closed() {
return Err("split loops do not close".into());
}
let side_a = rb
.iter()
.flat_map(|e| samples(&e.oriented_curve(), 4))
.filter_map(|(_, p)| uv_of(s, p).map(|uv| coord(d, uv)))
.find(|w| (w - mid).abs() > period * 1e-3)
.map(|w| w < mid)
.ok_or("cannot place the split halves")?;
let (mut ha, mut hb) = (vec![wire_a], vec![wire_b]);
for (i, w) in wires.iter().enumerate() {
if i == outer_idx {
continue;
}
let ws: Vec<f64> = w.edge_iter().flat_map(|e| samples(&e.curve(), 4)).filter_map(|(_, p)| uv_of(s, p).map(|uv| coord(d, uv))).collect();
if ws.iter().all(|x| (*x < mid) == side_a) {
ha.push(w.clone());
} else if ws.iter().all(|x| (*x < mid) != side_a) {
hb.push(w.clone());
} else {
return Err("a hole crosses the split line".into());
}
}
let mut fa = mt::Face::new_unchecked(ha, s.clone());
let mut fb = mt::Face::new_unchecked(hb, s.clone());
if !face.orientation() {
fa.invert();
fb.invert();
}
Ok(Plan { cuts: c.into_iter().collect(), faces: (fa, fb) })
}
/// Replace cut edges in a face's loops.
fn recut(face: &mt::Face, cuts: &[(mt::Edge, (mt::Edge, mt::Edge))]) -> mt::Face {
let touches = face.absolute_boundaries().iter().flat_map(|w| w.edge_iter()).any(|e| cuts.iter().any(|(c, _)| c.id() == e.id()));