STEP import: a circle edge between two vertices is the arc between them, however short

An edge on a circle or ellipse read as a full turn whenever its two vertices lay
within the file's uncertainty of each other. The engine assembly's water pump
housing has a 20.4 um arc between two vertices in a part whose uncertainty is
20.5 um: read as a turn, its cylinder face's loop wound once round the axis with
no second bound ("a face around an open surface of revolution with one bound")
and the housing came in as surfaces. A full turn is now an edge with one vertex,
or two at one point; two vertices apart bound the arc between them.

Engine assembly: HOUSING_MAIN_WATER_PUMP_01 comes in as a closed solid
(refused 5 -> 4). No other edge of the engine or Poppy Humanoid read this way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:27:48 -04:00
committed by Irakli Kourtis
parent 30dc4c8601
commit d2858cb3d9
2 changed files with 91 additions and 9 deletions
+79
View File
@@ -2477,3 +2477,82 @@ fn a_round_face_off_its_frames_quarter_lines_is_formed_within_one_turn() {
assert!(rel(m.area, area) < 1e-6, "{start}: area {} vs {area}", m.area);
}
}
/// A circle edge between two vertices its file's uncertainty does not tell apart is the short arc
/// between them, not a full turn (the engine assembly's water pump housing has a 20.4 um arc
/// between two vertices in a part whose uncertainty is 20.5 um). A can whose wall is
/// split by two rulings 0.6 um apart, under the default 1 um: read as turns, the sliver of wall
/// between them wound once round the can with no second bound and the can came in as surfaces.
#[test]
fn a_circle_edge_between_two_vertices_closer_than_the_tolerance_is_the_arc_between_them() {
let (r, h) = (5.0f64, 10.0f64);
let delta = 0.6e-3 / r;
let (c, s) = (r * delta.cos(), r * delta.sin());
let text = format!(
"{HEAD}
#1=CARTESIAN_POINT('',(0.,0.,0.));
#2=DIRECTION('',(0.,0.,1.));
#3=DIRECTION('',(1.,0.,0.));
#5=AXIS2_PLACEMENT_3D('',#1,#2,#3);
#6=CARTESIAN_POINT('',(0.,0.,{h:?}));
#7=AXIS2_PLACEMENT_3D('',#6,#2,#3);
#10=CARTESIAN_POINT('',({r:?},0.,0.));
#11=CARTESIAN_POINT('',({c:?},{s:?},0.));
#12=CARTESIAN_POINT('',({r:?},0.,{h:?}));
#13=CARTESIAN_POINT('',({c:?},{s:?},{h:?}));
#14=VERTEX_POINT('',#10);
#15=VERTEX_POINT('',#11);
#16=VERTEX_POINT('',#12);
#17=VERTEX_POINT('',#13);
#20=CIRCLE('',#5,{r:?});
#21=CIRCLE('',#7,{r:?});
#22=VECTOR('',#2,1.);
#23=LINE('',#10,#22);
#24=LINE('',#11,#22);
#30=EDGE_CURVE('',#14,#15,#20,.T.);
#31=EDGE_CURVE('',#15,#14,#20,.T.);
#32=EDGE_CURVE('',#16,#17,#21,.T.);
#33=EDGE_CURVE('',#17,#16,#21,.T.);
#34=EDGE_CURVE('',#14,#16,#23,.T.);
#35=EDGE_CURVE('',#15,#17,#24,.T.);
#40=ORIENTED_EDGE('',*,*,#31,.F.);
#41=ORIENTED_EDGE('',*,*,#30,.F.);
#42=EDGE_LOOP('',(#40,#41));
#43=FACE_OUTER_BOUND('',#42,.T.);
#44=PLANE('',#5);
#45=ADVANCED_FACE('Bottom',(#43),#44,.F.);
#50=ORIENTED_EDGE('',*,*,#32,.T.);
#51=ORIENTED_EDGE('',*,*,#33,.T.);
#52=EDGE_LOOP('',(#50,#51));
#53=FACE_OUTER_BOUND('',#52,.T.);
#54=PLANE('',#7);
#55=ADVANCED_FACE('Top',(#53),#54,.T.);
#60=ORIENTED_EDGE('',*,*,#30,.T.);
#61=ORIENTED_EDGE('',*,*,#35,.T.);
#62=ORIENTED_EDGE('',*,*,#32,.F.);
#63=ORIENTED_EDGE('',*,*,#34,.F.);
#64=EDGE_LOOP('',(#60,#61,#62,#63));
#65=FACE_OUTER_BOUND('',#64,.T.);
#66=CYLINDRICAL_SURFACE('',#5,{r:?});
#67=ADVANCED_FACE('Sliver',(#65),#66,.T.);
#70=ORIENTED_EDGE('',*,*,#31,.T.);
#71=ORIENTED_EDGE('',*,*,#34,.T.);
#72=ORIENTED_EDGE('',*,*,#33,.F.);
#73=ORIENTED_EDGE('',*,*,#35,.F.);
#74=EDGE_LOOP('',(#70,#71,#72,#73));
#75=FACE_OUTER_BOUND('',#74,.T.);
#77=ADVANCED_FACE('Wall',(#75),#66,.T.);
#80=CLOSED_SHELL('',(#45,#55,#67,#77));
#81=MANIFOLD_SOLID_BREP('Can',#80);
#82=SHAPE_REPRESENTATION('',(#81),$);
{TAIL}"
);
let imp = step_import(&text).unwrap();
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
let can = &imp.bodies[0];
assert!(can.closed && can.refused.is_none() && !can.body.is_surface(), "{:?} {:?}", can.refused, imp.warnings);
let pi = std::f64::consts::PI;
let m = measure(&can.body).unwrap();
assert!(rel(m.volume, pi * r * r * h) < 1e-6, "{}", m.volume);
assert!(rel(m.area, 2.0 * pi * r * r + 2.0 * pi * r * h) < 1e-6, "{}", m.area);
}
+12 -9
View File
@@ -699,8 +699,11 @@ impl<'b, 'a> Builder<'b, 'a> {
let curve = self.edge_geometry(rid(p.get(3), "edge geometry")?, 0)?;
let (ps, pe) = (self.point(vs)?, self.point(ve)?);
let closed = vs == ve;
let near = closed || v3(ps).dist(v3(pe)) <= self.tol;
let mapped = map_edge(&curve, ps, pe, same_sense, closed, near, self.tol).map_err(|m| format!("edge #{id}: {m}"))?;
// A full turn when the edge has one vertex, or two at one point. Two vertices apart bound
// the arc between them, however short: a 20.4 um arc between vertices a 20.5 um uncertainty
// does not tell apart is that arc, not a turn around its face.
let full = closed || ps == pe;
let mapped = map_edge(&curve, ps, pe, same_sense, closed, full, self.tol).map_err(|m| format!("edge #{id}: {m}"))?;
if mapped.sense_mismatch {
self.sense_mismatches += 1;
}
@@ -1654,9 +1657,9 @@ fn turn(x: f64) -> f64 {
}
/// The edge from `ps` to `pe` on `curve`: its schema curve and interval (min → max) and which
/// way it runs. `closed`: the same vertex at both ends; `near`: the ends coincide within the
/// tolerance (a full turn on a periodic curve).
fn map_edge(curve: &Curve, ps: P3, pe: P3, same_sense: bool, closed: bool, near: bool, tol: f64) -> R<Mapped> {
/// way it runs. `closed`: the same vertex at both ends; `full`: the ends are one point (a full
/// turn on a periodic curve).
fn map_edge(curve: &Curve, ps: P3, pe: P3, same_sense: bool, closed: bool, full: bool, tol: f64) -> R<Mapped> {
match curve {
Curve::Line { origin, direction, magnitude } => {
if closed {
@@ -1675,11 +1678,11 @@ fn map_edge(curve: &Curve, ps: P3, pe: P3, same_sense: bool, closed: bool, near:
Ok(Mapped { curve: BCurve::Line { start, end }, interval: [0.0, hi - lo], with_step, sense_mismatch: with_step != same_sense })
}
Curve::Circle { position, radius } => {
conic(BCurve::Circle { frame: frame(position), radius: *radius }, position, [*radius; 2], ps, pe, same_sense, near)
conic(BCurve::Circle { frame: frame(position), radius: *radius }, position, [*radius; 2], ps, pe, same_sense, full)
}
Curve::Ellipse { position, semi_axis_1, semi_axis_2 } => {
let radii = [*semi_axis_1, *semi_axis_2];
conic(BCurve::Ellipse { frame: frame(position), radii }, position, radii, ps, pe, same_sense, near)
conic(BCurve::Ellipse { frame: frame(position), radii }, position, radii, ps, pe, same_sense, full)
}
Curve::BSpline(b) => open_or_closed(map_bspline_curve(b)?, ps, pe, same_sense, closed, tol),
Curve::Polyline(points) => {
@@ -1722,7 +1725,7 @@ fn open_conic(curve: &Curve, ps: P3, pe: P3, same_sense: bool, closed: bool, spl
/// A circle or ellipse: the vertices' angles, the arc between them in the edge's sense (a full
/// turn when they coincide).
fn conic(curve: BCurve, position: &Placement, radii: [f64; 2], ps: P3, pe: P3, same_sense: bool, near: bool) -> R<Mapped> {
fn conic(curve: BCurve, position: &Placement, radii: [f64; 2], ps: P3, pe: P3, same_sense: bool, full: bool) -> R<Mapped> {
let y = position.y();
let param = |p: P3| {
let q = sub(p, position.origin);
@@ -1730,7 +1733,7 @@ fn conic(curve: BCurve, position: &Placement, radii: [f64; 2], ps: P3, pe: P3, s
};
let (ts, te) = (param(ps), param(pe));
// Along the curve from `from` to `to`, increasing.
let span = |from: f64, to: f64| if near { TAU } else { turn(to - from) };
let span = |from: f64, to: f64| if full { TAU } else { turn(to - from) };
let (lo, d) = if same_sense { (ts, span(ts, te)) } else { (te, span(te, ts)) };
if !(d > 0.0) {
return Err("a degenerate edge on a conic".into());